From 1748c7a661c5cc9353b426300ddca073b0bd6de9 Mon Sep 17 00:00:00 2001 From: Bruno Carlos Date: Fri, 4 Sep 2026 16:10:38 -0300 Subject: [PATCH] refactor(spu): update common schemas Add acquireTimeSchema and sampleTagSchema to common schemas for uniformity. `sampleTagSchema` is set up to support being used in unix filenames. --- .changeset/fresh-dots-study.md | 5 ++++ .../app/_components/plans/schemas/common.ts | 20 +++++++++++++ .../setup1-complete-acquisition-form.tsx | 12 ++++---- .../plans/setup1-single-acquisition.tsx | 12 ++++---- .../_components/plans/setup2-acquisition.tsx | 10 +++++-- .../setup2-complete-acquisition-form.tsx | 28 ++++--------------- .../setup2-find-sample-horizontal-scan.tsx | 4 +-- .../setup2-find-sample-vertical-scan.tsx | 4 +-- .../plans/setup4-xps-acquisition.tsx | 10 +++++-- .../app/_components/store/setup1/constants.ts | 11 -------- .../store/setup1/load-sample-form.tsx | 4 +-- .../store/setup1/register-sample-form.tsx | 3 +- .../store/setup2/use-sample-store.tsx | 3 +- 13 files changed, 69 insertions(+), 57 deletions(-) create mode 100644 .changeset/fresh-dots-study.md diff --git a/.changeset/fresh-dots-study.md b/.changeset/fresh-dots-study.md new file mode 100644 index 00000000..3944fcb1 --- /dev/null +++ b/.changeset/fresh-dots-study.md @@ -0,0 +1,5 @@ +--- +"@sophys-web/spu-ui": patch +--- + +Update common schemas for better handling sampleTag fields (adding support for being used in unix filenames) and acquireTime fields (removing minimum value restriction of 0.1 seconds supporting positive numbers). diff --git a/apps/spu-ui/src/app/_components/plans/schemas/common.ts b/apps/spu-ui/src/app/_components/plans/schemas/common.ts index 239dbe3d..a7def02d 100644 --- a/apps/spu-ui/src/app/_components/plans/schemas/common.ts +++ b/apps/spu-ui/src/app/_components/plans/schemas/common.ts @@ -17,3 +17,23 @@ export const proposalSchema = z regexPatterns.proposal, "Proposal must be exactly 8 digits long and contain only numbers", ); + +export const acquireTimeSchema = z.coerce.number().positive(); + +/** + * sampleTag is a common field used in acquisition related forms and + * can be used to generate filenames for acquired data. + */ +export const sampleTagSchema = z + .string() + .min(1, "Sample tag must not be empty") + .max(100, "Sample tag must not exceed 100 characters") + .regex( + regexPatterns.noEmptySpaces, + "Sample tag must not contain empty spaces", + ) + .regex(regexPatterns.noDots, "Sample tag must not contain dots") + .regex( + regexPatterns.invalidChars, + 'Sample tag must not contain any of the following characters: \\ / : * ? " < > |', + ); diff --git a/apps/spu-ui/src/app/_components/plans/setup1-complete-acquisition-form.tsx b/apps/spu-ui/src/app/_components/plans/setup1-complete-acquisition-form.tsx index 36e546c7..cb8ab599 100644 --- a/apps/spu-ui/src/app/_components/plans/setup1-complete-acquisition-form.tsx +++ b/apps/spu-ui/src/app/_components/plans/setup1-complete-acquisition-form.tsx @@ -37,12 +37,16 @@ import { trayRows, } from "~/app/_components/store/setup1/constants"; import { useSampleStore } from "../store/setup1/use-sample-store"; -import { proposalSchema } from "./schemas/common"; +import { + acquireTimeSchema, + proposalSchema, + sampleTagSchema, +} from "./schemas/common"; export const planName = "setup1_complete_standard_acquisition"; export const planSchema = z.object({ - acquireTime: z.coerce.number().positive(), + acquireTime: acquireTimeSchema, numExposures: z.coerce.number().int().positive(), row: z .string() @@ -70,9 +74,7 @@ export const planSchema = z.object({ ), volume: z.coerce.number().positive(), proposal: proposalSchema, - sampleTag: z - .string() - .min(1, "Sample name or other form of identification is required"), + sampleTag: sampleTagSchema, sampleType: z .string() .transform((val) => val.trimStart().trimEnd()) diff --git a/apps/spu-ui/src/app/_components/plans/setup1-single-acquisition.tsx b/apps/spu-ui/src/app/_components/plans/setup1-single-acquisition.tsx index c49c843b..b53168ac 100644 --- a/apps/spu-ui/src/app/_components/plans/setup1-single-acquisition.tsx +++ b/apps/spu-ui/src/app/_components/plans/setup1-single-acquisition.tsx @@ -36,20 +36,22 @@ import { import { Switch } from "@sophys-web/ui/switch"; import type { LastSampleParams } from "~/app/_hooks/use-capillary-state"; import { sampleTypeOptions } from "~/app/_components/store/setup1/constants"; -import { proposalSchema } from "./schemas/common"; +import { + acquireTimeSchema, + proposalSchema, + sampleTagSchema, +} from "./schemas/common"; const planName = "setup1_acquisition"; const planSchema = z.object({ - acquireTime: z.coerce - .number() - .min(0.1, "Acquire time (in seconds) must be at least 0.1"), + acquireTime: acquireTimeSchema, numExposures: z.coerce .number() .min(1, "Number of exposures must be at least 1"), motionSpeed: z.coerce.number().positive().optional(), proposal: proposalSchema, sampleType: z.enum(sampleTypeOptions), - sampleTag: z.string(), + sampleTag: sampleTagSchema, temperature: z.coerce.number().positive().optional(), setTemperature: z.boolean().optional(), usePimega: z.boolean().optional(), diff --git a/apps/spu-ui/src/app/_components/plans/setup2-acquisition.tsx b/apps/spu-ui/src/app/_components/plans/setup2-acquisition.tsx index 54e5c72f..2d9040e7 100644 --- a/apps/spu-ui/src/app/_components/plans/setup2-acquisition.tsx +++ b/apps/spu-ui/src/app/_components/plans/setup2-acquisition.tsx @@ -31,15 +31,19 @@ import { import { Label } from "@sophys-web/ui/label"; import { Switch } from "@sophys-web/ui/switch"; import { InfoTooltip } from "@sophys-web/widgets/form-components/info-tooltip"; -import { proposalSchema } from "./schemas/common"; +import { + acquireTimeSchema, + proposalSchema, + sampleTagSchema, +} from "./schemas/common"; export const name = "setup2_acquisition"; export const schema = z.object({ - acquireTime: z.coerce.number().positive(), + acquireTime: acquireTimeSchema, numExposures: z.coerce.number().int().positive(), proposal: proposalSchema, - sampleTag: z.string().min(1), + sampleTag: sampleTagSchema, usePimega: z.boolean().optional().default(true), usePicolo: z.boolean().optional().default(true), detReadout: z.coerce.number().positive().optional().default(0.001), diff --git a/apps/spu-ui/src/app/_components/plans/setup2-complete-acquisition-form.tsx b/apps/spu-ui/src/app/_components/plans/setup2-complete-acquisition-form.tsx index f7cd47f4..8e485cc2 100644 --- a/apps/spu-ui/src/app/_components/plans/setup2-complete-acquisition-form.tsx +++ b/apps/spu-ui/src/app/_components/plans/setup2-complete-acquisition-form.tsx @@ -30,33 +30,17 @@ import { cardIndexOptions, cardRows, } from "../store/setup2/constants"; -import { proposalSchema, regexPatterns } from "./schemas/common"; - -const sampleTagSchema = z - .string() - .min(1) - .max( - 100, - "Sample name or other form of identification must be at most 100 characters long", - ) - .regex(regexPatterns.invalidChars, { - message: - "Sample tag can only contain letters, numbers, dashes, and underscores", - }) - .regex(regexPatterns.noEmptySpaces, { - message: "Sample tag must not contain empty spaces", - }) - .regex(regexPatterns.noDots, { - message: "Sample tag must not contain dots", - }); +import { + acquireTimeSchema, + proposalSchema, + sampleTagSchema, +} from "./schemas/common"; const schema = z .object({ proposal: proposalSchema, sampleTag: sampleTagSchema, - acquireTime: z.coerce - .number() - .min(0.1, "Acquire time (in seconds) must be at least 0.1"), + acquireTime: acquireTimeSchema, numExposures: z.coerce .number() .int() diff --git a/apps/spu-ui/src/app/_components/plans/setup2-find-sample-horizontal-scan.tsx b/apps/spu-ui/src/app/_components/plans/setup2-find-sample-horizontal-scan.tsx index 13d1cec4..ee2c0f82 100644 --- a/apps/spu-ui/src/app/_components/plans/setup2-find-sample-horizontal-scan.tsx +++ b/apps/spu-ui/src/app/_components/plans/setup2-find-sample-horizontal-scan.tsx @@ -18,13 +18,13 @@ import { InputGroupInput, } from "@sophys-web/ui/input-group"; import { InfoTooltip } from "@sophys-web/widgets/form-components/info-tooltip"; -import { proposalSchema } from "./schemas/common"; +import { proposalSchema, sampleTagSchema } from "./schemas/common"; export const name = "setup2_find_sample_horizontal_scan"; export const schema = z.object({ proposal: proposalSchema, - sampleTag: z.string().min(1), + sampleTag: sampleTagSchema, minX: z.coerce.number(), maxX: z.coerce.number(), xStepSize: z.coerce.number().positive().default(1), diff --git a/apps/spu-ui/src/app/_components/plans/setup2-find-sample-vertical-scan.tsx b/apps/spu-ui/src/app/_components/plans/setup2-find-sample-vertical-scan.tsx index 411ae14a..f676566f 100644 --- a/apps/spu-ui/src/app/_components/plans/setup2-find-sample-vertical-scan.tsx +++ b/apps/spu-ui/src/app/_components/plans/setup2-find-sample-vertical-scan.tsx @@ -18,13 +18,13 @@ import { InputGroupInput, } from "@sophys-web/ui/input-group"; import { InfoTooltip } from "@sophys-web/widgets/form-components/info-tooltip"; -import { proposalSchema } from "./schemas/common"; +import { proposalSchema, sampleTagSchema } from "./schemas/common"; export const name = "setup2_find_sample_vertical_scan"; export const schema = z.object({ proposal: proposalSchema, - sampleTag: z.string().min(1), + sampleTag: sampleTagSchema, minY: z.coerce.number(), maxY: z.coerce.number(), yStepSize: z.coerce.number().positive().default(1), diff --git a/apps/spu-ui/src/app/_components/plans/setup4-xps-acquisition.tsx b/apps/spu-ui/src/app/_components/plans/setup4-xps-acquisition.tsx index 67f0b9fb..6dc7bc83 100644 --- a/apps/spu-ui/src/app/_components/plans/setup4-xps-acquisition.tsx +++ b/apps/spu-ui/src/app/_components/plans/setup4-xps-acquisition.tsx @@ -28,16 +28,20 @@ import { InputGroupInput, } from "@sophys-web/ui/input-group"; import { InfoTooltip } from "@sophys-web/widgets/form-components/info-tooltip"; -import { proposalSchema } from "./schemas/common"; +import { + acquireTimeSchema, + proposalSchema, + sampleTagSchema, +} from "./schemas/common"; export const name = "setup4_xpcs_acquisition"; export const schema = z.object({ - acquirePeriod: z.coerce.number().positive(), + acquirePeriod: acquireTimeSchema, numExposures: z.coerce.number().int().positive(), detReadout: z.coerce.number().min(0.001).positive().optional(), proposal: proposalSchema, - sampleTag: z.string().min(1), + sampleTag: sampleTagSchema, }); export function Setup4XpcsAquisitionForm({ diff --git a/apps/spu-ui/src/app/_components/store/setup1/constants.ts b/apps/spu-ui/src/app/_components/store/setup1/constants.ts index 21b0730c..9832a849 100644 --- a/apps/spu-ui/src/app/_components/store/setup1/constants.ts +++ b/apps/spu-ui/src/app/_components/store/setup1/constants.ts @@ -14,17 +14,6 @@ export const trayColumns = [ ] as const; export const trayRows = ["A", "B", "C", "D", "E", "F", "G", "H"] as const; export const trayOptions = ["Tray1", "Tray2"] as const; -export const acquireTimeOptions = [ - "200", - "100", - "50", - "25", - "12.5", - "6.25", - "3.125", - "1.5625", - "0.5", -] as const; export const sampleTypeOptions = ["buffer", "sample"] as const; export const cleaningOptions = ["light", "normal", "heavy", "custom"] as const; export const standardCleaningOptions = ["light", "normal", "heavy"] as const; diff --git a/apps/spu-ui/src/app/_components/store/setup1/load-sample-form.tsx b/apps/spu-ui/src/app/_components/store/setup1/load-sample-form.tsx index b052bdd2..c2a80060 100644 --- a/apps/spu-ui/src/app/_components/store/setup1/load-sample-form.tsx +++ b/apps/spu-ui/src/app/_components/store/setup1/load-sample-form.tsx @@ -21,7 +21,7 @@ import { trayOptions, trayRows, } from "../../store/setup1/constants"; -import { proposalSchema } from "./../../plans/schemas/common"; +import { proposalSchema, sampleTagSchema } from "./../../plans/schemas/common"; import { useSampleStore } from "./use-sample-store"; export const planName = "setup1_load_procedure"; @@ -35,7 +35,7 @@ export const planSchema = z.object({ .positive() .max(100, "Volume must be between 0 and 100 µL"), proposal: proposalSchema, - sampleTag: z.string(), + sampleTag: sampleTagSchema, sampleType: z.enum(sampleTypeOptions), expUvTime: z.coerce.number().nonnegative().optional(), measureUvNumber: z.coerce.number().int().nonnegative().optional(), diff --git a/apps/spu-ui/src/app/_components/store/setup1/register-sample-form.tsx b/apps/spu-ui/src/app/_components/store/setup1/register-sample-form.tsx index 624881d7..19488529 100644 --- a/apps/spu-ui/src/app/_components/store/setup1/register-sample-form.tsx +++ b/apps/spu-ui/src/app/_components/store/setup1/register-sample-form.tsx @@ -22,12 +22,13 @@ import { } from "@sophys-web/ui/select"; import type { trayColumns, trayOptions, trayRows } from "./constants"; import type { Sample } from "./use-sample-store"; +import { sampleTagSchema } from "../../plans/schemas/common"; import { initialVolume } from "./constants"; import { sampleIdFromPosition, useSampleStore } from "./use-sample-store"; const sampleSchema = z.object({ sampleType: z.enum(["sample", "buffer"]), - sampleTag: z.string().min(2, { message: "Sample tag is required" }), + sampleTag: sampleTagSchema, volume: z.coerce.number().min(0), }); diff --git a/apps/spu-ui/src/app/_components/store/setup2/use-sample-store.tsx b/apps/spu-ui/src/app/_components/store/setup2/use-sample-store.tsx index be12a425..30cc3cf7 100644 --- a/apps/spu-ui/src/app/_components/store/setup2/use-sample-store.tsx +++ b/apps/spu-ui/src/app/_components/store/setup2/use-sample-store.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import { z } from "zod"; import { useStore } from "@sophys-web/api-client/hooks"; import { usePvData } from "@sophys-web/pvws-store"; +import { sampleTagSchema } from "../../plans/schemas/common"; import { cardCapillaryColumns, cardColumns, @@ -134,7 +135,7 @@ export const sampleIdEncoder = z */ export const sampleSchema = z.object({ id: z.string(), - sampleTag: z.string(), + sampleTag: sampleTagSchema, position: z .object({ x: z.coerce.number(),