From 1c554b4bfdf95e98508da33ede589d86329846dd Mon Sep 17 00:00:00 2001 From: Sebastien Ahkrin <30870051+Sebastien-Ahkrin@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:30:15 +0200 Subject: [PATCH 1/2] refactor: use tanstack & zod on `Save As` Refs: https://github.com/cheminfo/nmrium/issues/4162 --- src/component/modal/SaveAsModal.tsx | 171 +++++++++++++++------------- 1 file changed, 89 insertions(+), 82 deletions(-) diff --git a/src/component/modal/SaveAsModal.tsx b/src/component/modal/SaveAsModal.tsx index 7f8ab8bc8..585108727 100644 --- a/src/component/modal/SaveAsModal.tsx +++ b/src/component/modal/SaveAsModal.tsx @@ -1,52 +1,48 @@ -import { Checkbox, DialogFooter, Radio, RadioGroup } from '@blueprintjs/core'; +import { DialogBody, DialogFooter } from '@blueprintjs/core'; +import { revalidateLogic } from '@tanstack/react-form'; import { useMemo } from 'react'; -import { Controller, useForm } from 'react-hook-form'; +import { Button, Form, useForm } from 'react-science/ui'; +import { z } from 'zod/v4'; import { DataExportOptions } from '../../data/SpectraManager.js'; import { useChartData } from '../context/ChartContext.js'; -import ActionButtons from '../elements/ActionButtons.js'; -import { Input2Controller } from '../elements/Input2Controller.js'; -import type { LabelStyle } from '../elements/Label.js'; -import Label from '../elements/Label.js'; import { StandardDialog } from '../elements/StandardDialog.tsx'; -import { StyledDialogBody } from '../elements/StyledDialogBody.js'; -import type { SaveOptions } from '../hooks/useExport.js'; import { useExport } from '../hooks/useExport.js'; -const INITIAL_VALUE: SaveOptions = { +const formSchema = z.object({ + name: z.string().min(1, { error: 'Name is required' }), + include: z.object({ + dataType: z.enum([ + 'NO_DATA', + 'SELF_CONTAINED', + 'SELF_CONTAINED_EXTERNAL_DATASOURCE', + ]), + view: z.boolean(), + settings: z.boolean(), + }), +}); + +const INITIAL_VALUE: z.input = { name: '', include: { - dataType: DataExportOptions.SELF_CONTAINED, + dataType: 'SELF_CONTAINED', view: false, settings: false, }, }; -const labelStyle: LabelStyle = { - label: { - flex: 4, - color: '#232323', - }, - wrapper: { - flex: 8, - display: 'flex', - justifyContent: 'flex-start', - }, - container: { padding: '5px 0' }, -}; - interface InnerSaveAsModalProps { onCloseDialog: () => void; } + interface SaveAsModalProps extends InnerSaveAsModalProps { isOpen: boolean; } -function SaveAsModal(props: SaveAsModalProps) { +export default function SaveAsModal(props: SaveAsModalProps) { const { onCloseDialog, isOpen } = props; if (!isOpen) return; - return ; } @@ -54,21 +50,35 @@ function InnerSaveAsModal(props: InnerSaveAsModalProps) { const { onCloseDialog } = props; const { data, aggregator } = useChartData(); const { saveHandler } = useExport(); - const fileName = data[0]?.info?.name; - function submitHandler(values: SaveOptions) { - saveHandler(values); - onCloseDialog?.(); - } + const form = useForm({ + defaultValues: { ...INITIAL_VALUE, name: data[0]?.info?.name }, + validators: { onDynamic: formSchema }, + validationLogic: revalidateLogic({ modeAfterSubmission: 'change' }), + onSubmit: ({ value }) => { + const parsedValue = formSchema.parse(value); + saveHandler(parsedValue); - const { handleSubmit, control, register } = useForm({ - defaultValues: { ...INITIAL_VALUE, name: fileName }, + onCloseDialog(); + }, }); const containsLinkedFiles = useMemo(() => { return aggregator.sources.some((s) => !s.baseURL?.startsWith('ium:')); }, [aggregator]); + const options = useMemo(() => { + return [ + { value: DataExportOptions.SELF_CONTAINED, label: 'Embed data' }, + { + value: DataExportOptions.SELF_CONTAINED_EXTERNAL_DATASOURCE, + label: 'Link data', + disabled: !containsLinkedFiles, + }, + { value: DataExportOptions.NO_DATA, label: 'None' }, + ]; + }, [containsLinkedFiles]); + return ( - - - - - - - - { - void handleSubmit(submitHandler)(); + +
{ + event.preventDefault(); + void form.handleSubmit(); }} - doneLabel="Save" - onCancel={() => onCloseDialog?.()} - /> - + > + + + + {(field) => } + + + + + {(field) => } + + + + {(field) => } + + + + {(field) => ( + + )} + + + + + + + Save + + } + /> + +
); } - -export default SaveAsModal; From ca330d2d73f4b8439c0a2048a728277135ea92f6 Mon Sep 17 00:00:00 2001 From: Sebastien Ahkrin <30870051+Sebastien-Ahkrin@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:11:39 +0200 Subject: [PATCH 2/2] refactor: add autoFocus --- src/component/modal/SaveAsModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component/modal/SaveAsModal.tsx b/src/component/modal/SaveAsModal.tsx index 585108727..77d07b550 100644 --- a/src/component/modal/SaveAsModal.tsx +++ b/src/component/modal/SaveAsModal.tsx @@ -98,7 +98,7 @@ function InnerSaveAsModal(props: InnerSaveAsModalProps) { - {(field) => } + {(field) => }