diff --git a/web/ui/react-app/src/modals/service-edit.tsx b/web/ui/react-app/src/modals/service-edit.tsx index 050b571d..6d55c103 100644 --- a/web/ui/react-app/src/modals/service-edit.tsx +++ b/web/ui/react-app/src/modals/service-edit.tsx @@ -141,6 +141,7 @@ const ServiceEditModalWithData: FC = ({ schemaData, schemaDataDefaults, mainDataDefaults, + typeDataDefaults, serviceID: sID, } = useSchemaContext(); @@ -180,6 +181,8 @@ const ServiceEditModalWithData: FC = ({ const dataPayload = mapServiceToAPIRequest( dataParsed.data, schemaDataDefaults, + mainDataDefaults, + typeDataDefaults, ); await mutateAsync({ data: dataPayload, serviceID: serviceID ?? null }) diff --git a/web/ui/react-app/src/utils/api/types/config-edit/notify/api/conversions.ts b/web/ui/react-app/src/utils/api/types/config-edit/notify/api/conversions.ts index 1c5a4370..a77c8664 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/notify/api/conversions.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/notify/api/conversions.ts @@ -4,9 +4,10 @@ import { type NotifiersSchemaOutgoing, type NotifySchemaOutgoing, type NotifySchemaValues, - notifySchemaMapOutgoing, + type NotifyTypeSchema, notifySchemaMapOutgoingWithDefaults, } from '@/utils/api/types/config-edit/notify/schemas'; +import { applyDefaultsRecursive } from '@/utils/api/types/config-edit/util'; import diffLists from '@/utils/diff-lists'; /** @@ -14,14 +15,21 @@ import diffLists from '@/utils/diff-lists'; * * @param data - The `NotifiersSchema` data to map. * @param defaultValue - The default values to compare against (and omit if all defaults used and unmodified). + * @param mainDefaults - The 'notify' globals. + * @param typeDefaults - Type-specific notify form data. * @returns A `NotifiersSchemaOutgoing` representing the `NotifiersSchema`. */ export const mapNotifiersSchemaToAPIPayload = ( data: NotifiersSchema, defaultValue?: NotifiersSchema, + mainDefaults?: Record, + typeDefaults?: NotifyTypeSchema, ): NotifiersSchemaOutgoing => { - const dataMinimised = data.map((item, idx) => { - const defaultsForItem = defaultValue?.[idx]; + const dataMinimised = data.map((item) => { + const defaultsForItem = applyDefaultsRecursive( + mainDefaults?.[item.name] ?? null, + typeDefaults?.[item.type], + ); const d = mapNotifySchemaToAPIPayload(item, defaultsForItem); return removeEmptyValues(d) as NotifySchemaOutgoing; }); @@ -58,13 +66,10 @@ export const mapNotifySchemaToAPIPayload = ( defaults?: NotifySchemaValues, ): NotifySchemaOutgoing => { const itemType = item.type; - if (defaults?.type === itemType) { - const schema = notifySchemaMapOutgoingWithDefaults(defaults); - return removeEmptyValues( - schema.parse(item) as NotifySchemaOutgoing, - ) as NotifySchemaOutgoing; - } + const defaultsTyped = defaults ?? ({ type: itemType } as NotifySchemaValues); + const schema = notifySchemaMapOutgoingWithDefaults(defaultsTyped); + return removeEmptyValues( - notifySchemaMapOutgoing[itemType].parse(item), + schema.parse(item) as NotifySchemaOutgoing, ) as NotifySchemaOutgoing; }; diff --git a/web/ui/react-app/src/utils/api/types/config-edit/notify/schemas.ts b/web/ui/react-app/src/utils/api/types/config-edit/notify/schemas.ts index 4555d8ec..b2c8ec5b 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/notify/schemas.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/notify/schemas.ts @@ -22,8 +22,10 @@ import { SMTPEncryptionEnum, } from '@/utils/api/types/config-edit/notify/types/smtp'; import { TelegramParseModeEnum } from '@/utils/api/types/config-edit/notify/types/telegram'; -import { preprocessStringFromHeaderArrayWithDefaults } from '@/utils/api/types/config-edit/shared/header/preprocess'; -import { headersSchemaDefaults } from '@/utils/api/types/config-edit/shared/header/schemas'; +import { + headersSchemaDefaults, + preprocessStringFromHeaderArrayWithDefaults, +} from '@/utils/api/types/config-edit/shared/header/preprocess'; import { nullString } from '@/utils/api/types/config-edit/shared/null-string'; /* Notify 'Options' Schema */ import { preprocessBooleanFromString, diff --git a/web/ui/react-app/src/utils/api/types/config-edit/notify/types/ntfy.ts b/web/ui/react-app/src/utils/api/types/config-edit/notify/types/ntfy.ts index 8e05c1d5..56cd3182 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/notify/types/ntfy.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/notify/types/ntfy.ts @@ -5,8 +5,10 @@ import { ntfyPriorityOptions, ntfySchemeOptions, } from '@/utils/api/types/config/notify/ntfy'; -import { flattenHeaderArray } from '@/utils/api/types/config-edit/shared/header/preprocess'; -import { headersSchemaDefaults } from '@/utils/api/types/config-edit/shared/header/schemas'; +import { + flattenHeaderArray, + headersSchemaDefaults, +} from '@/utils/api/types/config-edit/shared/header/preprocess'; import { makeDefaultsAwareListPreprocessor, preprocessArrayJSONFromString, diff --git a/web/ui/react-app/src/utils/api/types/config-edit/service/api/conversions.ts b/web/ui/react-app/src/utils/api/types/config-edit/service/api/conversions.ts index a36d116b..0173433d 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/service/api/conversions.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/service/api/conversions.ts @@ -2,6 +2,7 @@ import { removeEmptyValues } from '@/utils'; import { DEPLOYED_VERSION_LOOKUP_TYPE } from '@/utils/api/types/config/service/deployed-version'; import { mapCommandsSchemaToAPIPayload } from '@/utils/api/types/config-edit/command/api/conversions'; import { mapNotifiersSchemaToAPIPayload } from '@/utils/api/types/config-edit/notify/api/conversions'; +import type { buildServiceSchemaWithFallbacks } from '@/utils/api/types/config-edit/service/form/builder'; import type { ServiceSchema, ServiceSchemaDefault, @@ -14,10 +15,18 @@ import { mapWebHooksSchemaToAPIPayload } from '@/utils/api/types/config-edit/web * * @param data - The `ServiceSchema` data to map. * @param defaults - The default values to compare against (and omit where all defaults used and unmodified). + * @param mainDefaults - The notify/webhook globals. + * @param typeDefaults - Type-specific notify/webhook form data. */ export const mapServiceToAPIRequest = ( data: ServiceSchema, defaults: ServiceSchemaDefault | null, + mainDefaults: ReturnType< + typeof buildServiceSchemaWithFallbacks + >['mainDataDefaults'], + typeDefaults: ReturnType< + typeof buildServiceSchemaWithFallbacks + >['typeDataDefaults'], ): ServiceSchemaOutgoing => { const dv = data.deployed_version; let deployedVersion = null; @@ -34,7 +43,17 @@ export const mapServiceToAPIRequest = ( command: mapCommandsSchemaToAPIPayload(data.command, defaults?.command), deployed_version: deployedVersion, id_name_separator: null, - notify: mapNotifiersSchemaToAPIPayload(data.notify, defaults?.notify), - webhook: mapWebHooksSchemaToAPIPayload(data.webhook, defaults?.webhook), + notify: mapNotifiersSchemaToAPIPayload( + data.notify, + defaults?.notify, + mainDefaults?.notify, + typeDefaults?.notify, + ), + webhook: mapWebHooksSchemaToAPIPayload( + data.webhook, + defaults?.webhook, + mainDefaults?.webhook, + typeDefaults?.webhook, + ), }) as ServiceSchemaOutgoing; }; diff --git a/web/ui/react-app/src/utils/api/types/config-edit/service/types/deployed-version.ts b/web/ui/react-app/src/utils/api/types/config-edit/service/types/deployed-version.ts index 62e6733f..a095b282 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/service/types/deployed-version.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/service/types/deployed-version.ts @@ -7,7 +7,7 @@ import { type DeployedVersionLookupURLMethod, deployedVersionLookupTypeOptions, } from '@/utils/api/types/config/service/deployed-version'; -import { headersSchemaDefaults } from '@/utils/api/types/config-edit/shared/header/schemas'; +import { headersSchemaDefaults } from '@/utils/api/types/config-edit/shared/header/preprocess'; import { nullString } from '@/utils/api/types/config-edit/shared/null-string'; import { regexStringWithFallback } from '@/utils/api/types/config-edit/validators'; diff --git a/web/ui/react-app/src/utils/api/types/config-edit/shared/header/builder.ts b/web/ui/react-app/src/utils/api/types/config-edit/shared/header/builder.ts index c9c56118..46a63768 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/shared/header/builder.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/shared/header/builder.ts @@ -4,7 +4,7 @@ import { addZodIssuesToContext } from '@/utils/api/types/config-edit/shared/add- import { headersSchema, headersSchemaDefaults, -} from '@/utils/api/types/config-edit/shared/header/schemas'; +} from '@/utils/api/types/config-edit/shared/header/preprocess'; import { overrideSchemaDefault } from '@/utils/api/types/config-edit/shared/override-schema-default'; import { safeParse } from '@/utils/api/types/config-edit/shared/safeparse'; import type { BuilderResponse } from '@/utils/api/types/config-edit/shared/types'; diff --git a/web/ui/react-app/src/utils/api/types/config-edit/shared/header/preprocess.ts b/web/ui/react-app/src/utils/api/types/config-edit/shared/header/preprocess.ts index 7c4b810a..2bb5b4c3 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/shared/header/preprocess.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/shared/header/preprocess.ts @@ -6,6 +6,10 @@ import { z, } from 'zod'; import type { CustomHeaders } from '@/utils/api/types/config/shared'; +import { + headerSchema, + headerSchemaDefaults, +} from '@/utils/api/types/config-edit/shared/header/schemas'; import { makeDefaultsAwareListPreprocessor } from '@/utils/api/types/config-edit/shared/preprocess'; /** @@ -96,3 +100,15 @@ export const preprocessToHeadersArray = ( return arg; }, z.array(schema)) .default([]); + +/* Array of Header objects (min length 1 on key and value) */ +export const headersSchema = preprocessToHeadersArray(headerSchema); +/* Array of Header objects (no validation) */ +export const headersSchemaDefaults = + preprocessToHeadersArray(headerSchemaDefaults); + +export const preprocessHeaderArrayWithDefaults = (defaults?: CustomHeaders) => + makeDefaultsAwareListPreprocessor(headersSchemaDefaults.nullable(), { + defaults: defaults, + matchingFields: ['key', 'value'], + }); diff --git a/web/ui/react-app/src/utils/api/types/config-edit/shared/header/schemas.ts b/web/ui/react-app/src/utils/api/types/config-edit/shared/header/schemas.ts index a0be198e..81ea8992 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/shared/header/schemas.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/shared/header/schemas.ts @@ -1,5 +1,4 @@ import { z } from 'zod'; -import { preprocessToHeadersArray } from '@/utils/api/types/config-edit/shared/header/preprocess'; import { REQUIRED_MESSAGE } from '@/utils/api/types/config-edit/validators'; /* Header object (min length 1 on key and value) */ @@ -14,9 +13,3 @@ export const headerSchemaDefaults = z.object({ old_index: z.number().nullable().default(null), value: z.string(), }); - -/* Array of Header objects (min length 1 on key and value) */ -export const headersSchema = preprocessToHeadersArray(headerSchema); -/* Array of Header objects (no validation) */ -export const headersSchemaDefaults = - preprocessToHeadersArray(headerSchemaDefaults); diff --git a/web/ui/react-app/src/utils/api/types/config-edit/shared/preprocess.ts b/web/ui/react-app/src/utils/api/types/config-edit/shared/preprocess.ts index d02f5bcb..b0cdbd64 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/shared/preprocess.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/shared/preprocess.ts @@ -1,4 +1,5 @@ import { type ZodEnum, z } from 'zod'; +import { nullString } from '@/utils/api/types/config-edit/shared/null-string'; import { isUsingDefaults } from '@/utils/api/types/config-edit/validators'; /** @@ -84,7 +85,7 @@ export const preprocessStringFromNumber = z.preprocess((arg) => { */ export const preprocessStringFromZodEnum = (enumSchema: ZodEnum) => z.preprocess((val: unknown) => { - if (val == null) return undefined; + if (val == null || val === nullString) return undefined; const enumValues = enumSchema.options; if (enumValues.includes(val as string)) return val; diff --git a/web/ui/react-app/src/utils/api/types/config-edit/webhook/api/conversions.ts b/web/ui/react-app/src/utils/api/types/config-edit/webhook/api/conversions.ts index 8dcc4ed0..aac4440a 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/webhook/api/conversions.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/webhook/api/conversions.ts @@ -1,10 +1,11 @@ import { removeEmptyValues } from '@/utils'; +import { applyDefaultsRecursive } from '@/utils/api/types/config-edit/util'; import { type WebHookSchema, type WebHookSchemaOutgoing, type WebHooksSchema, type WebHooksSchemaOutgoing, - webhookSchemaOutgoing, + webhookSchemaMapOutgoingWithDefaults, } from '@/utils/api/types/config-edit/webhook/schemas'; import diffLists from '@/utils/diff-lists'; @@ -12,15 +13,23 @@ import diffLists from '@/utils/diff-lists'; * Maps the webhook form schema to an API payload. * * @param data - The form schema. - * @param defaultValue - Default values for the form schema. + * @param defaultValue - The default values to compare against (and omit if all defaults used and unmodified). + * @param mainDefaults - The 'webhook' globals. + * @param typeDefaults - Type-specific webhook form data. * @returns The API payload with matching defaults removed. */ export const mapWebHooksSchemaToAPIPayload = ( data: WebHooksSchema, defaultValue?: WebHooksSchema, + mainDefaults?: Record, + typeDefaults?: WebHookSchema, ): WebHooksSchemaOutgoing => { const dataMinimised = data.map((item) => { - const d = mapWebHookSchemaToAPIPayload(item); + const defaultsForItem = applyDefaultsRecursive( + mainDefaults?.[item.name] ?? null, + typeDefaults, + ); + const d = mapWebHookSchemaToAPIPayload(item, defaultsForItem); return removeEmptyValues(d) as WebHookSchemaOutgoing; }); @@ -48,7 +57,16 @@ export const mapWebHooksSchemaToAPIPayload = ( * Maps the webhook form schema to an API payload. * * @param item - The form schema. + * @param defaults - The default values to compare against (and omit where used). + * @returns A `WebHookSchemaOutgoing` representing the `WebHookSchema`. */ export const mapWebHookSchemaToAPIPayload = ( item: WebHookSchema, -): WebHookSchemaOutgoing => webhookSchemaOutgoing.parse(item); + defaults?: WebHookSchema, +): WebHookSchemaOutgoing => { + const schema = webhookSchemaMapOutgoingWithDefaults(defaults); + + return removeEmptyValues( + schema.parse(item) as WebHookSchemaOutgoing, + ) as WebHookSchemaOutgoing; +}; diff --git a/web/ui/react-app/src/utils/api/types/config-edit/webhook/schemas.ts b/web/ui/react-app/src/utils/api/types/config-edit/webhook/schemas.ts index b5421106..f523c41b 100644 --- a/web/ui/react-app/src/utils/api/types/config-edit/webhook/schemas.ts +++ b/web/ui/react-app/src/utils/api/types/config-edit/webhook/schemas.ts @@ -1,7 +1,10 @@ import { z } from 'zod'; import { toZodEnumTuple } from '@/types/util'; import { WEBHOOK_TYPE } from '@/utils/api/types/config/webhook'; -import { headersSchemaDefaults } from '@/utils/api/types/config-edit/shared/header/schemas'; +import { + headersSchemaDefaults, + preprocessHeaderArrayWithDefaults, +} from '@/utils/api/types/config-edit/shared/header/preprocess'; import { preprocessNumberFromString, preprocessStringFromNumber, @@ -49,3 +52,19 @@ export const webhooksSchemaOutgoing = z .nullable() .default(null); export type WebHooksSchemaOutgoing = z.infer; + +/** + * Outgoing schemas that are defaults-aware for list-like fields. + * + * @returns a per-type schema with the provided defaults where + * preprocessors can null fields that match the defaults. + */ +export const webhookSchemaMapOutgoingWithDefaults = ( + defaults?: WebHookSchema, +) => { + return webhookSchema.extend({ + custom_headers: preprocessHeaderArrayWithDefaults(defaults?.custom_headers), + desired_status_code: preprocessNumberFromString, + max_tries: preprocessNumberFromString, + }); +};