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,