From 0baecbe0654d9d1c7c0248974d6f45abef78054a Mon Sep 17 00:00:00 2001 From: Sebastien Ahkrin <30870051+Sebastien-Ahkrin@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:17:00 +0200 Subject: [PATCH 1/4] refactor: change useForm on `AlignSpectra` Closes: https://github.com/cheminfo/nmrium/issues/4192 --- .../multipleAnalysisPanel/AlignSpectra.tsx | 237 ++++++++++-------- 1 file changed, 139 insertions(+), 98 deletions(-) diff --git a/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx b/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx index 7584d2a739..78816b5168 100644 --- a/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx +++ b/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx @@ -1,48 +1,42 @@ import styled from '@emotion/styled'; -import { yupResolver } from '@hookform/resolvers/yup'; +import { revalidateLogic } from '@tanstack/react-form'; import type { Spectrum1D } from '@zakodium/nmrium-core'; import { xFindClosestIndex } from 'ml-spectra-processing'; import { useMemo } from 'react'; -import { useForm } from 'react-hook-form'; -import * as Yup from 'yup'; +import { AppForm, Button, coerceNumberInput, useForm } from 'react-science/ui'; +import { z } from 'zod'; import { REFERENCES } from '../../../data/constants/References.js'; import type { CalibrateOptions } from '../../../data/data1d/Spectrum1D/getReferenceShift.js'; import { useDispatch } from '../../context/DispatchContext.js'; import { useToaster } from '../../context/ToasterContext.js'; -import ActionButtons from '../../elements/ActionButtons.js'; -import type { LabelStyle } from '../../elements/Label.js'; -import Label from '../../elements/Label.js'; -import { NumberInput2Controller } from '../../elements/NumberInput2Controller.js'; -import { Select2 } from '../../elements/Select2.js'; import useSpectraByActiveNucleus from '../../hooks/useSpectraPerNucleus.js'; import { useEvent } from '../../utility/Events.js'; -const labelStyle: LabelStyle = { - label: { flex: 4, fontWeight: '500' }, - wrapper: { flex: 8, display: 'flex', alignItems: 'center' }, - container: { padding: '5px 0' }, -}; - const baseList = [{ key: 1, value: 'manual', label: 'Manual' }]; interface AlignSpectraProps { - nucleus: any; + nucleus: string; onClose: () => void; } -const DEFAULT_OPTIONS: CalibrateOptions = { - from: -1, - to: 1, - nbPeaks: 1, - targetX: 0, +type FormInput = z.input; +type FormOutput = z.output; + +const DEFAULT_OPTIONS: FormInput = { + from: '-1', + to: '1', + nbPeaks: '1', + targetX: '0', + options: 'manual', }; -const schemaValidation = Yup.object({ - from: Yup.number().required(), - to: Yup.number().required(), - nbPeaks: Yup.number().required(), - targetX: Yup.number().required(), +const validation = z.object({ + from: coerceNumberInput(), + to: coerceNumberInput(), + nbPeaks: coerceNumberInput(), + targetX: coerceNumberInput(), + options: z.string(), }); function checkSpectra(options: CalibrateOptions, spectra: Spectrum1D[]) { @@ -51,11 +45,14 @@ function checkSpectra(options: CalibrateOptions, spectra: Spectrum1D[]) { const { data: { x }, } = spectrum; + const min = x[0]; const max = x.at(-1) as number; + if (from < min || to > max) { throw new Error('Some spectra do not have data in the selected range'); } + if (Math.abs(xFindClosestIndex(x, from) - xFindClosestIndex(x, to)) < 10) { throw new Error( 'The selected range is too small to provide accurate results', @@ -66,18 +63,23 @@ function checkSpectra(options: CalibrateOptions, spectra: Spectrum1D[]) { function checkOptions(options: CalibrateOptions) { const returnedOptions = { ...options }; + if (options.from > options.to) { returnedOptions.to = options.from; returnedOptions.from = options.to; } + return returnedOptions; } -function getList(nucleus: any) { - if (!(REFERENCES as any)?.[nucleus]) { +function getList(nucleus: string) { + const references = REFERENCES as any; + + if (!references?.[nucleus]) { return []; } - const list = Object.entries((REFERENCES as any)[nucleus]).map((item) => ({ + + const list = Object.entries(references[nucleus]).map((item) => ({ value: item[0], label: item[0], })); @@ -93,8 +95,9 @@ const Container = styled.div` padding: 10px 0 5px 20px; .body { + flex: 1; overflow: auto; - padding: 10px 10px 25px 0; + padding: 10px 10px 25px 1px; } .header { @@ -102,38 +105,64 @@ const Container = styled.div` font-weight: bold; padding: 5px 0; } +`; - .footer { - display: flex; - padding-top: 5px; - } +const Footer = styled.div` + display: flex; + flex-direction: row; + gap: 5px; `; -function AlignSpectra({ onClose = () => null, nucleus }: AlignSpectraProps) { +function inputToFormInputSchema(input: FormOutput): FormInput { + return { + from: String(input.from), + to: String(input.to), + targetX: String(input.targetX), + nbPeaks: String(input.nbPeaks), + options: input.options, + }; +} + +export default function AlignSpectra(props: AlignSpectraProps) { + const { onClose = () => null, nucleus } = props; const spectra = useSpectraByActiveNucleus(); const dispatch = useDispatch(); const toaster = useToaster(); - const { handleSubmit, reset, control, getValues } = useForm( - { - defaultValues: DEFAULT_OPTIONS, - resolver: yupResolver(schemaValidation), - }, - ); - function submitHandler(inputOptions: any) { - const options = checkOptions(inputOptions); - reset(options); - try { - checkSpectra(options, spectra as Spectrum1D[]); + const optionList = useMemo(() => { + return getList(nucleus); + }, [nucleus]); - dispatch({ type: 'ALIGN_SPECTRA', payload: options }); - onClose(); - } catch (error: unknown) { - const message = (error as Error).message; + const form = useForm({ + defaultValues: { + ...DEFAULT_OPTIONS, + options: optionList[0].value, + }, + validationLogic: revalidateLogic({ mode: 'change' }), + onSubmit: ({ value, formApi }) => { + const parsed = validation.parse(value); + const options = checkOptions(parsed); + + formApi.reset( + inputToFormInputSchema({ + ...options, + options: optionList[0].value, + }), + ); - toaster.show({ intent: 'danger', message }); - } - } + try { + checkSpectra(options, spectra as Spectrum1D[]); + dispatch({ type: 'ALIGN_SPECTRA', payload: options }); + onClose(); + } catch (error: unknown) { + const message = (error as Error).message; + toaster.show({ intent: 'danger', message }); + } + }, + validators: { + onDynamic: validation, + }, + }); useEvent({ onBrushEnd: (options) => { @@ -141,62 +170,74 @@ function AlignSpectra({ onClose = () => null, nucleus }: AlignSpectraProps) { range: [from, to], shiftKey, } = options; + if (shiftKey) { - reset({ ...getValues(), from, to }); + form.setFieldValue('from', String(from)); + form.setFieldValue('to', String(to)); } }, }); - function optionChangeHandler({ value: key }: { value: string }) { + function optionChangeHandlerRefactor(key: string) { const { delta: targetX = 0, ...otherOptions } = (REFERENCES as any)?.[nucleus]?.[key] || {}; - const value = { - ...DEFAULT_OPTIONS, - targetX, - ...otherOptions, - }; - reset(value); + + form.reset( + inputToFormInputSchema({ + from: otherOptions.from ?? Number(DEFAULT_OPTIONS.from), + to: otherOptions.to ?? Number(DEFAULT_OPTIONS.to), + nbPeaks: otherOptions.nbPeaks ?? Number(DEFAULT_OPTIONS.nbPeaks), + options: key, + targetX, + }), + ); } - const List = useMemo(() => getList(nucleus), [nucleus]); return ( - -
-
- Spectra calibration + + +
+ + { + optionChangeHandlerRefactor(value); + }, + }} + > + {(field) => } + + + + + + {(field) => } + + + + {(field) => } + + + + + + {(field) => } + + + + {(field) => } + +
- - - - - - -
-
- handleSubmit(submitHandler)()} - onCancel={onClose} - /> -
- +
+ Apply + + +
+ + ); } - -export default AlignSpectra; From e257e891a70e61e6a1e840e7ae937c2488629628 Mon Sep 17 00:00:00 2001 From: Sebastien Ahkrin <30870051+Sebastien-Ahkrin@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:54:06 +0200 Subject: [PATCH 2/4] refactor: according to reviewers --- .../multipleAnalysisPanel/AlignSpectra.tsx | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx b/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx index 78816b5168..4a548e2a9e 100644 --- a/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx +++ b/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx @@ -20,8 +20,8 @@ interface AlignSpectraProps { onClose: () => void; } -type FormInput = z.input; -type FormOutput = z.output; +type FormInput = z.input; +type FormOutput = z.output; const DEFAULT_OPTIONS: FormInput = { from: '-1', @@ -31,7 +31,7 @@ const DEFAULT_OPTIONS: FormInput = { options: 'manual', }; -const validation = z.object({ +const schemaValidation = z.object({ from: coerceNumberInput(), to: coerceNumberInput(), nbPeaks: coerceNumberInput(), @@ -113,16 +113,6 @@ const Footer = styled.div` gap: 5px; `; -function inputToFormInputSchema(input: FormOutput): FormInput { - return { - from: String(input.from), - to: String(input.to), - targetX: String(input.targetX), - nbPeaks: String(input.nbPeaks), - options: input.options, - }; -} - export default function AlignSpectra(props: AlignSpectraProps) { const { onClose = () => null, nucleus } = props; const spectra = useSpectraByActiveNucleus(); @@ -140,11 +130,11 @@ export default function AlignSpectra(props: AlignSpectraProps) { }, validationLogic: revalidateLogic({ mode: 'change' }), onSubmit: ({ value, formApi }) => { - const parsed = validation.parse(value); + const parsed = schemaValidation.parse(value); const options = checkOptions(parsed); formApi.reset( - inputToFormInputSchema({ + schemaValidation.encode({ ...options, options: optionList[0].value, }), @@ -160,7 +150,7 @@ export default function AlignSpectra(props: AlignSpectraProps) { } }, validators: { - onDynamic: validation, + onDynamic: schemaValidation, }, }); @@ -183,7 +173,7 @@ export default function AlignSpectra(props: AlignSpectraProps) { (REFERENCES as any)?.[nucleus]?.[key] || {}; form.reset( - inputToFormInputSchema({ + schemaValidation.encode({ from: otherOptions.from ?? Number(DEFAULT_OPTIONS.from), to: otherOptions.to ?? Number(DEFAULT_OPTIONS.to), nbPeaks: otherOptions.nbPeaks ?? Number(DEFAULT_OPTIONS.nbPeaks), From 584ee5c06d8f4de27f068b0db5655abca6d88f3e Mon Sep 17 00:00:00 2001 From: Sebastien Ahkrin <30870051+Sebastien-Ahkrin@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:07:08 +0200 Subject: [PATCH 3/4] fix: eslint --- src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx b/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx index 4a548e2a9e..05a6d94d22 100644 --- a/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx +++ b/src/component/panels/multipleAnalysisPanel/AlignSpectra.tsx @@ -21,7 +21,6 @@ interface AlignSpectraProps { } type FormInput = z.input; -type FormOutput = z.output; const DEFAULT_OPTIONS: FormInput = { from: '-1', From 371758348ec3f22a481c8e6129b8dae4541cd132 Mon Sep 17 00:00:00 2001 From: Sebastien Ahkrin <30870051+Sebastien-Ahkrin@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:56:44 +0200 Subject: [PATCH 4/4] fix: remove inner-container --- .../multipleAnalysisPanel/MultipleSpectraAnalysisPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component/panels/multipleAnalysisPanel/MultipleSpectraAnalysisPanel.tsx b/src/component/panels/multipleAnalysisPanel/MultipleSpectraAnalysisPanel.tsx index 07cec8b6af..c1cd991611 100644 --- a/src/component/panels/multipleAnalysisPanel/MultipleSpectraAnalysisPanel.tsx +++ b/src/component/panels/multipleAnalysisPanel/MultipleSpectraAnalysisPanel.tsx @@ -148,7 +148,7 @@ function MultipleSpectraAnalysisPanelInner({ onClose={settingsPanelHandler} /> )} -
+
{isFlipped ? (