From 745308abab6d1c8d5b30f5bd36978ab60d7e39f8 Mon Sep 17 00:00:00 2001
From: tpoisseau <22891227+tpoisseau@users.noreply.github.com>
Date: Mon, 28 Jul 2025 11:02:32 +0200
Subject: [PATCH 1/4] feat(plugin): support `topbar_right` plugin ui slot
* add `PluginUITopBar` demo view
* use yalc link (nmrium-core ui slot is in dev)
Refs: https://github.com/zakodium/nmrium/issues/350
---
src/component/header/Header.tsx | 15 ++++
src/component/utility/renderCoreSlot.tsx | 11 +++
src/demo/samples.json | 10 +++
src/demo/views/PluginUITopBar.tsx | 97 ++++++++++++++++++++++++
src/demo/views/Test.tsx | 2 +-
src/demo/views/View.helpers.ts | 41 ++++++++++
src/demo/views/View.tsx | 39 +---------
src/demo/views/index.ts | 1 +
8 files changed, 179 insertions(+), 37 deletions(-)
create mode 100644 src/component/utility/renderCoreSlot.tsx
create mode 100644 src/demo/views/PluginUITopBar.tsx
create mode 100644 src/demo/views/View.helpers.ts
diff --git a/src/component/header/Header.tsx b/src/component/header/Header.tsx
index 26a5eab748..443eda172b 100644
--- a/src/component/header/Header.tsx
+++ b/src/component/header/Header.tsx
@@ -1,3 +1,4 @@
+import styled from '@emotion/styled';
import { memo, useMemo } from 'react';
import {
FaFilm,
@@ -9,6 +10,7 @@ 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,
@@ -26,6 +28,7 @@ import AboutUsModal from '../modal/aboutUs/AboutUsModal.js';
import GeneralSettingsModal from '../modal/setting/GeneralSettings.js';
import WorkspaceItem from '../modal/setting/WorkspaceItem.js';
import { options } from '../toolbar/ToolTypes.js';
+import { renderCoreSlot } from '../utility/renderCoreSlot.js';
import { AutoPeakPickingOptionPanel } from './AutoPeakPickingOptionPanel.js';
import { HeaderWrapper } from './HeaderWrapper.js';
@@ -60,6 +63,7 @@ interface HeaderInnerProps {
function HeaderInner(props: HeaderInnerProps) {
const { selectedOptionPanel, height } = props;
+ const core = useCore();
const {
current: {
@@ -144,6 +148,10 @@ function HeaderInner(props: HeaderInnerProps) {
alignItems: 'center',
}}
>
+
+ {renderCoreSlot(core, 'topbar_right')}
+
+
{!hideWorkspaces && (
);
}
+
+const PluginTopBarRight = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ margin-right: 5px;
+`;
diff --git a/src/component/utility/renderCoreSlot.tsx b/src/component/utility/renderCoreSlot.tsx
new file mode 100644
index 0000000000..924ee2bbdf
--- /dev/null
+++ b/src/component/utility/renderCoreSlot.tsx
@@ -0,0 +1,11 @@
+import type { NMRiumCore, SupportedUISlot } from '@zakodium/nmrium-core';
+import type { ReactNode } from 'react';
+
+export function renderCoreSlot(
+ core: NMRiumCore,
+ slot: SupportedUISlot,
+): ReactNode[] {
+ return Array.from(core.slot(slot), ([key, Component]) => (
+
+ ));
+}
diff --git a/src/demo/samples.json b/src/demo/samples.json
index 0c676502e9..a39abc200d 100644
--- a/src/demo/samples.json
+++ b/src/demo/samples.json
@@ -345,5 +345,15 @@
"width": "1000px",
"height": "600px"
}
+ },
+ {
+ "groupName": "Plugin UI",
+ "children": [
+ {
+ "title": "TopBar Plugin",
+ "file": "./data/cytisine/1H.json",
+ "view": "PluginUITopBar"
+ }
+ ]
}
]
diff --git a/src/demo/views/PluginUITopBar.tsx b/src/demo/views/PluginUITopBar.tsx
new file mode 100644
index 0000000000..73bdbf301f
--- /dev/null
+++ b/src/demo/views/PluginUITopBar.tsx
@@ -0,0 +1,97 @@
+import type { IconName } from '@blueprintjs/icons';
+import { IconNames } from '@blueprintjs/icons';
+import styled from '@emotion/styled';
+import init from '@zakodium/nmrium-core-plugins';
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+} from 'react';
+import { Toolbar } from 'react-science/ui';
+
+import { NMRium } from '../../component/main/index.js';
+
+import type { ViewProps } from './View.helpers.js';
+import { useView } from './View.helpers.js';
+
+const Container = styled.section`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ & > h1 {
+ font-weight: 700;
+ font-size: 1.5em;
+ line-height: 1.4em;
+ padding: 0.75em;
+ }
+`;
+
+const NMRiumContainer = styled.div`
+ flex: 1;
+`;
+
+const DemoContext = createContext<{
+ setRandom: () => void;
+ icon: IconName;
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+}>({ icon: 'console', setRandom: () => {} });
+
+function DemoTopBarRight() {
+ const { setRandom, icon } = useContext(DemoContext);
+
+ return (
+
+
+
+ );
+}
+
+const core = init();
+core.registerPlugin({
+ id: 'demo-plugin-topbar',
+ version: 1,
+ migrations: [],
+ ui: {
+ // eslint-disable-next-line camelcase
+ topbar_right: DemoTopBarRight,
+ },
+});
+
+const possibleIcons = Object.values(IconNames);
+export default function PluginUITopBar(props: ViewProps) {
+ const [data, otherProps] = useView(props);
+
+ const { workspace, customWorkspaces } = otherProps;
+
+ const [icon, setIcon] = useState('console');
+ const setRandom = useCallback(() => {
+ const randomIndex = Math.floor(Math.random() * possibleIcons.length);
+ setIcon(possibleIcons[randomIndex]);
+ }, []);
+ const context = useMemo(() => ({ setRandom, icon }), [icon, setRandom]);
+
+ return (
+
+ TopBar Plugin
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/demo/views/Test.tsx b/src/demo/views/Test.tsx
index 107a6dd704..16278d28b3 100644
--- a/src/demo/views/Test.tsx
+++ b/src/demo/views/Test.tsx
@@ -5,7 +5,7 @@ import { DropZone } from 'react-science/ui';
import { NMRium } from '../../component/main/index.js';
-import { loadData } from './View.js';
+import { loadData } from './View.helpers.js';
function searchDeep(obj, searchKey) {
const result: any = [];
diff --git a/src/demo/views/View.helpers.ts b/src/demo/views/View.helpers.ts
new file mode 100644
index 0000000000..72a5bf43e1
--- /dev/null
+++ b/src/demo/views/View.helpers.ts
@@ -0,0 +1,41 @@
+import { useEffect, useState } from 'react';
+
+import type { BaseViewProps } from './BaseView.js';
+
+export async function loadData(file) {
+ const response = await fetch(file);
+ checkStatus(response);
+ const data = await response.json();
+ return data;
+}
+
+function checkStatus(response) {
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status} - ${response.statusText}`);
+ }
+ return response;
+}
+
+export interface ViewProps extends Omit {
+ file: string;
+ baseURL: string;
+}
+
+export function useView(props: ViewProps) {
+ const [data, setData] = useState();
+
+ const { file, baseURL, ...otherProps } = props;
+
+ useEffect(() => {
+ if (file) {
+ void loadData(file).then((d) => {
+ const _d = JSON.parse(JSON.stringify(d).replaceAll(/\.\/+?/g, baseURL));
+ setData(_d);
+ });
+ } else {
+ setData(undefined);
+ }
+ }, [baseURL, file]);
+
+ return [data, otherProps] as const;
+}
diff --git a/src/demo/views/View.tsx b/src/demo/views/View.tsx
index 41140dc9c1..5dae3fef8f 100644
--- a/src/demo/views/View.tsx
+++ b/src/demo/views/View.tsx
@@ -1,42 +1,9 @@
-import { useEffect, useState } from 'react';
-
-import type { BaseViewProps } from './BaseView.js';
import BaseView from './BaseView.js';
-
-export async function loadData(file) {
- const response = await fetch(file);
- checkStatus(response);
- const data = await response.json();
- return data;
-}
-
-function checkStatus(response) {
- if (!response.ok) {
- throw new Error(`HTTP ${response.status} - ${response.statusText}`);
- }
- return response;
-}
-
-interface ViewProps extends Omit {
- file: string;
- baseURL: string;
-}
+import type { ViewProps } from './View.helpers.js';
+import { useView } from './View.helpers.js';
export default function View(props: ViewProps) {
- const [data, setData] = useState();
-
- const { file, baseURL, ...otherProps } = props;
-
- useEffect(() => {
- if (file) {
- void loadData(file).then((d) => {
- const _d = JSON.parse(JSON.stringify(d).replaceAll(/\.\/+?/g, baseURL));
- setData(_d);
- });
- } else {
- setData(undefined);
- }
- }, [baseURL, file]);
+ const [data, otherProps] = useView(props);
return ;
}
diff --git a/src/demo/views/index.ts b/src/demo/views/index.ts
index 34c0e3f902..81f878ef32 100644
--- a/src/demo/views/index.ts
+++ b/src/demo/views/index.ts
@@ -16,4 +16,5 @@ export const possibleViews = {
TwoInstances: memo(lazy(() => import('./TwoInstances.js'))),
View: memo(lazy(() => import('./View.js'))),
WebSourceView: memo(lazy(() => import('./WebSourceView.js'))),
+ PluginUITopBar: memo(lazy(() => import('./PluginUITopBar.js'))),
};
From 639fa93cbdee2ddbb556110877e6afc983d8de4d Mon Sep 17 00:00:00 2001
From: tpoisseau <22891227+tpoisseau@users.noreply.github.com>
Date: Mon, 28 Jul 2025 16:49:42 +0200
Subject: [PATCH 2/4] refactor: rename slot to not break eslint `camelCase`
rule
Refs: https://github.com/zakodium/nmrium/pull/351#discussion_r2236629788
---
eslint.config.js | 9 +++++----
src/demo/views/PluginUITopBar.tsx | 3 +--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/eslint.config.js b/eslint.config.js
index 6c4ee50b1f..597dc78308 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -3,7 +3,6 @@ import ts from 'eslint-config-cheminfo-typescript/base';
import unicorn from 'eslint-config-cheminfo-typescript/unicorn';
export default [
-
{
ignores: [
'build',
@@ -14,15 +13,16 @@ export default [
'playwright-report',
'test-results',
'public',
+ '.yalc',
],
},
...ts,
...unicorn,
...react,
{
- files: ["**/pdnd.cts"],
+ files: ['**/pdnd.cts'],
rules: {
- "import/no-extraneous-dependencies": "off",
+ 'import/no-extraneous-dependencies': 'off',
},
},
{
@@ -54,7 +54,8 @@ export default [
{
name: '@simbathesailor/use-what-changed',
message: 'Remove use-what-changed before committing the code',
- }],
+ },
+ ],
'react/no-unknown-property': ['error', { ignore: ['css'] }],
'react/forbid-dom-props': [
'error',
diff --git a/src/demo/views/PluginUITopBar.tsx b/src/demo/views/PluginUITopBar.tsx
index 73bdbf301f..4b0cd98a08 100644
--- a/src/demo/views/PluginUITopBar.tsx
+++ b/src/demo/views/PluginUITopBar.tsx
@@ -60,8 +60,7 @@ core.registerPlugin({
version: 1,
migrations: [],
ui: {
- // eslint-disable-next-line camelcase
- topbar_right: DemoTopBarRight,
+ 'topbar.right': DemoTopBarRight,
},
});
From fa5fb8af931e8aae00a9b1dfd7e697b0a2ecd5ca Mon Sep 17 00:00:00 2001
From: tpoisseau <22891227+tpoisseau@users.noreply.github.com>
Date: Mon, 28 Jul 2025 17:17:35 +0200
Subject: [PATCH 3/4] chore: adapt to proper slot name convention
---
src/component/header/Header.tsx | 2 +-
src/demo/views/PluginUITopBar.tsx | 17 +++++++++--------
2 files changed, 10 insertions(+), 9 deletions(-)
diff --git a/src/component/header/Header.tsx b/src/component/header/Header.tsx
index 443eda172b..77e326d7a9 100644
--- a/src/component/header/Header.tsx
+++ b/src/component/header/Header.tsx
@@ -149,7 +149,7 @@ function HeaderInner(props: HeaderInnerProps) {
}}
>
- {renderCoreSlot(core, 'topbar_right')}
+ {renderCoreSlot(core, 'topbar.right')}
{!hideWorkspaces && (
diff --git a/src/demo/views/PluginUITopBar.tsx b/src/demo/views/PluginUITopBar.tsx
index 4b0cd98a08..855e0a6732 100644
--- a/src/demo/views/PluginUITopBar.tsx
+++ b/src/demo/views/PluginUITopBar.tsx
@@ -54,15 +54,16 @@ function DemoTopBarRight() {
);
}
-const core = init();
-core.registerPlugin({
- id: 'demo-plugin-topbar',
- version: 1,
- migrations: [],
- ui: {
- 'topbar.right': DemoTopBarRight,
+const core = init([
+ {
+ id: 'demo-plugin-topbar',
+ version: 1,
+ migrations: [],
+ ui: {
+ 'topbar.right': DemoTopBarRight,
+ },
},
-});
+]);
const possibleIcons = Object.values(IconNames);
export default function PluginUITopBar(props: ViewProps) {
From ea7f571661ee4d2fd14ea4ece8ea1af388546f41 Mon Sep 17 00:00:00 2001
From: tpoisseau <22891227+tpoisseau@users.noreply.github.com>
Date: Tue, 29 Jul 2025 10:04:31 +0200
Subject: [PATCH 4/4] chore: update core and plugins version
no need yalc link anymore
---
package-lock.json | 22 +++++++++++-----------
package.json | 4 ++--
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 21efefddc9..997b1df39f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,8 +18,8 @@
"@emotion/styled": "^11.14.1",
"@hookform/resolvers": "^5.1.1",
"@zakodium/nmr-types": "^0.1.0",
- "@zakodium/nmrium-core": "^0.1.4",
- "@zakodium/nmrium-core-plugins": "^0.1.7",
+ "@zakodium/nmrium-core": "^0.1.5",
+ "@zakodium/nmrium-core-plugins": "^0.1.8",
"@zip.js/zip.js": "^2.7.68",
"cheminfo-font": "^1.13.1",
"cheminfo-types": "^1.8.1",
@@ -4034,14 +4034,14 @@
}
},
"node_modules/@zakodium/nmrium-core": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.1.4.tgz",
- "integrity": "sha512-X47fJ+xKtuK93KJKzYTVf+R7GpVNLepuzIJ8V7q2AQxECHh+iCGeLhVL4DAUiULwa5H9x3xlD9xEatJdYeNHhg==",
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.1.5.tgz",
+ "integrity": "sha512-0QcPQ/EaS3LeDH9lsCBLRdQRo1KFhRWQbbdkIIPdYeykH8EfWUl5DVJQ56KBGVQjuKY3ddvrHDCVdlNDKvDtgA==",
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
"cheminfo-types": "^1.8.1",
"fifo-logger": "^2.0.0",
- "file-collection": "^5.0.0",
+ "file-collection": "^5.1.0",
"is-any-array": "^2.0.1",
"lodash.merge": "^4.6.2",
"ml-spectra-processing": "^14.12.0",
@@ -4049,15 +4049,15 @@
}
},
"node_modules/@zakodium/nmrium-core-plugins": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core-plugins/-/nmrium-core-plugins-0.1.7.tgz",
- "integrity": "sha512-M1sgSsB79KAIRNCePVKQFcoJcev0cy3otRePMx1776KMDP8+1qMB0mfWRKR5rDRgUaJrC7Ifs1Q67StJ+IkJoA==",
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core-plugins/-/nmrium-core-plugins-0.1.8.tgz",
+ "integrity": "sha512-elSFr5P2gwcK53wyxLDeVJdUGC7uR9bdOpogAsKt6dDhIPPeXtdzUPJcO/vwJ7P8zW+UMGB3hDYW07LiB1EDgQ==",
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
- "@zakodium/nmrium-core": "^0.1.4",
+ "@zakodium/nmrium-core": "^0.1.5",
"cheminfo-types": "^1.8.1",
"convert-to-jcamp": "^5.4.11",
- "file-collection": "^5.0.0",
+ "file-collection": "^5.1.0",
"gyromagnetic-ratio": "^2.0.0",
"is-any-array": "^2.0.1",
"jcampconverter": "^11.0.4",
diff --git a/package.json b/package.json
index 358ce259b2..242d9d0e56 100644
--- a/package.json
+++ b/package.json
@@ -64,8 +64,8 @@
"@emotion/styled": "^11.14.1",
"@hookform/resolvers": "^5.1.1",
"@zakodium/nmr-types": "^0.1.0",
- "@zakodium/nmrium-core": "^0.1.4",
- "@zakodium/nmrium-core-plugins": "^0.1.7",
+ "@zakodium/nmrium-core": "^0.1.5",
+ "@zakodium/nmrium-core-plugins": "^0.1.8",
"@zip.js/zip.js": "^2.7.68",
"cheminfo-font": "^1.13.1",
"cheminfo-types": "^1.8.1",