Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions src/component/elements/ToolbarPopoverItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function ToolbarPopoverItem<T = object>(
) {
const {
options,
onClick,
onClick: onClickHandler,
icon,
tooltip,
tooltipProps,
Expand Down Expand Up @@ -73,7 +73,6 @@ export function ToolbarPopoverItem<T = object>(
return (
<Tooltip
key={JSON.stringify({ data, text })}
disabled={disabled ?? !option.tooltip}
content={
typeof tooltip === 'string' ? (
tooltip
Expand All @@ -90,14 +89,22 @@ export function ToolbarPopoverItem<T = object>(
placement: 'right',
...tooltipProps,
})}
renderTarget={({ isOpen, ...props }) => (
<MenuItem
disabled={disabled}
text={text}
onClick={() => onClick?.(data)}
{...otherOptions}
{...props}
/>
renderTarget={({ isOpen, ...targetProps }) => (
<span
{...targetProps}
style={{
display: 'block',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
}}
>
<MenuItem
text={text}
{...otherOptions}
disabled={disabled}
onClick={() => !disabled && onClickHandler?.(data)}
/>
</span>
)}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@

type API = z.input<typeof externalAPIWithUUIDValidation>;
function emptyApi(): API {
return { key: 'CT', serverLink: '', APIKey: '', uuid: crypto.randomUUID() };
return {
uuid: crypto.randomUUID(),
key: 'ct',

Check failure on line 22 in src/component/modal/setting/tanstack_general_settings/tabs/external_api_tab.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Type '"ct"' is not assignable to type '"CT"'.

Check failure on line 22 in src/component/modal/setting/tanstack_general_settings/tabs/external_api_tab.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Type '"ct"' is not assignable to type '"CT"'.
name: '',
description: '',
apiUrl: '',
apiKey: '',
};
}

const itemsAPI = EXTERNAL_API_KEYS.slice();
Expand All @@ -34,13 +41,16 @@
return [
helper.accessor('key', {
header: 'Service',
meta: {
tdStyle: { width: '180px' },
},
cell: ({ row: { index } }) => (
<Field name={`${name}[${index}].key`}>
{(field) => (
<Select2
items={itemsAPI}
itemValueKey="key"
itemTextKey="description"
itemTextKey="label"

Check failure on line 53 in src/component/modal/setting/tanstack_general_settings/tabs/external_api_tab.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Type '"label"' is not assignable to type '"key" | "description" | undefined'.

Check failure on line 53 in src/component/modal/setting/tanstack_general_settings/tabs/external_api_tab.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Type '"label"' is not assignable to type '"key" | "description" | undefined'.
selectedItemValue={field.state.value}
onItemSelect={({ key }) => field.handleChange(key)}
intent={!field.state.meta.isValid ? 'danger' : 'none'}
Expand All @@ -50,18 +60,35 @@
</Field>
),
}),
helper.accessor('serverLink', {
header: 'Server link',
helper.accessor('name', {
header: 'Name',
cell: ({ row: { index } }) => (
<Field name={`${name}[${index}].serverLink`}>
<Field name={`${name}[${index}].name`}>
{(field) => <CellInput field={field} />}
</Field>
),
}),
helper.accessor('APIKey', {
helper.accessor('description', {
header: 'Description',
cell: ({ row: { index } }) => (
<Field name={`${name}[${index}].description`}>
{(field) => <CellInput field={field} />}
</Field>
),
}),
helper.accessor('apiUrl', {
header: 'API link',
cell: ({ row: { index } }) => (
<Field name={`${name}[${index}].apiUrl`}>
{(field) => <CellInput field={field} />}
</Field>
),
}),

helper.accessor('apiKey', {
header: 'API key',
cell: ({ row: { index } }) => (
<Field name={`${name}[${index}].APIKey`}>
<Field name={`${name}[${index}].apiKey`}>
{(field) => <CellInput field={field} />}
</Field>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { withUUID } from './utils.ts';

const externalAPIValidation = z.object({
key: z.enum(EXTERNAL_API_KEYS.map(({ key }) => key)),
serverLink: z.url(),
APIKey: z.string(),
name: z.string().nonempty(),
description: z.string(),
apiUrl: z.url().nonempty(),
apiKey: z.string(),
});
export const externalAPIWithUUIDValidation = withUUID(externalAPIValidation);
export const externalAPIsValidation = z.array(externalAPIWithUUIDValidation);
177 changes: 177 additions & 0 deletions src/component/panels/SpectraPanel/SpectraPanelHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import type { MaybeElement } from '@blueprintjs/core';
import type { IconName } from '@blueprintjs/icons';
import type {
ActiveSpectrum,
ExternalAPIKeyType,
Spectrum2D,
Spectrum,
} from '@zakodium/nmrium-core';
import { SvgNmrResetScale, SvgNmrSameTop } from 'cheminfo-font';
import { memo, useCallback } from 'react';
import { AiOutlineApi } from 'react-icons/ai';
import { FaCreativeCommonsSamplingPlus } from 'react-icons/fa';
import { IoColorPaletteOutline } from 'react-icons/io5';
import { MdFormatColorFill, MdOutlineFormatColorText } from 'react-icons/md';

import { isSpectrum1D } from '../../../data/data1d/Spectrum1D/isSpectrum1D.ts';
import { useChartData } from '../../context/ChartContext.js';
import { useDispatch } from '../../context/DispatchContext.js';
import { usePreferences } from '../../context/PreferencesContext.js';
import { useToaster } from '../../context/ToasterContext.js';
import { useAlert } from '../../elements/Alert.js';
import type { ToolbarPopoverMenuItem } from '../../elements/ToolbarPopoverItem.tsx';
import { ToolbarPopoverItem } from '../../elements/ToolbarPopoverItem.tsx';
import { useActiveSpectra } from '../../hooks/useActiveSpectra.js';
import { useSelectedSpectra } from '../../hooks/useSelectedSpectra.ts';
import useSpectrum from '../../hooks/useSpectrum.js';
import { useToggleSpectraVisibility } from '../../hooks/useToggleSpectraVisibility.js';
import type { DisplayerMode } from '../../reducer/Reducer.js';
Expand All @@ -24,6 +32,81 @@
import DefaultPanelHeader from '../header/DefaultPanelHeader.js';
import { SpectraAutomaticPickingButton } from '../header/SpectraAutomaticPickingButton.js';

interface ItemContext {
spectra: Spectrum[] | null;
activeTab: string;
}

interface ExternalApiItem {
icon: IconName | MaybeElement;
include?: (item: ItemContext) => boolean;
disable?: (item: ItemContext) => boolean;
disableMessage?: string;
}

const EXTERNAL_API_DEFINITIONS: Record<ExternalAPIKeyType, ExternalApiItem> = {
ct: {

Check failure on line 48 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Object literal may only specify known properties, but 'ct' does not exist in type 'Record<"CT", ExternalApiItem>'. Did you mean to write 'CT'?

Check failure on line 48 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Object literal may only specify known properties, but 'ct' does not exist in type 'Record<"CT", ExternalApiItem>'. Did you mean to write 'CT'?
icon: <AiOutlineApi />,
disable: ({ spectra, activeTab }) => {

Check failure on line 50 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Binding element 'activeTab' implicitly has an 'any' type.

Check failure on line 50 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Binding element 'spectra' implicitly has an 'any' type.

Check failure on line 50 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Binding element 'activeTab' implicitly has an 'any' type.

Check failure on line 50 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Binding element 'spectra' implicitly has an 'any' type.
return activeTab !== '1H' || !spectra || spectra.length === 0;
},
disableMessage:
'CT can only be used when there is at least one proton (1H) spectrum selected',
},

mixonat: {
icon: <AiOutlineApi />,
disable: ({ spectra, activeTab }) => {

Check failure on line 59 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Binding element 'activeTab' implicitly has an 'any' type.

Check failure on line 59 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Binding element 'spectra' implicitly has an 'any' type.

Check failure on line 59 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Binding element 'activeTab' implicitly has an 'any' type.

Check failure on line 59 in src/component/panels/SpectraPanel/SpectraPanelHeader.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Binding element 'spectra' implicitly has an 'any' type.
return activeTab !== '13C' || !spectra || spectra.length === 0;
},
disableMessage:
'MixOnat can only be used when there is at least one carbon (13C) spectrum selected',
},
};

interface ExternalMenuOptions {
id: ExternalAPIKeyType;
apiUrl: string;
apiKey?: string;
}

function useExternalApiMenuItems(): Array<
ToolbarPopoverMenuItem<ExternalMenuOptions>
> {
const spectra = useSelectedSpectra();
const {
view: {
spectra: { activeTab },
},
} = useChartData();
const {
current: { externalAPIs },
} = usePreferences();

return externalAPIs
.filter(({ key }) => {
const option = EXTERNAL_API_DEFINITIONS?.[key];
return !option?.include || option.include({ spectra, activeTab });
})
.map((options) => {
const { key, name, description, apiUrl, apkKey } = options;
const { icon, disable, disableMessage } =
EXTERNAL_API_DEFINITIONS?.[key] || {};
const isDisabled = disable ? disable({ spectra, activeTab }) : false;

return {
icon,
text: name,
data: { id: key, apiUrl, apkKey },
tooltip: {
title: name,
description: isDisabled ? disableMessage : description,
},
disabled: isDisabled,
tooltipProps: { intent: isDisabled ? 'danger' : undefined },
};
});
}
function getMissingProjection(spectraData: any, activeTab: any) {
let nucleus = activeTab.split(',');
nucleus = nucleus[0] === nucleus[1] ? [nucleus[0]] : nucleus;
Expand All @@ -36,6 +119,22 @@
}
return missingNucleus;
}

function buildMixonatData(spectra: Spectrum[]) {
const result = [];

for (const spectrum of spectra) {
if (!isSpectrum1D(spectrum)) continue;

result.push({
info: spectrum.info,
peaks: spectrum.peaks.values.map(({ x, y }) => ({ x, y })),
});
}

return { spectra: result };
}

interface SpectraPanelHeaderProps {
onSettingClick: () => void;
}
Expand Down Expand Up @@ -138,6 +237,74 @@
: {},
);

const externalApiMenuItems = useExternalApiMenuItems();
const spectra = useSelectedSpectra();

function handleSendToMixonat(options: ExternalMenuOptions) {
const { apiUrl: url } = options;

if (!spectra) return;

if (!url) {
toaster.show({
message: 'MixOnat URL is not configured',
intent: 'danger',
});
return;
}

setTimeout(async () => {
const hideLoading = await toaster.showAsyncLoading({
message: 'Sending spectra to Mixonat...',
});

try {
const body = buildMixonatData(spectra);

const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});

if (!response.ok) {
throw new Error(
`Failed to submit the selected spectra to MixOnat (HTTP ${response.status}).`,
);
}

const result = await response.json();
// eslint-disable-next-line no-console
console.log('MixOnat response:', result);
toaster.show({
message: 'Spectra successfully sent to MixOnat',
intent: 'success',
});
} catch (error) {
toaster.show({
message: `MixOnat error: ${error instanceof Error ? error.message : 'Unknown error'}`,
intent: 'danger',
});
} finally {
hideLoading();
}
}, 0);
}

function handleServiceClick(selected?: ExternalMenuOptions) {
if (!selected) return;

const { id } = selected;

if (id === 'ct') {
return;
}

if (id === 'mixonat') {
return handleSendToMixonat(selected);
}
}

let leftButtons: ToolbarItemProps[] =
getToggleVisibilityButtons(!hasActiveSpectra);

Expand Down Expand Up @@ -184,6 +351,16 @@
active: spectraLabel.visible,
onClick: toggleSpectraLabelHandler,
},
{
component: (
<ToolbarPopoverItem
options={externalApiMenuItems}
onClick={handleServiceClick}
icon="more"
id="trigger-external-service"
/>
),
},
]);

return (
Expand Down
Loading