Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0b6af84
wip
bosiraphael Feb 18, 2026
307d610
fix action link and navigate
bosiraphael Feb 19, 2026
8574e12
command menu pages navigation
bosiraphael Feb 19, 2026
58bb066
add isHeadless
bosiraphael Feb 19, 2026
884d386
mount headless front components and unmount them at the end of the ex…
bosiraphael Feb 19, 2026
1cb7352
remove modal
bosiraphael Feb 19, 2026
1b4a0d6
remove comment
bosiraphael Feb 19, 2026
2859660
Merge branch 'main' into r--poc-front-component-actions
bosiraphael Feb 19, 2026
874dedd
lint
bosiraphael Feb 19, 2026
2f25a19
remove deprecated API function
bosiraphael Feb 19, 2026
96e65ec
fix
bosiraphael Feb 19, 2026
9970d00
Merge branch 'main' into r--poc-front-component-actions
bosiraphael Feb 19, 2026
dfeef36
fix
bosiraphael Feb 19, 2026
f523957
use states
bosiraphael Feb 19, 2026
a885a19
improve typing
bosiraphael Feb 20, 2026
d7e9f22
fix typing
bosiraphael Feb 20, 2026
fec85bc
improve typing
bosiraphael Feb 20, 2026
5bf5ad6
remove unnecessary useRef
bosiraphael Feb 20, 2026
b256f66
fix
bosiraphael Feb 20, 2026
0feba5b
fixes
bosiraphael Feb 20, 2026
1ba608a
fix
bosiraphael Feb 20, 2026
78cf919
fix tests
bosiraphael Feb 20, 2026
9830627
Merge branch 'main' into r--poc-front-component-actions
bosiraphael Feb 20, 2026
15b0179
fix
bosiraphael Feb 20, 2026
b015f04
update snapshot
bosiraphael Feb 20, 2026
a754344
use ref
bosiraphael Feb 20, 2026
69c1764
Merge branch 'main' into r--poc-front-component-actions
bosiraphael Feb 20, 2026
22dab57
lint
bosiraphael Feb 20, 2026
20d3c31
Merge branch 'main' into r--poc-front-component-actions
bosiraphael Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,7 @@ export type FrontComponent = {
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
isHeadless: Scalars['Boolean'];
name: Scalars['String'];
sourceComponentPath: Scalars['String'];
universalIdentifier?: Maybe<Scalars['UUID']>;
Expand Down Expand Up @@ -5740,12 +5741,12 @@ export type ListPlansQueryVariables = Exact<{ [key: string]: never; }>;

export type ListPlansQuery = { __typename?: 'Query', listPlans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, licensedProducts: Array<{ __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array<string> | null, prices?: Array<{ __typename?: 'BillingPriceLicensed', stripePriceId: string, unitAmount: number, recurringInterval: SubscriptionInterval, priceUsageType: BillingUsageType }> | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } }>, meteredProducts: Array<{ __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array<string> | null, prices?: Array<{ __typename?: 'BillingPriceMetered', priceUsageType: BillingUsageType, recurringInterval: SubscriptionInterval, stripePriceId: string, tiers: Array<{ __typename?: 'BillingPriceTier', flatAmount?: number | null, unitAmount?: number | null, upTo?: number | null }> }> | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } }> }> };

export type CommandMenuItemFieldsFragment = { __typename?: 'CommandMenuItem', id: string, workflowVersionId?: string | null, frontComponentId?: string | null, label: string, icon?: string | null, isPinned: boolean, availabilityType: CommandMenuItemAvailabilityType, availabilityObjectMetadataId?: string | null, frontComponent?: { __typename?: 'FrontComponent', id: string, name: string } | null };
export type CommandMenuItemFieldsFragment = { __typename?: 'CommandMenuItem', id: string, workflowVersionId?: string | null, frontComponentId?: string | null, label: string, icon?: string | null, isPinned: boolean, availabilityType: CommandMenuItemAvailabilityType, availabilityObjectMetadataId?: string | null, frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, isHeadless: boolean } | null };

export type FindManyCommandMenuItemsQueryVariables = Exact<{ [key: string]: never; }>;


export type FindManyCommandMenuItemsQuery = { __typename?: 'Query', commandMenuItems: Array<{ __typename?: 'CommandMenuItem', id: string, workflowVersionId?: string | null, frontComponentId?: string | null, label: string, icon?: string | null, isPinned: boolean, availabilityType: CommandMenuItemAvailabilityType, availabilityObjectMetadataId?: string | null, frontComponent?: { __typename?: 'FrontComponent', id: string, name: string } | null }> };
export type FindManyCommandMenuItemsQuery = { __typename?: 'Query', commandMenuItems: Array<{ __typename?: 'CommandMenuItem', id: string, workflowVersionId?: string | null, frontComponentId?: string | null, label: string, icon?: string | null, isPinned: boolean, availabilityType: CommandMenuItemAvailabilityType, availabilityObjectMetadataId?: string | null, frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, isHeadless: boolean } | null }> };

export type PageLayoutFragmentFragment = { __typename?: 'PageLayout', id: string, name: string, objectMetadataId?: string | null, type: PageLayoutType, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array<string> | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array<string> | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array<string> | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array<string> | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array<string> | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null };

Expand Down Expand Up @@ -5804,7 +5805,7 @@ export type FindOneFrontComponentQueryVariables = Exact<{
}>;


export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, isHeadless: boolean, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };

export type LogicFunctionFieldsFragment = { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string };

Expand Down Expand Up @@ -7072,6 +7073,7 @@ export const CommandMenuItemFieldsFragmentDoc = gql`
frontComponent {
id
name
isHeadless
}
label
icon
Expand Down Expand Up @@ -10593,6 +10595,7 @@ export const FindOneFrontComponentDocument = gql`
name
applicationId
builtComponentChecksum
isHeadless
applicationTokenPair {
applicationAccessToken {
token
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,6 +69,7 @@ export const AppRouterProviders = () => {
<PageFavicon />
<Outlet />
<GlobalFilePreviewModal />
<HeadlessFrontComponentMountRoot />
</StrictMode>
</DialogManager>
</DialogComponentInstanceContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useEffect, useState } from 'react';
import {
matchPath,
useLocation,
useNavigate,
useParams,
matchPath,
useLocation,
useNavigate,
useParams,
} from 'react-router-dom';
import { useRecoilCallback, useRecoilValue } from 'recoil';

import {
setSessionId,
useEventTracker,
setSessionId,
useEventTracker,
} from '@/analytics/hooks/useEventTracker';
import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksOnAnyLocationChange';
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
Expand All @@ -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';
Expand All @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const COMMAND_MENU_ITEM_FRAGMENT = gql`
frontComponent {
id
name
isHeadless
}
label
icon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -35,6 +36,7 @@ type BuildActionFromItemParams = {
pageTitle: string;
pageIcon: IconComponent;
}) => void;
mountHeadlessFrontComponent: (frontComponentId: string) => void;
};

const buildActionFromItem = ({
Expand All @@ -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}`,
Expand All @@ -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}
/>
),
};
Expand All @@ -78,6 +89,7 @@ export const useCommandMenuItemFrontComponentActions = () => {
const { getIcon } = useIcons();
const { openFrontComponentInCommandMenu } =
useOpenFrontComponentInCommandMenu();
const mountHeadlessFrontComponent = useMountHeadlessFrontComponent();

const isPageInEditMode = useRecoilComponentValue(
contextStoreIsPageInEditModeComponentState,
Expand Down Expand Up @@ -140,6 +152,7 @@ export const useCommandMenuItemFrontComponentActions = () => {
isPinned: !isPageInEditMode && item.isPinned,
getIcon,
openFrontComponentInCommandMenu,
mountHeadlessFrontComponent,
}),
);

Expand All @@ -151,6 +164,7 @@ export const useCommandMenuItemFrontComponentActions = () => {
isPinned: !isPageInEditMode && item.isPinned,
getIcon,
openFrontComponentInCommandMenu,
mountHeadlessFrontComponent,
}),
);

Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Loading