diff --git a/package-lock.json b/package-lock.json index d571a3089..fa2dd9b2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "react-ocl-nmr": "^4.1.1", "react-plot": "^3.1.2", "react-rnd": "^10.5.2", - "react-science": "^19.8.0", + "react-science": "^19.9.0", "react-table": "^7.8.0", "smart-array-filter": "^5.0.0", "yup": "^1.7.1", @@ -10642,9 +10642,9 @@ } }, "node_modules/react-science": { - "version": "19.8.0", - "resolved": "https://registry.npmjs.org/react-science/-/react-science-19.8.0.tgz", - "integrity": "sha512-nfsp1uInGhlpSYy7p+ezmJauhLrGiZLx/hDa/L6kWnRGckvB6O3JllZnqEm0/B6gkA+Mgvr3zeocg6i+UnFJ4A==", + "version": "19.9.0", + "resolved": "https://registry.npmjs.org/react-science/-/react-science-19.9.0.tgz", + "integrity": "sha512-JrmeyozB88BECY8rqWlIv9O3s9qNP7Nebo2Cxh1OSkMYFFtenkefmc17heG0b2nxGfKsIUduA9IgHi53MULRGg==", "license": "MIT", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", diff --git a/package.json b/package.json index 67064ee85..c4bd78409 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "react-ocl-nmr": "^4.1.1", "react-plot": "^3.1.2", "react-rnd": "^10.5.2", - "react-science": "^19.8.0", + "react-science": "^19.9.0", "react-table": "^7.8.0", "smart-array-filter": "^5.0.0", "yup": "^1.7.1", diff --git a/src/component/modal/setting/tanstack_general_settings/general_settings.tsx b/src/component/modal/setting/tanstack_general_settings/general_settings.tsx index 3089a177d..77284129c 100644 --- a/src/component/modal/setting/tanstack_general_settings/general_settings.tsx +++ b/src/component/modal/setting/tanstack_general_settings/general_settings.tsx @@ -37,7 +37,9 @@ export function GeneralSettings(props: GeneralSettingsProps) { onDynamic: workspaceValidation, }, validationLogic: revalidateLogic({ mode: 'change' }), - defaultValues: currentWorkspace as GeneralSettingsFormType, + defaultValues: workspaceValidation.encode( + currentWorkspace as unknown as z.output, + ), onSubmit: ({ value }) => { const safeParseResult = workspaceValidation.safeParse(value); @@ -45,7 +47,7 @@ export function GeneralSettings(props: GeneralSettingsProps) { throw new Error('Failed to parse workspace validation'); } - saveSettings(value as Partial); + saveSettings(value as unknown as Partial); close(); }, }); @@ -54,7 +56,7 @@ export function GeneralSettings(props: GeneralSettingsProps) { dispatch({ type: 'APPLY_General_PREFERENCES', payload: { - data: values as Omit, + data: values as unknown as Omit, }, }); diff --git a/src/component/modal/setting/tanstack_general_settings/general_settings_dialog_body.tsx b/src/component/modal/setting/tanstack_general_settings/general_settings_dialog_body.tsx index 6a58c8772..eb33e0db0 100644 --- a/src/component/modal/setting/tanstack_general_settings/general_settings_dialog_body.tsx +++ b/src/component/modal/setting/tanstack_general_settings/general_settings_dialog_body.tsx @@ -4,6 +4,7 @@ import { withForm } from 'react-science/ui'; import { StyledDialogBody } from '../../../elements/StyledDialogBody.tsx'; +import { ExportTab } from './tabs/export_tab.tsx'; import { GeneralTab } from './tabs/general_tab.tsx'; import { defaultGeneralSettingsFormValues } from './validation.ts'; @@ -17,7 +18,7 @@ const Tabs = styled(BPTabs)` div[role='tabpanel'] { max-height: 100%; overflow: auto; - padding: 0.8rem; + padding: 0 0.8rem 0.8rem; width: 100%; } `; @@ -41,6 +42,8 @@ export const GeneralSettingsDialogBody = withForm({ id="general" panel={} /> + + } /> diff --git a/src/component/modal/setting/tanstack_general_settings/tabs/export_tab.fields.tsx b/src/component/modal/setting/tanstack_general_settings/tabs/export_tab.fields.tsx new file mode 100644 index 000000000..dffd03907 --- /dev/null +++ b/src/component/modal/setting/tanstack_general_settings/tabs/export_tab.fields.tsx @@ -0,0 +1,238 @@ +import { Checkbox, Tag } from '@blueprintjs/core'; +import { useStore } from '@tanstack/react-form'; +import type { PageSizeName, Unit } from '@zakodium/nmrium-core'; +import { units } from '@zakodium/nmrium-core'; +import { memo, useMemo } from 'react'; +import { FormGroup, assertUnreachable, withFieldGroup } from 'react-science/ui'; +import type { z } from 'zod/v4'; + +import { convertToPixels } from '../../../../elements/export/units.ts'; +import { useExportConfigurer } from '../../../../elements/export/useExportConfigurer.tsx'; +import { + getExportDefaultOptionsByMode, + getExportOptions, +} from '../../../../elements/export/utilities/getExportOptions.ts'; +import { pageSizes } from '../../../../elements/print/pageSize.ts'; +import { + defaultGeneralSettingsFormValues, + exportSettingsValidation, +} from '../validation.ts'; + +type Mode = 'basic' | 'advance'; +type Layout = 'portrait' | 'landscape'; + +interface SelectItem { + label: string; + value: Value; +} + +const pageSizeItems: Record>> = { + portrait: pageSizes.map((item) => ({ + value: item.name, + label: `${item.name} (${item.portrait.width} cm x ${item.portrait.height} cm)`, + })), + landscape: pageSizes.map((item) => ({ + value: item.name, + label: `${item.name} (${item.landscape.width} cm x ${item.landscape.height} cm)`, + })), +}; +const modeItems: Array> = [ + { label: 'Basic', value: 'basic' }, + { label: 'Advanced', value: 'advance' }, +]; +const unitItems: Array> = units.map((u) => ({ + label: u.name, + value: u.unit, +})); +const layoutItems: Array> = [ + { value: 'portrait', label: 'Portrait' }, + { value: 'landscape', label: 'Landscape' }, +]; + +export const ExportFields = withFieldGroup({ + defaultValues: defaultGeneralSettingsFormValues.export.png, + render: function ExportFields({ group }) { + const inputValues = useStore(group.store, (s) => s.values); + const outputValues = useMemo(() => { + return exportSettingsValidation.decode(inputValues); + }, [inputValues]); + const advancedTransforms = useExportConfigurer(outputValues); + + function onModeChange({ value }: { value: Mode }) { + const newOptions = getExportDefaultOptionsByMode(value); + + for (const [key, value] of Object.entries(newOptions)) { + group.setFieldValue(key as keyof typeof newOptions, value, { + dontRunListeners: true, + }); + } + } + + function onChangeUnit({ value }: { value: Unit }) { + const { width, height } = advancedTransforms.changeUnit({ unit: value }); + group.setFieldValue('width', String(width), { + dontRunListeners: true, + }); + group.setFieldValue('height', String(height), { + dontRunListeners: true, + }); + } + function onWidthChange({ value }: { value: string }) { + const height = advancedTransforms.changeSize( + Number(value), + 'height', + 'width', + ); + if (!advancedTransforms.isAspectRatioEnabled) { + return; + } + group.setFieldValue('height', String(height), { + dontRunListeners: true, + }); + } + function onHeightChange({ value }: { value: string }) { + const width = advancedTransforms.changeSize( + Number(value), + 'width', + 'height', + ); + if (!advancedTransforms.isAspectRatioEnabled) { + return; + } + group.setFieldValue('width', String(width), { + dontRunListeners: true, + }); + } + + function onDPIChange({ value }: { value: string }) { + if (group.state.values.mode !== 'advance') return; + if (group.state.values.unit !== 'px') return; + + const { width, height } = advancedTransforms.changeDPI(Number(value)); + group.setFieldValue('width', String(width), { + dontRunListeners: true, + }); + group.setFieldValue('height', String(height), { + dontRunListeners: true, + }); + } + + return ( + <> + + {(field) => } + + + { + const mode = state.values.mode; + switch (state.values.mode) { + case 'basic': + return { mode: state.values.mode, layout: state.values.layout }; + case 'advance': + return { mode: state.values.mode, unit: state.values.unit }; + default: + assertUnreachable(mode as never); + } + }} + > + {({ mode, layout, unit }) => { + switch (mode) { + case 'basic': + return ( + <> + + {(field) => ( + + )} + + + {(field) => ( + + )} + + + ); + case 'advance': + return ( + <> + + {(field) => ( + + )} + + { + advancedTransforms.enableAspectRatio( + event.currentTarget.checked, + ); + }} + /> + + {(field) => ( + {unit}} + /> + )} + + + {(field) => ( + {unit}} + /> + )} + + + ); + default: + assertUnreachable(mode); + } + }} + + + {(field) => } + + + {(field) => ( + + )} + + + ); + }, +}); + +type DescriptionPreviewProps = z.output; +const DescriptionPreview = memo(function DescriptionPreview( + props: DescriptionPreviewProps, +) { + const { width, height, dpi, unit } = getExportOptions(props); + const convertOptions = { precision: 0 }; + const widthInPixel = convertToPixels(width, unit, dpi, convertOptions); + const heightInPixel = convertToPixels(height, unit, dpi, convertOptions); + + return ( + + {`${widthInPixel} px x ${heightInPixel} px @ ${dpi}DPI`} + + ); +}); diff --git a/src/component/modal/setting/tanstack_general_settings/tabs/export_tab.tsx b/src/component/modal/setting/tanstack_general_settings/tabs/export_tab.tsx new file mode 100644 index 000000000..3e0c659fe --- /dev/null +++ b/src/component/modal/setting/tanstack_general_settings/tabs/export_tab.tsx @@ -0,0 +1,26 @@ +import { withForm } from 'react-science/ui'; + +import { defaultGeneralSettingsFormValues } from '../validation.ts'; + +import { ExportFields } from './export_tab.fields.tsx'; + +export const ExportTab = withForm({ + defaultValues: defaultGeneralSettingsFormValues, + render: ({ form }) => { + return ( + <> +
+ + + +
+ + + + + + + + ); + }, +}); diff --git a/src/component/modal/setting/tanstack_general_settings/validation.ts b/src/component/modal/setting/tanstack_general_settings/validation.ts index 59be0abdf..6a9c15c75 100644 --- a/src/component/modal/setting/tanstack_general_settings/validation.ts +++ b/src/component/modal/setting/tanstack_general_settings/validation.ts @@ -1,6 +1,9 @@ +import type { Layout, PageSizeName } from '@zakodium/nmrium-core'; +import { units } from '@zakodium/nmrium-core'; import { z } from 'zod/v4'; import type { LoggerType } from '../../../context/LoggerContext.tsx'; +import { workspaceDefaultProperties } from '../../../workspaces/workspaceDefaultProperties.ts'; const loggingLevel: LoggerType[] = [ 'fatal', @@ -12,6 +15,21 @@ const loggingLevel: LoggerType[] = [ 'silent', ]; +const exportSizes: PageSizeName[] = [ + 'Letter', + 'Legal', + 'Tabloid', + 'Executive', + 'Statement', + 'Folio', + 'A3', + 'A4', + 'A5', + 'B4', + 'B5', +]; +const exportLayouts: Layout[] = ['portrait', 'landscape']; + const peaksLabelValidation = z.object({ marginTop: z.coerce.number().int().min(0), }); @@ -38,10 +56,57 @@ const displayValidation = z.object({ }), }); +/** + * @see {import("@zakodium/nmrium-core").BaseExportSettings} + */ +const baseExportSettings = z.object({ + useDefaultSettings: z.boolean(), + dpi: z.coerce.number(), +}); + +/** + * @see {import("@zakodium/nmrium-core").BasicExportSettings} + */ +const basicExportSettings = z.object({ + mode: z.literal('basic'), + ...baseExportSettings.shape, + size: z.enum(exportSizes), + layout: z.enum(exportLayouts), +}); + +/** + * @see {import("@zakodium/nmrium-core").AdvanceExportSettings} + */ +const advancedExportSettings = z.object({ + mode: z.literal('advance'), + ...baseExportSettings.shape, + width: z.coerce.number(), + height: z.coerce.number(), + unit: z.enum(units.map((u) => u.unit)), +}); + +/** + * @see {import("@zakodium/nmrium-core").ExportSettings} + */ +export const exportSettingsValidation = z.discriminatedUnion('mode', [ + basicExportSettings, + advancedExportSettings, +]); + +/** + * @see {import("@zakodium/nmrium-core").ExportPreferences} + */ +const exportPreferencesValidation = z.object({ + png: exportSettingsValidation, + svg: exportSettingsValidation, + clipboard: exportSettingsValidation, +}); + export const workspaceValidation = z.object({ peaksLabel: peaksLabelValidation, general: generalValidation, display: displayValidation, + export: exportPreferencesValidation, }); // This object is used to define type not real values. Do not use it as values @@ -66,4 +131,5 @@ export const defaultGeneralSettingsFormValues: z.input< }, }, }, + export: exportPreferencesValidation.encode(workspaceDefaultProperties.export), };