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:
Bill WallaceandClaude Fable 5 authored and GitHub committed 2026-07-10 08:16:16 -04:00
1 parent dbb8b1526e
commit 6b6761088b
11 files changed
+230 -27

No files matched your search

@@ -77,9 +77,19 @@ function InputMultiSelectRoot({ options, value, onChange, children }: InputMulti
const filtered = React.useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return normalized;
return normalized.filter(
opt => opt.label.toLowerCase().includes(q) || opt.value.toLowerCase().includes(q)
);
// Prefix matches rank above substring matches (typing "PT" lists PT before CTPT).
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]);
React.useEffect(() => {
@@ -135,6 +135,7 @@ function Table({
toolbarRightActionsComponent,
isLoading,
loadingComponent,
onStudyDoubleClick,
children,
}: TableProps) {
const { defaultPreviewSizePercent } = useLayout();
@@ -168,6 +169,7 @@ function Table({
filters={filters}
isLoading={isLoading}
loadingComponent={loadingComponent}
onStudyDoubleClick={onStudyDoubleClick}
/>
</div>
</div>
@@ -7,9 +7,20 @@ import { DatePickerWithRange } from '../../DateRange';
import { InputMultiSelect } from '../../InputMultiSelect';
import type { StudyDateRangeFilter, StudyRow } from '../types/types';
import { tokenizeModalities } from '../utils/tokenizeModalities';
import { useWorkflows } from './WorkflowsProvider';
import { useWorkflows, type Workflow } from './WorkflowsProvider';
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'> & {
title?: ReactNode;
showColumnVisibility?: boolean;
@@ -19,6 +30,7 @@ export type TableProps = Omit<DataTableProps<StudyRow>, 'children' | 'getRowId'>
toolbarRightComponent?: ReactNode;
isLoading?: boolean;
loadingComponent?: ReactNode;
onStudyDoubleClick?: OnStudyDoubleClick;
};
export function Table({
@@ -42,6 +54,7 @@ export function Table({
isLoading,
loadingComponent,
manualFiltering,
onStudyDoubleClick,
}: TableProps) {
return (
<DataTable<StudyRow>
@@ -68,6 +81,7 @@ export function Table({
toolbarRightComponent={toolbarRightComponent}
isLoading={isLoading}
loadingComponent={loadingComponent}
onStudyDoubleClick={onStudyDoubleClick}
/>
</DataTable>
);
@@ -82,6 +96,7 @@ function TableContent({
toolbarRightComponent,
isLoading,
loadingComponent,
onStudyDoubleClick,
}: {
title?: ReactNode;
showColumnVisibility?: boolean;
@@ -91,6 +106,7 @@ function TableContent({
toolbarRightComponent?: ReactNode;
isLoading?: boolean;
loadingComponent?: ReactNode;
onStudyDoubleClick?: TableProps['onStudyDoubleClick'];
}) {
const { t } = useTranslation('StudyList');
const { table } = useDataTable<StudyRow>();
@@ -102,7 +118,7 @@ function TableContent({
return Array.from(new Set(tokens)).sort();
}, [table.options?.data]);
// Access workflow provider for default workflow + launch
const { getDefaultWorkflowForStudy } = useWorkflows();
const { getDefaultWorkflowForStudy, getWorkflowsForStudy } = useWorkflows();
return (
<div className="flex h-full flex-col">
@@ -198,10 +214,12 @@ function TableContent({
className: 'group cursor-pointer',
onClick: row => {
const original = row.original as StudyRow;
const defaultWorkflow = getDefaultWorkflowForStudy(original);
// When a default workflow is set, do not allow a second click to unselect.
// Always select on click; otherwise toggle selection.
if (defaultWorkflow) {
const canDoubleClickLaunch =
Boolean(onStudyDoubleClick) ||
getWorkflowsForStudy(original).length > 0;
// 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()) {
row.toggleSelected(true);
}
@@ -211,15 +229,20 @@ function TableContent({
},
onDoubleClick: row => {
const original = row.original as StudyRow;
const workflows = getWorkflowsForStudy(original);
const defaultWorkflow = getDefaultWorkflowForStudy(original);
if (!defaultWorkflow) {
return;
}
// Ensure the row is selected, then launch with the default workflow
// Ensure the row is selected before launching
if (!row.getIsSelected()) {
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
export * from './types/types';
export type { Workflow } from './components/WorkflowsProvider';
export type { OnStudyDoubleClick } from './components/Table';
// Column ID constants
export { COLUMN_IDS, FILTERABLE_COLUMN_IDS, TEXT_FILTER_COLUMN_IDS };