[FRONT COMPONENTS] Headless components (#18096)

## Description

- Add `isHeadless` field to `FrontComponent` entity so front components
can run without rendering UI in the command menu
- Introduce headless front component mounting logic:
`HeadlessFrontComponentMountRoot` at the application root,
`useMountHeadlessFrontComponent`, and `useUnmountHeadlessFrontComponent`
hooks to mount/unmount headless components
- Expand the SDK with new action components (`Action`, `ActionLink`,
`ActionOpenSidePanelPage`) and host communication functions
(`openSidePanelPage`, `unmountFrontComponent`)
- Move `CommandMenuPages` type to twenty-shared so the SDK can reference
it for side panel navigation

## Video QA



https://github.com/user-attachments/assets/4f9e3bb1-fcd1-42be-b3f4-a97e80c2add2
This commit is contained in:
Raphaël Bosi
2026-02-20 15:14:42 +01:00
committed by GitHub
parent d444648cc0
commit 7da8450075
100 changed files with 518 additions and 143 deletions
File diff suppressed because one or more lines are too long
@@ -2,11 +2,11 @@ import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDi
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type MessageDescriptor } from '@lingui/core';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { type CommandMenuPages } from 'twenty-shared/types';
import { type IconComponent } from 'twenty-ui/display';
export const ActionOpenSidePanelPage = ({
@@ -4,8 +4,8 @@ import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
import { ActionType } from '@/action-menu/actions/types/ActionType';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { msg } from '@lingui/core/macro';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconHistory, IconSearch, IconSparkles } from 'twenty-ui/display';
export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
@@ -11,6 +11,7 @@ import { ClientConfigProviderEffect } from '@/client-config/components/ClientCon
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect';
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
@@ -68,6 +69,7 @@ export const AppRouterProviders = () => {
<PageFavicon />
<Outlet />
<GlobalFilePreviewModal />
<HeadlessFrontComponentMountRoot />
</StrictMode>
</DialogManager>
</DialogComponentInstanceContext.Provider>
@@ -18,7 +18,6 @@ import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoad
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
@@ -37,7 +36,7 @@ import { PageFocusId } from '@/types/PageFocusId';
import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { AppBasePath, AppPath } from 'twenty-shared/types';
import { AppBasePath, AppPath, CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { AnalyticsType } from '~/generated-metadata/graphql';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
@@ -8,6 +8,7 @@ export const COMMAND_MENU_ITEM_FRAGMENT = gql`
frontComponent {
id
name
isHeadless
}
label
icon
@@ -6,6 +6,7 @@ import { useOpenFrontComponentInCommandMenu } from '@/command-menu/hooks/useOpen
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useMountHeadlessFrontComponent } from '@/front-components/hooks/useMountHeadlessFrontComponent';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react';
@@ -35,6 +36,7 @@ type BuildActionFromItemParams = {
pageTitle: string;
pageIcon: IconComponent;
}) => void;
mountHeadlessFrontComponent: (frontComponentId: string) => void;
};
const buildActionFromItem = ({
@@ -44,11 +46,26 @@ const buildActionFromItem = ({
isPinned,
getIcon,
openFrontComponentInCommandMenu,
mountHeadlessFrontComponent,
}: BuildActionFromItemParams) => {
const displayLabel = item.label;
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
const isHeadless = item.frontComponent?.isHeadless === true;
const handleClick = () => {
if (isHeadless) {
mountHeadlessFrontComponent(item.frontComponentId);
} else {
openFrontComponentInCommandMenu({
frontComponentId: item.frontComponentId,
pageTitle: displayLabel,
pageIcon: Icon,
});
}
};
return {
type: ActionType.FrontComponent,
key: `command-menu-item-front-component-${item.id}`,
@@ -61,14 +78,8 @@ const buildActionFromItem = ({
shouldBeRegistered: () => true,
component: (
<Action
onClick={() =>
openFrontComponentInCommandMenu({
frontComponentId: item.frontComponentId,
pageTitle: displayLabel,
pageIcon: Icon,
})
}
closeSidePanelOnCommandMenuListActionExecution={false}
onClick={handleClick}
closeSidePanelOnCommandMenuListActionExecution={isHeadless}
/>
),
};
@@ -78,6 +89,7 @@ export const useCommandMenuItemFrontComponentActions = () => {
const { getIcon } = useIcons();
const { openFrontComponentInCommandMenu } =
useOpenFrontComponentInCommandMenu();
const mountHeadlessFrontComponent = useMountHeadlessFrontComponent();
const isPageInEditMode = useRecoilComponentValue(
contextStoreIsPageInEditModeComponentState,
@@ -140,6 +152,7 @@ export const useCommandMenuItemFrontComponentActions = () => {
isPinned: !isPageInEditMode && item.isPinned,
getIcon,
openFrontComponentInCommandMenu,
mountHeadlessFrontComponent,
}),
);
@@ -151,6 +164,7 @@ export const useCommandMenuItemFrontComponentActions = () => {
isPinned: !isPageInEditMode && item.isPinned,
getIcon,
openFrontComponentInCommandMenu,
mountHeadlessFrontComponent,
}),
);
@@ -1,8 +1,8 @@
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { Fragment } from 'react/jsx-runtime';
import { type CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
@@ -10,11 +10,11 @@ import { CommandMenuObjectViewRecordInfo } from '@/command-menu/components/Comma
import { CommandMenuPageLayoutInfo } from '@/command-menu/components/CommandMenuPageLayoutInfo';
import { CommandMenuRecordInfo } from '@/command-menu/components/CommandMenuRecordInfo';
import { CommandMenuWorkflowStepInfo } from '@/command-menu/components/CommandMenuWorkflowStepInfo';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeStateV2 } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { CommandMenuPages } from 'twenty-shared/types';
import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
@@ -3,7 +3,6 @@ import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateComm
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
@@ -16,6 +15,7 @@ import { useTheme } from '@emotion/react';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout';
@@ -11,7 +11,6 @@ import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuC
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@@ -21,6 +20,7 @@ import { useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { useRef } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconX } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
@@ -1,8 +1,8 @@
import { useRecoilValue } from 'recoil';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useEffect } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
type CommandMenuTopBarInputFocusEffectProps = {
inputRef: React.RefObject<HTMLInputElement>;
@@ -1,10 +1,10 @@
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconEdit, IconSparkles } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
@@ -2,7 +2,6 @@ import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateComm
import { useCommandMenuWorkflowIdOrThrow } from '@/command-menu/pages/workflow/hooks/useCommandMenuWorkflowIdOrThrow';
import { commandMenuWorkflowStepIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { TitleInput } from '@/ui/input/components/TitleInput';
@@ -22,6 +21,7 @@ import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { useIcons } from 'twenty-ui/display';
@@ -28,12 +28,12 @@ import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/Com
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { HttpResponse, graphql } from 'msw';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconDotsVertical } from 'twenty-ui/display';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
@@ -1,11 +1,11 @@
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
import { getCurrentGraphTypeFromConfig } from '@/command-menu/pages/page-layout/utils/getCurrentGraphTypeFromConfig';
import { isWidgetConfigurationOfTypeGraph } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
IconAppWindow,
@@ -24,7 +24,7 @@ import { CommandMenuWorkflowEditStepType } from '@/command-menu/pages/workflow/s
import { CommandMenuWorkflowRunViewStep } from '@/command-menu/pages/workflow/step/view-run/components/CommandMenuWorkflowRunViewStep';
import { CommandMenuWorkflowViewStep } from '@/command-menu/pages/workflow/step/view/components/CommandMenuWorkflowViewStep';
import { CommandMenuWorkflowSelectTriggerType } from '@/command-menu/pages/workflow/trigger-type/components/CommandMenuWorkflowSelectTriggerType';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CommandMenuPages } from 'twenty-shared/types';
export const COMMAND_MENU_PAGES_CONFIG = new Map<
CommandMenuPages,
@@ -8,7 +8,7 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconDotsVertical } from 'twenty-ui/display';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
@@ -15,8 +15,8 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconList } from 'twenty-ui/display';
const mockCloseDropdown = jest.fn();
@@ -10,7 +10,7 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconList, IconSearch } from 'twenty-ui/display';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
@@ -7,8 +7,8 @@ import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandM
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { CommandMenuPages } from 'twenty-shared/types';
import { Icon123, useIcons } from 'twenty-ui/display';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
@@ -1,12 +1,12 @@
import { renderHook, act } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { type ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconSparkles } from 'twenty-ui/display';
const navigateCommandMenuMock = jest.fn();
@@ -4,9 +4,9 @@ import { act } from 'react';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useOpenCalendarEventInCommandMenu } from '@/command-menu/hooks/useOpenCalendarEventInCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconCalendarEvent } from 'twenty-ui/display';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
@@ -4,9 +4,9 @@ import { act } from 'react';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useOpenEmailThreadInCommandMenu } from '@/command-menu/hooks/useOpenEmailThreadInCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconMail } from 'twenty-ui/display';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
@@ -8,13 +8,13 @@ import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { CommandMenuPages } from 'twenty-shared/types';
import { useIcons } from 'twenty-ui/display';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
@@ -1,10 +1,10 @@
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconArrowDown, IconDotsVertical } from 'twenty-ui/display';
const mockedPageInfo = {
@@ -2,13 +2,13 @@ import { renderHook } from '@testing-library/react';
import { useRecoilValue } from 'recoil';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState';
import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState';
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
@@ -17,10 +17,10 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { act } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconBolt, IconSettingsAutomation, useIcons } from 'twenty-ui/display';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('mocked-uuid'),
@@ -6,14 +6,14 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { addToNavPayloadRegistryStateV2 } from '@/navigation-menu-item/states/addToNavPayloadRegistryStateV2';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { emitSidePanelOpenEvent } from '@/ui/layout/right-drawer/utils/emitSidePanelOpenEvent';
import { useStore } from 'jotai';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconDotsVertical } from 'twenty-ui/display';
export const useCommandMenu = () => {
@@ -12,7 +12,6 @@ import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelect
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@@ -28,6 +27,7 @@ import { WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workfl
import { WorkflowLogicFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowLogicFunctionTabId';
import { useStore } from 'jotai';
import { useRecoilCallback } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const useCommandMenuCloseAnimationCompleteCleanup = () => {
@@ -2,7 +2,7 @@ import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/C
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CommandMenuPages } from 'twenty-shared/types';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { recordStoreIdentifiersFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifiersSelector';
import { recordStoreRecordsSelector } from '@/object-record/record-store/states/selectors/recordStoreRecordsSelector';
@@ -7,7 +7,6 @@ import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useO
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
@@ -17,6 +16,7 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { CommandMenuPages } from 'twenty-shared/types';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useCommandMenuHotKeys = () => {
@@ -11,12 +11,12 @@ import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelect
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useStore } from 'jotai';
import { useRecoilCallback } from 'recoil';
import { type CommandMenuPages } from 'twenty-shared/types';
import { type IconComponent } from 'twenty-ui/display';
import { v4 } from 'uuid';
@@ -1,9 +1,9 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { t } from '@lingui/core/macro';
import { useCallback } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconSparkles } from 'twenty-ui/display';
import { v4 } from 'uuid';
@@ -1,10 +1,10 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconCalendarEvent } from 'twenty-ui/display';
import { v4 } from 'uuid';
export const useOpenCalendarEventInCommandMenu = () => {
const { navigateCommandMenu } = useNavigateCommandMenu();
@@ -1,10 +1,10 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconMail } from 'twenty-ui/display';
import { v4 } from 'uuid';
export const useOpenEmailThreadInCommandMenu = () => {
const { navigateCommandMenu } = useNavigateCommandMenu();
@@ -1,7 +1,7 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { viewableFrontComponentIdComponentState } from '@/command-menu/pages/front-component/states/viewableFrontComponentIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useRecoilCallback } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { type IconComponent } from 'twenty-ui/display';
import { v4 } from 'uuid';
@@ -1,10 +1,10 @@
import { useCommandMenuUpdateNavigationMorphItemsByPage } from '@/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { CommandMenuPages } from 'twenty-shared/types';
import { msg, t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
@@ -3,7 +3,6 @@ import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
@@ -17,6 +16,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { CommandMenuPages } from 'twenty-shared/types';
import { useRunWorkflowRunOpeningInCommandMenuSideEffects } from '@/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects';
import { useTheme } from '@emotion/react';
@@ -1,10 +1,10 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { t } from '@lingui/core/macro';
import { v4 } from 'uuid';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconSearch } from 'twenty-ui/display';
import { v4 } from 'uuid';
export const useOpenRecordsSearchPageInCommandMenu = () => {
const { navigateCommandMenu } = useCommandMenu();
@@ -1,5 +1,5 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CommandMenuPages } from 'twenty-shared/types';
import { msg, t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
@@ -1,9 +1,9 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { viewableRichTextComponentStateV2 } from '@/command-menu/pages/rich-text-page/states/viewableRichTextComponentStateV2';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useStore } from 'jotai';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconPencil } from 'twenty-ui/display';
export const useRichTextCommandMenu = () => {
@@ -3,12 +3,12 @@ import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workfl
import { commandMenuWorkflowRunIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowRunIdComponentState';
import { commandMenuWorkflowStepIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState';
import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type WorkflowRunStepStatus } from '@/workflow/types/Workflow';
import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
IconBolt,
@@ -5,15 +5,15 @@ import { IconPlus } from 'twenty-ui/display';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { type OrganizeActionsProps } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { addMenuItemInsertionContextStateV2 } from '@/navigation-menu-item/states/addMenuItemInsertionContextStateV2';
import { selectedNavigationMenuItemInEditModeStateV2 } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeStateV2';
import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/types/AddMenuItemInsertionContext';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/types/AddMenuItemInsertionContext';
import { CommandMenuPages } from 'twenty-shared/types';
const getAddMenuItemInsertionContext = (
selectedItem: { id: string; folderId?: string | null },
@@ -5,11 +5,11 @@ import { WidgetSettingsFooter } from '@/command-menu/pages/page-layout/component
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconLayoutSidebarRight } from 'twenty-ui/display';
import { type FieldsConfiguration } from '~/generated-metadata/graphql';
@@ -6,7 +6,6 @@ import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layo
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { getFrontComponentWidgetTypeSelectItemId } from '@/command-menu/pages/page-layout/utils/getFrontComponentWidgetTypeSelectItemId';
import { isExistingWidgetMissingOrDifferentType } from '@/command-menu/pages/page-layout/utils/isExistingWidgetMissingOrDifferentType';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { FIND_MANY_FRONT_COMPONENTS } from '@/front-components/graphql/queries/findManyFrontComponents';
import { useCreatePageLayoutFrontComponentWidget } from '@/page-layout/hooks/useCreatePageLayoutFrontComponentWidget';
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
@@ -22,6 +21,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useQuery } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
IconAlignBoxLeftTop,
@@ -14,7 +14,6 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
import { isMinMaxRangeValid } from '@/command-menu/pages/page-layout/utils/isMinMaxRangeValid';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
@@ -22,6 +21,7 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { t } from '@lingui/core/macro';
import { isString } from '@sniptt/guards';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
type ChartSettingItemProps = {
@@ -1,4 +1,4 @@
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type CommandMenuPages } from 'twenty-shared/types';
export type PageLayoutCommandMenuPage =
| CommandMenuPages.PageLayoutWidgetTypeSelect
@@ -1,5 +1,5 @@
import { type PageLayoutCommandMenuPage } from '@/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CommandMenuPages } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import {
IconAppWindow,
@@ -1,6 +1,6 @@
import { type PageLayoutCommandMenuPage } from '@/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
import { CommandMenuPages } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
export const getPageLayoutPageTitle = (page: PageLayoutCommandMenuPage) => {
@@ -1,6 +1,6 @@
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type IconComponent } from 'twenty-ui/display';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type CommandMenuPages } from 'twenty-shared/types';
import { type IconComponent } from 'twenty-ui/display';
export type CommandMenuNavigationStackItem = {
page: CommandMenuPages;
@@ -1,5 +1,5 @@
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { createState } from '@/ui/utilities/state/utils/createState';
import { CommandMenuPages } from 'twenty-shared/types';
export const commandMenuPageState = createState<CommandMenuPages>({
key: 'command-menu/commandMenuPageState',
@@ -20,7 +20,7 @@ export const FrontComponentRenderer = ({
const theme = useTheme();
const { enqueueErrorSnackBar } = useSnackBar();
const { executionContext, frontComponentHostCommunicationApi } =
useFrontComponentExecutionContext();
useFrontComponentExecutionContext({ frontComponentId });
const handleError = useCallback(
(error?: Error) => {
@@ -0,0 +1,31 @@
import { Suspense, lazy } from 'react';
import { useRecoilValue } from 'recoil';
import { mountedHeadlessFrontComponentIdsState } from '@/front-components/states/mountedHeadlessFrontComponentIdsState';
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
(module) => ({ default: module.FrontComponentRenderer }),
),
);
export const HeadlessFrontComponentMountRoot = () => {
const mountedHeadlessFrontComponentIds = useRecoilValue(
mountedHeadlessFrontComponentIdsState,
);
if (mountedHeadlessFrontComponentIds.size === 0) {
return null;
}
return (
<>
{[...mountedHeadlessFrontComponentIds].map((frontComponentId) => (
<Suspense key={frontComponentId} fallback={null}>
<FrontComponentRenderer frontComponentId={frontComponentId} />
</Suspense>
))}
</>
);
};
@@ -7,6 +7,7 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
name
applicationId
builtComponentChecksum
isHeadless
applicationTokenPair {
applicationAccessToken {
token
@@ -1,4 +1,4 @@
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
type FrontComponentExecutionContext,
type FrontComponentHostCommunicationApi,
@@ -6,14 +6,26 @@ import {
import { type AppPath } from 'twenty-shared/types';
import { currentUserState } from '@/auth/states/currentUserState';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
import { useIcons } from 'twenty-ui/display';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useFrontComponentExecutionContext = (): {
export const useFrontComponentExecutionContext = ({
frontComponentId,
}: {
frontComponentId: string;
}): {
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
} => {
const currentUser = useRecoilValue(currentUserState);
const navigateApp = useNavigateApp();
const { navigateCommandMenu } = useNavigateCommandMenu();
const setCommandMenuSearchState = useSetRecoilState(commandMenuSearchState);
const { getIcon } = useIcons();
const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
const navigate: FrontComponentHostCommunicationApi['navigate'] = async (
to,
@@ -29,13 +41,33 @@ export const useFrontComponentExecutionContext = (): {
);
};
const openSidePanelPage: FrontComponentHostCommunicationApi['openSidePanelPage'] =
async ({ page, pageTitle, pageIcon, shouldResetSearchState }) => {
navigateCommandMenu({
page,
pageTitle,
pageIcon: getIcon(pageIcon),
});
if (shouldResetSearchState === true) {
setCommandMenuSearchState('');
}
};
const executionContext: FrontComponentExecutionContext = {
userId: currentUser?.id ?? null,
};
const unmountFrontComponent: FrontComponentHostCommunicationApi['unmountFrontComponent'] =
async () => {
unmountHeadlessFrontComponent(frontComponentId);
};
const frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
{
navigate,
openSidePanelPage,
unmountFrontComponent,
};
return {
@@ -0,0 +1,17 @@
import { useRecoilCallback } from 'recoil';
import { mountedHeadlessFrontComponentIdsState } from '@/front-components/states/mountedHeadlessFrontComponentIdsState';
export const useMountHeadlessFrontComponent = () => {
const mountHeadlessFrontComponent = useRecoilCallback(
({ set }) =>
(frontComponentId: string) => {
set(mountedHeadlessFrontComponentIdsState, (previousIds) =>
new Set(previousIds).add(frontComponentId),
);
},
[],
);
return mountHeadlessFrontComponent;
};
@@ -0,0 +1,19 @@
import { useRecoilCallback } from 'recoil';
import { mountedHeadlessFrontComponentIdsState } from '@/front-components/states/mountedHeadlessFrontComponentIdsState';
export const useUnmountHeadlessFrontComponent = () => {
const unmountHeadlessFrontComponent = useRecoilCallback(
({ set }) =>
(frontComponentId: string) => {
set(mountedHeadlessFrontComponentIdsState, (previousIds) => {
const next = new Set(previousIds);
next.delete(frontComponentId);
return next;
});
},
[],
);
return unmountHeadlessFrontComponent;
};
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const mountedHeadlessFrontComponentIdsState = atom<Set<string>>({
key: 'mountedHeadlessFrontComponentIdsState',
default: new Set(),
});
@@ -1,6 +1,5 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft';
import { isNavigationMenuInEditModeStateV2 } from '@/navigation-menu-item/states/isNavigationMenuInEditModeStateV2';
@@ -16,6 +15,7 @@ import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconCheck, useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
@@ -7,7 +7,6 @@ import { LightIconButton } from 'twenty-ui/input';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
@@ -31,6 +30,7 @@ import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetReco
import { useStore } from 'jotai';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isNonEmptyString } from '@sniptt/guards';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
const StyledRightIconsContainer = styled.div`
@@ -1,5 +1,5 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CommandMenuPages } from 'twenty-shared/types';
import type { IconComponent } from 'twenty-ui/display';
export const useOpenNavigationMenuItemInCommandMenu = () => {
@@ -1,5 +1,4 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList';
import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect';
@@ -24,6 +23,7 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
@@ -28,7 +28,6 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateV2';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds';
import { PageLayoutTabListReorderableOverflowDropdown } from '@/page-layout/components/PageLayoutTabListReorderableOverflowDropdown';
import { PageLayoutTabListStaticOverflowDropdown } from '@/page-layout/components/PageLayoutTabListStaticOverflowDropdown';
@@ -45,6 +44,7 @@ import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/Ta
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type PageLayoutType } from '~/generated-metadata/graphql';
@@ -9,7 +9,6 @@ import {
} from '@hello-pangea/dnd';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds';
import { PageLayoutTabListDroppableMoreButton } from '@/page-layout/components/PageLayoutTabListDroppableMoreButton';
import { PageLayoutTabMenuItemSelectAvatar } from '@/page-layout/components/PageLayoutTabMenuItemSelectAvatar';
@@ -29,6 +28,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useContext } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { type PageLayoutType } from '~/generated-metadata/graphql';
const StyledOverflowDropdownListDraggableWrapper = styled.div`
@@ -1,11 +1,11 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useCreateWidgetFromClick } from '@/page-layout/hooks/useCreateWidgetFromClick';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { useCreateWidgetFromClick } from '@/page-layout/hooks/useCreateWidgetFromClick';
import { CommandMenuPages } from 'twenty-shared/types';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
@@ -1,13 +1,13 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useEndPageLayoutDragSelection } from '@/page-layout/hooks/useEndPageLayoutDragSelection';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
import { calculateGridBoundsFromSelectedCells } from '@/page-layout/utils/calculateGridBoundsFromSelectedCells';
import { useEndPageLayoutDragSelection } from '@/page-layout/hooks/useEndPageLayoutDragSelection';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
@@ -1,10 +1,10 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { parseCellIdToCoordinates } from '@/page-layout/utils/parseCellIdToCoordinates';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
export const useCreateWidgetFromClick = () => {
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
@@ -1,6 +1,6 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { CommandMenuPages } from 'twenty-shared/types';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
@@ -3,8 +3,8 @@ import { useSetRecoilState } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { CommandMenuPages } from 'twenty-shared/types';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
@@ -1,14 +1,14 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
import { calculateGridBoundsFromSelectedCells } from '@/page-layout/utils/calculateGridBoundsFromSelectedCells';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
import { calculateGridBoundsFromSelectedCells } from '@/page-layout/utils/calculateGridBoundsFromSelectedCells';
export const useEndPageLayoutDragSelection = (
pageLayoutIdFromProps?: string,
@@ -1,5 +1,4 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
@@ -9,6 +8,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { CommandMenuPages } from 'twenty-shared/types';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@@ -46,6 +46,7 @@ export const EXPECTED_MANIFEST: Manifest = {
name: 'root-component',
sourceComponentPath: 'src/root.front-component.tsx',
universalIdentifier: 'a0a1a2a3-a4a5-4000-8000-000000000001',
isHeadless: false,
},
{
builtComponentPath: 'src/components/card.front-component.mjs',
@@ -55,6 +56,7 @@ export const EXPECTED_MANIFEST: Manifest = {
name: 'card-component',
sourceComponentPath: 'src/components/card.front-component.tsx',
universalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
isHeadless: false,
},
{
builtComponentPath: 'src/components/greeting.front-component.mjs',
@@ -64,6 +66,7 @@ export const EXPECTED_MANIFEST: Manifest = {
name: 'greeting-component',
sourceComponentPath: 'src/components/greeting.front-component.tsx',
universalIdentifier: '370ae182-743f-4ecb-b625-7ac48e21f0e5',
isHeadless: false,
},
{
builtComponentPath: 'src/components/test.front-component.mjs',
@@ -73,6 +76,7 @@ export const EXPECTED_MANIFEST: Manifest = {
name: 'test-component',
sourceComponentPath: 'src/components/test.front-component.tsx',
universalIdentifier: 'f1234567-abcd-4000-8000-000000000001',
isHeadless: false,
},
],
@@ -343,6 +343,7 @@ export const EXPECTED_MANIFEST: Manifest = {
sourceComponentPath: 'my.front-component.tsx',
builtComponentPath: 'my.front-component.mjs',
builtComponentChecksum: '[checksum]',
isHeadless: false,
},
],
views: [],
@@ -227,6 +227,7 @@ export const buildManifest = async (
sourceComponentPath: relativeFilePath,
builtComponentPath: relativeFilePath.replace(/\.tsx?$/, '.mjs'),
builtComponentChecksum: '',
isHeadless: rest.isHeadless ?? false,
};
frontComponents.push(config);
@@ -29,38 +29,32 @@ export const FrontComponentWorkerEffect = ({
setThread,
setError,
}: FrontComponentWorkerEffectProps) => {
const frontComponentHostCommunicationApiRef = useRef(
frontComponentHostCommunicationApi,
);
frontComponentHostCommunicationApiRef.current =
frontComponentHostCommunicationApi;
const isInitializedRef = useRef(false);
useEffect(() => {
if (isInitializedRef.current) {
return;
}
const newReceiver = new RemoteReceiver({ retain, release });
const worker = createRemoteWorker();
worker.onerror = (event: ErrorEvent) => {
const workerError =
event.error ??
new Error(event.message || 'Unknown worker error');
event.error ?? new Error(event.message || 'Unknown worker error');
console.error('[FrontComponentRenderer] Worker error:', workerError);
setError(workerError);
};
const stableFrontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
{
navigate: (...args) =>
frontComponentHostCommunicationApiRef.current.navigate(...args),
};
const thread = new ThreadWebWorker<
WorkerExports,
FrontComponentHostCommunicationApi
>(worker, {
exports: stableFrontComponentHostCommunicationApi,
exports: frontComponentHostCommunicationApi,
});
setThread(thread);
thread.imports
@@ -74,6 +68,7 @@ export const FrontComponentWorkerEffect = ({
});
setReceiver(newReceiver);
isInitializedRef.current = true;
return () => {
setThread(null);
@@ -86,6 +81,7 @@ export const FrontComponentWorkerEffect = ({
setError,
setReceiver,
setThread,
frontComponentHostCommunicationApi,
]);
return null;
@@ -17,7 +17,6 @@ import { installStylePropertyOnRemoteElements } from '@/front-component-renderer
import { patchRemoteElementSetAttribute } from '@/front-component-renderer/remote/utils/patchRemoteElementSetAttribute';
import { HTML_TAG_TO_CUSTOM_ELEMENT_TAG } from '@/sdk/front-component-api/constants/HtmlTagToRemoteComponent';
import { setFrontComponentExecutionContext } from '@/sdk/front-component-api/context/frontComponentContext';
import { setNavigate } from '@/sdk/front-component-api/functions/navigate';
import { type FrontComponentExecutionContext } from '../../types/FrontComponentExecutionContext';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
@@ -89,7 +88,12 @@ const initializeHostCommunicationApi: WorkerExports['initializeHostCommunication
async () => {
const hostApi =
ThreadWebWorker.self.import<FrontComponentHostCommunicationApi>();
setNavigate(hostApi.navigate);
frontComponentHostCommunicationApi.navigate = hostApi.navigate;
frontComponentHostCommunicationApi.openSidePanelPage =
hostApi.openSidePanelPage;
frontComponentHostCommunicationApi.unmountFrontComponent =
hostApi.unmountFrontComponent;
};
const updateContext: WorkerExports['updateContext'] = async (
@@ -1,10 +1,11 @@
import { type AppPath, type NavigateOptions } from 'twenty-shared/types';
import {
type NavigateFunction,
type OpenSidePanelPageFunction,
type UnmountFrontComponentFunction,
} from '../../sdk/front-component-api/globals/frontComponentHostCommunicationApi';
export type FrontComponentHostCommunicationApi = {
navigate: (
to: AppPath,
params?: Record<string, string | null>,
queryParams?: Record<string, unknown>,
options?: NavigateOptions,
) => Promise<void>;
navigate: NavigateFunction;
openSidePanelPage: OpenSidePanelPageFunction;
unmountFrontComponent: UnmountFrontComponentFunction;
};
@@ -0,0 +1,31 @@
import { useEffect, useState } from 'react';
import { unmountFrontComponent } from '../front-component-api';
export type ActionProps = {
execute: () => void | Promise<void>;
};
export const Action = ({ execute }: ActionProps) => {
const [hasExecuted, setHasExecuted] = useState(false);
useEffect(() => {
if (hasExecuted) {
return;
}
setHasExecuted(true);
const run = async () => {
try {
await execute();
} finally {
await unmountFrontComponent();
}
};
run();
}, [execute, hasExecuted]);
return null;
};
@@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';
import { type NavigateOptions } from 'react-router-dom';
import { type AppPath } from 'twenty-shared/types';
import { type getAppPath } from 'twenty-shared/utils';
import { navigate, unmountFrontComponent } from '../front-component-api';
export type ActionLinkProps<T extends AppPath> = {
to: T;
params?: Parameters<typeof getAppPath<T>>[1];
queryParams?: Record<string, any>;
options?: NavigateOptions;
};
export const ActionLink = <T extends AppPath>({
to,
params,
queryParams,
options,
}: ActionLinkProps<T>) => {
const [hasExecuted, setHasExecuted] = useState(false);
useEffect(() => {
if (hasExecuted) {
return;
}
setHasExecuted(true);
const run = async () => {
try {
await navigate(to, params, queryParams, options);
} finally {
await unmountFrontComponent();
}
};
run();
}, [to, params, queryParams, options, hasExecuted]);
return null;
};
@@ -0,0 +1,52 @@
import {
openSidePanelPage,
unmountFrontComponent,
} from '@/sdk/front-component-api';
import { useEffect, useState } from 'react';
import { type CommandMenuPages } from 'twenty-shared/types';
export type ActionOpenSidePanelPageProps = {
page: CommandMenuPages;
pageTitle: string;
pageIcon: string;
onClick?: () => void;
shouldResetSearchState?: boolean;
};
export const ActionOpenSidePanelPage = ({
page,
pageTitle,
pageIcon,
onClick,
shouldResetSearchState = false,
}: ActionOpenSidePanelPageProps) => {
const [hasExecuted, setHasExecuted] = useState(false);
useEffect(() => {
if (hasExecuted) {
return;
}
setHasExecuted(true);
const run = async () => {
onClick?.();
try {
await openSidePanelPage({
page,
pageTitle,
pageIcon,
shouldResetSearchState,
});
} finally {
await unmountFrontComponent();
}
};
run();
}, [page, pageTitle, pageIcon, shouldResetSearchState, onClick, hasExecuted]);
return null;
};
@@ -0,0 +1,6 @@
export { Action } from './Action';
export type { ActionProps } from './Action';
export { ActionLink } from './ActionLink';
export type { ActionLinkProps } from './ActionLink';
export { ActionOpenSidePanelPage } from './ActionOpenSidePanelPage';
export type { ActionOpenSidePanelPageProps } from './ActionOpenSidePanelPage';
@@ -1,30 +1,17 @@
import { type AppPath, type NavigateOptions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
type NavigateFunction = (
to: AppPath,
params?: Record<string, string | null>,
queryParams?: Record<string, unknown>,
options?: NavigateOptions,
) => Promise<void>;
// State is stored on globalThis so the worker's SDK instance and each
// front component's bundled SDK copy share the same backing store.
const NAVIGATE_KEY = '__twentySdkNavigateFunction__';
export const setNavigate = (fn: NavigateFunction): void => {
(globalThis as Record<string, unknown>)[NAVIGATE_KEY] = fn;
};
import {
frontComponentHostCommunicationApi,
type NavigateFunction,
} from '../globals/frontComponentHostCommunicationApi';
export const navigate: NavigateFunction = (
to: AppPath,
params?: Record<string, string | null>,
queryParams?: Record<string, unknown>,
options?: NavigateOptions,
): Promise<void> => {
const navigateFunction = (globalThis as Record<string, unknown>)[
NAVIGATE_KEY
] as NavigateFunction | undefined;
to,
params,
queryParams,
options,
) => {
const navigateFunction = frontComponentHostCommunicationApi.navigate;
if (!isDefined(navigateFunction)) {
throw new Error('navigateFunction is not set');
@@ -0,0 +1,17 @@
import { isDefined } from 'twenty-shared/utils';
import {
frontComponentHostCommunicationApi,
type OpenSidePanelPageFunction,
} from '../globals/frontComponentHostCommunicationApi';
export const openSidePanelPage: OpenSidePanelPageFunction = (params) => {
const openSidePanelPageFunction =
frontComponentHostCommunicationApi.openSidePanelPage;
if (!isDefined(openSidePanelPageFunction)) {
throw new Error('openSidePanelPageFunction is not set');
}
return openSidePanelPageFunction(params);
};
@@ -0,0 +1,17 @@
import { isDefined } from 'twenty-shared/utils';
import {
frontComponentHostCommunicationApi,
type UnmountFrontComponentFunction,
} from '../globals/frontComponentHostCommunicationApi';
export const unmountFrontComponent: UnmountFrontComponentFunction = () => {
const unmountFrontComponentFunction =
frontComponentHostCommunicationApi.unmountFrontComponent;
if (!isDefined(unmountFrontComponentFunction)) {
throw new Error('unmountFrontComponentFunction is not set');
}
return unmountFrontComponentFunction();
};
@@ -0,0 +1,37 @@
import {
type AppPath,
type CommandMenuPages,
type NavigateOptions,
} from 'twenty-shared/types';
import { type getAppPath } from 'twenty-shared/utils';
export type NavigateFunction = <T extends AppPath>(
to: T,
params?: Parameters<typeof getAppPath<T>>[1],
queryParams?: Record<string, any>,
options?: NavigateOptions,
) => Promise<void>;
export type OpenSidePanelPageFunction = (params: {
page: CommandMenuPages;
pageTitle: string;
pageIcon?: string;
shouldResetSearchState?: boolean;
}) => Promise<void>;
export type UnmountFrontComponentFunction = () => Promise<void>;
export type FrontComponentHostCommunicationApiStore = {
navigate?: NavigateFunction;
openSidePanelPage?: OpenSidePanelPageFunction;
unmountFrontComponent?: UnmountFrontComponentFunction;
};
declare global {
var frontComponentHostCommunicationApi: FrontComponentHostCommunicationApiStore;
}
globalThis.frontComponentHostCommunicationApi ??= {};
export const frontComponentHostCommunicationApi =
globalThis.frontComponentHostCommunicationApi;
@@ -1,11 +1,13 @@
export { setFrontComponentExecutionContext } from './context/frontComponentContext';
export { navigate, setNavigate } from './functions/navigate';
export { navigate } from './functions/navigate';
export { openSidePanelPage } from './functions/openSidePanelPage';
export { unmountFrontComponent } from './functions/unmountFrontComponent';
export { useFrontComponentExecutionContext } from './hooks/useFrontComponentExecutionContext';
export { useUserId } from './hooks/useUserId';
export type { FrontComponentExecutionContext } from './types/FrontComponentExecutionContext';
export type { AllowedHtmlElement } from './constants/AllowedHtmlElements';
export { ALLOWED_HTML_ELEMENTS } from './constants/AllowedHtmlElements';
export type { AllowedHtmlElement } from './constants/AllowedHtmlElements';
export { COMMON_HTML_EVENTS } from './constants/CommonHtmlEvents';
export { EVENT_TO_REACT } from './constants/EventToReact';
export { HTML_COMMON_PROPERTIES } from './constants/HtmlCommonProperties';
+12
View File
@@ -63,14 +63,26 @@ export { defineSkill } from './skills/define-skill';
export { defineView } from './views/define-view';
export type { ViewConfig } from './views/view-config';
// Action components for front components
export { Action } from './action';
export type { ActionProps } from './action';
export { ActionLink } from './action';
export type { ActionLinkProps } from './action';
export { ActionOpenSidePanelPage } from './action';
export type { ActionOpenSidePanelPageProps } from './action';
// Front Component API exports
export {
navigate,
openSidePanelPage,
unmountFrontComponent,
useFrontComponentExecutionContext,
useUserId,
} from './front-component-api';
export type { FrontComponentExecutionContext } from './front-component-api';
export { AppPath, CommandMenuPages } from 'twenty-shared/types';
// Front Component Common exports
export {
ALLOWED_HTML_ELEMENTS,
@@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddIsHeadlessToFrontComponent1771509478665
implements MigrationInterface
{
name = 'AddIsHeadlessToFrontComponent1771509478665';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."frontComponent" ADD "isHeadless" boolean NOT NULL DEFAULT false`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."frontComponent" DROP COLUMN "isHeadless"`,
);
}
}
@@ -20,6 +20,7 @@ export const fromFrontComponentManifestToUniversalFlatFrontComponent = ({
builtComponentPath: frontComponentManifest.builtComponentPath,
componentName: frontComponentManifest.componentName,
builtComponentChecksum: frontComponentManifest.builtComponentChecksum,
isHeadless: frontComponentManifest.isHeadless ?? false,
createdAt: now,
updatedAt: now,
};
@@ -59,6 +59,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"sourceComponentPath",
"builtComponentPath",
"componentName",
"isHeadless",
],
"propertiesToStringify": [],
},
@@ -1221,6 +1221,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
toStringify: false,
universalProperty: undefined,
},
isHeadless: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
@@ -7,4 +7,5 @@ export const FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES = [
'sourceComponentPath',
'builtComponentPath',
'componentName',
'isHeadless',
] as const satisfies MetadataEntityPropertyName<'frontComponent'>[];
@@ -33,6 +33,7 @@ export const fromCreateFrontComponentInputToFlatFrontComponentToCreate = ({
builtComponentPath: createFrontComponentInput.builtComponentPath,
componentName: createFrontComponentInput.componentName,
builtComponentChecksum: createFrontComponentInput.builtComponentChecksum,
isHeadless: false,
workspaceId,
createdAt: now,
updatedAt: now,
@@ -20,6 +20,7 @@ export const fromFlatFrontComponentToFrontComponentDto = (
: undefined,
workspaceId: flatFrontComponent.workspaceId,
applicationId: flatFrontComponent.applicationId,
isHeadless: flatFrontComponent.isHeadless,
createdAt: new Date(flatFrontComponent.createdAt),
updatedAt: new Date(flatFrontComponent.updatedAt),
});
@@ -31,6 +31,7 @@ export const fromFrontComponentEntityToFlatFrontComponent = ({
builtComponentPath: frontComponentEntity.builtComponentPath,
componentName: frontComponentEntity.componentName,
builtComponentChecksum: frontComponentEntity.builtComponentChecksum,
isHeadless: frontComponentEntity.isHeadless,
workspaceId: frontComponentEntity.workspaceId,
universalIdentifier: frontComponentEntity.universalIdentifier,
applicationId: frontComponentEntity.applicationId,
@@ -1,6 +1,7 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import {
IsBoolean,
IsDateString,
IsNotEmpty,
IsOptional,
@@ -63,6 +64,10 @@ export class FrontComponentDTO {
@Field()
updatedAt: Date;
@IsBoolean()
@Field()
isHeadless: boolean;
@Field(() => ApplicationTokenPairDTO, { nullable: true })
applicationTokenPair?: ApplicationTokenPairDTO;
}
@@ -34,6 +34,9 @@ export class FrontComponentEntity
@Column({ nullable: false })
builtComponentChecksum: string;
@Column({ default: false })
isHeadless: boolean;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -22,5 +22,6 @@ export type FrontComponentManifest = {
builtComponentPath: string;
builtComponentChecksum: string;
componentName: string;
isHeadless?: boolean;
command?: FrontComponentCommandManifest;
};
@@ -14,6 +14,7 @@ export { AppBasePath } from './AppBasePath';
export { AppPath } from './AppPath';
export type { Arrayable } from './Arrayable';
export type { ArraySortDirection } from './ArraySortDirection';
export { CommandMenuPages } from './CommandMenuPages';
export type { ActorMetadata } from './composite-types/actor.composite-type';
export {
FieldActorSource,