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:
+3
-1
@@ -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) => {
|
||||
|
||||
+7
-3
@@ -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>
|
||||
|
||||
+19
-1
@@ -7,6 +7,7 @@ import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
|
||||
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
|
||||
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
|
||||
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
@@ -15,14 +16,17 @@ import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
export const useFrontComponentExecutionContext = ({
|
||||
frontComponentId,
|
||||
commandMenuItemId,
|
||||
}: {
|
||||
frontComponentId: string;
|
||||
commandMenuItemId?: string;
|
||||
}): {
|
||||
executionContext: FrontComponentExecutionContext;
|
||||
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
|
||||
@@ -44,6 +48,10 @@ export const useFrontComponentExecutionContext = ({
|
||||
enqueueWarningSnackBar,
|
||||
} = useSnackBar();
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
const setCommandMenuItemProgress = useSetAtomFamilyState(
|
||||
commandMenuItemProgressFamilyState,
|
||||
commandMenuItemId ?? '',
|
||||
);
|
||||
|
||||
const navigate: FrontComponentHostCommunicationApi['navigate'] = async (
|
||||
to,
|
||||
@@ -133,6 +141,15 @@ export const useFrontComponentExecutionContext = ({
|
||||
closeSidePanelMenu();
|
||||
};
|
||||
|
||||
const updateProgress: FrontComponentHostCommunicationApi['updateProgress'] =
|
||||
async (progress) => {
|
||||
if (!isDefined(commandMenuItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCommandMenuItemProgress(Math.max(0, Math.min(100, progress)));
|
||||
};
|
||||
|
||||
const frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
|
||||
{
|
||||
navigate,
|
||||
@@ -142,6 +159,7 @@ export const useFrontComponentExecutionContext = ({
|
||||
enqueueSnackbar,
|
||||
unmountFrontComponent,
|
||||
closeSidePanel,
|
||||
updateProgress,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+2
-5
@@ -9,13 +9,10 @@ import { useStore } from 'jotai';
|
||||
export const useMountHeadlessFrontComponent = () => {
|
||||
const store = useStore();
|
||||
const mountHeadlessFrontComponent = useCallback(
|
||||
(
|
||||
frontComponentId: string,
|
||||
context?: HeadlessFrontComponentMountContext,
|
||||
) => {
|
||||
(frontComponentId: string, context: HeadlessFrontComponentMountContext) => {
|
||||
store.set(mountedHeadlessFrontComponentMapsState.atom, (previousMap) => {
|
||||
const next = new Map(previousMap);
|
||||
next.set(frontComponentId, context ?? undefined);
|
||||
next.set(frontComponentId, context);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
|
||||
+13
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
|
||||
import { mountedHeadlessFrontComponentMapsState } from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
@@ -7,11 +8,23 @@ export const useUnmountHeadlessFrontComponent = () => {
|
||||
const store = useStore();
|
||||
const unmountHeadlessFrontComponent = useCallback(
|
||||
(frontComponentId: string) => {
|
||||
const currentMap = store.get(mountedHeadlessFrontComponentMapsState.atom);
|
||||
const mountContext = currentMap.get(frontComponentId);
|
||||
|
||||
store.set(mountedHeadlessFrontComponentMapsState.atom, (previousMap) => {
|
||||
const next = new Map(previousMap);
|
||||
next.delete(frontComponentId);
|
||||
return next;
|
||||
});
|
||||
|
||||
if (mountContext) {
|
||||
store.set(
|
||||
commandMenuItemProgressFamilyState.atomFamily(
|
||||
mountContext.commandMenuItemId,
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
+5
-6
@@ -1,11 +1,10 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export type HeadlessFrontComponentMountContext =
|
||||
| {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
}
|
||||
| undefined;
|
||||
export type HeadlessFrontComponentMountContext = {
|
||||
commandMenuItemId: string;
|
||||
recordId?: string;
|
||||
objectNameSingular?: string;
|
||||
};
|
||||
|
||||
export const mountedHeadlessFrontComponentMapsState = createAtomState<
|
||||
Map<string, HeadlessFrontComponentMountContext>
|
||||
|
||||
Reference in New Issue
Block a user