diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItem.tsx
index afbeb39552..f212514482 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItem.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItem.tsx
@@ -5,22 +5,24 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { Loader } from 'twenty-ui/feedback';
+import { CommandListItemLoader } from './CommandListItemLoader';
export const CommandListItem = ({
action,
onClick,
to,
disabled = false,
+ progress,
showDisabledLoader = false,
}: {
action: CommandMenuItemDisplayProps;
onClick?: () => void;
to?: string;
disabled?: boolean;
+ progress?: number;
showDisabledLoader?: boolean;
}) => {
const navigate = useNavigate();
-
const handleClick = () => {
if (disabled) {
return;
@@ -32,6 +34,15 @@ export const CommandListItem = ({
}
};
+ const loaderComponent =
+ disabled && showDisabledLoader ? (
+ isDefined(progress) ? (
+
+ ) : (
+
+ )
+ ) : undefined;
+
return (
: undefined}
+ RightComponent={loaderComponent}
/>
);
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItemLoader.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItemLoader.tsx
new file mode 100644
index 0000000000..56f46a1533
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItemLoader.tsx
@@ -0,0 +1,24 @@
+import { styled } from '@linaria/react';
+import { Loader } from 'twenty-ui/feedback';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+`;
+
+const StyledProgressText = styled.span`
+ color: ${themeCssVariables.font.color.tertiary};
+ font-size: ${themeCssVariables.font.size.sm};
+ white-space: nowrap;
+`;
+
+export const CommandListItemLoader = ({ progress }: { progress: number }) => {
+ return (
+
+ {Math.round(progress)}%
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemDisplay.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemDisplay.tsx
index c89c943fa7..eda344a0be 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemDisplay.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemDisplay.tsx
@@ -26,11 +26,13 @@ export const CommandMenuItemDisplay = ({
onClick,
to,
disabled,
+ progress,
showDisabledLoader = false,
}: {
onClick?: (event?: React.MouseEvent) => void;
to?: string;
disabled?: boolean;
+ progress?: number;
showDisabledLoader?: boolean;
}) => {
const action = useContext(CommandConfigContext);
@@ -66,6 +68,7 @@ export const CommandMenuItemDisplay = ({
onClick={onClickWhenEnabled}
to={toWhenEnabled}
disabled={isDisabled}
+ progress={progress}
showDisabledLoader={showDisabledLoader}
/>
);
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/EngineCommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/EngineCommandMenuItem.tsx
new file mode 100644
index 0000000000..d0c97ebf60
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/EngineCommandMenuItem.tsx
@@ -0,0 +1,43 @@
+import { isEngineCommandMountedFamilySelector } from '@/command-menu-item/engine-command/selectors/isEngineCommandMountedFamilySelector';
+import { useMountEngineCommand } from '@/command-menu-item/engine-command/hooks/useMountEngineCommand';
+import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
+import { type EngineComponentKey } from '~/generated-metadata/graphql';
+
+import { HeadlessCommandMenuItem } from './HeadlessCommandMenuItem';
+
+export const EngineCommandMenuItem = ({
+ commandMenuItemId,
+ engineComponentKey,
+}: {
+ commandMenuItemId: string;
+ engineComponentKey: EngineComponentKey;
+}) => {
+ const mountEngineCommand = useMountEngineCommand();
+
+ const contextStoreInstanceId = useAvailableComponentInstanceIdOrThrow(
+ ContextStoreComponentInstanceContext,
+ );
+
+ const isMounted = useAtomFamilySelectorValue(
+ isEngineCommandMountedFamilySelector,
+ commandMenuItemId,
+ );
+
+ const handleClick = () => {
+ mountEngineCommand(
+ commandMenuItemId,
+ contextStoreInstanceId,
+ engineComponentKey,
+ );
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessCommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessCommandMenuItem.tsx
new file mode 100644
index 0000000000..6414cd3353
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessCommandMenuItem.tsx
@@ -0,0 +1,52 @@
+import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
+import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
+import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
+import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
+import { useContext } from 'react';
+
+import { isDefined } from 'twenty-shared/utils';
+import { CommandMenuItemDisplay } from './CommandMenuItemDisplay';
+
+export const HeadlessCommandMenuItem = ({
+ isMounted,
+ commandMenuItemId,
+ onClick,
+}: {
+ isMounted: boolean;
+ commandMenuItemId: string;
+ onClick: () => void;
+}) => {
+ const commandMenuItemConfig = useContext(CommandConfigContext);
+
+ const { closeCommandMenu } = useCloseCommandMenu({
+ closeSidePanelOnShowPageOptionsExecution: false,
+ closeSidePanelOnCommandMenuListExecution: false,
+ });
+
+ const commandMenuItemProgress = useAtomFamilyStateValue(
+ commandMenuItemProgressFamilyState,
+ commandMenuItemId,
+ );
+
+ if (!isDefined(commandMenuItemConfig)) {
+ return null;
+ }
+
+ const handleClick = () => {
+ if (isMounted) {
+ return;
+ }
+
+ closeCommandMenu();
+ onClick();
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
index da7ab84126..c7f0fde91c 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
@@ -1,48 +1,40 @@
-import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
-import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
+import { useMountHeadlessFrontComponent } from '@/front-components/hooks/useMountHeadlessFrontComponent';
import { isHeadlessFrontComponentMountedFamilySelector } from '@/front-components/selectors/isHeadlessFrontComponentMountedFamilySelector';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
-import { useContext } from 'react';
-import { CommandMenuItemDisplay } from './CommandMenuItemDisplay';
+import { HeadlessCommandMenuItem } from './HeadlessCommandMenuItem';
export const HeadlessFrontComponentCommandMenuItem = ({
frontComponentId,
- onClick,
+ commandMenuItemId,
+ recordId,
+ objectNameSingular,
}: {
frontComponentId: string;
- onClick: () => void;
+ commandMenuItemId: string;
+ recordId?: string;
+ objectNameSingular?: string;
}) => {
- const commandMenuItemConfig = useContext(CommandConfigContext);
-
- const { closeCommandMenu } = useCloseCommandMenu({
- closeSidePanelOnShowPageOptionsExecution: false,
- closeSidePanelOnCommandMenuListExecution: false,
- });
+ const mountHeadlessFrontComponent = useMountHeadlessFrontComponent();
const isMounted = useAtomFamilySelectorValue(
isHeadlessFrontComponentMountedFamilySelector,
frontComponentId,
);
- if (!commandMenuItemConfig) {
- return null;
- }
-
const handleClick = () => {
- if (isMounted) {
- return;
- }
-
- closeCommandMenu();
- onClick();
+ mountHeadlessFrontComponent(frontComponentId, {
+ commandMenuItemId,
+ recordId,
+ objectNameSingular,
+ });
};
return (
-
);
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useUnmountEngineCommand.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useUnmountEngineCommand.ts
index caa8858a20..777a950784 100644
--- a/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useUnmountEngineCommand.ts
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useUnmountEngineCommand.ts
@@ -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],
);
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx
index 1bea3b5a6e..1632c688a3 100644
--- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx
@@ -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 ;
+};
+
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 ;
+ return (
+
+
+
+ );
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/selectors/isEngineCommandMountedFamilySelector.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/selectors/isEngineCommandMountedFamilySelector.ts
new file mode 100644
index 0000000000..a2d9558132
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/selectors/isEngineCommandMountedFamilySelector.ts
@@ -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);
+ },
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
index 01e951f2e2..5c7c51f2ea 100644
--- a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
@@ -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 ? (
) : (
-
+
),
};
};
@@ -140,12 +131,6 @@ type BuildCommandMenuItemFromStandardKeyParams = {
isPinned: boolean;
getIcon: ReturnType['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: ,
+ component: (
+
+ ),
};
};
@@ -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,
});
}
diff --git a/packages/twenty-front/src/modules/command-menu-item/states/commandMenuItemProgressFamilyState.ts b/packages/twenty-front/src/modules/command-menu-item/states/commandMenuItemProgressFamilyState.ts
new file mode 100644
index 0000000000..fb0960f2cd
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/states/commandMenuItemProgressFamilyState.ts
@@ -0,0 +1,9 @@
+import { createAtomFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomFamilyState';
+
+export const commandMenuItemProgressFamilyState = createAtomFamilyState<
+ number | undefined,
+ string
+>({
+ key: 'commandMenuItemProgressFamilyState',
+ defaultValue: undefined,
+});
diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx
index 06fb94182f..483ccbe3a5 100644
--- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx
+++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx
@@ -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) => {
diff --git a/packages/twenty-front/src/modules/front-components/components/HeadlessFrontComponentMountRoot.tsx b/packages/twenty-front/src/modules/front-components/components/HeadlessFrontComponentMountRoot.tsx
index df6463c160..e2674f4fab 100644
--- a/packages/twenty-front/src/modules/front-components/components/HeadlessFrontComponentMountRoot.tsx
+++ b/packages/twenty-front/src/modules/front-components/components/HeadlessFrontComponentMountRoot.tsx
@@ -18,6 +18,7 @@ export const HeadlessFrontComponentMountRoot = () => {
const mountedHeadlessFrontComponentMaps = useAtomStateValue(
mountedHeadlessFrontComponentMapsState,
);
+
const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
return (
@@ -33,8 +34,8 @@ export const HeadlessFrontComponentMountRoot = () => {
{
isInSidePanel: false,
}}
>
-
+
diff --git a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts
index f943e4e70d..b7a0e08885 100644
--- a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts
+++ b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts
@@ -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 {
diff --git a/packages/twenty-front/src/modules/front-components/hooks/useMountHeadlessFrontComponent.ts b/packages/twenty-front/src/modules/front-components/hooks/useMountHeadlessFrontComponent.ts
index cdf560727e..a40ba9b33d 100644
--- a/packages/twenty-front/src/modules/front-components/hooks/useMountHeadlessFrontComponent.ts
+++ b/packages/twenty-front/src/modules/front-components/hooks/useMountHeadlessFrontComponent.ts
@@ -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;
});
},
diff --git a/packages/twenty-front/src/modules/front-components/hooks/useUnmountHeadlessFrontComponent.ts b/packages/twenty-front/src/modules/front-components/hooks/useUnmountHeadlessFrontComponent.ts
index 475a48e634..a36e312ac7 100644
--- a/packages/twenty-front/src/modules/front-components/hooks/useUnmountHeadlessFrontComponent.ts
+++ b/packages/twenty-front/src/modules/front-components/hooks/useUnmountHeadlessFrontComponent.ts
@@ -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],
);
diff --git a/packages/twenty-front/src/modules/front-components/states/mountedHeadlessFrontComponentMapsState.ts b/packages/twenty-front/src/modules/front-components/states/mountedHeadlessFrontComponentMapsState.ts
index 0a2f13ec60..5bada25950 100644
--- a/packages/twenty-front/src/modules/front-components/states/mountedHeadlessFrontComponentMapsState.ts
+++ b/packages/twenty-front/src/modules/front-components/states/mountedHeadlessFrontComponentMapsState.ts
@@ -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
diff --git a/packages/twenty-sdk/src/front-component-renderer/remote/worker/remote-worker.ts b/packages/twenty-sdk/src/front-component-renderer/remote/worker/remote-worker.ts
index 59ffb7d092..e8251a84c4 100644
--- a/packages/twenty-sdk/src/front-component-renderer/remote/worker/remote-worker.ts
+++ b/packages/twenty-sdk/src/front-component-renderer/remote/worker/remote-worker.ts
@@ -108,6 +108,7 @@ const initializeHostCommunicationApi: WorkerExports['initializeHostCommunication
frontComponentHostCommunicationApi.enqueueSnackbar =
hostApi.enqueueSnackbar;
frontComponentHostCommunicationApi.closeSidePanel = hostApi.closeSidePanel;
+ frontComponentHostCommunicationApi.updateProgress = hostApi.updateProgress;
};
const onConfirmationModalResult: WorkerExports['onConfirmationModalResult'] =
diff --git a/packages/twenty-sdk/src/front-component-renderer/types/FrontComponentHostCommunicationApi.ts b/packages/twenty-sdk/src/front-component-renderer/types/FrontComponentHostCommunicationApi.ts
index a24027cb46..afd8d5e6c6 100644
--- a/packages/twenty-sdk/src/front-component-renderer/types/FrontComponentHostCommunicationApi.ts
+++ b/packages/twenty-sdk/src/front-component-renderer/types/FrontComponentHostCommunicationApi.ts
@@ -6,6 +6,7 @@ import {
type OpenSidePanelPageFunction,
type RequestAccessTokenRefreshFunction,
type UnmountFrontComponentFunction,
+ type UpdateProgressFunction,
} from '../../sdk/front-component-api/globals/frontComponentHostCommunicationApi';
export type FrontComponentHostCommunicationApi = {
@@ -16,4 +17,5 @@ export type FrontComponentHostCommunicationApi = {
unmountFrontComponent: UnmountFrontComponentFunction;
enqueueSnackbar: EnqueueSnackbarFunction;
closeSidePanel: CloseSidePanelFunction;
+ updateProgress: UpdateProgressFunction;
};
diff --git a/packages/twenty-sdk/src/sdk/front-component-api/functions/updateProgress.ts b/packages/twenty-sdk/src/sdk/front-component-api/functions/updateProgress.ts
new file mode 100644
index 0000000000..15c2509ac6
--- /dev/null
+++ b/packages/twenty-sdk/src/sdk/front-component-api/functions/updateProgress.ts
@@ -0,0 +1,17 @@
+import { isDefined } from 'twenty-shared/utils';
+
+import {
+ frontComponentHostCommunicationApi,
+ type UpdateProgressFunction,
+} from '../globals/frontComponentHostCommunicationApi';
+
+export const updateProgress: UpdateProgressFunction = (progress) => {
+ const updateProgressFunction =
+ frontComponentHostCommunicationApi.updateProgress;
+
+ if (!isDefined(updateProgressFunction)) {
+ throw new Error('updateProgressFunction is not set');
+ }
+
+ return updateProgressFunction(progress);
+};
diff --git a/packages/twenty-sdk/src/sdk/front-component-api/globals/frontComponentHostCommunicationApi.ts b/packages/twenty-sdk/src/sdk/front-component-api/globals/frontComponentHostCommunicationApi.ts
index 2bfcaf93fe..025392e038 100644
--- a/packages/twenty-sdk/src/sdk/front-component-api/globals/frontComponentHostCommunicationApi.ts
+++ b/packages/twenty-sdk/src/sdk/front-component-api/globals/frontComponentHostCommunicationApi.ts
@@ -39,6 +39,8 @@ export type EnqueueSnackbarFunction = (
export type CloseSidePanelFunction = () => Promise;
+export type UpdateProgressFunction = (progress: number) => Promise;
+
export type RequestAccessTokenRefreshFunction = () => Promise;
export type OpenCommandConfirmationModalHostFunction = (
@@ -53,6 +55,7 @@ export type FrontComponentHostCommunicationApiStore = {
unmountFrontComponent?: UnmountFrontComponentFunction;
enqueueSnackbar?: EnqueueSnackbarFunction;
closeSidePanel?: CloseSidePanelFunction;
+ updateProgress?: UpdateProgressFunction;
};
declare global {
diff --git a/packages/twenty-sdk/src/sdk/front-component-api/index.ts b/packages/twenty-sdk/src/sdk/front-component-api/index.ts
index 45e7d572b1..ee6791593c 100644
--- a/packages/twenty-sdk/src/sdk/front-component-api/index.ts
+++ b/packages/twenty-sdk/src/sdk/front-component-api/index.ts
@@ -33,6 +33,7 @@ export { navigate } from './functions/navigate';
export { openCommandConfirmationModal } from './functions/openCommandConfirmationModal';
export { openSidePanelPage } from './functions/openSidePanelPage';
export { unmountFrontComponent } from './functions/unmountFrontComponent';
+export { updateProgress } from './functions/updateProgress';
export { useFrontComponentExecutionContext } from './hooks/useFrontComponentExecutionContext';
export { useFrontComponentId } from './hooks/useFrontComponentId';
export { useRecordId } from './hooks/useRecordId';
diff --git a/packages/twenty-sdk/src/sdk/index.ts b/packages/twenty-sdk/src/sdk/index.ts
index f9d0458541..b87c95c855 100644
--- a/packages/twenty-sdk/src/sdk/index.ts
+++ b/packages/twenty-sdk/src/sdk/index.ts
@@ -128,6 +128,7 @@ export {
openCommandConfirmationModal,
openSidePanelPage,
unmountFrontComponent,
+ updateProgress,
useFrontComponentExecutionContext,
useFrontComponentId,
useRecordId,