From 487484f7609b8c06f5ee894da25f55d7814730c8 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:31:57 +0200 Subject: [PATCH 1/7] feat: prepare processings UI --- package.json | 2 +- src/component/elements/Sections.tsx | 8 +- .../panels/filtersPanel/FilterPanel.tsx | 10 +++ .../Filters/base/BaseApodizationOptions.tsx | 1 + .../processings_sections_panel.tsx | 84 +++++++++++++++++++ 5 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/component/panels/filtersPanel/processings_sections_panel.tsx diff --git a/package.json b/package.json index e17bd30f0..f0ebbac0a 100644 --- a/package.json +++ b/package.json @@ -167,4 +167,4 @@ "volta": { "node": "24.15.0" } -} \ No newline at end of file +} diff --git a/src/component/elements/Sections.tsx b/src/component/elements/Sections.tsx index 2547ee3ed..14ece4232 100644 --- a/src/component/elements/Sections.tsx +++ b/src/component/elements/Sections.tsx @@ -205,7 +205,7 @@ const InnerHeader = styled.div` `; interface BaseSectionProps { - title: string; + title: ReactNode; serial?: number; rightElement?: ReactNode | ((isOpen: boolean) => ReactNode); leftElement?: ReactNode | ((isOpen: boolean) => ReactNode); @@ -214,14 +214,14 @@ interface BaseSectionProps { } interface SectionItemProps extends BaseSectionProps { - id?: string; + id: string; index?: number; onClick?: (id: any, event?: MouseEvent) => void; children?: ReactNode | ((options: { isOpen?: boolean }) => ReactNode); isOpen: boolean; sticky?: boolean; onReorder?: (sourceId: number, targetId: number) => void; - dragLabel?: string; + dragLabel?: ReactNode; } interface SectionProps { @@ -269,7 +269,7 @@ function SectionItem(props: SectionItemProps) { const { title, dragLabel = title, - id = title, + id, onClick, serial, rightElement, diff --git a/src/component/panels/filtersPanel/FilterPanel.tsx b/src/component/panels/filtersPanel/FilterPanel.tsx index b6db5a60a..9a2da618a 100644 --- a/src/component/panels/filtersPanel/FilterPanel.tsx +++ b/src/component/panels/filtersPanel/FilterPanel.tsx @@ -2,17 +2,20 @@ import { useDispatch } from '../../context/DispatchContext.js'; import { useToaster } from '../../context/ToasterContext.js'; import type { AlertButton } from '../../elements/Alert.js'; import { useAlert } from '../../elements/Alert.js'; +import useCheckExperimentalFeature from '../../hooks/useCheckExperimentalFeature.ts'; import useSpectrum from '../../hooks/useSpectrum.js'; import { TablePanel } from '../extra/BasicPanelStyle.js'; import DefaultPanelHeader from '../header/DefaultPanelHeader.js'; import { FiltersSectionsPanel } from './Filters/FiltersSectionsPanel.js'; +import { ProcessingsSectionsPanel } from './processings_sections_panel.tsx'; export default function FiltersPanel() { const dispatch = useDispatch(); const toaster = useToaster(); const { showAlert } = useAlert(); const { filters } = useSpectrum({ filters: [] }); + const isExperimental = useCheckExperimentalFeature(); function handleDeleteFilter() { const buttons: AlertButton[] = [ @@ -46,6 +49,13 @@ export default function FiltersPanel() { />
+ + {isExperimental && ( + <> +
+ + + )}
); diff --git a/src/component/panels/filtersPanel/Filters/base/BaseApodizationOptions.tsx b/src/component/panels/filtersPanel/Filters/base/BaseApodizationOptions.tsx index 5a5e5332c..61a881ee2 100644 --- a/src/component/panels/filtersPanel/Filters/base/BaseApodizationOptions.tsx +++ b/src/component/panels/filtersPanel/Filters/base/BaseApodizationOptions.tsx @@ -330,6 +330,7 @@ function OptionsSection(options: OptionsSectionProps) { return ( (null); + + function handleDeleteFilter() { + const buttons: AlertButton[] = [ + { + text: 'Yes', + intent: 'danger', + }, + { text: 'No' }, + ]; + + showAlert({ + message: + 'You are about to delete all processing steps, Are you sure?. Experimental, not implemented yet', + buttons, + }); + } + + function toggleSection(operationId: string) { + setOpenedOperation(openedOperation === operationId ? null : operationId); + } + + return ( + <> + + + + {processings?.map((operation, index) => ( + toggleSection(operation.uid)} + > + + {renderCoreSlot( + core, + 'panels.processings.operation.expanded', + operation.settings !== null ? ( + + ) : ( + + ), + )} + + + ))} + + + ); +} From 35106020159b5abaa7aa9f6db44e803b27dbf0ef Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:02:51 +0200 Subject: [PATCH 2/7] feat: slot supports props --- src/component/header/Header.tsx | 4 +- src/component/modal/aboutUs/AboutUsModal.tsx | 8 +++- .../processings_sections_panel.tsx | 46 +++++++++++-------- src/component/utility/CoreSlot.tsx | 37 +++++++++++++++ src/component/utility/renderCoreSlot.tsx | 21 --------- 5 files changed, 71 insertions(+), 45 deletions(-) create mode 100644 src/component/utility/CoreSlot.tsx delete mode 100644 src/component/utility/renderCoreSlot.tsx diff --git a/src/component/header/Header.tsx b/src/component/header/Header.tsx index 068bc84c8..376dc7ec9 100644 --- a/src/component/header/Header.tsx +++ b/src/component/header/Header.tsx @@ -28,7 +28,7 @@ import AboutUsModal from '../modal/aboutUs/AboutUsModal.js'; import WorkspaceItem from '../modal/setting/WorkspaceItem.js'; import { GeneralSettingsToolbarItem } from '../modal/setting/general_settings.js'; import { options } from '../toolbar/ToolTypes.js'; -import { renderCoreSlot } from '../utility/renderCoreSlot.js'; +import { CoreSlot } from '../utility/CoreSlot.tsx'; import { AutoPeakPickingOptionPanel } from './AutoPeakPickingOptionPanel.js'; import { HeaderWrapper } from './HeaderWrapper.js'; @@ -149,7 +149,7 @@ function HeaderInner(props: HeaderInnerProps) { }} > - {renderCoreSlot(core, 'topbar.right')} + {!hideWorkspaces && ( diff --git a/src/component/modal/aboutUs/AboutUsModal.tsx b/src/component/modal/aboutUs/AboutUsModal.tsx index 640452b8e..7b901ce6f 100644 --- a/src/component/modal/aboutUs/AboutUsModal.tsx +++ b/src/component/modal/aboutUs/AboutUsModal.tsx @@ -7,7 +7,7 @@ import { useCore } from '../../context/CoreContext.js'; import Logo from '../../elements/Logo.js'; import { StandardDialog } from '../../elements/StandardDialog.tsx'; import { StyledDialogBody } from '../../elements/StyledDialogBody.js'; -import { renderCoreSlot } from '../../utility/renderCoreSlot.js'; +import { CoreSlot } from '../../utility/CoreSlot.tsx'; import AboutUsZakodium from './AboutUsZakodium.js'; @@ -137,7 +137,11 @@ function AboutUsModal() { title="About NMRium" > - {renderCoreSlot(core, 'topbar.about_us.modal', modalContentFallback)} + diff --git a/src/component/panels/filtersPanel/processings_sections_panel.tsx b/src/component/panels/filtersPanel/processings_sections_panel.tsx index 90d1f64a6..bc892b1ce 100644 --- a/src/component/panels/filtersPanel/processings_sections_panel.tsx +++ b/src/component/panels/filtersPanel/processings_sections_panel.tsx @@ -7,7 +7,7 @@ import { useAlert } from '../../elements/Alert.tsx'; import { EmptyText } from '../../elements/EmptyText.tsx'; import { Sections } from '../../elements/Sections.tsx'; import useSpectrum from '../../hooks/useSpectrum.ts'; -import { renderCoreSlot } from '../../utility/renderCoreSlot.tsx'; +import { CoreSlot } from '../../utility/CoreSlot.tsx'; import DefaultPanelHeader from '../header/DefaultPanelHeader.tsx'; export function ProcessingsSectionsPanel() { @@ -51,30 +51,36 @@ export function ProcessingsSectionsPanel() { + } isOpen={openedOperation === operation.uid} serial={index + 1} onClick={() => toggleSection(operation.uid)} > - {renderCoreSlot( - core, - 'panels.processings.operation.expanded', - operation.settings !== null ? ( - - ) : ( - - ), - )} + + ) : ( + + ) + } + operation={operation} + /> ))} diff --git a/src/component/utility/CoreSlot.tsx b/src/component/utility/CoreSlot.tsx new file mode 100644 index 000000000..215bd067f --- /dev/null +++ b/src/component/utility/CoreSlot.tsx @@ -0,0 +1,37 @@ +import type { + NMRiumCore, + PluginUIComponentProps, + SupportedUISlot, +} from '@zakodium/nmrium-core'; +import { castSlotProps } from '@zakodium/nmrium-core'; +import type { ReactNode } from 'react'; + +type SlotProps = + Omit, 'slot'> extends Record< + string, + never + > + ? object + : Omit, 'slot'>; + +interface CoreSlotProps { + slot: Slot; + core: NMRiumCore; + fallback?: ReactNode; +} + +/** + * Render all components registered in the given slot. + */ +export function CoreSlot( + props: CoreSlotProps & SlotProps, +) { + const { slot, core, fallback, ...slotProps } = props; + castSlotProps(slotProps, slot); + + const jsx = Array.from(core.slot(slot), ([key, Component]) => ( + + )); + + return jsx.length > 0 ? jsx : fallback; +} diff --git a/src/component/utility/renderCoreSlot.tsx b/src/component/utility/renderCoreSlot.tsx deleted file mode 100644 index 2c9169cf7..000000000 --- a/src/component/utility/renderCoreSlot.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { NMRiumCore, SupportedUISlot } from '@zakodium/nmrium-core'; -import type { ReactNode } from 'react'; - -/** - * Render all components registered in the given slot. - * - * @param core - * @param slot - * @param fallback - return fallback if no component is registered in the slot - */ -export function renderCoreSlot( - core: NMRiumCore, - slot: SupportedUISlot, - fallback?: ReactNode, -): ReactNode { - const jsx = Array.from(core.slot(slot), ([key, Component]) => ( - - )); - - return jsx.length > 0 ? jsx : fallback; -} From c88972cfb0ecb36989ebd0e83ae0334afb897e60 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:12:03 +0200 Subject: [PATCH 3/7] refactor: use `useCore` inside `CoreSlot` component --- src/component/header/Header.tsx | 4 +--- src/component/modal/aboutUs/AboutUsModal.tsx | 3 --- .../panels/filtersPanel/processings_sections_panel.tsx | 5 ----- src/component/utility/CoreSlot.tsx | 8 +++++--- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/component/header/Header.tsx b/src/component/header/Header.tsx index 376dc7ec9..1f4549bd8 100644 --- a/src/component/header/Header.tsx +++ b/src/component/header/Header.tsx @@ -10,7 +10,6 @@ import { Toolbar, useFullscreen } from 'react-science/ui'; import { docsBaseUrl } from '../../constants.js'; import { useChartData } from '../context/ChartContext.js'; -import { useCore } from '../context/CoreContext.js'; import { usePreferences, useWorkspacesList, @@ -63,7 +62,6 @@ interface HeaderInnerProps { function HeaderInner(props: HeaderInnerProps) { const { selectedOptionPanel, height } = props; - const core = useCore(); const { current: { @@ -149,7 +147,7 @@ function HeaderInner(props: HeaderInnerProps) { }} > - + {!hideWorkspaces && ( diff --git a/src/component/modal/aboutUs/AboutUsModal.tsx b/src/component/modal/aboutUs/AboutUsModal.tsx index 7b901ce6f..7969144a2 100644 --- a/src/component/modal/aboutUs/AboutUsModal.tsx +++ b/src/component/modal/aboutUs/AboutUsModal.tsx @@ -3,7 +3,6 @@ import { SvgLogoNmrium } from 'cheminfo-font'; import { Toolbar, useOnOff } from 'react-science/ui'; import versionInfo from '../../../versionInfo.js'; -import { useCore } from '../../context/CoreContext.js'; import Logo from '../../elements/Logo.js'; import { StandardDialog } from '../../elements/StandardDialog.tsx'; import { StyledDialogBody } from '../../elements/StyledDialogBody.js'; @@ -116,7 +115,6 @@ const modalContentFallback = ( function AboutUsModal() { const [isOpenDialog, openDialog, closeDialog] = useOnOff(false); - const core = useCore(); return ( <> @@ -139,7 +137,6 @@ function AboutUsModal() { diff --git a/src/component/panels/filtersPanel/processings_sections_panel.tsx b/src/component/panels/filtersPanel/processings_sections_panel.tsx index bc892b1ce..92ad5f84e 100644 --- a/src/component/panels/filtersPanel/processings_sections_panel.tsx +++ b/src/component/panels/filtersPanel/processings_sections_panel.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import { ObjectInspector } from 'react-inspector'; -import { useCore } from '../../context/CoreContext.tsx'; import type { AlertButton } from '../../elements/Alert.tsx'; import { useAlert } from '../../elements/Alert.tsx'; import { EmptyText } from '../../elements/EmptyText.tsx'; @@ -11,8 +10,6 @@ import { CoreSlot } from '../../utility/CoreSlot.tsx'; import DefaultPanelHeader from '../header/DefaultPanelHeader.tsx'; export function ProcessingsSectionsPanel() { - const core = useCore(); - const { showAlert } = useAlert(); const { processings } = useSpectrum({ filters: [] }); const [openedOperation, setOpenedOperation] = useState(null); @@ -54,7 +51,6 @@ export function ProcessingsSectionsPanel() { title={ @@ -66,7 +62,6 @@ export function ProcessingsSectionsPanel() { = Omit, 'slot'> extends Record< string, @@ -16,7 +17,6 @@ type SlotProps = interface CoreSlotProps { slot: Slot; - core: NMRiumCore; fallback?: ReactNode; } @@ -26,9 +26,11 @@ interface CoreSlotProps { export function CoreSlot( props: CoreSlotProps & SlotProps, ) { - const { slot, core, fallback, ...slotProps } = props; + const { slot, fallback, ...slotProps } = props; castSlotProps(slotProps, slot); + const core = useCore(); + const jsx = Array.from(core.slot(slot), ([key, Component]) => ( )); From 3770ffcdec64236afac6e95b6eae4ce2e7f413e0 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:55:51 +0200 Subject: [PATCH 4/7] wip: continue refactor of core ui rendering --- .../processings_sections_panel.tsx | 14 ++++--- src/component/utility/CoreSlot.tsx | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/component/panels/filtersPanel/processings_sections_panel.tsx b/src/component/panels/filtersPanel/processings_sections_panel.tsx index 92ad5f84e..9760ea9e0 100644 --- a/src/component/panels/filtersPanel/processings_sections_panel.tsx +++ b/src/component/panels/filtersPanel/processings_sections_panel.tsx @@ -6,7 +6,10 @@ import { useAlert } from '../../elements/Alert.tsx'; import { EmptyText } from '../../elements/EmptyText.tsx'; import { Sections } from '../../elements/Sections.tsx'; import useSpectrum from '../../hooks/useSpectrum.ts'; -import { CoreSlot } from '../../utility/CoreSlot.tsx'; +import { + CoreOperatorExpanded, + CoreOperatorName, +} from '../../utility/CoreSlot.tsx'; import DefaultPanelHeader from '../header/DefaultPanelHeader.tsx'; export function ProcessingsSectionsPanel() { @@ -49,10 +52,9 @@ export function ProcessingsSectionsPanel() { key={operation.uid} id={operation.uid} title={ - } isOpen={openedOperation === operation.uid} @@ -60,8 +62,8 @@ export function ProcessingsSectionsPanel() { onClick={() => toggleSection(operation.uid)} > - ( return jsx.length > 0 ? jsx : fallback; } + +interface CoreOperatorNameProps { + id: Id; + fallback?: ReactNode; +} + +export function CoreOperatorName( + props: CoreOperatorNameProps, +) { + const { id, fallback } = props; + const core = useCore(); + + const operator = core.slotOperator(id); + if (!operator) return fallback; + + return operator.name; +} + +interface CoreOperatorExtendedProps { + id: Id; + fallback?: ReactNode; +} + +export function CoreOperatorExpanded( + props: CoreOperatorExtendedProps & ProcessingOperatorUIExpandedProps, +) { + const { id, fallback, ...operatorProps } = props; + const core = useCore(); + + const operator = core.slotOperator(id); + const Expanded = operator?.Expanded; + if (!Expanded) return fallback; + + return ; +} From b3accca6193b563b786ef14141049142cff75b02 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:50:26 +0200 Subject: [PATCH 5/7] feat: mock processings events put processings in a local state to apply basic implementation on it. * delete all processings * delete a processings * open section on edit button click * reorder processings --- package.json | 1 + .../processings_sections_panel.tsx | 221 +++++++++++++++--- 2 files changed, 185 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index f0ebbac0a..2626ebffd 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@zakodium/nmrium-core": "^0.7.24", "@zakodium/nmrium-core-plugins": "^0.7.32", "@zakodium/pdnd-esm": "^1.0.2", + "@zakodium/utils": "^0.2.0", "@zip.js/zip.js": "^2.8.26", "cheminfo-font": "^1.27.0", "cheminfo-types": "^1.15.0", diff --git a/src/component/panels/filtersPanel/processings_sections_panel.tsx b/src/component/panels/filtersPanel/processings_sections_panel.tsx index 9760ea9e0..b61a6503e 100644 --- a/src/component/panels/filtersPanel/processings_sections_panel.tsx +++ b/src/component/panels/filtersPanel/processings_sections_panel.tsx @@ -1,27 +1,95 @@ -import { useState } from 'react'; +import { Classes, Switch } from '@blueprintjs/core'; +import styled from '@emotion/styled'; +import type { ProcessingOperatorId, Spectrum } from '@zakodium/nmrium-core'; +import { cast } from '@zakodium/utils'; +import type { Filter1D, Filter2D } from 'nmr-processing'; +import { useEffect, useMemo, useState } from 'react'; import { ObjectInspector } from 'react-inspector'; +import { Button } from 'react-science/ui'; +import { useChartData } from '../../context/ChartContext.tsx'; +import { useCore } from '../../context/CoreContext.tsx'; import type { AlertButton } from '../../elements/Alert.tsx'; import { useAlert } from '../../elements/Alert.tsx'; import { EmptyText } from '../../elements/EmptyText.tsx'; import { Sections } from '../../elements/Sections.tsx'; import useSpectrum from '../../hooks/useSpectrum.ts'; +import type { Tool } from '../../toolbar/ToolTypes.ts'; import { CoreOperatorExpanded, CoreOperatorName, } from '../../utility/CoreSlot.tsx'; import DefaultPanelHeader from '../header/DefaultPanelHeader.tsx'; +const mapToolsToProcessing: Partial> = { + apodization: '@zakodium/nmrium-core-plugins#apodization1D', + zeroFilling: '@zakodium/nmrium-core-plugins#zeroFilling1D', + fft: '@zakodium/nmrium-core-plugins#fft1D', + phaseCorrection: '@zakodium/nmrium-core-plugins#phaseCorrection1D', +}; + +type FilterName = Filter1D['name'] | Filter2D['name']; +const mapFiltersToProcessings: Partial< + Record +> = { + apodization: '@zakodium/nmrium-core-plugins#apodization1D', + digitalFilter: '@zakodium/nmrium-core-plugins#digitalFilter1D', + fft: '@zakodium/nmrium-core-plugins#fft1D', + phaseCorrection: '@zakodium/nmrium-core-plugins#phaseCorrection1D', + shiftX: '@zakodium/nmrium-core-plugins#shiftX1D', + zeroFilling: '@zakodium/nmrium-core-plugins#zeroFilling1D', +}; + +const unremoveableProcessings = new Set([ + '@zakodium/nmrium-core-plugins#digitalFilter1D', + '@zakodium/nmrium-core-plugins#digitalFilter2D', +]); + export function ProcessingsSectionsPanel() { + const core = useCore(); const { showAlert } = useAlert(); - const { processings } = useSpectrum({ filters: [] }); - const [openedOperation, setOpenedOperation] = useState(null); + const spectrum: Spectrum | undefined = useSpectrum(); + const { toolOptions } = useChartData(); + + cast(toolOptions.data.activeFilterID); + const selectedToolProcessing = mapToolsToProcessing[toolOptions.selectedTool]; + const activeProcessing = + mapFiltersToProcessings[toolOptions.data.activeFilterID]; + const [openedOperation, setOpenedOperation] = useState(); + + const [processings, setProcessings] = useState( + () => spectrum?.processings ?? [], + ); + useEffect(() => { + setProcessings(spectrum?.processings ?? []); + }, [spectrum?.processings]); + + const selectedProcessing = useMemo<{ + operatorId?: ProcessingOperatorId; + operationId?: string; + }>(() => { + if (selectedToolProcessing) return { operatorId: selectedToolProcessing }; + if ( + activeProcessing && + spectrum?.processings?.some((p) => p.operatorId === activeProcessing) + ) { + return { operatorId: activeProcessing }; + } + + return { operationId: openedOperation }; + }, [ + activeProcessing, + openedOperation, + selectedToolProcessing, + spectrum?.processings, + ]); function handleDeleteFilter() { const buttons: AlertButton[] = [ { text: 'Yes', intent: 'danger', + onClick: () => setProcessings([]), }, { text: 'No' }, ]; @@ -34,9 +102,22 @@ export function ProcessingsSectionsPanel() { } function toggleSection(operationId: string) { - setOpenedOperation(openedOperation === operationId ? null : operationId); + setOpenedOperation( + openedOperation === operationId ? undefined : operationId, + ); + } + + function onReorder(sourceIndex: number, targetIndex: number) { + const newProcessings = [...processings]; + [newProcessings[sourceIndex], newProcessings[targetIndex]] = [ + newProcessings[targetIndex], + newProcessings[sourceIndex], + ]; + setProcessings(newProcessings); } + if (!spectrum) return null; + return ( <> - - {processings?.map((operation, index) => ( - - } - isOpen={openedOperation === operation.uid} - serial={index + 1} - onClick={() => toggleSection(operation.uid)} - > - - } + {processings.length > 0 && ( + + {processings?.map((operation, index) => { + const operatorUI = core.slotOperator(operation.operatorId); + const isOpen = + selectedProcessing.operatorId === operation.operatorId || + selectedProcessing.operationId === operation.uid; + const isEditable = operatorUI?.isEditable; + + return ( + + } + onReorder={onReorder} + isOpen={isOpen} + serial={index + 1} + onClick={() => toggleSection(operation.uid)} + rightElement={ + + {isEditable && ( + + + )} + ); @@ -224,6 +212,79 @@ export function ProcessingsSectionsPanel() { ); } +interface ProcessingItemExtraProps { + operation: SpectrumProcessingOperation; + isOpen: boolean; + isEditable: boolean | undefined; + setOpenedOperation: Dispatch>; + setProcessings: Dispatch< + SetStateAction>> + >; +} + +function ProcessingItemExtra(props: ProcessingItemExtraProps) { + const { operation, isOpen, isEditable, setOpenedOperation, setProcessings } = + props; + + return ( + + {isEditable && ( +