From 4941d77e988b893a3b1650acf872ef29892270a8 Mon Sep 17 00:00:00 2001 From: Candid Dauth Date: Thu, 28 Mar 2024 12:50:45 +0100 Subject: [PATCH] Validate unique dropdown options --- .../src/types/types-fields.test.ts | 50 +++++++++++++++++++ types/src/type.ts | 28 +++++++++-- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/integration-tests/src/types/types-fields.test.ts b/integration-tests/src/types/types-fields.test.ts index eb1069c7..90affea1 100644 --- a/integration-tests/src/types/types-fields.test.ts +++ b/integration-tests/src/types/types-fields.test.ts @@ -239,4 +239,54 @@ test("Update type with duplicate fields", async () => { }); }).rejects.toThrowError("Field names must be unique."); }); +}); + +test("Create type with duplicate dropdown values", async () => { + const client = await openClient(); + + await createTemporaryPad(client, { createDefaultTypes: false }, async () => { + await expect(async () => { + await client.addType({ + name: "Test type", + type: "marker", + fields: [ + { + name: "Dropdown", + type: "dropdown", + options: [ + { value: "Value 1" }, + { value: "Value 1" } + ] + } + ] + }); + }).rejects.toThrowError("Dropdown option values must be unique."); + }); +}); + +test("Update type with duplicate dropdown values", async () => { + const client = await openClient(); + + await createTemporaryPad(client, { createDefaultTypes: false }, async () => { + const type = await client.addType({ + name: "Test type", + type: "marker" + }); + + await expect(async () => { + await client.editType({ + id: type.id, + fields: [ + { + name: "Dropdown", + type: "dropdown", + options: [ + { value: "Value 1" }, + { value: "Value 1" } + ] + } + ] + }); + }).rejects.toThrowError("Dropdown option values must be unique."); + }); }); \ No newline at end of file diff --git a/types/src/type.ts b/types/src/type.ts index 1cc091f0..556d8bbd 100644 --- a/types/src/type.ts +++ b/types/src/type.ts @@ -22,6 +22,28 @@ export const fieldOptionValidator = cruValidator({ export type FieldOption = CRUType; export type FieldOptionUpdate = FieldOption; +const noDuplicateOptionValues = (options: Array>, ctx: z.RefinementCtx) => { + const values: Record = {}; + for (const option of options) { + values[option.value] = (values[option.value] ?? 0) + 1; + } + + for (let i = 0; i < options.length; i++) { + if (values[options[i].value] > 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Dropdown option values must be unique.", + path: [i, "value"] + }); + } + } +}; +export const fieldOptionsValidator = { + read: z.array(fieldOptionValidator.read).superRefine(noDuplicateOptionValues), + create: z.array(fieldOptionValidator.create).superRefine(noDuplicateOptionValues), + update: z.array(fieldOptionValidator.update).superRefine(noDuplicateOptionValues) +}; + export const fieldValidator = cruValidator({ name: z.string().trim().min(1), type: fieldTypeValidator, @@ -34,9 +56,9 @@ export const fieldValidator = cruValidator({ controlStroke: z.boolean().optional(), options: { - read: z.array(fieldOptionValidator.read).optional(), - create: z.array(fieldOptionValidator.create).optional(), - update: z.array(fieldOptionValidator.update).optional() + read: fieldOptionsValidator.read.optional(), + create: fieldOptionsValidator.create.optional(), + update: fieldOptionsValidator.update.optional() }, oldName: onlyUpdate(z.string().optional())