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:
Raphaël Bosi
2026-03-18 15:37:27 +01:00
committed by GitHub
parent eb51f8e6da
commit f2fa958f20
23 changed files with 348 additions and 110 deletions
@@ -16,10 +16,12 @@ import { FindOneFrontComponentDocument } from '~/generated-metadata/graphql';
type FrontComponentRendererProps = {
frontComponentId: string;
commandMenuItemId?: string;
};
export const FrontComponentRenderer = ({
frontComponentId,
commandMenuItemId,
}: FrontComponentRendererProps) => {
const { colorScheme } = useContext(ThemeContext);
const { enqueueErrorSnackBar } = useSnackBar();
@@ -30,7 +32,7 @@ export const FrontComponentRenderer = ({
);
const { executionContext, frontComponentHostCommunicationApi } =
useFrontComponentExecutionContext({ frontComponentId });
useFrontComponentExecutionContext({ frontComponentId, commandMenuItemId });
const handleError = useCallback(
(error?: Error) => {
@@ -18,6 +18,7 @@ export const HeadlessFrontComponentMountRoot = () => {
const mountedHeadlessFrontComponentMaps = useAtomStateValue(
mountedHeadlessFrontComponentMapsState,
);
const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
return (
@@ -33,8 +34,8 @@ export const HeadlessFrontComponentMountRoot = () => {
<LayoutRenderingProvider
value={{
targetRecordIdentifier:
isDefined(mountContext) &&
isDefined(mountContext.objectNameSingular)
isDefined(mountContext.objectNameSingular) &&
isDefined(mountContext.recordId)
? {
id: mountContext.recordId,
targetObjectNameSingular:
@@ -45,7 +46,10 @@ export const HeadlessFrontComponentMountRoot = () => {
isInSidePanel: false,
}}
>
<FrontComponentRenderer frontComponentId={frontComponentId} />
<FrontComponentRenderer
frontComponentId={frontComponentId}
commandMenuItemId={mountContext.commandMenuItemId}
/>
</LayoutRenderingProvider>
</Suspense>
</CommandMenuItemErrorBoundary>