Improve image processing settings url target adding search parameters
This commit is contained in:
@@ -10,7 +10,7 @@ import { d } from 'koration';
|
||||
import {
|
||||
ImageProcessingTargetMode,
|
||||
TSettingsDownloadConversion,
|
||||
TSettingsDownloadConversionHeader,
|
||||
TSettingsDownloadConversionKeyValueItem,
|
||||
} from '@/features/settings/Settings.types.ts';
|
||||
import {
|
||||
DEFAULT_MIME_TYPE,
|
||||
@@ -21,10 +21,13 @@ import {
|
||||
TARGET_DISABLED,
|
||||
} from '@/features/settings/Settings.constants.ts';
|
||||
import {
|
||||
Maybe,
|
||||
SettingsDownloadConversion,
|
||||
SettingsDownloadConversionHeader,
|
||||
SettingsDownloadConversionType,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
import { UrlUtil } from '@/lib/UrlUtil.ts';
|
||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
let COUNTER = 0;
|
||||
|
||||
@@ -33,14 +36,14 @@ const normalizeMimeType = (mimeType: string): string => mimeType.replace(MIME_TY
|
||||
export const isDefaultMimeType = (mimeType: string): boolean =>
|
||||
normalizeMimeType(mimeType.toLowerCase().trim()) === DEFAULT_MIME_TYPE;
|
||||
|
||||
export const isDuplicateHeader = (
|
||||
export const isDuplicateKeyValueItem = (
|
||||
header: string,
|
||||
index: number,
|
||||
headers: SettingsDownloadConversionType['headers'],
|
||||
): boolean => headers?.slice(0, index).some(({ name }) => name === header) ?? false;
|
||||
|
||||
const hasDuplicateHeaders = (headers: SettingsDownloadConversionType['headers']): boolean =>
|
||||
headers?.some((header, index) => isDuplicateHeader(header.name, index, headers)) ?? false;
|
||||
const hasDuplicateKeyValueItems = (headers: SettingsDownloadConversionType['headers']): boolean =>
|
||||
headers?.some((header, index) => isDuplicateKeyValueItem(header.name, index, headers)) ?? false;
|
||||
|
||||
export const isDuplicateConversion = (
|
||||
mimeType: string,
|
||||
@@ -82,14 +85,15 @@ const isInvalidTarget = (target: string, mode: ImageProcessingTargetMode, mimeTy
|
||||
|
||||
export const containsInvalidConversion = (conversions: TSettingsDownloadConversion[]): boolean =>
|
||||
conversions.some(
|
||||
({ mimeType, compressionLevel, target, callTimeout, connectTimeout, headers, mode }, index) =>
|
||||
({ mimeType, compressionLevel, target, callTimeout, connectTimeout, headers, searchParams, mode }, index) =>
|
||||
isUnsetConversion(mimeType, target) ||
|
||||
isInvalidTarget(target, mode, mimeType) ||
|
||||
!isValidCompressionLevel(compressionLevel) ||
|
||||
!isValidCallTimeoutSetting(callTimeout) ||
|
||||
!isValidConnectTimeoutSetting(connectTimeout) ||
|
||||
isDuplicateConversion(mimeType, index, conversions) ||
|
||||
hasDuplicateHeaders(headers),
|
||||
hasDuplicateKeyValueItems(headers) ||
|
||||
hasDuplicateKeyValueItems(searchParams),
|
||||
);
|
||||
|
||||
export const getTargetMode = (target: string): ImageProcessingTargetMode => {
|
||||
@@ -106,13 +110,22 @@ export const getTargetMode = (target: string): ImageProcessingTargetMode => {
|
||||
return ImageProcessingTargetMode.IMAGE;
|
||||
};
|
||||
|
||||
export const addStableIdToHeaders = (
|
||||
headers: (SettingsDownloadConversionHeader | TSettingsDownloadConversionHeader)[],
|
||||
): TSettingsDownloadConversionHeader[] =>
|
||||
headers.map((header) => ({
|
||||
export const extractSearchParams = (url: string): SettingsDownloadConversionHeader[] => {
|
||||
const urlObject = UrlUtil.asUrl(url);
|
||||
|
||||
return [...(urlObject?.searchParams ?? []).entries()].map(([key, value]) => ({
|
||||
name: String(key),
|
||||
value: jsonSaveParse(value) ?? value,
|
||||
}));
|
||||
};
|
||||
|
||||
export const addStableIdToKeyValueItems = (
|
||||
items: (SettingsDownloadConversionHeader | TSettingsDownloadConversionKeyValueItem)[],
|
||||
): TSettingsDownloadConversionKeyValueItem[] =>
|
||||
items.map((item) => ({
|
||||
// eslint-disable-next-line no-plusplus
|
||||
id: (header as TSettingsDownloadConversionHeader).id ?? COUNTER++,
|
||||
...header,
|
||||
id: (item as TSettingsDownloadConversionKeyValueItem).id ?? COUNTER++,
|
||||
...item,
|
||||
}));
|
||||
|
||||
export const addStableIdToConversions = (
|
||||
@@ -123,9 +136,29 @@ export const addStableIdToConversions = (
|
||||
id: (conversion as TSettingsDownloadConversion).id ?? COUNTER++,
|
||||
...conversion,
|
||||
mode: getTargetMode(normalizeMimeType(conversion.target)),
|
||||
headers: conversion.headers ? addStableIdToHeaders(conversion.headers) : null,
|
||||
headers: conversion.headers ? addStableIdToKeyValueItems(conversion.headers) : null,
|
||||
searchParams: addStableIdToKeyValueItems(extractSearchParams(conversion.target)),
|
||||
}));
|
||||
|
||||
export const getUpdatedSearchParams = (
|
||||
url: string,
|
||||
existingParams: Maybe<TSettingsDownloadConversionKeyValueItem[] | undefined>,
|
||||
): TSettingsDownloadConversionKeyValueItem[] => {
|
||||
const urlObject = UrlUtil.asUrl(url);
|
||||
|
||||
const params = [...(urlObject?.searchParams?.entries() ?? [])].map(([key, value]) => ({ name: key, value }));
|
||||
const paramsWithRetainedStableId = params.map((param) => {
|
||||
const existingParam = existingParams?.find(({ name }) => name === param.name);
|
||||
|
||||
return {
|
||||
id: existingParam?.id,
|
||||
...param,
|
||||
};
|
||||
});
|
||||
|
||||
return addStableIdToKeyValueItems(paramsWithRetainedStableId);
|
||||
};
|
||||
|
||||
export const normalizeConversions = (conversions: TSettingsDownloadConversion[]): TSettingsDownloadConversion[] =>
|
||||
conversions.map((conversion) => ({
|
||||
...conversion,
|
||||
@@ -144,7 +177,7 @@ const toValidServerMimeType = (mimeType: string): string => {
|
||||
export const toValidServerConversions = (conversions: TSettingsDownloadConversion[]): SettingsDownloadConversion[] =>
|
||||
conversions
|
||||
.filter(({ mimeType, target }) => !!mimeType && !!target)
|
||||
.map(({ id, mode, ...conversion }) => ({
|
||||
.map(({ id, mode, searchParams, ...conversion }) => ({
|
||||
...conversion,
|
||||
mimeType: toValidServerMimeType(conversion.mimeType),
|
||||
target: isUrlTargetMode(conversion.target) ? conversion.target : `${MIME_TYPE_PREFIX}${conversion.target}`,
|
||||
|
||||
@@ -70,7 +70,7 @@ export enum ImageProcessingType {
|
||||
SERVE = 'serve',
|
||||
}
|
||||
|
||||
export type TSettingsDownloadConversionHeader = SettingsDownloadConversionHeader & {
|
||||
export type TSettingsDownloadConversionKeyValueItem = SettingsDownloadConversionHeader & {
|
||||
/**
|
||||
* The conversion object does not have a stable key, which causes issues when editing the settings
|
||||
*/
|
||||
@@ -83,5 +83,6 @@ export type TSettingsDownloadConversion = Omit<SettingsDownloadConversion, 'head
|
||||
*/
|
||||
id: number;
|
||||
mode: ImageProcessingTargetMode;
|
||||
headers?: Maybe<TSettingsDownloadConversionHeader[]>;
|
||||
headers?: Maybe<TSettingsDownloadConversionKeyValueItem[]>;
|
||||
searchParams?: Maybe<TSettingsDownloadConversionKeyValueItem[]>;
|
||||
};
|
||||
|
||||
@@ -14,9 +14,9 @@ import IconButton from '@mui/material/IconButton';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { IMAGE_PROCESSING_INPUT_WIDTH } from '@/features/settings/Settings.constants.ts';
|
||||
import { TSettingsDownloadConversionHeader } from '@/features/settings/Settings.types';
|
||||
import { TSettingsDownloadConversionKeyValueItem } from '@/features/settings/Settings.types';
|
||||
|
||||
export const Header = ({
|
||||
export const KeyValueItem = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
@@ -24,8 +24,8 @@ export const Header = ({
|
||||
isDuplicate,
|
||||
}: {
|
||||
isDuplicate: boolean;
|
||||
onChange: (header: TSettingsDownloadConversionHeader | null) => void;
|
||||
} & TSettingsDownloadConversionHeader) => {
|
||||
onChange: (header: TSettingsDownloadConversionKeyValueItem | null) => void;
|
||||
} & TSettingsDownloadConversionKeyValueItem) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -12,43 +12,47 @@ import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import { Maybe } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { addStableIdToHeaders, isDuplicateHeader } from '@/features/settings/ImageProcessing.utils.ts';
|
||||
import { Header } from '@/features/settings/components/images/Header.tsx';
|
||||
import { TSettingsDownloadConversionHeader } from '@/features/settings/Settings.types.ts';
|
||||
import { addStableIdToKeyValueItems, isDuplicateKeyValueItem } from '@/features/settings/ImageProcessing.utils.ts';
|
||||
import { KeyValueItem } from '@/features/settings/components/images/KeyValueItem.tsx';
|
||||
import { TSettingsDownloadConversionKeyValueItem } from '@/features/settings/Settings.types.ts';
|
||||
|
||||
export const Headers = ({
|
||||
export const KeyValueItems = ({
|
||||
title,
|
||||
open,
|
||||
headers,
|
||||
items,
|
||||
onChange,
|
||||
}: {
|
||||
title: string;
|
||||
open: boolean;
|
||||
headers: Maybe<TSettingsDownloadConversionHeader[] | undefined>;
|
||||
onChange: (headers: Maybe<TSettingsDownloadConversionHeader[]>) => void;
|
||||
items: Maybe<TSettingsDownloadConversionKeyValueItem[] | undefined>;
|
||||
onChange: (items: Maybe<TSettingsDownloadConversionKeyValueItem[]>) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Collapse in={open}>
|
||||
<Stack sx={{ justifyContent: 'start', gap: 2, pt: 2 }}>
|
||||
<Typography>{t('download.settings.conversion.headers.title')}</Typography>
|
||||
{headers?.map((header, index) => (
|
||||
<Header
|
||||
<Typography>{title}</Typography>
|
||||
{items?.map((header, index) => (
|
||||
<KeyValueItem
|
||||
key={header.id}
|
||||
{...header}
|
||||
isDuplicate={isDuplicateHeader(header.name, index, headers)}
|
||||
isDuplicate={isDuplicateKeyValueItem(header.name, index, items)}
|
||||
onChange={(updatedHeader) => {
|
||||
const isDeletion = updatedHeader == null;
|
||||
if (isDeletion) {
|
||||
onChange(headers?.toSpliced(index, 1));
|
||||
onChange(items?.toSpliced(index, 1));
|
||||
return;
|
||||
}
|
||||
|
||||
onChange((headers ?? []).toSpliced(index, 1, updatedHeader));
|
||||
onChange((items ?? []).toSpliced(index, 1, updatedHeader));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => onChange([...(headers ?? []), ...addStableIdToHeaders([{ name: '', value: '' }])])}
|
||||
onClick={() =>
|
||||
onChange([...(items ?? []), ...addStableIdToKeyValueItems([{ name: '', value: '' }])])
|
||||
}
|
||||
variant="contained"
|
||||
sx={{ width: 'fit-content' }}
|
||||
>
|
||||
@@ -20,7 +20,7 @@ import InputLabel from '@mui/material/InputLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import { d } from 'koration';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
import { Headers } from '@/features/settings/components/images/Headers.tsx';
|
||||
import { KeyValueItems } from '@/features/settings/components/images/KeyValueItems.tsx';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
import {
|
||||
@@ -36,15 +36,18 @@ import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.t
|
||||
import { MimeTypeTextField } from '@/features/settings/components/images/MimeTypeTextField.tsx';
|
||||
import { ImageProcessingTargetMode, TSettingsDownloadConversion } from '@/features/settings/Settings.types.ts';
|
||||
import {
|
||||
getUpdatedSearchParams,
|
||||
isDefaultMimeType,
|
||||
isUrlTargetMode,
|
||||
isValidCallTimeoutSetting,
|
||||
isValidCompressionLevel,
|
||||
isValidConnectTimeoutSetting,
|
||||
} from '@/features/settings/ImageProcessing.utils.ts';
|
||||
import { UrlUtil } from '@/lib/UrlUtil.ts';
|
||||
|
||||
export const Processing = ({
|
||||
conversion,
|
||||
conversion: { mode, mimeType, target, compressionLevel, headers, callTimeout, connectTimeout },
|
||||
conversion: { mode, mimeType, target, compressionLevel, headers, searchParams, callTimeout, connectTimeout },
|
||||
onChange,
|
||||
isDuplicate,
|
||||
}: {
|
||||
@@ -57,10 +60,12 @@ export const Processing = ({
|
||||
|
||||
const { ref: textFieldRef, height: textFieldHeight } = useElementSize();
|
||||
|
||||
const [areSearchParamsCollapsed, setAreSearchParamsCollapsed] = useState(true);
|
||||
const [areHeadersCollapsed, setAreHeadersCollapsed] = useState(true);
|
||||
|
||||
const isDisabledMode = mode === ImageProcessingTargetMode.DISABLED;
|
||||
const isImageMode = mode === ImageProcessingTargetMode.IMAGE;
|
||||
const isUrlMode = mode === ImageProcessingTargetMode.URL;
|
||||
|
||||
const isCallTimeoutValid = isValidCallTimeoutSetting(callTimeout);
|
||||
const isConnectTimeoutValid = isValidConnectTimeoutSetting(connectTimeout);
|
||||
@@ -122,14 +127,21 @@ export const Processing = ({
|
||||
labelId="image-conversion-target-mode-label"
|
||||
label={t('download.settings.conversion.target_modes.title')}
|
||||
value={mode}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
if (!isUrlMode) {
|
||||
setAreSearchParamsCollapsed(true);
|
||||
setAreHeadersCollapsed(true);
|
||||
}
|
||||
|
||||
onChange({
|
||||
...conversion,
|
||||
mode: e.target.value,
|
||||
target:
|
||||
e.target.value === ImageProcessingTargetMode.DISABLED ? TARGET_DISABLED : '',
|
||||
})
|
||||
}
|
||||
headers: !isUrlMode ? null : headers,
|
||||
searchParams: !isUrlMode ? null : searchParams,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{IMAGE_PROCESSING_TARGET_MODES_SELECT_VALUES.map(([selectValue, { text: selectText }]) => (
|
||||
<MenuItem key={selectValue} value={selectValue}>
|
||||
@@ -146,12 +158,13 @@ export const Processing = ({
|
||||
isDuplicate={false}
|
||||
label={t('download.settings.conversion.target')}
|
||||
value={target}
|
||||
onUpdate={(value) =>
|
||||
onUpdate={(value) => {
|
||||
onChange({
|
||||
...conversion,
|
||||
target: value,
|
||||
})
|
||||
}
|
||||
searchParams: isImageMode ? null : getUpdatedSearchParams(value, searchParams),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(() => {
|
||||
@@ -237,6 +250,16 @@ export const Processing = ({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!isUrlTargetMode(target)}
|
||||
onClick={() => setAreSearchParamsCollapsed(!areSearchParamsCollapsed)}
|
||||
variant={areSearchParamsCollapsed ? 'outlined' : 'contained'}
|
||||
sx={{ height: textFieldHeight }}
|
||||
>
|
||||
{t('download.settings.conversion.search_params.button', {
|
||||
count: searchParams?.length ?? 0,
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setAreHeadersCollapsed(!areHeadersCollapsed)}
|
||||
variant={areHeadersCollapsed ? 'outlined' : 'contained'}
|
||||
@@ -261,9 +284,25 @@ export const Processing = ({
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
</Stack>
|
||||
<Headers
|
||||
open={!areHeadersCollapsed}
|
||||
headers={headers}
|
||||
<KeyValueItems
|
||||
title={t('download.settings.conversion.search_params.title')}
|
||||
open={isUrlMode && !areSearchParamsCollapsed}
|
||||
items={searchParams}
|
||||
onChange={(params) => {
|
||||
const baseUrl = target?.split('?')[0] ?? '';
|
||||
const updatedParams = Object.fromEntries(params?.map(({ name, value }) => [name, value]) ?? []);
|
||||
|
||||
onChange({
|
||||
...conversion,
|
||||
target: UrlUtil.addParams(baseUrl, updatedParams),
|
||||
searchParams: params,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<KeyValueItems
|
||||
title={t('download.settings.conversion.headers.title')}
|
||||
open={isUrlMode && !areHeadersCollapsed}
|
||||
items={headers}
|
||||
onChange={(updatedHeaders) =>
|
||||
onChange({
|
||||
...conversion,
|
||||
|
||||
@@ -92,7 +92,7 @@ export const ImageProcessingSetting = ({ type }: { type: ImageProcessingType })
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack sx={{ p: 2, gap: 3 }}>
|
||||
<Stack sx={{ p: 2, gap: 5 }}>
|
||||
<Typography>{t('download.settings.conversion.description', { value: TARGET_DISABLED })}</Typography>
|
||||
<Stack sx={{ flexDirection: 'column', gap: 5 }}>
|
||||
{tmpConversions.map((conversion, index) => {
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
import { SearchParam } from '@/base/Base.types.ts';
|
||||
|
||||
export class UrlUtil {
|
||||
static asUrl(url: string): URL | null {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static createParams(params: Record<SearchParam | string, string | null | undefined>): URLSearchParams {
|
||||
const paramEntries = Object.entries(params).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
||||
|
||||
Reference in New Issue
Block a user