diff --git a/package-lock.json b/package-lock.json index ea8cd5d7a..e3a8eac5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@emotion/styled": "^11.14.1", "@hookform/resolvers": "^5.4.0", "@tanstack/react-form": "^1.33.0", + "@tanstack/react-store": "^0.11.0", "@tanstack/react-table": "^8.21.3", "@zakodium/nmr-types": "^0.5.12", "@zakodium/nmrium-core": "^0.7.24", @@ -3160,7 +3161,7 @@ } } }, - "node_modules/@tanstack/react-form/node_modules/@tanstack/react-store": { + "node_modules/@tanstack/react-store": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.11.0.tgz", "integrity": "sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w==", @@ -9402,15 +9403,6 @@ "ml-matrix-convolution": "^1.0.0" } }, - "node_modules/ml-peak-shape-generator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ml-peak-shape-generator/-/ml-peak-shape-generator-5.1.0.tgz", - "integrity": "sha512-W2Vx/+R65zyr/B13p5TLyqwjTgIzwR0xtjRyQ2ZSxDHdVaXxb6m+ajyyaHJqGG5mYzeqi97nGvpklhz0jrsbrg==", - "license": "MIT", - "dependencies": { - "cheminfo-types": "^1.15.0" - } - }, "node_modules/ml-regression-base": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/ml-regression-base/-/ml-regression-base-4.0.1.tgz", diff --git a/package.json b/package.json index 408bdd573..ecb555e3a 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@emotion/styled": "^11.14.1", "@hookform/resolvers": "^5.4.0", "@tanstack/react-form": "^1.33.0", + "@tanstack/react-store": "^0.11.0", "@tanstack/react-table": "^8.21.3", "@zakodium/nmr-types": "^0.5.12", "@zakodium/nmrium-core": "^0.7.24", @@ -167,4 +168,4 @@ "volta": { "node": "24.15.0" } -} \ No newline at end of file +} diff --git a/src/component/elements/export/ExportOptionsModal.tsx b/src/component/elements/export/ExportOptionsModal.tsx index 1a3f082cd..de6f30b96 100644 --- a/src/component/elements/export/ExportOptionsModal.tsx +++ b/src/component/elements/export/ExportOptionsModal.tsx @@ -1,35 +1,21 @@ -import { - Button, - DialogFooter, - Radio, - RadioGroup, - SegmentedControl, - Tag, -} from '@blueprintjs/core'; -import { yupResolver } from '@hookform/resolvers/yup'; +import { Button, DialogFooter, SegmentedControl, Tag } from '@blueprintjs/core'; +import { revalidateLogic } from '@tanstack/react-form'; +import { useSelector } from '@tanstack/react-store'; import type { AdvanceExportSettings, BasicExportSettings, - ExportSettings, } from '@zakodium/nmrium-core'; -import { useEffect, useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; +import { useState } from 'react'; +import { Form, FormGroup, useForm } from 'react-science/ui'; -import ActionButtons from '../ActionButtons.js'; -import { CheckController } from '../CheckController.js'; -import type { LabelStyle } from '../Label.js'; -import Label from '../Label.js'; -import { NumberInput2Controller } from '../NumberInput2Controller.js'; -import { Select2Controller } from '../Select2Controller.js'; import { StandardDialog } from '../StandardDialog.tsx'; import { StyledDialogBody } from '../StyledDialogBody.js'; -import type { SizeItem } from '../print/pageSize.js'; import { getSizesList } from '../print/pageSize.js'; import type { BaseExportProps } from './ExportContent.js'; import { units } from './units.js'; import { useExportConfigurer } from './useExportConfigurer.js'; -import { exportOptionValidationSchema } from './utilities/exportOptionValidationSchema.js'; +import { exportOptionValidationSchemaZod } from './utilities/exportOptionValidationSchema.js'; import { getExportDefaultOptions, getExportDefaultOptionsByMode, @@ -39,7 +25,6 @@ import { MODES } from './utilities/getModes.js'; interface InnerExportOptionsModalProps extends BaseExportProps { onCloseDialog: () => void; - confirmButtonText?: string; } interface ExportOptionsModalProps extends InnerExportOptionsModalProps { isOpen: boolean; @@ -53,49 +38,45 @@ export function ExportOptionsModal(props: ExportOptionsModalProps) { return ; } -const labelStyle: LabelStyle = { - label: { - color: '#232323', - width: '80px', - }, - wrapper: { - display: 'flex', - justifyContent: 'flex-start', - }, - container: { margin: '5px 0' }, -}; - function InnerExportOptionsModal(props: InnerExportOptionsModalProps) { - const { - onCloseDialog, - onExportOptionsChange, - confirmButtonText, - defaultExportOptions, - } = props; - const defaultValues = getExportDefaultOptions(defaultExportOptions); + const { onCloseDialog, defaultExportOptions, onExportOptionsChange } = props; + const defaultValues = getExportDefaultOptions(defaultExportOptions); const [mode, setMode] = useState(defaultValues.mode); - const methods = useForm({ - defaultValues, - resolver: yupResolver(exportOptionValidationSchema as any), + const form = useForm({ + defaultValues: getExportDefaultOptionsByMode(mode), + validators: { + onDynamic: exportOptionValidationSchemaZod as any, + }, + validationLogic: revalidateLogic({ modeAfterSubmission: 'change' }), + onSubmit: ({ value }) => { + const parsedValue = exportOptionValidationSchemaZod.parse(value); + onExportOptionsChange(parsedValue); + }, }); - const { - handleSubmit, - control, - watch, - setValue, - formState: { isValid, errors }, - reset, - setFocus, - } = methods; - const watchSettings = watch(); - const { - unit, - dpi = 0, - layout, - } = watchSettings as AdvanceExportSettings & BasicExportSettings; + const { unit, layout } = useSelector(form.store, (state) => { + const { unit, layout } = state.values as AdvanceExportSettings & + BasicExportSettings; + + return { + unit, + layout, + }; + }); + + const { values, isValid, errors } = useSelector( + form.store, + ({ values, isValid, errors }) => { + return { + values, + isValid, + errors, + }; + }, + ); + const { widthInPixel, heightInPixel, @@ -104,51 +85,9 @@ function InnerExportOptionsModal(props: InnerExportOptionsModalProps) { enableAspectRatio, changeSize, changeUnit, - } = useExportConfigurer(watchSettings); - - let sizesList: SizeItem[] = []; - - if (layout) { - sizesList = getSizesList(layout); - } - - function handleChangeMode(mode: any) { - const options = defaultValues; - setMode(mode); - if (options.mode === mode) { - reset(defaultValues); - } else { - reset(getExportDefaultOptionsByMode(mode)); - } - } - - useEffect(() => { - const handleRenderComplete = () => { - setTimeout(() => { - if (mode === 'advance') { - setFocus('width'); - } else { - setFocus('dpi'); - } - }, 0); - }; - - const handleKeyDown = (event: globalThis.KeyboardEvent) => { - if (event.key === 'Enter') { - void handleSubmit(onExportOptionsChange)(); - } - }; - - globalThis.addEventListener('keydown', handleKeyDown); - - const animationFrameId = requestAnimationFrame(handleRenderComplete); - - return () => { - cancelAnimationFrame(animationFrameId); - globalThis.removeEventListener('keydown', handleKeyDown); - }; - }, [handleSubmit, mode, onExportOptionsChange, setFocus]); + } = useExportConfigurer(values); + // TODO: focus width if mode === advance, else focus dpi + save on "Enter" return ( - -
+
{ + event.preventDefault(); + void form.handleSubmit(); }} > - -
- - - {mode === 'basic' && ( - <> - - - - )} - {mode === 'advance' && ( - <> - - - - )} - -
- -
- + + + 0 + ? 'danger' + : 'none' + } + > + {widthInPixel} px + x + {heightInPixel} px + state.values.dpi}> + {(dpi) => @ {dpi}DPI} + + + + + {mode === 'basic' && ( + <> + + {(field) => ( + + )} + + + {(field) => ( + + )} + + + )} + + {mode === 'advance' && ( + <> + { + const newHeight = changeSize( + value, + 'height', + 'width', + ); + form.setFieldValue('height', newHeight); + }, + }} + > + {(field) => ( + {unit}} + required + placeholder="width" + /> + )} + + + + + Save +
+ +
+ +
); } diff --git a/src/component/elements/export/utilities/exportOptionValidationSchema.ts b/src/component/elements/export/utilities/exportOptionValidationSchema.ts index 8e2d07bb8..62fb5e67a 100644 --- a/src/component/elements/export/utilities/exportOptionValidationSchema.ts +++ b/src/component/elements/export/utilities/exportOptionValidationSchema.ts @@ -1,10 +1,5 @@ -import type { - ExportSettings, - Layout, - PageSizeName, - Unit, -} from '@zakodium/nmrium-core'; -import * as Yup from 'yup'; +import type { Unit } from '@zakodium/nmrium-core'; +import { z } from 'zod/v4'; import { pageSizes } from '../../print/pageSize.js'; import { convertToPixels, units } from '../units.js'; @@ -18,89 +13,52 @@ function testSize(value: number, unit: Unit, dpi: number) { const valueInPixel = convertToPixels(value, unit, dpi, { precision: 2, }); + if (value) { return valueInPixel <= MAX_PIXEL; } + return true; } -// export const exportOptionValidationSchema = Yup.object().shape({ -// width: Yup.number() -// .required() -// .test( -// 'width-test', -// `Width should be less or equal to ${MAX_PIXEL}`, -// // eslint-disable-next-line func-names -// function (value) { -// // eslint-disable-next-line no-invalid-this -// const { unit, dpi } = this.parent; -// return testSize(value, unit, dpi); -// }, -// ), - -// height: Yup.number() -// .required() -// .test( -// 'height-test', -// `Height should be less or equal to ${MAX_PIXEL}`, -// // eslint-disable-next-line func-names -// function (value) { -// // eslint-disable-next-line no-invalid-this -// const { unit, dpi } = this.parent; -// return testSize(value, unit, dpi); -// }, -// ), - -// dpi: Yup.number().required(), -// unit: Yup.mixed().oneOf(unitsKeys, 'Unit is invalid').required(), -// useDefaultSettings: Yup.boolean().required(), -// }); - -const advanceExportOptionValidationSchema = Yup.object().shape({ - mode: Yup.string().oneOf(['basic', 'advance']).required(), - width: Yup.number() - .required() - .test( - 'width-test', - `Width should be less or equal to ${MAX_PIXEL}`, - (value, context) => { - const { unit, dpi } = context.parent; - return testSize(value, unit, dpi); - }, - ), +const basicExportOptionValidationSchemaZod = z.object({ + mode: z.literal('basic'), + size: z.enum(pageSizesKeys, { error: 'Size is invalid' }), + layout: z.enum(['portrait', 'landscape'], 'Layout is invalid'), + dpi: z.coerce.number().min(1, { error: 'DPI is invalid' }), + useDefaultSettings: z.boolean(), +}); - height: Yup.number() - .required() - .test( - 'height-test', - `Height should be less or equal to ${MAX_PIXEL}`, - (value, context) => { - const { unit, dpi } = context.parent; - return testSize(value, unit, dpi); - }, - ), +const advanceExportOptionValidationSchemaZod = z + .object({ + mode: z.literal('advance'), + width: z.coerce.number(), + height: z.coerce.number(), + dpi: z.coerce.number().min(1, { error: 'DPI is invalid' }), + unit: z.enum(unitsKeys), + useDefaultSettings: z.boolean(), + }) + .superRefine((data, context) => { + const { dpi, unit, height, width } = data; - dpi: Yup.number().required(), - unit: Yup.mixed().oneOf(unitsKeys).required(), - useDefaultSettings: Yup.boolean().required(), -}); + if (!testSize(width, unit, dpi)) { + context.addIssue({ + code: 'custom', + message: `Width should be less or equal to ${MAX_PIXEL}`, + path: ['width'], + }); + } -const basicExportOptionValidationSchema = Yup.object().shape({ - mode: Yup.string().oneOf(['basic', 'advance']).required(), - size: Yup.mixed() - .oneOf(pageSizesKeys, 'Size is invalid') - .required(), - layout: Yup.mixed() - .oneOf(['portrait', 'landscape'], 'Layout is invalid') - .required(), - dpi: Yup.number().required(), - useDefaultSettings: Yup.boolean().required(), -}); + if (!testSize(height, unit, dpi)) { + context.addIssue({ + code: 'custom', + message: `Height should be less or equal to ${MAX_PIXEL}`, + path: ['height'], + }); + } + }); -export const exportOptionValidationSchema = Yup.lazy( - (values: ExportSettings) => { - return values?.mode === 'advance' - ? advanceExportOptionValidationSchema - : basicExportOptionValidationSchema; - }, -); +export const exportOptionValidationSchemaZod = z.discriminatedUnion('mode', [ + basicExportOptionValidationSchemaZod, + advanceExportOptionValidationSchemaZod, +]); diff --git a/src/component/elements/export/utilities/getExportOptions.ts b/src/component/elements/export/utilities/getExportOptions.ts index b388905e6..b1d27e1e6 100644 --- a/src/component/elements/export/utilities/getExportOptions.ts +++ b/src/component/elements/export/utilities/getExportOptions.ts @@ -50,6 +50,7 @@ export function getExportDefaultOptions(options?: ExportSettings) { return options || INITIAL_ADVANCE_EXPORT_OPTIONS; } + export function getExportDefaultOptionsByMode(mode: 'basic' | 'advance') { if (mode === 'basic') { return INITIAL_BASIC_EXPORT_OPTIONS; diff --git a/src/component/elements/export/utilities/getModes.ts b/src/component/elements/export/utilities/getModes.ts index 015397265..62f41a976 100644 --- a/src/component/elements/export/utilities/getModes.ts +++ b/src/component/elements/export/utilities/getModes.ts @@ -1,6 +1,7 @@ import type { OptionProps } from '@blueprintjs/core'; -export const MODES: Array> = [ +type PossibleMode = 'basic' | 'advance'; +export const MODES: Array> = [ { label: 'Basic', value: 'basic',