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:
+6
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
|
||||
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useUnmountEngineCommand = () => {
|
||||
@@ -15,6 +16,11 @@ export const useUnmountEngineCommand = () => {
|
||||
|
||||
return newMap;
|
||||
});
|
||||
|
||||
store.set(
|
||||
commandMenuItemProgressFamilyState.atomFamily(engineCommandId),
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
+59
-8
@@ -1,24 +1,75 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
|
||||
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecordIndexExportRecords } from '@/object-record/record-index/export/hooks/useRecordIndexExportRecords';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const ExportMultipleRecordsCommandContent = ({
|
||||
objectMetadataItem,
|
||||
recordIndexId,
|
||||
setCommandMenuItemProgress,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordIndexId: string;
|
||||
setCommandMenuItemProgress: (value: number | undefined) => void;
|
||||
}) => {
|
||||
const { download, progress } = useRecordIndexExportRecords({
|
||||
delayMs: 100,
|
||||
objectMetadataItem,
|
||||
recordIndexId,
|
||||
filename: `${objectMetadataItem.nameSingular}.csv`,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDefined(progress.totalRecordCount) &&
|
||||
isDefined(progress.processedRecordCount) &&
|
||||
progress.totalRecordCount > 0
|
||||
) {
|
||||
const percentage = Math.round(
|
||||
(progress.processedRecordCount / progress.totalRecordCount) * 100,
|
||||
);
|
||||
setCommandMenuItemProgress(percentage);
|
||||
}
|
||||
}, [progress, setCommandMenuItemProgress]);
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={download} />;
|
||||
};
|
||||
|
||||
export const ExportMultipleRecordsCommand = () => {
|
||||
const { objectMetadataItem, recordIndexId } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
|
||||
EngineCommandComponentInstanceContext,
|
||||
);
|
||||
|
||||
const setCommandMenuItemProgress = useSetAtomFamilyState(
|
||||
commandMenuItemProgressFamilyState,
|
||||
engineCommandId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
|
||||
throw new Error(
|
||||
'Object metadata item and record index ID are required to export multiple records',
|
||||
);
|
||||
}
|
||||
|
||||
const { download } = useRecordIndexExportRecords({
|
||||
delayMs: 100,
|
||||
objectMetadataItem,
|
||||
recordIndexId,
|
||||
filename: `${objectMetadataItem.nameSingular}.csv`,
|
||||
});
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={download} />;
|
||||
return (
|
||||
<ViewComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordIndexId }}
|
||||
>
|
||||
<ExportMultipleRecordsCommandContent
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
recordIndexId={recordIndexId}
|
||||
setCommandMenuItemProgress={setCommandMenuItemProgress}
|
||||
/>
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
|
||||
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
|
||||
|
||||
export const isEngineCommandMountedFamilySelector = createAtomFamilySelector<
|
||||
boolean,
|
||||
string
|
||||
>({
|
||||
key: 'isEngineCommandMountedFamilySelector',
|
||||
get:
|
||||
(engineCommandId: string) =>
|
||||
({ get }) => {
|
||||
const mountedMap = get(mountedEngineCommandsState);
|
||||
|
||||
return mountedMap.has(engineCommandId);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user