From f9973e0ef384c302b35b72aa2031d4eea037e3c8 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 28 Dec 2025 02:07:52 +0100 Subject: [PATCH] Improve image processing settings url target adding search parameters --- public/locales/en.json | 4 ++ .../settings/ImageProcessing.utils.ts | 61 ++++++++++++++----- src/features/settings/Settings.types.ts | 5 +- .../images/{Header.tsx => KeyValueItem.tsx} | 8 +-- .../images/{Headers.tsx => KeyValueItems.tsx} | 32 +++++----- .../settings/components/images/Processing.tsx | 61 +++++++++++++++---- .../screens/ImageProcessingSetting.tsx | 2 +- src/lib/UrlUtil.ts | 8 +++ 8 files changed, 135 insertions(+), 46 deletions(-) rename src/features/settings/components/images/{Header.tsx => KeyValueItem.tsx} (91%) rename src/features/settings/components/images/{Headers.tsx => KeyValueItems.tsx} (56%) diff --git a/public/locales/en.json b/public/locales/en.json index b30d80e0..9c3c14ec 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -225,6 +225,10 @@ "value": "Value" }, "mime_type": "MIME-Type", + "search_params": { + "button": "Search parameters ({{count}})", + "title": "Search parameters" + }, "target": "MIME-Type target", "target_modes": { "image": { diff --git a/src/features/settings/ImageProcessing.utils.ts b/src/features/settings/ImageProcessing.utils.ts index d2833e31..c9041afb 100644 --- a/src/features/settings/ImageProcessing.utils.ts +++ b/src/features/settings/ImageProcessing.utils.ts @@ -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[] => { + 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}`, diff --git a/src/features/settings/Settings.types.ts b/src/features/settings/Settings.types.ts index 07a7239b..336efb48 100644 --- a/src/features/settings/Settings.types.ts +++ b/src/features/settings/Settings.types.ts @@ -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; + headers?: Maybe; + searchParams?: Maybe; }; diff --git a/src/features/settings/components/images/Header.tsx b/src/features/settings/components/images/KeyValueItem.tsx similarity index 91% rename from src/features/settings/components/images/Header.tsx rename to src/features/settings/components/images/KeyValueItem.tsx index aef49e2d..f99423e4 100644 --- a/src/features/settings/components/images/Header.tsx +++ b/src/features/settings/components/images/KeyValueItem.tsx @@ -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(); diff --git a/src/features/settings/components/images/Headers.tsx b/src/features/settings/components/images/KeyValueItems.tsx similarity index 56% rename from src/features/settings/components/images/Headers.tsx rename to src/features/settings/components/images/KeyValueItems.tsx index 0e3ee9ad..91dd65ac 100644 --- a/src/features/settings/components/images/Headers.tsx +++ b/src/features/settings/components/images/KeyValueItems.tsx @@ -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; - onChange: (headers: Maybe) => void; + items: Maybe; + onChange: (items: Maybe) => void; }) => { const { t } = useTranslation(); return ( - {t('download.settings.conversion.headers.title')} - {headers?.map((header, index) => ( -
{title} + {items?.map((header, index) => ( + { 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)); }} /> ))}