Add progress tracking to command menu items (#18732)
Introduces a progress indicator for command menu items (both engine commands and front components). When a command is running, the menu item now displays a percentage alongside the loader spinner instead of just a spinner. - Added `commandMenuItemProgressFamilyState` to track per-item progress and `CommandListItemLoader` to render it - Exposed `updateProgress` in the twenty-sdk public API so front components can report execution progress back to the host - Wired progress reporting into `ExportMultipleRecordsCommand` as the first consumer, showing CSV export progress - Progress state is cleaned up on unmount for both engine commands and headless front components - Refactor
This commit is contained in:
+32
-59
@@ -1,16 +1,15 @@
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { EngineCommandMenuItem } from '@/command-menu-item/display/components/EngineCommandMenuItem';
|
||||
import { HeadlessFrontComponentCommandMenuItem } from '@/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem';
|
||||
import { useMountEngineCommand } from '@/command-menu-item/engine-command/hooks/useMountEngineCommand';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useMountHeadlessFrontComponent } from '@/front-components/hooks/useMountHeadlessFrontComponent';
|
||||
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
@@ -21,7 +20,6 @@ import {
|
||||
} from 'twenty-shared/utils';
|
||||
import { type IconComponent, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { type HeadlessFrontComponentMountContext } from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
|
||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
@@ -55,11 +53,8 @@ type BuildCommandMenuItemFromFrontComponentParams = {
|
||||
objectNameSingular: string;
|
||||
};
|
||||
}) => void;
|
||||
mountHeadlessFrontComponent: (
|
||||
frontComponentId: string,
|
||||
context?: HeadlessFrontComponentMountContext,
|
||||
) => void;
|
||||
mountContext?: HeadlessFrontComponentMountContext;
|
||||
recordId?: string;
|
||||
objectNameSingular?: string;
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
@@ -70,8 +65,8 @@ const buildCommandMenuItemFromFrontComponent = ({
|
||||
isPinned,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
mountContext,
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
commandMenuContextApi,
|
||||
}: BuildCommandMenuItemFromFrontComponentParams) => {
|
||||
const displayLabel = interpolateCommandMenuItemLabel({
|
||||
@@ -88,22 +83,16 @@ const buildCommandMenuItemFromFrontComponent = ({
|
||||
|
||||
const isHeadless = item.frontComponent?.isHeadless === true;
|
||||
|
||||
const handleClick = () => {
|
||||
if (isHeadless) {
|
||||
mountHeadlessFrontComponent(item.frontComponentId, mountContext);
|
||||
} else {
|
||||
openFrontComponentInSidePanel({
|
||||
frontComponentId: item.frontComponentId,
|
||||
pageTitle: displayLabel ?? '',
|
||||
pageIcon: Icon,
|
||||
recordContext: isDefined(mountContext)
|
||||
? {
|
||||
recordId: mountContext.recordId,
|
||||
objectNameSingular: mountContext.objectNameSingular,
|
||||
}
|
||||
const handleNonHeadlessClick = () => {
|
||||
openFrontComponentInSidePanel({
|
||||
frontComponentId: item.frontComponentId,
|
||||
pageTitle: displayLabel ?? '',
|
||||
pageIcon: Icon,
|
||||
recordContext:
|
||||
isDefined(recordId) && isDefined(objectNameSingular)
|
||||
? { recordId, objectNameSingular }
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -124,10 +113,12 @@ const buildCommandMenuItemFromFrontComponent = ({
|
||||
component: isHeadless ? (
|
||||
<HeadlessFrontComponentCommandMenuItem
|
||||
frontComponentId={item.frontComponentId}
|
||||
onClick={handleClick}
|
||||
commandMenuItemId={item.id}
|
||||
recordId={recordId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
) : (
|
||||
<Command onClick={handleClick} />
|
||||
<Command onClick={handleNonHeadlessClick} />
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -140,12 +131,6 @@ type BuildCommandMenuItemFromStandardKeyParams = {
|
||||
isPinned: boolean;
|
||||
getIcon: ReturnType<typeof useIcons>['getIcon'];
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
mountEngineCommand: (
|
||||
engineCommandId: string,
|
||||
contextStoreInstanceId: string,
|
||||
engineComponentKey: EngineComponentKey,
|
||||
) => void;
|
||||
contextStoreInstanceId: string;
|
||||
};
|
||||
|
||||
const buildCommandItemFromEngineKey = ({
|
||||
@@ -156,15 +141,9 @@ const buildCommandItemFromEngineKey = ({
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
mountEngineCommand,
|
||||
contextStoreInstanceId,
|
||||
}: BuildCommandMenuItemFromStandardKeyParams) => {
|
||||
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
const handleClick = () => {
|
||||
mountEngineCommand(item.id, contextStoreInstanceId, engineComponentKey);
|
||||
};
|
||||
|
||||
return {
|
||||
type,
|
||||
key: `command-menu-item-engine-${item.id}`,
|
||||
@@ -186,7 +165,12 @@ const buildCommandItemFromEngineKey = ({
|
||||
item.conditionalAvailabilityExpression,
|
||||
commandMenuContextApi,
|
||||
),
|
||||
component: <Command onClick={handleClick} />,
|
||||
component: (
|
||||
<EngineCommandMenuItem
|
||||
commandMenuItemId={item.id}
|
||||
engineComponentKey={engineComponentKey}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -195,12 +179,6 @@ export const useCommandMenuItemsFromBackend = (
|
||||
) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
|
||||
const mountHeadlessFrontComponent = useMountHeadlessFrontComponent();
|
||||
const mountEngineCommand = useMountEngineCommand();
|
||||
|
||||
const contextStoreInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
ContextStoreComponentInstanceContext,
|
||||
);
|
||||
|
||||
const contextStoreIsPageInEditMode = useAtomComponentStateValue(
|
||||
contextStoreIsPageInEditModeComponentState,
|
||||
@@ -229,13 +207,10 @@ export const useCommandMenuItemsFromBackend = (
|
||||
selectedRecordIds.length >= 1 ||
|
||||
contextStoreTargetedRecordsRule.mode === 'exclusion';
|
||||
|
||||
const mountContext: HeadlessFrontComponentMountContext | undefined =
|
||||
selectedRecordIds.length === 1 && isDefined(currentObjectMetadataItem)
|
||||
? {
|
||||
recordId: selectedRecordIds[0],
|
||||
objectNameSingular: currentObjectMetadataItem.nameSingular,
|
||||
}
|
||||
: undefined;
|
||||
const recordId =
|
||||
selectedRecordIds.length === 1 ? selectedRecordIds[0] : undefined;
|
||||
|
||||
const objectNameSingular = currentObjectMetadataItem?.nameSingular;
|
||||
|
||||
const { data } = useQuery(FindManyCommandMenuItemsDocument);
|
||||
|
||||
@@ -268,8 +243,6 @@ export const useCommandMenuItemsFromBackend = (
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
mountEngineCommand,
|
||||
contextStoreInstanceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -281,9 +254,9 @@ export const useCommandMenuItemsFromBackend = (
|
||||
isPinned,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
mountContext,
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user