Skip to content

Commit eb2cf49

Browse files
authored
feat: save to NMRium Archive v1 format (#3915)
* feat: save to NMRium Archive v1 format * remove save to json / json in zip options * test: fix UI checks * test: remove tests about removed feature * fix: re-add no-data option fix: don't throw on missing molecules * fix: restore assert and fix incoherent values in reducer * fix: add check in missing path * fix: sync core, it should fix ci * test: adapt tests cases to new available options
1 parent 426709e commit eb2cf49

9 files changed

Lines changed: 94 additions & 240 deletions

File tree

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@
7070
"@hookform/resolvers": "^5.2.2",
7171
"@tanstack/react-form": "^1.27.7",
7272
"@zakodium/nmr-types": "^0.5.0",
73-
"@zakodium/nmrium-core": "^0.5.8",
74-
"@zakodium/nmrium-core-plugins": "^0.6.27",
73+
"@zakodium/nmrium-core": "^0.6.0",
74+
"@zakodium/nmrium-core-plugins": "^0.6.29",
7575
"@zakodium/pdnd-esm": "^1.0.2",
7676
"@zip.js/zip.js": "^2.8.15",
7777
"cheminfo-font": "^1.13.1",

src/component/EventsTrackers/KeysListenerTracker.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function KeysListenerTracker(props: KeysListenerTrackerProps) {
5151
changeDisplayViewModeHandler,
5252
} = useToolsFunctions();
5353

54-
const { saveAsJSONHandler } = useExport();
54+
const { defaultSaveAsHandler } = useExport();
5555
const isToolVisible = useCheckToolsVisibility();
5656

5757
const { highlight, remove } = useHighlightData();
@@ -428,7 +428,7 @@ function KeysListenerTracker(props: KeysListenerTrackerProps) {
428428
break;
429429
case 's':
430430
if (isToolVisible('exportAs')) {
431-
saveAsJSONHandler();
431+
defaultSaveAsHandler();
432432
e.preventDefault();
433433
}
434434
break;
@@ -488,7 +488,7 @@ function KeysListenerTracker(props: KeysListenerTrackerProps) {
488488
nuclei,
489489
openLoader,
490490
openSaveAsDialog,
491-
saveAsJSONHandler,
491+
defaultSaveAsHandler,
492492
toaster,
493493
],
494494
);

src/component/hooks/useExport.tsx

Lines changed: 29 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { NmriumState } from '@zakodium/nmrium-core';
22
import { useCallback } from 'react';
33

4-
import type { ExportOptions } from '../../data/SpectraManager.js';
54
import { toJSON } from '../../data/SpectraManager.js';
65
import { useChartData } from '../context/ChartContext.js';
76
import { useCore } from '../context/CoreContext.js';
@@ -10,17 +9,21 @@ import { useToaster } from '../context/ToasterContext.js';
109
import {
1110
browserNotSupportedErrorToast,
1211
copyPNGToClipboard,
13-
exportAsJsonBlob,
1412
exportAsPng,
1513
exportAsSVG,
1614
} from '../utility/export.js';
1715
import { saveAs } from '../utility/save_as.ts';
1816

19-
interface SaveOptions {
20-
include: ExportOptions;
17+
export interface SaveOptions {
18+
include: {
19+
settings: boolean;
20+
view: boolean;
21+
dataType:
22+
| 'NO_DATA'
23+
| 'SELF_CONTAINED'
24+
| 'SELF_CONTAINED_EXTERNAL_DATASOURCE';
25+
};
2126
name: string;
22-
compressed: boolean;
23-
pretty: boolean;
2427
}
2528

2629
export function useExport() {
@@ -29,80 +32,29 @@ export function useExport() {
2932
const preferencesState = usePreferences();
3033
const core = useCore();
3134

32-
const saveAsJSONHandler = useCallback(
33-
(spaceIndent = 0, isCompressed = true) => {
34-
const hideLoading = toaster.showLoading({
35-
message: 'Exporting as NMRium process in progress',
36-
});
37-
setTimeout(async () => {
38-
try {
39-
const name = state.data[0]?.info?.name || 'experiment';
40-
const exportedData = toJSON(core, state, preferencesState, {
41-
exportTarget: 'nmrium',
42-
view: true,
43-
});
44-
45-
const blob = await exportAsJsonBlob(
46-
exportedData,
47-
name,
48-
spaceIndent,
49-
isCompressed,
50-
);
51-
saveAs({ blob, name, extension: '.nmrium' });
52-
} catch (error) {
53-
toaster.show({
54-
intent: 'danger',
55-
message: `Export failed due to an unexpected error: ${(error as Error)?.message || 'Unknown error'}`,
56-
});
57-
reportError(error);
58-
} finally {
59-
hideLoading();
60-
}
61-
}, 0);
62-
},
63-
[core, preferencesState, state, toaster],
64-
);
65-
6635
const saveHandler = useCallback(
6736
(options: SaveOptions) => {
6837
async function handler() {
69-
const { pretty, compressed, include } = options;
7038
const name = options.name || 'experiment';
71-
const exportArchive =
72-
include.dataType?.startsWith('SELF_CONTAINED') ?? false;
39+
const include = options.include;
7340

7441
const hideLoading = toaster.showLoading({
75-
message: `Exporting as ${name}.nmrium process in progress`,
42+
message: `Exporting as ${name}.nmrium.zip process in progress`,
7643
});
7744
setTimeout(async () => {
7845
try {
79-
if (!exportArchive) {
80-
const exportedData = toJSON(core, state, preferencesState, {
81-
...include,
82-
serialize: true,
83-
exportTarget: 'nmrium',
84-
});
85-
const spaceIndent = pretty ? 2 : 0;
86-
const blob = await exportAsJsonBlob(
87-
exportedData,
88-
name,
89-
spaceIndent,
90-
compressed,
91-
);
92-
93-
return saveAs({ blob, name, extension: '.nmrium' });
94-
}
95-
9646
const nmriumState = toJSON(core, state, preferencesState, {
9747
serialize: false,
9848
exportTarget: 'nmrium',
9949
}) as NmriumState;
10050
const archive = await core.serializeNmriumArchive({
10151
state: nmriumState,
10252
aggregator: state.aggregator,
103-
includeData: options.include.dataType === 'SELF_CONTAINED',
104-
includeSettings: options.include.settings,
105-
includeView: options.include.view,
53+
includeData: include.dataType !== 'NO_DATA',
54+
externalData:
55+
include.dataType === 'SELF_CONTAINED' ? 'embedded' : 'linked',
56+
includeSettings: include.settings,
57+
includeView: include.view,
10658
});
10759
const zipBlob = new Blob([archive], {
10860
type: 'chemical/x-nmrium+zip',
@@ -125,8 +77,20 @@ export function useExport() {
12577
[core, preferencesState, state, toaster],
12678
);
12779

80+
const defaultName = state.data[0]?.info?.name || 'experiment';
81+
const defaultSaveAsHandler = useCallback(() => {
82+
saveHandler({
83+
name: defaultName,
84+
include: {
85+
dataType: 'SELF_CONTAINED',
86+
view: true,
87+
settings: false,
88+
},
89+
});
90+
}, [saveHandler, defaultName]);
91+
12892
return {
129-
saveAsJSONHandler,
93+
defaultSaveAsHandler,
13094
saveHandler,
13195
};
13296
}

src/component/modal/SaveAsModal.tsx

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,23 @@ import {
88
import { useMemo } from 'react';
99
import { Controller, useForm } from 'react-hook-form';
1010

11-
import type { ExportOptions } from '../../data/SpectraManager.js';
1211
import { DataExportOptions } from '../../data/SpectraManager.js';
1312
import { useChartData } from '../context/ChartContext.js';
1413
import ActionButtons from '../elements/ActionButtons.js';
1514
import { Input2Controller } from '../elements/Input2Controller.js';
1615
import type { LabelStyle } from '../elements/Label.js';
1716
import Label from '../elements/Label.js';
1817
import { StyledDialogBody } from '../elements/StyledDialogBody.js';
19-
import useCheckExperimentalFeature from '../hooks/useCheckExperimentalFeature.js';
18+
import type { SaveOptions } from '../hooks/useExport.js';
2019
import { useExport } from '../hooks/useExport.js';
2120

22-
const INITIAL_VALUE = {
21+
const INITIAL_VALUE: SaveOptions = {
2322
name: '',
24-
compressed: false,
25-
pretty: false,
2623
include: {
27-
dataType: DataExportOptions.RAW_DATA,
24+
dataType: DataExportOptions.SELF_CONTAINED,
2825
view: false,
2926
settings: false,
30-
} satisfies ExportOptions,
27+
},
3128
};
3229

3330
export const labelStyle: LabelStyle = {
@@ -57,15 +54,14 @@ function SaveAsModal(props: SaveAsModalProps) {
5754

5855
return <InnerSaveAsModal onCloseDialog={onCloseDialog} />;
5956
}
57+
6058
function InnerSaveAsModal(props: InnerSaveAsModalProps) {
6159
const { onCloseDialog } = props;
62-
const { sources, data, aggregator } = useChartData();
60+
const { data, aggregator } = useChartData();
6361
const { saveHandler } = useExport();
64-
const experimentalFlagEnabled = useCheckExperimentalFeature();
65-
6662
const fileName = data[0]?.info?.name;
6763

68-
function submitHandler(values: any) {
64+
function submitHandler(values: SaveOptions) {
6965
saveHandler(values);
7066
onCloseDialog?.();
7167
}
@@ -94,12 +90,6 @@ function InnerSaveAsModal(props: InnerSaveAsModalProps) {
9490
controllerProps={{ rules: { required: true } }}
9591
/>
9692
</Label>
97-
<Label style={labelStyle} title="Compressed">
98-
<Checkbox style={{ margin: 0 }} {...register(`compressed`)} />
99-
</Label>
100-
<Label style={labelStyle} title="Pretty format">
101-
<Checkbox style={{ margin: 0 }} {...register(`pretty`)} />
102-
</Label>
10393
<Label style={labelStyle} title="Include view">
10494
<Checkbox style={{ margin: 0 }} {...register(`include.view`)} />
10595
</Label>
@@ -114,33 +104,16 @@ function InnerSaveAsModal(props: InnerSaveAsModalProps) {
114104
const { value, ref, ...otherFieldProps } = field;
115105
return (
116106
<RadioGroup inline selectedValue={value} {...otherFieldProps}>
117-
<Radio label="Raw data" value={DataExportOptions.RAW_DATA} />
118107
<Radio
119-
label="Data source"
120-
value={DataExportOptions.DATA_SOURCE}
121-
disabled={Object.keys(sources).length === 0}
108+
label="External data embed"
109+
value={DataExportOptions.SELF_CONTAINED}
110+
/>
111+
<Radio
112+
label="External data linked"
113+
disabled={!containsLinkedFiles}
114+
value={DataExportOptions.SELF_CONTAINED_EXTERNAL_DATASOURCE}
122115
/>
123116
<Radio label="No data" value={DataExportOptions.NO_DATA} />
124-
125-
{/*
126-
* Radio group works with Children.map.
127-
* So Radio must be direct children of RadioGroup.
128-
*/}
129-
{experimentalFlagEnabled && (
130-
<Radio
131-
label="Full data (external data embed, experimental)"
132-
value={DataExportOptions.SELF_CONTAINED}
133-
/>
134-
)}
135-
{experimentalFlagEnabled && (
136-
<Radio
137-
label="Full data (external data linked, experimental)"
138-
disabled={!containsLinkedFiles}
139-
value={
140-
DataExportOptions.SELF_CONTAINED_EXTERNAL_DATASOURCE
141-
}
142-
/>
143-
)}
144117
</RadioGroup>
145118
);
146119
}}

src/component/reducer/actions/LoadAction.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,11 @@ function initData(
292292
if (view) {
293293
const defaultViewState = getDefaultViewState();
294294
draft.view = lodashMerge(defaultViewState, view);
295+
draft.view.molecules = Object.fromEntries(
296+
Object.entries(draft.view.molecules).filter(([id]) =>
297+
draft.molecules.some((molecule) => molecule.id === id),
298+
),
299+
);
295300
}
296301
draft.actionType = action.type;
297302
draft.isLoading = false;
@@ -335,6 +340,12 @@ function handleLoadDropFiles(draft: Draft<State>, action: LoadDropFilesAction) {
335340
draft.sources = {};
336341
}
337342

343+
draft.view.molecules = Object.fromEntries(
344+
Object.entries(draft.view.molecules).filter(([id]) =>
345+
draft.molecules.some((molecule) => molecule.id === id),
346+
),
347+
);
348+
338349
draft.actionType = type;
339350
draft.isLoading = false;
340351
return undefined;

0 commit comments

Comments
 (0)