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",