fix: Several new worklist issues (#6130)
* fix: Several new worklist issues * refactor: Export a single OnStudyDoubleClick type from the StudyList barrel Addresses PR review feedback: the double-click handler signature was written out in both TableProps and the WorkList customization cast, so the two could drift apart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: Run worklist study double-click as a command, registrable by modes - Modes can now export getCommandsModule on their definition; appInit registers it (via the new ExtensionManager.registerCommandsModule) before the mode is instantiated, in a new 'WORKLIST' commands context, so the commands are available on the worklist before any mode route is entered. - The workList.onStudyDoubleClick customization is now a command run input (name/options) instead of a bare function, defaulting to the new launchDefaultMode command, which launches the default workflow falling back to the first applicable one. commandOptions.workflowId overrides it to a specific mode. - Duplicate mode ids are now skipped before running their modeFactory rather than after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
dbb8b1526e
commit
6b6761088b
@ -842,6 +842,33 @@ const commandsModule = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches a workflow (mode) for a study from the worklist. This is the
|
||||||
|
* default `workList.onStudyDoubleClick` command.
|
||||||
|
*
|
||||||
|
* @param study - the StudyRow the action applies to
|
||||||
|
* @param workflows - the workflows applicable to the study, in menu order;
|
||||||
|
* each has `id`, `displayName`, `isDefault` and `launchWithStudy(study)`
|
||||||
|
* @param defaultWorkflow - the user's default workflow when it applies to
|
||||||
|
* the study
|
||||||
|
* @param workflowId - command option to force a specific workflow (mode id)
|
||||||
|
* instead of the default/first applicable one
|
||||||
|
*/
|
||||||
|
launchDefaultMode: ({ study, workflows = [], defaultWorkflow, workflowId }) => {
|
||||||
|
const workflow = workflowId
|
||||||
|
? workflows.find(w => w.id === workflowId)
|
||||||
|
: (defaultWorkflow ?? workflows[0]);
|
||||||
|
if (!workflow) {
|
||||||
|
console.warn(
|
||||||
|
workflowId
|
||||||
|
? `launchDefaultMode: workflow '${workflowId}' is not applicable to the study`
|
||||||
|
: 'launchDefaultMode: no workflow is applicable to the study'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workflow.launchWithStudy(study);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
@ -871,6 +898,10 @@ const commandsModule = ({
|
|||||||
addDisplaySetAsLayer: actions.addDisplaySetAsLayer,
|
addDisplaySetAsLayer: actions.addDisplaySetAsLayer,
|
||||||
removeDisplaySetLayer: actions.removeDisplaySetLayer,
|
removeDisplaySetLayer: actions.removeDisplaySetLayer,
|
||||||
createStoreFunction: actions.createStoreFunction,
|
createStoreFunction: actions.createStoreFunction,
|
||||||
|
launchDefaultMode: {
|
||||||
|
commandFn: actions.launchDefaultMode,
|
||||||
|
context: 'WORKLIST',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -67,6 +67,29 @@ import { StudyList } from '@ohif/ui-next';
|
|||||||
* `<StudyList.PreviewContainer>` layout is used.
|
* `<StudyList.PreviewContainer>` layout is used.
|
||||||
* Currently only applies when `workList.variant` is `'default'`.
|
* Currently only applies when `workList.variant` is `'default'`.
|
||||||
*
|
*
|
||||||
|
* - `workList.onStudyDoubleClick`: command run input (default:
|
||||||
|
* `{ commandName: 'launchDefaultMode' }`)
|
||||||
|
* The command(s) run when a study row is double-clicked (the row is selected
|
||||||
|
* first). Accepts anything `commandsManager.run` does: a command name string,
|
||||||
|
* `{ commandName, commandOptions, context }`, an array of those, or a plain
|
||||||
|
* function. At call time these are merged into the command options (a plain
|
||||||
|
* function receives them as its single argument):
|
||||||
|
* - `study`: the double-clicked `StudyRow`.
|
||||||
|
* - `workflows`: the workflows applicable to the study, in the same order as
|
||||||
|
* the row's Launch Workflow menu. Each has `id`, `displayName`,
|
||||||
|
* `isDefault`, and `launchWithStudy(study)`.
|
||||||
|
* - `defaultWorkflow`: the user's default workflow when it applies to the
|
||||||
|
* study, else `undefined`.
|
||||||
|
* The default `launchDefaultMode` command launches `defaultWorkflow`,
|
||||||
|
* falling back to the first applicable workflow. Override the options to
|
||||||
|
* always launch a specific mode:
|
||||||
|
* `{ commandName: 'launchDefaultMode', commandOptions: { workflowId: 'segmentation' } }`.
|
||||||
|
* Modes can contribute their own commands for this via a `getCommandsModule`
|
||||||
|
* export on the mode definition, registered at app init in the 'WORKLIST'
|
||||||
|
* context — before any mode route is entered. When set to a falsy value the
|
||||||
|
* built-in StudyList.Table double-click behavior applies.
|
||||||
|
* Currently only applies when `workList.variant` is `'default'`.
|
||||||
|
*
|
||||||
* - `workList.settingsMenuItems`: `(defaults) => SettingsMenuItem[]` (default: identity)
|
* - `workList.settingsMenuItems`: `(defaults) => SettingsMenuItem[]` (default: identity)
|
||||||
* Builds the items in the WorkList settings popover. Receives the default
|
* Builds the items in the WorkList settings popover. Receives the default
|
||||||
* items (`about`, `userPreferences`, and `logout` when OIDC is configured)
|
* items (`about`, `userPreferences`, and `logout` when OIDC is configured)
|
||||||
@ -82,6 +105,7 @@ export default function getWorkListCustomization() {
|
|||||||
'workList.previewSeriesView': 'all',
|
'workList.previewSeriesView': 'all',
|
||||||
'workList.columns': StudyList.defaultColumns,
|
'workList.columns': StudyList.defaultColumns,
|
||||||
'workList.renderPreviewContent': undefined,
|
'workList.renderPreviewContent': undefined,
|
||||||
|
'workList.onStudyDoubleClick': { commandName: 'launchDefaultMode' },
|
||||||
'workList.settingsMenuItems': (defaults: unknown) => defaults,
|
'workList.settingsMenuItems': (defaults: unknown) => defaults,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,19 +130,39 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
|||||||
}
|
}
|
||||||
const { id } = mode;
|
const { id } = mode;
|
||||||
|
|
||||||
if (mode.modeFactory) {
|
|
||||||
// If the appConfig contains configuration for this mode, use it.
|
|
||||||
const modeConfiguration =
|
|
||||||
appConfig.modesConfiguration && appConfig.modesConfiguration[id]
|
|
||||||
? appConfig.modesConfiguration[id]
|
|
||||||
: {};
|
|
||||||
|
|
||||||
mode = await mode.modeFactory({ modeConfiguration, loadModules });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modesById.has(id)) {
|
if (modesById.has(id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the appConfig contains configuration for this mode, use it.
|
||||||
|
const modeConfiguration =
|
||||||
|
appConfig.modesConfiguration && appConfig.modesConfiguration[id]
|
||||||
|
? appConfig.modesConfiguration[id]
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// Mirrors the extension commands path for modes, but registers before the
|
||||||
|
// mode is instantiated so the commands are usable on the worklist, ahead
|
||||||
|
// of any mode route being entered. Definitions land in the 'WORKLIST'
|
||||||
|
// context unless the module (or an individual command) declares its own.
|
||||||
|
if (typeof mode.getCommandsModule === 'function') {
|
||||||
|
extensionManager.registerCommandsModule(
|
||||||
|
mode.getCommandsModule({
|
||||||
|
appConfig,
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
serviceProvidersManager,
|
||||||
|
hotkeysManager,
|
||||||
|
extensionManager,
|
||||||
|
modeConfiguration,
|
||||||
|
}),
|
||||||
|
'WORKLIST'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.modeFactory) {
|
||||||
|
mode = await mode.modeFactory({ modeConfiguration, loadModules });
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent duplication
|
// Prevent duplication
|
||||||
modesById.add(id);
|
modesById.add(id);
|
||||||
if (!mode || typeof mode !== 'object') {
|
if (!mode || typeof mode !== 'object') {
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
import type { RunInput } from '@ohif/core/src/classes/CommandsManager';
|
||||||
import { preserveQueryParameters } from '../../utils/preserveQueryParameters';
|
import { preserveQueryParameters } from '../../utils/preserveQueryParameters';
|
||||||
import { useStudyListStateSync, useWorkListToolbarActions } from '../../hooks';
|
import { useStudyListStateSync, useWorkListToolbarActions } from '../../hooks';
|
||||||
|
|
||||||
import { StudyList, Icons, InvestigationalUseDialog, type StudyRow } from '@ohif/ui-next';
|
import {
|
||||||
|
StudyList,
|
||||||
|
Icons,
|
||||||
|
InvestigationalUseDialog,
|
||||||
|
type StudyRow,
|
||||||
|
type OnStudyDoubleClick,
|
||||||
|
} from '@ohif/ui-next';
|
||||||
import { StudyListSettingsPopover } from './StudyListSettingsPopover';
|
import { StudyListSettingsPopover } from './StudyListSettingsPopover';
|
||||||
import { SidePanelPreview } from './SidePanelPreview';
|
import { SidePanelPreview } from './SidePanelPreview';
|
||||||
|
|
||||||
@ -26,6 +33,7 @@ export default function WorkList({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
|
commandsManager,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
const { customizationService } = servicesManager.services;
|
const { customizationService } = servicesManager.services;
|
||||||
@ -47,6 +55,22 @@ export default function WorkList({
|
|||||||
const [selected, setSelected] = useState<StudyRow | null>(null);
|
const [selected, setSelected] = useState<StudyRow | null>(null);
|
||||||
const [isPreviewOpen, setPreviewOpen] = useState(true);
|
const [isPreviewOpen, setPreviewOpen] = useState(true);
|
||||||
|
|
||||||
|
// `workList.onStudyDoubleClick` is the command (or command list) run when a
|
||||||
|
// study row is double-clicked — by default `launchDefaultMode`, which
|
||||||
|
// launches the default workflow, falling back to the first applicable one.
|
||||||
|
// The study and its applicable workflows are merged into the command options
|
||||||
|
// at call time, so an override only needs to name a command and any static
|
||||||
|
// options (e.g. a specific `workflowId`).
|
||||||
|
const studyDoubleClickCommand = customizationService.getCustomization(
|
||||||
|
'workList.onStudyDoubleClick'
|
||||||
|
) as RunInput;
|
||||||
|
const onStudyDoubleClick = useCallback<OnStudyDoubleClick>(
|
||||||
|
(study, { defaultWorkflow, workflows }) => {
|
||||||
|
commandsManager.run(studyDoubleClickCommand, { study, defaultWorkflow, workflows });
|
||||||
|
},
|
||||||
|
[commandsManager, studyDoubleClickCommand]
|
||||||
|
);
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
// `workList.columns` is registered as a value (StudyList.defaultColumns) and
|
// `workList.columns` is registered as a value (StudyList.defaultColumns) and
|
||||||
// merged via customization commands, so we read the result directly.
|
// merged via customization commands, so we read the result directly.
|
||||||
@ -122,6 +146,7 @@ export default function WorkList({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
title={'Study List'}
|
title={'Study List'}
|
||||||
|
onStudyDoubleClick={studyDoubleClickCommand ? onStudyDoubleClick : undefined}
|
||||||
onSelectionChange={sel => setSelected((sel as StudyRow[])[0] ?? null)}
|
onSelectionChange={sel => setSelected((sel as StudyRow[])[0] ?? null)}
|
||||||
toolbarLeftComponent={logoComponent}
|
toolbarLeftComponent={logoComponent}
|
||||||
toolbarRightActionsComponent={toolbarActions}
|
toolbarRightActionsComponent={toolbarActions}
|
||||||
|
|||||||
@ -131,7 +131,7 @@ const createRoutes = ({
|
|||||||
path: '/',
|
path: '/',
|
||||||
children: DataSourceWrapper,
|
children: DataSourceWrapper,
|
||||||
private: true,
|
private: true,
|
||||||
props: { children: WorkListComponent, servicesManager, extensionManager },
|
props: { children: WorkListComponent, servicesManager, extensionManager, commandsManager },
|
||||||
};
|
};
|
||||||
|
|
||||||
const customRoutes = customizationService.getCustomization('routes.customRoutes');
|
const customRoutes = customizationService.getCustomization('routes.customRoutes');
|
||||||
|
|||||||
@ -614,6 +614,24 @@ export default class ExtensionManager extends PubSubService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a commands module produced outside the extension registration
|
||||||
|
* path — e.g. a mode's `getCommandsModule`, registered at app init so its
|
||||||
|
* commands are available before any mode route is entered (worklist time).
|
||||||
|
*
|
||||||
|
* @param commandsModule - as returned by a `getCommandsModule` function
|
||||||
|
* @param defaultContext - context used when the module does not declare one
|
||||||
|
*/
|
||||||
|
public registerCommandsModule(
|
||||||
|
commandsModule: CommandsModule,
|
||||||
|
defaultContext: string = 'VIEWER'
|
||||||
|
): void {
|
||||||
|
this._initCommandsModule({
|
||||||
|
...commandsModule,
|
||||||
|
defaultContext: commandsModule.defaultContext || defaultContext,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
@ -521,6 +521,54 @@ window.config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workList.onStudyDoubleClick',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
The command run when a study row is double-clicked (the row is selected first). Accepts
|
||||||
|
anything <code>commandsManager.run</code> does: a command name string,{' '}
|
||||||
|
<code>{"{ commandName, commandOptions, context }"}</code>, an array of those, or a plain
|
||||||
|
function. At call time the following are merged into the command options (a plain
|
||||||
|
function receives them as its single argument):
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>study</code>: the double-clicked <code>StudyRow</code>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>workflows</code>: the workflows applicable to the study, in the same order as
|
||||||
|
the row's Launch Workflow menu. Each has <code>id</code>, <code>displayName</code>,{' '}
|
||||||
|
<code>isDefault</code>, and <code>launchWithStudy(study)</code>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>defaultWorkflow</code>: the user's default workflow when it applies to the
|
||||||
|
study, else <code>undefined</code>.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
The default <code>launchDefaultMode</code> command launches the default workflow, falling
|
||||||
|
back to the first applicable one. Modes can contribute their own commands via a{' '}
|
||||||
|
<code>getCommandsModule</code> export on the mode definition — these are registered at
|
||||||
|
app init in the <code>WORKLIST</code> context, before any mode route is entered.
|
||||||
|
Currently only applies when <code>workList.variant</code> is <code>'default'</code>.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
default: "{ commandName: 'launchDefaultMode' }",
|
||||||
|
configuration: `
|
||||||
|
window.config = {
|
||||||
|
// rest of window config
|
||||||
|
customizationService: [
|
||||||
|
{
|
||||||
|
'workList.onStudyDoubleClick': {
|
||||||
|
// Always launch a specific mode on double click, regardless of the default.
|
||||||
|
$set: {
|
||||||
|
commandName: 'launchDefaultMode',
|
||||||
|
commandOptions: { workflowId: '@ohif/mode-longitudinal' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -77,9 +77,19 @@ function InputMultiSelectRoot({ options, value, onChange, children }: InputMulti
|
|||||||
const filtered = React.useMemo(() => {
|
const filtered = React.useMemo(() => {
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
if (!q) return normalized;
|
if (!q) return normalized;
|
||||||
return normalized.filter(
|
// Prefix matches rank above substring matches (typing "PT" lists PT before CTPT).
|
||||||
opt => opt.label.toLowerCase().includes(q) || opt.value.toLowerCase().includes(q)
|
const prefixMatches: NormalizedOption[] = [];
|
||||||
);
|
const substringMatches: NormalizedOption[] = [];
|
||||||
|
for (const opt of normalized) {
|
||||||
|
const label = opt.label.toLowerCase();
|
||||||
|
const value = opt.value.toLowerCase();
|
||||||
|
if (label.startsWith(q) || value.startsWith(q)) {
|
||||||
|
prefixMatches.push(opt);
|
||||||
|
} else if (label.includes(q) || value.includes(q)) {
|
||||||
|
substringMatches.push(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...prefixMatches, ...substringMatches];
|
||||||
}, [normalized, query]);
|
}, [normalized, query]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@ -135,6 +135,7 @@ function Table({
|
|||||||
toolbarRightActionsComponent,
|
toolbarRightActionsComponent,
|
||||||
isLoading,
|
isLoading,
|
||||||
loadingComponent,
|
loadingComponent,
|
||||||
|
onStudyDoubleClick,
|
||||||
children,
|
children,
|
||||||
}: TableProps) {
|
}: TableProps) {
|
||||||
const { defaultPreviewSizePercent } = useLayout();
|
const { defaultPreviewSizePercent } = useLayout();
|
||||||
@ -168,6 +169,7 @@ function Table({
|
|||||||
filters={filters}
|
filters={filters}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
loadingComponent={loadingComponent}
|
loadingComponent={loadingComponent}
|
||||||
|
onStudyDoubleClick={onStudyDoubleClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,9 +7,20 @@ import { DatePickerWithRange } from '../../DateRange';
|
|||||||
import { InputMultiSelect } from '../../InputMultiSelect';
|
import { InputMultiSelect } from '../../InputMultiSelect';
|
||||||
import type { StudyDateRangeFilter, StudyRow } from '../types/types';
|
import type { StudyDateRangeFilter, StudyRow } from '../types/types';
|
||||||
import { tokenizeModalities } from '../utils/tokenizeModalities';
|
import { tokenizeModalities } from '../utils/tokenizeModalities';
|
||||||
import { useWorkflows } from './WorkflowsProvider';
|
import { useWorkflows, type Workflow } from './WorkflowsProvider';
|
||||||
import { COLUMN_IDS } from '../columns/defaultColumns';
|
import { COLUMN_IDS } from '../columns/defaultColumns';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the built-in double-click action (launch the default workflow,
|
||||||
|
* falling back to the first applicable one). The row is selected before this
|
||||||
|
* is called. `workflows` are the workflows applicable to the study, in menu
|
||||||
|
* order; `defaultWorkflow` is the user's default when it applies to the study.
|
||||||
|
*/
|
||||||
|
export type OnStudyDoubleClick = (
|
||||||
|
study: StudyRow,
|
||||||
|
context: { defaultWorkflow?: Workflow; workflows: Workflow[] }
|
||||||
|
) => void;
|
||||||
|
|
||||||
export type TableProps = Omit<DataTableProps<StudyRow>, 'children' | 'getRowId'> & {
|
export type TableProps = Omit<DataTableProps<StudyRow>, 'children' | 'getRowId'> & {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
showColumnVisibility?: boolean;
|
showColumnVisibility?: boolean;
|
||||||
@ -19,6 +30,7 @@ export type TableProps = Omit<DataTableProps<StudyRow>, 'children' | 'getRowId'>
|
|||||||
toolbarRightComponent?: ReactNode;
|
toolbarRightComponent?: ReactNode;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
loadingComponent?: ReactNode;
|
loadingComponent?: ReactNode;
|
||||||
|
onStudyDoubleClick?: OnStudyDoubleClick;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Table({
|
export function Table({
|
||||||
@ -42,6 +54,7 @@ export function Table({
|
|||||||
isLoading,
|
isLoading,
|
||||||
loadingComponent,
|
loadingComponent,
|
||||||
manualFiltering,
|
manualFiltering,
|
||||||
|
onStudyDoubleClick,
|
||||||
}: TableProps) {
|
}: TableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable<StudyRow>
|
<DataTable<StudyRow>
|
||||||
@ -68,6 +81,7 @@ export function Table({
|
|||||||
toolbarRightComponent={toolbarRightComponent}
|
toolbarRightComponent={toolbarRightComponent}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
loadingComponent={loadingComponent}
|
loadingComponent={loadingComponent}
|
||||||
|
onStudyDoubleClick={onStudyDoubleClick}
|
||||||
/>
|
/>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
);
|
);
|
||||||
@ -82,6 +96,7 @@ function TableContent({
|
|||||||
toolbarRightComponent,
|
toolbarRightComponent,
|
||||||
isLoading,
|
isLoading,
|
||||||
loadingComponent,
|
loadingComponent,
|
||||||
|
onStudyDoubleClick,
|
||||||
}: {
|
}: {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
showColumnVisibility?: boolean;
|
showColumnVisibility?: boolean;
|
||||||
@ -91,6 +106,7 @@ function TableContent({
|
|||||||
toolbarRightComponent?: ReactNode;
|
toolbarRightComponent?: ReactNode;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
loadingComponent?: ReactNode;
|
loadingComponent?: ReactNode;
|
||||||
|
onStudyDoubleClick?: TableProps['onStudyDoubleClick'];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('StudyList');
|
const { t } = useTranslation('StudyList');
|
||||||
const { table } = useDataTable<StudyRow>();
|
const { table } = useDataTable<StudyRow>();
|
||||||
@ -102,7 +118,7 @@ function TableContent({
|
|||||||
return Array.from(new Set(tokens)).sort();
|
return Array.from(new Set(tokens)).sort();
|
||||||
}, [table.options?.data]);
|
}, [table.options?.data]);
|
||||||
// Access workflow provider for default workflow + launch
|
// Access workflow provider for default workflow + launch
|
||||||
const { getDefaultWorkflowForStudy } = useWorkflows();
|
const { getDefaultWorkflowForStudy, getWorkflowsForStudy } = useWorkflows();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
@ -198,10 +214,12 @@ function TableContent({
|
|||||||
className: 'group cursor-pointer',
|
className: 'group cursor-pointer',
|
||||||
onClick: row => {
|
onClick: row => {
|
||||||
const original = row.original as StudyRow;
|
const original = row.original as StudyRow;
|
||||||
const defaultWorkflow = getDefaultWorkflowForStudy(original);
|
const canDoubleClickLaunch =
|
||||||
// When a default workflow is set, do not allow a second click to unselect.
|
Boolean(onStudyDoubleClick) ||
|
||||||
// Always select on click; otherwise toggle selection.
|
getWorkflowsForStudy(original).length > 0;
|
||||||
if (defaultWorkflow) {
|
// When a double click can launch, the second click must not read
|
||||||
|
// as an unselect — clicking only ever selects. Otherwise toggle.
|
||||||
|
if (canDoubleClickLaunch) {
|
||||||
if (!row.getIsSelected()) {
|
if (!row.getIsSelected()) {
|
||||||
row.toggleSelected(true);
|
row.toggleSelected(true);
|
||||||
}
|
}
|
||||||
@ -211,15 +229,20 @@ function TableContent({
|
|||||||
},
|
},
|
||||||
onDoubleClick: row => {
|
onDoubleClick: row => {
|
||||||
const original = row.original as StudyRow;
|
const original = row.original as StudyRow;
|
||||||
|
const workflows = getWorkflowsForStudy(original);
|
||||||
const defaultWorkflow = getDefaultWorkflowForStudy(original);
|
const defaultWorkflow = getDefaultWorkflowForStudy(original);
|
||||||
if (!defaultWorkflow) {
|
// Ensure the row is selected before launching
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Ensure the row is selected, then launch with the default workflow
|
|
||||||
if (!row.getIsSelected()) {
|
if (!row.getIsSelected()) {
|
||||||
row.toggleSelected(true);
|
row.toggleSelected(true);
|
||||||
}
|
}
|
||||||
defaultWorkflow.launchWithStudy(original);
|
if (onStudyDoubleClick) {
|
||||||
|
onStudyDoubleClick(original, { defaultWorkflow, workflows });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Launch the default workflow, or fall back to the first
|
||||||
|
// applicable one (the top entry of the row's workflow menu).
|
||||||
|
const workflow = defaultWorkflow ?? workflows[0];
|
||||||
|
workflow?.launchWithStudy(original);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import { WorkflowsProvider } from './components/WorkflowsProvider';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
export * from './types/types';
|
export * from './types/types';
|
||||||
|
export type { Workflow } from './components/WorkflowsProvider';
|
||||||
|
export type { OnStudyDoubleClick } from './components/Table';
|
||||||
|
|
||||||
// Column ID constants
|
// Column ID constants
|
||||||
export { COLUMN_IDS, FILTERABLE_COLUMN_IDS, TEXT_FILTER_COLUMN_IDS };
|
export { COLUMN_IDS, FILTERABLE_COLUMN_IDS, TEXT_FILTER_COLUMN_IDS };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user