['getIcon'];
+ commandMenuContextApi: CommandMenuContextApi;
+};
+
+const buildCommandItemFromEngineKey = ({
+ item,
+ type = CommandMenuItemType.Standard,
+ scope,
+ isPinned,
+ getIcon,
+ commandMenuContextApi,
+}: BuildCommandMenuItemFromStandardKeyParams) => {
+ const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
+
+ return {
+ type,
+ key: `command-menu-item-engine-${item.id}`,
+ id: item.id,
+ scope,
+ label: interpolateCommandMenuItemLabel({
+ label: item.label,
+ context: commandMenuContextApi,
+ }),
+ shortLabel: interpolateCommandMenuItemLabel({
+ label: item.shortLabel,
+ context: commandMenuContextApi,
+ }),
+ position: item.position,
+ isPinned,
+ Icon,
+ hotKeys: item.hotKeys,
+ shouldBeRegistered: () =>
+ evaluateConditionalAvailabilityExpression(
+ item.conditionalAvailabilityExpression,
+ commandMenuContextApi,
+ ),
+ component: ,
+ };
+};
+
+export const useCommandMenuItemsFromBackend = (
+ commandMenuContextApi: CommandMenuContextApi,
+) => {
+ const { getIcon } = useIcons();
+ const currentObjectMetadataItemId =
+ commandMenuContextApi.objectMetadataItem.id;
+
+ const hasRecordSelection = commandMenuContextApi.numberOfSelectedRecords >= 1;
+
+ const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
+
+ const itemsWithObjectMatches = commandMenuItems.filter(
+ doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
+ );
+
+ const buildCommandMenuItem = ({
+ item,
+ scope,
+ isPinned,
+ typeOverride,
+ }: {
+ item: CommandMenuItemFieldsFragment;
+ scope: CommandMenuItemScope;
+ isPinned: boolean;
+ typeOverride?: CommandMenuItemType;
+ }) => {
+ if (isDefined(item.engineComponentKey)) {
+ return buildCommandItemFromEngineKey({
+ item,
+ type: typeOverride,
+ scope,
+ isPinned,
+ getIcon,
+ commandMenuContextApi,
+ });
+ }
+
+ if (isDefined(item.frontComponentId)) {
+ return buildCommandMenuItemFromFrontComponent({
+ item: item as CommandMenuItemWithFrontComponent,
+ type: typeOverride,
+ scope,
+ isPinned,
+ getIcon,
+ commandMenuContextApi,
+ });
+ }
+
+ return null;
+ };
+
+ const globalItems = itemsWithObjectMatches.filter(
+ (item) => item.availabilityType === CommandMenuItemAvailabilityType.GLOBAL,
+ );
+
+ const recordScopedItems = itemsWithObjectMatches.filter(
+ (item) =>
+ item.availabilityType ===
+ CommandMenuItemAvailabilityType.RECORD_SELECTION,
+ );
+
+ const fallbackItems = itemsWithObjectMatches.filter(
+ (item) =>
+ item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK,
+ );
+
+ const globalCommandMenuItems = globalItems
+ .map((item) =>
+ buildCommandMenuItem({
+ item,
+ scope: CommandMenuItemScope.Global,
+ isPinned: item.isPinned,
+ }),
+ )
+ .filter(isDefined);
+
+ const recordScopedCommandMenuItems = hasRecordSelection
+ ? recordScopedItems
+ .map((item) =>
+ buildCommandMenuItem({
+ item,
+ scope: CommandMenuItemScope.RecordSelection,
+ isPinned: item.isPinned,
+ }),
+ )
+ .filter(isDefined)
+ : [];
+
+ const fallbackCommandMenuItems = fallbackItems
+ .map((item) =>
+ buildCommandMenuItem({
+ item,
+ scope: CommandMenuItemScope.Global,
+ isPinned: false,
+ typeOverride: CommandMenuItemType.Fallback,
+ }),
+ )
+ .filter(isDefined);
+
+ return [
+ ...globalCommandMenuItems,
+ ...recordScopedCommandMenuItems,
+ ...fallbackCommandMenuItems,
+ ]
+ .filter((item) => item.shouldBeRegistered())
+ .sort((a, b) => a.position - b.position);
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useConvertBackendItemToCommandMenuItemConfig.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/useConvertBackendItemToCommandMenuItemConfig.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useConvertBackendItemToCommandMenuItemConfig.tsx
rename to packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/useConvertBackendItemToCommandMenuItemConfig.tsx
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useWorkflowsWithCurrentVersions.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/useWorkflowsWithCurrentVersions.ts
similarity index 100%
rename from packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useWorkflowsWithCurrentVersions.ts
rename to packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/useWorkflowsWithCurrentVersions.ts
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/common/states/commandMenuItemsSelector.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/common/states/commandMenuItemsSelector.ts
new file mode 100644
index 0000000000..ce5ca08a40
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/common/states/commandMenuItemsSelector.ts
@@ -0,0 +1,14 @@
+import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
+import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
+import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
+
+export const commandMenuItemsSelector = createAtomSelector<
+ CommandMenuItemFieldsFragment[]
+>({
+ key: 'commandMenuItemsSelector',
+ get: ({ get }) => {
+ const storeItem = get(metadataStoreState, 'commandMenuItems');
+
+ return storeItem.current as CommandMenuItemFieldsFragment[];
+ },
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/common/utils/doesCommandMenuItemMatchObjectMetadataId.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/common/utils/doesCommandMenuItemMatchObjectMetadataId.ts
new file mode 100644
index 0000000000..62b8f19076
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/common/utils/doesCommandMenuItemMatchObjectMetadataId.ts
@@ -0,0 +1,7 @@
+import { isDefined } from 'twenty-shared/utils';
+import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
+
+export const doesCommandMenuItemMatchObjectMetadataId =
+ (objectMetadataItemId: unknown) => (item: CommandMenuItemFieldsFragment) =>
+ !isDefined(item.availabilityObjectMetadataId) ||
+ item.availabilityObjectMetadataId === objectMetadataItemId;
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/CommandMenuItemMoreActionsButton.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/CommandMenuItemMoreActionsButton.tsx
new file mode 100644
index 0000000000..602812372c
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/CommandMenuItemMoreActionsButton.tsx
@@ -0,0 +1,64 @@
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
+import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
+import { PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderSidePanelButtonClickOutsideId';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { styled } from '@linaria/react';
+import { i18n } from '@lingui/core';
+import { t } from '@lingui/core/macro';
+import {
+ AppTooltip,
+ IconLayoutSidebarRightExpand,
+ IconX,
+ TooltipDelay,
+ TooltipPosition,
+} from 'twenty-ui/display';
+import { AnimatedButton } from 'twenty-ui/input';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { AnimatedIconCrossfade, useIsMobile } from 'twenty-ui/utilities';
+
+const StyledTooltipWrapper = styled.div`
+ font-size: ${themeCssVariables.font.size.md};
+`;
+
+export const CommandMenuItemMoreActionsButton = () => {
+ const { toggleSidePanelMenu } = useSidePanelMenu();
+ const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
+
+ const isMobile = useIsMobile();
+
+ const ariaLabel = isSidePanelOpened
+ ? t`Close side panel`
+ : t`Open side panel`;
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/PinnedCommandMenuItemButtons.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/PinnedCommandMenuItemButtons.tsx
new file mode 100644
index 0000000000..5d9e0b6ac6
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/PinnedCommandMenuItemButtons.tsx
@@ -0,0 +1,99 @@
+import { CommandMenuItemComponent } from '@/command-menu-item/display/components/CommandMenuItemComponent';
+import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
+import { PinnedCommandMenuItemsInlineMeasurements } from '@/command-menu-item/server-items/display/components/PinnedCommandMenuItemsInlineMeasurements';
+import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/server-items/display/constants/PinnedCommandMenuItemsGap';
+import { usePinnedCommandMenuItemsInlineLayout } from '@/command-menu-item/server-items/display/hooks/usePinnedCommandMenuItemsInlineLayout';
+import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
+import { styled } from '@linaria/react';
+import { motion } from 'framer-motion';
+import { useContext, useMemo } from 'react';
+import { ThemeContext } from 'twenty-ui/theme-constants';
+
+const StyledActionContainer = styled(motion.div)`
+ align-items: center;
+ display: flex;
+ justify-content: center;
+`;
+
+const StyledWrapper = styled.div`
+ flex: 1 1 0;
+ min-width: 0;
+ overflow: hidden;
+`;
+
+const StyledContainer = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ min-width: 0;
+ width: 100%;
+`;
+
+const StyledItemsContainer = styled.div`
+ display: flex;
+ gap: ${PINNED_COMMAND_MENU_ITEMS_GAP}px;
+ max-width: 100%;
+ overflow: hidden;
+`;
+
+export const PinnedCommandMenuItemButtons = () => {
+ const { theme } = useContext(ThemeContext);
+ const { commandMenuItems } = useContext(CommandMenuContext);
+
+ const pinnedCommandMenuItems = useMemo(
+ () =>
+ commandMenuItems.filter(
+ (commandMenuItem) => commandMenuItem.isPinned === true,
+ ),
+ [commandMenuItems],
+ );
+
+ const {
+ pinnedInlineCommandMenuItems,
+ pinnedOverflowCommandMenuItems,
+ onContainerDimensionChange,
+ onCommandMenuItemDimensionChange,
+ } = usePinnedCommandMenuItemsInlineLayout({
+ pinnedCommandMenuItems,
+ });
+
+ return (
+ <>
+
+
+
+
+
+ {pinnedInlineCommandMenuItems.map(
+ (pinnedInlineCommandMenuItem) => (
+
+
+
+ ),
+ )}
+
+
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/PinnedCommandMenuItemsInlineMeasurements.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/PinnedCommandMenuItemsInlineMeasurements.tsx
new file mode 100644
index 0000000000..92997a7d0f
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/PinnedCommandMenuItemsInlineMeasurements.tsx
@@ -0,0 +1,54 @@
+import { CommandMenuButton } from '@/command-menu/components/CommandMenuButton';
+import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
+import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/server-items/display/constants/PinnedCommandMenuItemsGap';
+import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
+import { styled } from '@linaria/react';
+
+type ElementDimensions = {
+ width: number;
+ height: number;
+};
+
+type PinnedCommandMenuItemsInlineMeasurementsProps = {
+ pinnedCommandMenuItems: CommandMenuItemConfig[];
+ onPinnedCommandMenuItemDimensionChange: (
+ commandMenuItemKey: string,
+ ) => (dimensions: ElementDimensions) => void;
+};
+
+const StyledHiddenMeasurementsContainer = styled.div`
+ display: flex;
+ gap: ${PINNED_COMMAND_MENU_ITEMS_GAP}px;
+ pointer-events: none;
+ position: absolute;
+ top: -9999px;
+ visibility: hidden;
+`;
+
+export const PinnedCommandMenuItemsInlineMeasurements = ({
+ pinnedCommandMenuItems,
+ onPinnedCommandMenuItemDimensionChange,
+}: PinnedCommandMenuItemsInlineMeasurementsProps) => {
+ return (
+
+ {pinnedCommandMenuItems.map((pinnedCommandMenuItem) => (
+
+
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/SidePanelCommandMenuItemDisplayPage.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/SidePanelCommandMenuItemDisplayPage.tsx
new file mode 100644
index 0000000000..03d35ab69f
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/components/SidePanelCommandMenuItemDisplayPage.tsx
@@ -0,0 +1,127 @@
+import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
+import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/server-items/display/constants/PinnedCommandMenuItemsGap';
+import { commandMenuPinnedInlineLayoutState } from '@/command-menu-item/server-items/display/states/commandMenuPinnedInlineLayoutState';
+import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/server-items/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
+import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
+import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
+import { SidePanelList } from '@/side-panel/components/SidePanelList';
+import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
+import { SIDE_PANEL_RESET_CONTEXT_TO_SELECTION } from '@/side-panel/constants/SidePanelResetContextToSelection';
+import { SidePanelResetContextToSelectionButton } from '@/side-panel/pages/root/components/SidePanelResetContextToSelectionButton';
+import { useFilterActionsWithSidePanelSearch } from '@/side-panel/pages/root/hooks/useFilterActionsWithSidePanelSearch';
+import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
+import { type SidePanelCommandMenuItemGroupConfig } from '@/side-panel/types/SidePanelCommandMenuItemGroupConfig';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useLingui } from '@lingui/react/macro';
+import { useContext } from 'react';
+import { isNumber } from '@sniptt/guards';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SidePanelCommandMenuItemDisplayPage = () => {
+ const { t } = useLingui();
+
+ const sidePanelSearch = useAtomStateValue(sidePanelSearchState);
+ const { commandMenuItems } = useContext(CommandMenuContext);
+ const commandMenuPinnedInlineLayout = useAtomStateValue(
+ commandMenuPinnedInlineLayoutState,
+ );
+
+ const { filterActionsWithSidePanelSearch } =
+ useFilterActionsWithSidePanelSearch({
+ sidePanelSearch,
+ });
+
+ const pinnedCommandMenuItems = commandMenuItems
+ .filter((commandMenuItem) => commandMenuItem.isPinned === true)
+ .sort(
+ (firstPinnedCommandMenuItem, secondPinnedCommandMenuItem) =>
+ firstPinnedCommandMenuItem.position -
+ secondPinnedCommandMenuItem.position,
+ );
+
+ const unpinnedCommandMenuItems = commandMenuItems
+ .filter((commandMenuItem) => commandMenuItem.isPinned !== true)
+ .sort(
+ (firstUnpinnedCommandMenuItem, secondUnpinnedCommandMenuItem) =>
+ firstUnpinnedCommandMenuItem.position -
+ secondUnpinnedCommandMenuItem.position,
+ );
+
+ const pinnedCommandMenuItemKeysInDisplayOrder = pinnedCommandMenuItems.map(
+ (pinnedCommandMenuItem) => pinnedCommandMenuItem.key,
+ );
+
+ const visiblePinnedCommandMenuItemCount =
+ getVisibleCommandMenuItemCountForContainerWidth({
+ commandMenuItemKeysInDisplayOrder:
+ pinnedCommandMenuItemKeysInDisplayOrder,
+ commandMenuItemWidthsByKey:
+ commandMenuPinnedInlineLayout.commandMenuItemWidthsByKey,
+ commandMenuItemsContainerWidth:
+ commandMenuPinnedInlineLayout.containerWidth,
+ commandMenuItemsGapWidth: PINNED_COMMAND_MENU_ITEMS_GAP,
+ });
+
+ const hasKnownPinnedInlineLayout =
+ commandMenuPinnedInlineLayout.containerWidth > 0 &&
+ pinnedCommandMenuItemKeysInDisplayOrder.every((commandMenuItemKey) =>
+ isNumber(
+ commandMenuPinnedInlineLayout.commandMenuItemWidthsByKey[
+ commandMenuItemKey
+ ],
+ ),
+ );
+
+ const pinnedOverflowCommandMenuItems = hasKnownPinnedInlineLayout
+ ? pinnedCommandMenuItems.slice(visiblePinnedCommandMenuItemCount)
+ : pinnedCommandMenuItems;
+
+ const matchingPinnedItems = filterActionsWithSidePanelSearch(
+ pinnedOverflowCommandMenuItems,
+ );
+ const matchingOtherItems = filterActionsWithSidePanelSearch(
+ unpinnedCommandMenuItems,
+ );
+
+ const noResults = !matchingPinnedItems.length && !matchingOtherItems.length;
+
+ const commandGroups: SidePanelCommandMenuItemGroupConfig[] = [
+ {
+ heading: t`Pinned`,
+ items: matchingPinnedItems,
+ },
+ {
+ heading: t`Other`,
+ items: matchingOtherItems,
+ },
+ ];
+
+ const selectableItems = commandGroups.flatMap((group) => group.items ?? []);
+ const selectableItemIds = selectableItems.map((item) => item.key);
+
+ // oxlint-disable-next-line twenty/matching-state-variable
+ const previousContextStoreCurrentObjectMetadataItemId =
+ useAtomComponentStateValue(
+ contextStoreCurrentObjectMetadataItemIdComponentState,
+ SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
+ );
+
+ if (isDefined(previousContextStoreCurrentObjectMetadataItemId)) {
+ selectableItemIds.unshift(SIDE_PANEL_RESET_CONTEXT_TO_SELECTION);
+ }
+
+ return (
+
+ {isDefined(previousContextStoreCurrentObjectMetadataItemId) && (
+
+
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/constants/PinnedCommandMenuItemsGap.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/display/constants/PinnedCommandMenuItemsGap.ts
new file mode 100644
index 0000000000..12540d15e9
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/constants/PinnedCommandMenuItemsGap.ts
@@ -0,0 +1 @@
+export const PINNED_COMMAND_MENU_ITEMS_GAP = 8;
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/hooks/usePinnedCommandMenuItemsInlineLayout.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/display/hooks/usePinnedCommandMenuItemsInlineLayout.ts
new file mode 100644
index 0000000000..c2eac684bf
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/hooks/usePinnedCommandMenuItemsInlineLayout.ts
@@ -0,0 +1,114 @@
+import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
+import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/server-items/display/constants/PinnedCommandMenuItemsGap';
+import { commandMenuPinnedInlineLayoutState } from '@/command-menu-item/server-items/display/states/commandMenuPinnedInlineLayoutState';
+import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/server-items/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
+import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
+import { useCallback, useMemo } from 'react';
+
+type ElementDimensions = {
+ width: number;
+ height: number;
+};
+
+type UsePinnedCommandMenuItemsInlineLayoutParams = {
+ pinnedCommandMenuItems: CommandMenuItemConfig[];
+};
+
+export const usePinnedCommandMenuItemsInlineLayout = ({
+ pinnedCommandMenuItems,
+}: UsePinnedCommandMenuItemsInlineLayoutParams) => {
+ const [commandMenuPinnedInlineLayout, setCommandMenuPinnedInlineLayout] =
+ useAtomState(commandMenuPinnedInlineLayoutState);
+
+ const pinnedCommandMenuItemsSortedByPosition = useMemo(
+ () =>
+ [...pinnedCommandMenuItems].sort(
+ (firstPinnedCommandMenuItem, secondPinnedCommandMenuItem) =>
+ firstPinnedCommandMenuItem.position -
+ secondPinnedCommandMenuItem.position,
+ ),
+ [pinnedCommandMenuItems],
+ );
+
+ const pinnedCommandMenuItemKeysInDisplayOrder = useMemo(
+ () =>
+ pinnedCommandMenuItemsSortedByPosition.map(
+ (pinnedCommandMenuItem) => pinnedCommandMenuItem.key,
+ ),
+ [pinnedCommandMenuItemsSortedByPosition],
+ );
+
+ const visiblePinnedCommandMenuItemCount = useMemo(
+ () =>
+ getVisibleCommandMenuItemCountForContainerWidth({
+ commandMenuItemKeysInDisplayOrder:
+ pinnedCommandMenuItemKeysInDisplayOrder,
+ commandMenuItemWidthsByKey:
+ commandMenuPinnedInlineLayout.commandMenuItemWidthsByKey,
+ commandMenuItemsContainerWidth:
+ commandMenuPinnedInlineLayout.containerWidth,
+ commandMenuItemsGapWidth: PINNED_COMMAND_MENU_ITEMS_GAP,
+ }),
+ [commandMenuPinnedInlineLayout, pinnedCommandMenuItemKeysInDisplayOrder],
+ );
+
+ const pinnedInlineCommandMenuItems = useMemo(
+ () =>
+ pinnedCommandMenuItemsSortedByPosition.slice(
+ 0,
+ visiblePinnedCommandMenuItemCount,
+ ),
+ [pinnedCommandMenuItemsSortedByPosition, visiblePinnedCommandMenuItemCount],
+ );
+
+ const pinnedOverflowCommandMenuItems = useMemo(
+ () =>
+ pinnedCommandMenuItemsSortedByPosition.slice(
+ visiblePinnedCommandMenuItemCount,
+ ),
+ [pinnedCommandMenuItemsSortedByPosition, visiblePinnedCommandMenuItemCount],
+ );
+
+ const onContainerDimensionChange = useCallback(
+ (dimensions: ElementDimensions) => {
+ setCommandMenuPinnedInlineLayout(
+ (previousCommandMenuPinnedInlineLayout) =>
+ previousCommandMenuPinnedInlineLayout.containerWidth !==
+ dimensions.width
+ ? {
+ ...previousCommandMenuPinnedInlineLayout,
+ containerWidth: dimensions.width,
+ }
+ : previousCommandMenuPinnedInlineLayout,
+ );
+ },
+ [setCommandMenuPinnedInlineLayout],
+ );
+
+ const onCommandMenuItemDimensionChange = useCallback(
+ (commandMenuItemKey: string) => (dimensions: ElementDimensions) => {
+ setCommandMenuPinnedInlineLayout(
+ (previousCommandMenuPinnedInlineLayout) =>
+ previousCommandMenuPinnedInlineLayout.commandMenuItemWidthsByKey[
+ commandMenuItemKey
+ ] !== dimensions.width
+ ? {
+ ...previousCommandMenuPinnedInlineLayout,
+ commandMenuItemWidthsByKey: {
+ ...previousCommandMenuPinnedInlineLayout.commandMenuItemWidthsByKey,
+ [commandMenuItemKey]: dimensions.width,
+ },
+ }
+ : previousCommandMenuPinnedInlineLayout,
+ );
+ },
+ [setCommandMenuPinnedInlineLayout],
+ );
+
+ return {
+ pinnedInlineCommandMenuItems,
+ pinnedOverflowCommandMenuItems,
+ onContainerDimensionChange,
+ onCommandMenuItemDimensionChange,
+ };
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/states/commandMenuPinnedInlineLayoutState.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/display/states/commandMenuPinnedInlineLayoutState.ts
new file mode 100644
index 0000000000..865435e946
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/states/commandMenuPinnedInlineLayoutState.ts
@@ -0,0 +1,11 @@
+import { type CommandMenuPinnedInlineLayout } from '@/command-menu-item/server-items/display/types/CommandMenuPinnedInlineLayout';
+import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
+
+export const commandMenuPinnedInlineLayoutState =
+ createAtomState({
+ key: 'commandMenuPinnedInlineLayoutState',
+ defaultValue: {
+ containerWidth: 0,
+ commandMenuItemWidthsByKey: {},
+ },
+ });
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/types/CommandMenuPinnedInlineLayout.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/display/types/CommandMenuPinnedInlineLayout.ts
new file mode 100644
index 0000000000..abf70f0896
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/types/CommandMenuPinnedInlineLayout.ts
@@ -0,0 +1,4 @@
+export type CommandMenuPinnedInlineLayout = {
+ containerWidth: number;
+ commandMenuItemWidthsByKey: Record;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/utils/__tests__/getVisibleCommandMenuItemCountForContainerWidth.test.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/display/utils/__tests__/getVisibleCommandMenuItemCountForContainerWidth.test.ts
new file mode 100644
index 0000000000..ae2b7a0d80
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/utils/__tests__/getVisibleCommandMenuItemCountForContainerWidth.test.ts
@@ -0,0 +1,39 @@
+import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/server-items/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
+
+describe('getVisibleCommandMenuItemCountForContainerWidth', () => {
+ it('should return all items when container width is zero', () => {
+ const visibleCommandMenuItemCount =
+ getVisibleCommandMenuItemCountForContainerWidth({
+ commandMenuItemKeysInDisplayOrder: ['a', 'b', 'c'],
+ commandMenuItemWidthsByKey: { a: 20, b: 20, c: 20 },
+ commandMenuItemsContainerWidth: 0,
+ commandMenuItemsGapWidth: 8,
+ });
+
+ expect(visibleCommandMenuItemCount).toBe(3);
+ });
+
+ it('should return all items when at least one width is missing', () => {
+ const visibleCommandMenuItemCount =
+ getVisibleCommandMenuItemCountForContainerWidth({
+ commandMenuItemKeysInDisplayOrder: ['a', 'b'],
+ commandMenuItemWidthsByKey: { a: 20 },
+ commandMenuItemsContainerWidth: 40,
+ commandMenuItemsGapWidth: 8,
+ });
+
+ expect(visibleCommandMenuItemCount).toBe(2);
+ });
+
+ it('should stop before exceeding the container width with gaps', () => {
+ const visibleCommandMenuItemCount =
+ getVisibleCommandMenuItemCountForContainerWidth({
+ commandMenuItemKeysInDisplayOrder: ['a', 'b', 'c'],
+ commandMenuItemWidthsByKey: { a: 40, b: 40, c: 40 },
+ commandMenuItemsContainerWidth: 100,
+ commandMenuItemsGapWidth: 8,
+ });
+
+ expect(visibleCommandMenuItemCount).toBe(2);
+ });
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/display/utils/getVisibleCommandMenuItemCountForContainerWidth.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/display/utils/getVisibleCommandMenuItemCountForContainerWidth.ts
new file mode 100644
index 0000000000..a1c8816a2d
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/display/utils/getVisibleCommandMenuItemCountForContainerWidth.ts
@@ -0,0 +1,42 @@
+type GetVisibleCommandMenuItemCountForContainerWidthParams = {
+ commandMenuItemKeysInDisplayOrder: string[];
+ commandMenuItemWidthsByKey: Record;
+ commandMenuItemsContainerWidth: number;
+ commandMenuItemsGapWidth: number;
+};
+
+export const getVisibleCommandMenuItemCountForContainerWidth = ({
+ commandMenuItemKeysInDisplayOrder,
+ commandMenuItemWidthsByKey,
+ commandMenuItemsContainerWidth,
+ commandMenuItemsGapWidth,
+}: GetVisibleCommandMenuItemCountForContainerWidthParams): number => {
+ if (commandMenuItemsContainerWidth <= 0) {
+ return commandMenuItemKeysInDisplayOrder.length;
+ }
+
+ let usedWidth = 0;
+ let visibleCommandMenuItemCount = 0;
+
+ for (const commandMenuItemKey of commandMenuItemKeysInDisplayOrder) {
+ const commandMenuItemWidth = commandMenuItemWidthsByKey[commandMenuItemKey];
+
+ if (typeof commandMenuItemWidth !== 'number') {
+ return commandMenuItemKeysInDisplayOrder.length;
+ }
+
+ const nextWidth =
+ visibleCommandMenuItemCount === 0
+ ? commandMenuItemWidth
+ : usedWidth + commandMenuItemsGapWidth + commandMenuItemWidth;
+
+ if (nextWidth > commandMenuItemsContainerWidth) {
+ break;
+ }
+
+ usedWidth = nextWidth;
+ visibleCommandMenuItemCount += 1;
+ }
+
+ return visibleCommandMenuItemCount;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemEditButton.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemEditButton.tsx
new file mode 100644
index 0000000000..4ff5c6ca66
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemEditButton.tsx
@@ -0,0 +1,72 @@
+import { AnimatedIconCrossfade } from 'twenty-ui/utilities';
+import { commandMenuItemEditSelectionModeState } from '@/command-menu-item/server-items/edit/states/commandMenuItemEditSelectionModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
+import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
+import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
+import { useLingui } from '@lingui/react/macro';
+import { useStore } from 'jotai';
+import { SidePanelPages } from 'twenty-shared/types';
+import { IconPencil, IconX } from 'twenty-ui/display';
+import { AnimatedButton } from 'twenty-ui/input';
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
+
+export const CommandMenuItemEditButton = () => {
+ const { t } = useLingui();
+ const store = useStore();
+ const { navigateSidePanel } = useNavigateSidePanel();
+ const { closeSidePanelMenu } = useSidePanelMenu();
+
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
+ );
+ const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
+ const sidePanelPage = useAtomStateValue(sidePanelPageState);
+
+ const isCommandMenuItemEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
+ );
+
+ const isCommandMenuEditPageActive =
+ isSidePanelOpened && sidePanelPage === SidePanelPages.CommandMenuEdit;
+
+ if (!isLayoutCustomizationModeEnabled || !isCommandMenuItemEnabled) {
+ return null;
+ }
+
+ const handleClick = () => {
+ if (isCommandMenuEditPageActive) {
+ closeSidePanelMenu();
+
+ return;
+ }
+
+ store.set(commandMenuItemEditSelectionModeState.atom, 'selection');
+
+ navigateSidePanel({
+ page: SidePanelPages.CommandMenuEdit,
+ pageTitle: t`Edit actions`,
+ pageIcon: IconPencil,
+ resetNavigationStack: true,
+ });
+ };
+
+ return (
+
+ }
+ title={t`Edit actions`}
+ variant="secondary"
+ size="small"
+ onClick={handleClick}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemEditRecordSelectionDropdown.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemEditRecordSelectionDropdown.tsx
new file mode 100644
index 0000000000..780c221150
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemEditRecordSelectionDropdown.tsx
@@ -0,0 +1,107 @@
+import { COMMAND_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/command-menu-item/constants/CommandMenuDropdownClickOutsideId';
+import { commandMenuItemEditSelectionModeState } from '@/command-menu-item/server-items/edit/states/commandMenuItemEditSelectionModeState';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
+import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import {
+ IconChevronDown,
+ IconSquareCheck,
+ IconSquareX,
+} from 'twenty-ui/display';
+import { MenuItemSelect } from 'twenty-ui/navigation';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const DROPDOWN_ID = 'command-menu-edit-record-selection-dropdown';
+
+const StyledClickableArea = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.background.transparent.lighter};
+ border: 1px solid ${themeCssVariables.border.color.medium};
+ border-radius: ${themeCssVariables.border.radius.sm};
+ cursor: pointer;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+ height: 24px;
+ padding-left: ${themeCssVariables.spacing[2]};
+ padding-right: ${themeCssVariables.spacing[1]};
+`;
+
+const StyledLabel = styled.span`
+ color: ${themeCssVariables.font.color.primary};
+ font-size: ${themeCssVariables.font.size.md};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const StyledDropdownMenuContainer = styled.div`
+ width: 100%;
+`;
+
+export const CommandMenuItemEditRecordSelectionDropdown = () => {
+ const { t } = useLingui();
+ const { closeDropdown } = useCloseDropdown();
+
+ const commandMenuItemEditSelectionMode = useAtomStateValue(
+ commandMenuItemEditSelectionModeState,
+ );
+ const setCommandMenuItemEditSelectionMode = useSetAtomState(
+ commandMenuItemEditSelectionModeState,
+ );
+
+ const isNoneSelected = commandMenuItemEditSelectionMode === 'none';
+
+ const handleSelectMode = (mode: 'none' | 'selection') => {
+ setCommandMenuItemEditSelectionMode(mode);
+ closeDropdown(DROPDOWN_ID);
+ };
+
+ const TriggerIcon = isNoneSelected ? IconSquareX : IconSquareCheck;
+ const triggerLabel = isNoneSelected
+ ? t`No record selected`
+ : t`Records selected`;
+
+ return (
+
+
+ {triggerLabel}
+
+
+ }
+ dropdownPlacement="bottom-start"
+ dropdownComponents={
+
+
+
+ handleSelectMode('none')}
+ />
+ handleSelectMode('selection')}
+ />
+
+
+
+ }
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemOptionsDropdown.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemOptionsDropdown.tsx
new file mode 100644
index 0000000000..06f2307bef
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/CommandMenuItemOptionsDropdown.tsx
@@ -0,0 +1,85 @@
+import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/server-items/edit/hooks/useUpdateCommandMenuItemInDraft';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
+import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
+import { useLingui } from '@lingui/react/macro';
+import { type ReactElement } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { IconRefresh, IconTag } from 'twenty-ui/display';
+import { MenuItem, MenuItemToggle } from 'twenty-ui/navigation';
+import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
+
+type CommandMenuItemOptionsDropdownProps = Pick<
+ CommandMenuItemFieldsFragment,
+ 'shortLabel'
+> & {
+ itemId: string;
+ serverShortLabel: string | null | undefined;
+ iconButton: ReactElement;
+};
+
+const getCommandMenuItemOptionsDropdownId = (itemId: string) =>
+ `command-menu-item-options-${itemId}`;
+
+export const CommandMenuItemOptionsDropdown = ({
+ itemId,
+ shortLabel,
+ serverShortLabel,
+ iconButton,
+}: CommandMenuItemOptionsDropdownProps) => {
+ const { t } = useLingui();
+
+ const dropdownId = getCommandMenuItemOptionsDropdownId(itemId);
+ const { closeDropdown } = useCloseDropdown();
+ const { updateCommandMenuItemInDraft } = useUpdateCommandMenuItemInDraft();
+
+ const normalizedServerShortLabel = serverShortLabel ?? null;
+ const normalizedShortLabel = shortLabel ?? null;
+ const isLabelHidden =
+ normalizedShortLabel === null && isDefined(normalizedServerShortLabel);
+ const hasShortLabelOverride =
+ normalizedShortLabel !== normalizedServerShortLabel;
+
+ const handleToggleHideLabel = (toggled: boolean) => {
+ updateCommandMenuItemInDraft(itemId, {
+ shortLabel: toggled ? null : normalizedServerShortLabel,
+ });
+ };
+
+ const handleResetLabelToDefault = () => {
+ updateCommandMenuItemInDraft(itemId, {
+ shortLabel: normalizedServerShortLabel,
+ });
+ closeDropdown(dropdownId);
+ };
+
+ return (
+
+
+
+
+
+
+ }
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/PinnedCommandMenuItemButtonsEditMode.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/PinnedCommandMenuItemButtonsEditMode.tsx
new file mode 100644
index 0000000000..a2428ecd5d
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/PinnedCommandMenuItemButtonsEditMode.tsx
@@ -0,0 +1,172 @@
+import { CommandMenuButton } from '@/command-menu/components/CommandMenuButton';
+import { useCommandMenuContextApi } from '@/command-menu-item/server-items/common/hooks/useCommandMenuContextApi';
+import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/server-items/common/utils/doesCommandMenuItemMatchObjectMetadataId';
+import { PinnedCommandMenuItemsInlineMeasurements } from '@/command-menu-item/server-items/display/components/PinnedCommandMenuItemsInlineMeasurements';
+import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/server-items/display/constants/PinnedCommandMenuItemsGap';
+import { usePinnedCommandMenuItemsInlineLayout } from '@/command-menu-item/server-items/display/hooks/usePinnedCommandMenuItemsInlineLayout';
+import { commandMenuItemEditSelectionModeState } from '@/command-menu-item/server-items/edit/states/commandMenuItemEditSelectionModeState';
+import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
+import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
+import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
+import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
+import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
+import { styled } from '@linaria/react';
+import { motion } from 'framer-motion';
+import { useContext, useMemo } from 'react';
+import { interpolateCommandMenuItemLabel } from 'twenty-shared/utils';
+import { useIcons } from 'twenty-ui/display';
+import { ThemeContext } from 'twenty-ui/theme-constants';
+import {
+ CommandMenuItemAvailabilityType,
+ type CommandMenuItemFieldsFragment,
+} from '~/generated-metadata/graphql';
+
+const StyledActionContainer = styled(motion.div)`
+ align-items: center;
+ display: flex;
+ justify-content: center;
+`;
+
+const StyledWrapper = styled.div`
+ flex: 1 1 0;
+ min-width: 0;
+ overflow: hidden;
+`;
+
+const StyledContainer = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ min-width: 0;
+ width: 100%;
+`;
+
+const StyledItemsContainer = styled.div`
+ display: flex;
+ gap: ${PINNED_COMMAND_MENU_ITEMS_GAP}px;
+ max-width: 100%;
+ overflow: hidden;
+`;
+
+export const PinnedCommandMenuItemButtonsEditMode = () => {
+ const { theme } = useContext(ThemeContext);
+ const { getIcon } = useIcons();
+ const commandMenuContextApi = useCommandMenuContextApi();
+
+ const currentObjectMetadataItemId =
+ commandMenuContextApi.objectMetadataItem.id;
+
+ const commandMenuItemsDraft =
+ useAtomStateValue(commandMenuItemsDraftState) ?? [];
+ const commandMenuItemEditSelectionMode = useAtomStateValue(
+ commandMenuItemEditSelectionModeState,
+ );
+
+ const allowedAvailabilityTypes = useMemo(
+ () =>
+ new Set([
+ CommandMenuItemAvailabilityType.GLOBAL,
+ commandMenuItemEditSelectionMode === 'selection'
+ ? CommandMenuItemAvailabilityType.RECORD_SELECTION
+ : CommandMenuItemAvailabilityType.FALLBACK,
+ ]),
+ [commandMenuItemEditSelectionMode],
+ );
+
+ const interpolateLabel = (rawLabel: string | null | undefined) =>
+ interpolateCommandMenuItemLabel({
+ label: rawLabel,
+ context: commandMenuContextApi,
+ });
+
+ const pinnedCommandMenuItems: CommandMenuItemConfig[] = useMemo(
+ () =>
+ commandMenuItemsDraft
+ .filter(
+ doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
+ )
+ .filter((item) => allowedAvailabilityTypes.has(item.availabilityType))
+ .filter((item) => item.isPinned)
+ .map((item) => {
+ const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
+ const label = interpolateLabel(item.label) ?? item.label;
+ const shortLabel = interpolateLabel(item.shortLabel);
+ const key = `edit-preview-${item.id}`;
+
+ return {
+ type: CommandMenuItemType.Standard,
+ scope: CommandMenuItemScope.Global,
+ key,
+ label,
+ shortLabel,
+ position: item.position,
+ Icon,
+ isPinned: true,
+ shouldBeRegistered: () => true,
+ component: (
+
+ ),
+ };
+ }),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [
+ commandMenuItemsDraft,
+ currentObjectMetadataItemId,
+ allowedAvailabilityTypes,
+ commandMenuContextApi,
+ getIcon,
+ ],
+ );
+
+ const {
+ pinnedInlineCommandMenuItems,
+ pinnedOverflowCommandMenuItems,
+ onContainerDimensionChange,
+ onCommandMenuItemDimensionChange,
+ } = usePinnedCommandMenuItemsInlineLayout({
+ pinnedCommandMenuItems,
+ });
+
+ return (
+ <>
+
+
+
+
+
+ {pinnedInlineCommandMenuItems.map(
+ (pinnedInlineCommandMenuItem) => (
+
+ {pinnedInlineCommandMenuItem.component}
+
+ ),
+ )}
+
+
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage.tsx
new file mode 100644
index 0000000000..9ef975c1b0
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage.tsx
@@ -0,0 +1,339 @@
+import { useCommandMenuContextApi } from '@/command-menu-item/server-items/common/hooks/useCommandMenuContextApi';
+import { commandMenuItemsSelector } from '@/command-menu-item/server-items/common/states/commandMenuItemsSelector';
+import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/server-items/common/utils/doesCommandMenuItemMatchObjectMetadataId';
+import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
+import { CommandMenuItemEditRecordSelectionDropdown } from '@/command-menu-item/server-items/edit/components/CommandMenuItemEditRecordSelectionDropdown';
+import { CommandMenuItemOptionsDropdown } from '@/command-menu-item/server-items/edit/components/CommandMenuItemOptionsDropdown';
+import { useReorderCommandMenuItemsInDraft } from '@/command-menu-item/server-items/edit/hooks/useReorderCommandMenuItemsInDraft';
+import { useResetCommandMenuItemsDraft } from '@/command-menu-item/server-items/edit/hooks/useResetCommandMenuItemsDraft';
+import { commandMenuItemEditSelectionModeState } from '@/command-menu-item/server-items/edit/states/commandMenuItemEditSelectionModeState';
+import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/server-items/edit/hooks/useUpdateCommandMenuItemInDraft';
+import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
+import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
+import { SidePanelList } from '@/side-panel/components/SidePanelList';
+import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
+import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
+import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
+import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
+import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { type DropResult } from '@hello-pangea/dnd';
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import {
+ interpolateCommandMenuItemLabel,
+ isDefined,
+} from 'twenty-shared/utils';
+import {
+ IconDotsVertical,
+ IconPin,
+ IconPinnedOff,
+ IconRefresh,
+ useIcons,
+} from 'twenty-ui/display';
+import { Button } from 'twenty-ui/input';
+import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+import {
+ CommandMenuItemAvailabilityType,
+ type CommandMenuItemFieldsFragment,
+} from '~/generated-metadata/graphql';
+import { normalizeSearchText } from '~/utils/normalizeSearchText';
+
+const partitionByPinned = (items: CommandMenuItemFieldsFragment[]) => {
+ const pinned = items
+ .filter((item) => item.isPinned)
+ .sort((a, b) => a.position - b.position);
+ const other = items
+ .filter((item) => !item.isPinned)
+ .sort((a, b) => a.position - b.position);
+
+ return { pinned, other };
+};
+
+const StyledContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+`;
+
+const StyledViewbar = styled.div`
+ align-items: center;
+ backdrop-filter: blur(5px);
+ border-bottom: 1px solid ${themeCssVariables.border.color.light};
+ display: flex;
+ flex-shrink: 0;
+ height: 40px;
+ padding: ${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]};
+`;
+
+const StyledContent = styled.div`
+ flex: 1;
+ overflow: auto;
+`;
+
+export const SidePanelCommandMenuItemEditPage = () => {
+ const { t } = useLingui();
+ const { getIcon } = useIcons();
+ const commandMenuContextApi = useCommandMenuContextApi();
+
+ const currentObjectMetadataItemId =
+ commandMenuContextApi.objectMetadataItem.id;
+
+ const commandMenuItemEditSelectionMode = useAtomStateValue(
+ commandMenuItemEditSelectionModeState,
+ );
+
+ const sidePanelSearch = useAtomStateValue(sidePanelSearchState);
+
+ const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
+ const serverItemsById = new Map(
+ commandMenuItems.map((item) => [item.id, item]),
+ );
+ const commandMenuItemsDraft =
+ useAtomStateValue(commandMenuItemsDraftState) ?? [];
+ const { updateCommandMenuItemInDraft } = useUpdateCommandMenuItemInDraft();
+ const { reorderCommandMenuItemInDraft } = useReorderCommandMenuItemsInDraft();
+ const { resetCommandMenuItemsDraft } = useResetCommandMenuItemsDraft();
+
+ const allowedAvailabilityTypes = new Set([
+ CommandMenuItemAvailabilityType.GLOBAL,
+ commandMenuItemEditSelectionMode === 'selection'
+ ? CommandMenuItemAvailabilityType.RECORD_SELECTION
+ : CommandMenuItemAvailabilityType.FALLBACK,
+ ]);
+
+ const filteredCommandMenuItems = commandMenuItemsDraft
+ .filter(
+ doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
+ )
+ .filter((item) => allowedAvailabilityTypes.has(item.availabilityType));
+
+ const filteredCommandMenuItemIds = new Set(
+ filteredCommandMenuItems.map((item) => item.id),
+ );
+
+ const getDisplayLabel = (item: CommandMenuItemFieldsFragment) =>
+ interpolateCommandMenuItemLabel({
+ label: item.label,
+ context: commandMenuContextApi,
+ }) ?? item.label;
+
+ const { pinned: allPinnedItems, other: allOtherItems } = partitionByPinned(
+ filteredCommandMenuItems,
+ );
+
+ const normalizedSearch =
+ sidePanelSearch.length > 0
+ ? normalizeSearchText(sidePanelSearch)
+ : undefined;
+
+ const matchesSearch = (item: CommandMenuItemFieldsFragment) =>
+ normalizedSearch === undefined ||
+ normalizeSearchText(getDisplayLabel(item)).includes(normalizedSearch);
+
+ const displayedPinnedItems = allPinnedItems.filter(matchesSearch);
+ const displayedOtherItems = allOtherItems.filter(matchesSearch);
+
+ const selectableItemIds = [
+ ...displayedPinnedItems.map((item) => item.id),
+ ...displayedOtherItems.map((item) => item.id),
+ ];
+
+ const handleTogglePin = (itemId: string, currentlyPinned: boolean) => {
+ if (currentlyPinned) {
+ const nextOtherPosition =
+ allOtherItems.length === 0
+ ? 0
+ : allOtherItems[allOtherItems.length - 1].position + 1;
+
+ updateCommandMenuItemInDraft(itemId, {
+ isPinned: false,
+ position: nextOtherPosition,
+ });
+
+ return;
+ }
+
+ const nextPinnedPosition =
+ allPinnedItems.length === 0
+ ? 0
+ : allPinnedItems[allPinnedItems.length - 1].position + 1;
+
+ updateCommandMenuItemInDraft(itemId, {
+ isPinned: true,
+ position: nextPinnedPosition,
+ });
+ };
+
+ const makeOptionsDropdownWrapper =
+ (item: Pick) =>
+ ({ iconButton }: { iconButton: React.ReactElement }) => (
+
+ );
+
+ const handlePinnedDragEnd = (result: DropResult) => {
+ const { source, destination, draggableId } = result;
+
+ if (!isDefined(destination)) {
+ return;
+ }
+
+ if (source.index === destination.index) {
+ return;
+ }
+
+ const displayedPinnedItemIdsWithoutSource = displayedPinnedItems
+ .map((item) => item.id)
+ .filter((itemId) => itemId !== draggableId);
+
+ const allPinnedItemsWithoutSource = allPinnedItems.filter(
+ (item) => item.id !== draggableId,
+ );
+
+ const nextDisplayedPinnedItemId =
+ displayedPinnedItemIdsWithoutSource[destination.index];
+ const previousDisplayedPinnedItemId =
+ displayedPinnedItemIdsWithoutSource[destination.index - 1];
+
+ let destinationIndexInAllPinnedItemsWithoutSource: number;
+
+ if (isDefined(nextDisplayedPinnedItemId)) {
+ destinationIndexInAllPinnedItemsWithoutSource =
+ allPinnedItemsWithoutSource.findIndex(
+ (item) => item.id === nextDisplayedPinnedItemId,
+ );
+ } else if (isDefined(previousDisplayedPinnedItemId)) {
+ const previousIndex = allPinnedItemsWithoutSource.findIndex(
+ (item) => item.id === previousDisplayedPinnedItemId,
+ );
+
+ if (previousIndex === -1) {
+ return;
+ }
+
+ destinationIndexInAllPinnedItemsWithoutSource = previousIndex + 1;
+ } else {
+ return;
+ }
+
+ if (destinationIndexInAllPinnedItemsWithoutSource === -1) {
+ return;
+ }
+
+ reorderCommandMenuItemInDraft(
+ draggableId,
+ destinationIndexInAllPinnedItemsWithoutSource,
+ 'pinned',
+ filteredCommandMenuItemIds,
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+ {
+ const ItemIcon = isDefined(item.icon)
+ ? getIcon(item.icon)
+ : undefined;
+
+ return (
+ handleTogglePin(item.id, true)}
+ >
+ {
+ event.stopPropagation();
+ handleTogglePin(item.id, true);
+ },
+ },
+ {
+ Icon: IconDotsVertical,
+ Wrapper: makeOptionsDropdownWrapper(item),
+ onClick: () => {},
+ },
+ ]}
+ />
+
+ }
+ />
+ );
+ })}
+ />
+
+
+
+ {displayedOtherItems.map((item) => {
+ const ItemIcon = isDefined(item.icon)
+ ? getIcon(item.icon)
+ : undefined;
+
+ return (
+ handleTogglePin(item.id, false)}
+ >
+
+ );
+ })}
+
+
+
+ ,
+ ]}
+ />
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useReorderCommandMenuItemsInDraft.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useReorderCommandMenuItemsInDraft.ts
new file mode 100644
index 0000000000..4c6a1a846c
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useReorderCommandMenuItemsInDraft.ts
@@ -0,0 +1,92 @@
+import { useStore } from 'jotai';
+import { useCallback } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
+import { computeInsertPositionFromBounds } from '@/command-menu-item/server-items/edit/utils/computeInsertPositionFromBounds';
+import { getPositionBoundsAtInsertionPoint } from '@/command-menu-item/server-items/edit/utils/getPositionBoundsAtInsertionPoint';
+
+export const useReorderCommandMenuItemsInDraft = () => {
+ const store = useStore();
+
+ const reorderCommandMenuItemInDraft = useCallback(
+ (
+ sourceId: string,
+ destinationIndex: number,
+ targetSection: 'pinned' | 'other',
+ contextualItemIds?: ReadonlySet,
+ ) => {
+ const draft = store.get(commandMenuItemsDraftState.atom);
+
+ if (!isDefined(draft)) {
+ return;
+ }
+
+ const isPinned = targetSection === 'pinned';
+
+ const fullSectionItems = draft
+ .filter((item) => item.isPinned === isPinned)
+ .sort((a, b) => a.position - b.position);
+
+ const contextualSectionItems = isDefined(contextualItemIds)
+ ? fullSectionItems.filter((item) => contextualItemIds.has(item.id))
+ : fullSectionItems;
+
+ const fullSectionItemsWithoutSource = fullSectionItems.filter(
+ (item) => item.id !== sourceId,
+ );
+ const contextualSectionItemsWithoutSource = contextualSectionItems.filter(
+ (item) => item.id !== sourceId,
+ );
+
+ const clampedDestinationIndex = Math.max(
+ 0,
+ Math.min(destinationIndex, contextualSectionItemsWithoutSource.length),
+ );
+
+ const nextContextualItem =
+ contextualSectionItemsWithoutSource[clampedDestinationIndex];
+ const previousContextualItem =
+ contextualSectionItemsWithoutSource[clampedDestinationIndex - 1];
+
+ if (
+ !isDefined(nextContextualItem) &&
+ !isDefined(previousContextualItem)
+ ) {
+ return;
+ }
+
+ const positionBounds = isDefined(nextContextualItem)
+ ? getPositionBoundsAtInsertionPoint(
+ nextContextualItem.id,
+ 'before',
+ fullSectionItemsWithoutSource,
+ )
+ : getPositionBoundsAtInsertionPoint(
+ previousContextualItem!.id,
+ 'after',
+ fullSectionItemsWithoutSource,
+ );
+
+ if (!isDefined(positionBounds)) {
+ return;
+ }
+
+ const newPosition = computeInsertPositionFromBounds(
+ positionBounds.previousPosition,
+ positionBounds.nextPosition,
+ );
+
+ const updatedDraft = draft.map((item) =>
+ item.id === sourceId
+ ? { ...item, isPinned, position: newPosition }
+ : item,
+ );
+
+ store.set(commandMenuItemsDraftState.atom, updatedDraft);
+ },
+ [store],
+ );
+
+ return { reorderCommandMenuItemInDraft };
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useResetCommandMenuItemsDraft.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useResetCommandMenuItemsDraft.ts
new file mode 100644
index 0000000000..a03bce9fe3
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useResetCommandMenuItemsDraft.ts
@@ -0,0 +1,21 @@
+import { useCallback } from 'react';
+import { useStore } from 'jotai';
+
+import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
+import { commandMenuItemsSelector } from '@/command-menu-item/server-items/common/states/commandMenuItemsSelector';
+
+// Resets the draft to the current server state, discarding all user edits.
+export const useResetCommandMenuItemsDraft = () => {
+ const store = useStore();
+
+ const resetCommandMenuItemsDraft = useCallback(() => {
+ const serverItems = store.get(commandMenuItemsSelector.atom);
+
+ store.set(
+ commandMenuItemsDraftState.atom,
+ serverItems.map((item) => ({ ...item })),
+ );
+ }, [store]);
+
+ return { resetCommandMenuItemsDraft };
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useSaveCommandMenuItemsDraft.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useSaveCommandMenuItemsDraft.ts
new file mode 100644
index 0000000000..556b9bfa73
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useSaveCommandMenuItemsDraft.ts
@@ -0,0 +1,57 @@
+import { useCallback } from 'react';
+import { useStore } from 'jotai';
+import { useMutation } from '@apollo/client/react';
+import { isDefined } from 'twenty-shared/utils';
+
+import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
+import { UPDATE_COMMAND_MENU_ITEM } from '@/command-menu-item/server-items/common/graphql/mutations/updateCommandMenuItem';
+import { commandMenuItemsSelector } from '@/command-menu-item/server-items/common/states/commandMenuItemsSelector';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { type UpdateCommandMenuItemInput } from '~/generated-metadata/graphql';
+
+export const useSaveCommandMenuItemsDraft = () => {
+ const store = useStore();
+ const [updateCommandMenuItem] = useMutation(UPDATE_COMMAND_MENU_ITEM);
+ const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
+
+ const saveCommandMenuItemsDraft = useCallback(async () => {
+ const draft = store.get(commandMenuItemsDraftState.atom);
+
+ if (!isDefined(draft)) {
+ return;
+ }
+
+ const serverItemsById = new Map(
+ commandMenuItems.map((item) => [item.id, item]),
+ );
+
+ const changedItems = draft.filter((draftItem) => {
+ const serverItem = serverItemsById.get(draftItem.id);
+
+ if (!isDefined(serverItem)) {
+ return false;
+ }
+
+ return (
+ draftItem.isPinned !== serverItem.isPinned ||
+ draftItem.position !== serverItem.position ||
+ draftItem.shortLabel !== serverItem.shortLabel
+ );
+ });
+
+ await Promise.all(
+ changedItems.map((item) => {
+ const input: UpdateCommandMenuItemInput = {
+ id: item.id,
+ isPinned: item.isPinned,
+ position: item.position,
+ shortLabel: item.shortLabel,
+ };
+
+ return updateCommandMenuItem({ variables: { input } });
+ }),
+ );
+ }, [store, commandMenuItems, updateCommandMenuItem]);
+
+ return { saveCommandMenuItemsDraft };
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useUpdateCommandMenuItemInDraft.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useUpdateCommandMenuItemInDraft.ts
new file mode 100644
index 0000000000..c6513eccd6
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/hooks/useUpdateCommandMenuItemInDraft.ts
@@ -0,0 +1,29 @@
+import { useCallback } from 'react';
+import { useStore } from 'jotai';
+import { isDefined } from 'twenty-shared/utils';
+
+import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
+import { type CommandMenuItemEditableFields } from '@/command-menu-item/server-items/edit/types/CommandMenuItemEditableFields';
+
+export const useUpdateCommandMenuItemInDraft = () => {
+ const store = useStore();
+
+ const updateCommandMenuItemInDraft = useCallback(
+ (id: string, fields: Partial) => {
+ const draft = store.get(commandMenuItemsDraftState.atom);
+
+ if (!isDefined(draft)) {
+ return;
+ }
+
+ const updatedDraft = draft.map((item) =>
+ item.id === id ? { ...item, ...fields } : item,
+ );
+
+ store.set(commandMenuItemsDraftState.atom, updatedDraft);
+ },
+ [store],
+ );
+
+ return { updateCommandMenuItemInDraft };
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/states/commandMenuItemEditSelectionModeState.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/states/commandMenuItemEditSelectionModeState.ts
new file mode 100644
index 0000000000..5ee29846bc
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/states/commandMenuItemEditSelectionModeState.ts
@@ -0,0 +1,9 @@
+import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
+
+export type CommandMenuItemEditSelectionMode = 'none' | 'selection';
+
+export const commandMenuItemEditSelectionModeState =
+ createAtomState({
+ key: 'commandMenuItemEditSelectionModeState',
+ defaultValue: 'selection',
+ });
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/states/commandMenuItemsDraftState.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/states/commandMenuItemsDraftState.ts
new file mode 100644
index 0000000000..3499c80c42
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/states/commandMenuItemsDraftState.ts
@@ -0,0 +1,10 @@
+import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
+
+import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
+
+export const commandMenuItemsDraftState = createAtomState<
+ CommandMenuItemFieldsFragment[] | null
+>({
+ key: 'commandMenuItemsDraftState',
+ defaultValue: null,
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/types/CommandMenuItemEditableFields.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/types/CommandMenuItemEditableFields.ts
new file mode 100644
index 0000000000..bbddd7141e
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/types/CommandMenuItemEditableFields.ts
@@ -0,0 +1,6 @@
+import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
+
+export type CommandMenuItemEditableFields = Pick<
+ CommandMenuItemFieldsFragment,
+ 'isPinned' | 'position' | 'shortLabel'
+>;
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/__tests__/computeInsertPositionFromBounds.test.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/__tests__/computeInsertPositionFromBounds.test.ts
new file mode 100644
index 0000000000..f7d54e10aa
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/__tests__/computeInsertPositionFromBounds.test.ts
@@ -0,0 +1,39 @@
+import { computeInsertPositionFromBounds } from '@/command-menu-item/server-items/edit/utils/computeInsertPositionFromBounds';
+
+describe('computeInsertPositionFromBounds', () => {
+ it('returns midpoint when both bounds are defined', () => {
+ expect(computeInsertPositionFromBounds(2, 4)).toBe(3);
+ });
+
+ it('returns midpoint for non-integer result', () => {
+ expect(computeInsertPositionFromBounds(1, 2)).toBe(1.5);
+ });
+
+ it('returns previous - 1 when only next is undefined', () => {
+ expect(computeInsertPositionFromBounds(5, undefined)).toBe(6);
+ });
+
+ it('returns next - 1 when only previous is undefined', () => {
+ expect(computeInsertPositionFromBounds(undefined, 3)).toBe(2);
+ });
+
+ it('returns 0 when both bounds are undefined', () => {
+ expect(computeInsertPositionFromBounds(undefined, undefined)).toBe(0);
+ });
+
+ it('returns previous - 1 when bounds are equal', () => {
+ expect(computeInsertPositionFromBounds(5, 5)).toBe(4);
+ });
+
+ it('handles negative positions', () => {
+ expect(computeInsertPositionFromBounds(-4, -2)).toBe(-3);
+ });
+
+ it('handles zero as previous position', () => {
+ expect(computeInsertPositionFromBounds(0, 2)).toBe(1);
+ });
+
+ it('handles zero as next position', () => {
+ expect(computeInsertPositionFromBounds(-2, 0)).toBe(-1);
+ });
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/__tests__/getPositionBoundsAtInsertionPoint.test.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/__tests__/getPositionBoundsAtInsertionPoint.test.ts
new file mode 100644
index 0000000000..efb826167d
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/__tests__/getPositionBoundsAtInsertionPoint.test.ts
@@ -0,0 +1,105 @@
+import { getPositionBoundsAtInsertionPoint } from '@/command-menu-item/server-items/edit/utils/getPositionBoundsAtInsertionPoint';
+
+const makeItems = (positions: number[]) =>
+ positions.map((position, index) => ({
+ id: `item-${index}`,
+ position,
+ }));
+
+describe('getPositionBoundsAtInsertionPoint', () => {
+ const items = makeItems([10, 20, 30, 40, 50]);
+
+ describe('insert before', () => {
+ it('returns previous=undefined and next=10 when inserting before the first item', () => {
+ expect(
+ getPositionBoundsAtInsertionPoint('item-0', 'before', items),
+ ).toEqual({
+ previousPosition: undefined,
+ nextPosition: 10,
+ });
+ });
+
+ it('returns previous=10 and next=20 when inserting before the second item', () => {
+ expect(
+ getPositionBoundsAtInsertionPoint('item-1', 'before', items),
+ ).toEqual({
+ previousPosition: 10,
+ nextPosition: 20,
+ });
+ });
+
+ it('returns previous=40 and next=50 when inserting before the last item', () => {
+ expect(
+ getPositionBoundsAtInsertionPoint('item-4', 'before', items),
+ ).toEqual({
+ previousPosition: 40,
+ nextPosition: 50,
+ });
+ });
+ });
+
+ describe('insert after', () => {
+ it('returns previous=10 and next=20 when inserting after the first item', () => {
+ expect(
+ getPositionBoundsAtInsertionPoint('item-0', 'after', items),
+ ).toEqual({
+ previousPosition: 10,
+ nextPosition: 20,
+ });
+ });
+
+ it('returns previous=50 and next=undefined when inserting after the last item', () => {
+ expect(
+ getPositionBoundsAtInsertionPoint('item-4', 'after', items),
+ ).toEqual({
+ previousPosition: 50,
+ nextPosition: undefined,
+ });
+ });
+
+ it('returns previous=30 and next=40 when inserting after the middle item', () => {
+ expect(
+ getPositionBoundsAtInsertionPoint('item-2', 'after', items),
+ ).toEqual({
+ previousPosition: 30,
+ nextPosition: 40,
+ });
+ });
+ });
+
+ describe('edge cases', () => {
+ it('returns undefined when anchor item is not found', () => {
+ expect(
+ getPositionBoundsAtInsertionPoint('nonexistent', 'before', items),
+ ).toBeUndefined();
+ });
+
+ it('handles single-item list inserting before', () => {
+ const singleItem = makeItems([5]);
+
+ expect(
+ getPositionBoundsAtInsertionPoint('item-0', 'before', singleItem),
+ ).toEqual({
+ previousPosition: undefined,
+ nextPosition: 5,
+ });
+ });
+
+ it('handles single-item list inserting after', () => {
+ const singleItem = makeItems([5]);
+
+ expect(
+ getPositionBoundsAtInsertionPoint('item-0', 'after', singleItem),
+ ).toEqual({
+ previousPosition: 5,
+ nextPosition: undefined,
+ });
+ });
+
+ it('returns undefined for empty list', () => {
+ expect(
+ getPositionBoundsAtInsertionPoint('item-0', 'before', []),
+ ).toBeUndefined();
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/computeInsertPositionFromBounds.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/computeInsertPositionFromBounds.ts
new file mode 100644
index 0000000000..407a9ca35c
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/computeInsertPositionFromBounds.ts
@@ -0,0 +1,24 @@
+import { isDefined } from 'twenty-shared/utils';
+
+export const computeInsertPositionFromBounds = (
+ previousPosition: number | undefined,
+ nextPosition: number | undefined,
+): number => {
+ if (!isDefined(previousPosition) && isDefined(nextPosition)) {
+ return nextPosition - 1;
+ }
+
+ if (isDefined(previousPosition) && !isDefined(nextPosition)) {
+ return previousPosition + 1;
+ }
+
+ if (isDefined(previousPosition) && isDefined(nextPosition)) {
+ if (previousPosition === nextPosition) {
+ return previousPosition - 1;
+ }
+
+ return (previousPosition + nextPosition) / 2;
+ }
+
+ return 0;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/getPositionBoundsAtInsertionPoint.ts b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/getPositionBoundsAtInsertionPoint.ts
new file mode 100644
index 0000000000..2bc5684d24
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/utils/getPositionBoundsAtInsertionPoint.ts
@@ -0,0 +1,23 @@
+type PositionedItem = { id: string; position: number };
+
+export const getPositionBoundsAtInsertionPoint = (
+ anchorItemId: string,
+ insertionSide: 'before' | 'after',
+ sectionItems: PositionedItem[],
+) => {
+ const anchorIndex = sectionItems.findIndex(
+ (item) => item.id === anchorItemId,
+ );
+
+ if (anchorIndex === -1) {
+ return undefined;
+ }
+
+ const insertIndex =
+ insertionSide === 'before' ? anchorIndex : anchorIndex + 1;
+
+ return {
+ previousPosition: sectionItems[insertIndex - 1]?.position,
+ nextPosition: sectionItems[insertIndex]?.position,
+ };
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
deleted file mode 100644
index 36cc6f43e5..0000000000
--- a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { useConvertBackendItemToCommandMenuItemConfig } from '@/command-menu-item/server-items/hooks/useConvertBackendItemToCommandMenuItemConfig';
-import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
-import { useQuery } from '@apollo/client/react';
-import { type CommandMenuContextApi } from 'twenty-shared/types';
-import { isDefined } from 'twenty-shared/utils';
-import {
- type CommandMenuItemFieldsFragment,
- FindManyCommandMenuItemsDocument,
-} from '~/generated-metadata/graphql';
-
-export const useCommandMenuItemsFromBackend = (
- commandMenuContextApi: CommandMenuContextApi,
-) => {
- const { convertBackendItemToCommandMenuItemConfig } =
- useConvertBackendItemToCommandMenuItemConfig();
-
- const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
- contextStoreCurrentObjectMetadataItemIdComponentState,
- );
-
- const { data } = useQuery(FindManyCommandMenuItemsDocument);
-
- const allItems = data?.commandMenuItems ?? [];
-
- const objectMatches = (item: CommandMenuItemFieldsFragment) =>
- !isDefined(item.availabilityObjectMetadataId) ||
- item.availabilityObjectMetadataId ===
- contextStoreCurrentObjectMetadataItemId;
-
- return allItems
- .filter(objectMatches)
- .map((item) =>
- convertBackendItemToCommandMenuItemConfig(item, commandMenuContextApi),
- )
- .filter(isDefined)
- .filter((item) => item.shouldBeRegistered())
- .sort((a, b) => a.position - b.position);
-};
diff --git a/packages/twenty-front/src/modules/command-menu-item/types/CommandMenuItemConfig.ts b/packages/twenty-front/src/modules/command-menu-item/types/CommandMenuItemConfig.ts
index 30ef106b00..af2d171b03 100644
--- a/packages/twenty-front/src/modules/command-menu-item/types/CommandMenuItemConfig.ts
+++ b/packages/twenty-front/src/modules/command-menu-item/types/CommandMenuItemConfig.ts
@@ -14,6 +14,7 @@ export type CommandMenuItemConfig = {
type: CommandMenuItemType;
scope: CommandMenuItemScope;
key: string;
+ id?: string;
label: Nullable;
shortLabel?: Nullable;
description?: MessageDescriptor | string;
diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
index 893a2a2e36..d0a71561d0 100644
--- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
@@ -95,7 +95,7 @@ const meta: Meta = {
});
jotaiStore.set(sidePanelNavigationStackState.atom, [
{
- page: SidePanelPages.Root,
+ page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Command Menu',
pageIcon: IconDotsVertical,
pageId: '1',
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx
index 8749049699..3986797cc0 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx
+++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx
@@ -83,14 +83,16 @@ describe('useSidePanelMenu', () => {
act(() => {
result.current.commandMenu.navigateSidePanelMenu({
- page: SidePanelPages.Root,
+ page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'First Page',
pageIcon: IconDotsVertical,
resetNavigationStack: false,
});
});
- expect(jotaiStore.get(sidePanelPageState.atom)).toBe(SidePanelPages.Root);
+ expect(jotaiStore.get(sidePanelPageState.atom)).toBe(
+ SidePanelPages.CommandMenuDisplay,
+ );
expect(jotaiStore.get(sidePanelPageInfoState.atom).title).toBe(
'First Page',
);
@@ -109,14 +111,16 @@ describe('useSidePanelMenu', () => {
act(() => {
result.current.commandMenu.navigateSidePanelMenu({
- page: SidePanelPages.Root,
+ page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Reset Page',
pageIcon: IconDotsVertical,
resetNavigationStack: true,
});
});
- expect(jotaiStore.get(sidePanelPageState.atom)).toBe(SidePanelPages.Root);
+ expect(jotaiStore.get(sidePanelPageState.atom)).toBe(
+ SidePanelPages.CommandMenuDisplay,
+ );
expect(jotaiStore.get(sidePanelPageInfoState.atom).title).toBe(
'Reset Page',
);
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts
index 8a43ea1931..73199d2b71 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts
@@ -100,7 +100,7 @@ export const useCommandMenuHotKeys = () => {
}
if (
- sidePanelPage === SidePanelPages.Root &&
+ sidePanelPage === SidePanelPages.CommandMenuDisplay &&
!(
contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0
@@ -108,7 +108,7 @@ export const useCommandMenuHotKeys = () => {
) {
setGlobalCommandMenuContext();
}
- if (sidePanelPage !== SidePanelPages.Root) {
+ if (sidePanelPage !== SidePanelPages.CommandMenuDisplay) {
goBackOneSubPageOrMainPage();
}
},
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
index ee013c4721..7a923993c4 100644
--- a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
@@ -1,13 +1,30 @@
-import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
-import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
-import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
-import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
-import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
+import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
+import { SidePanelPages } from 'twenty-shared/types';
+import { IconPencil } from 'twenty-ui/display';
+
+import { commandMenuItemEditSelectionModeState } from '@/command-menu-item/server-items/edit/states/commandMenuItemEditSelectionModeState';
+import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
+import { commandMenuItemsSelector } from '@/command-menu-item/server-items/common/states/commandMenuItemsSelector';
+import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
+import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
+import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
+import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
+import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
+import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
+
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useEnterLayoutCustomizationMode = () => {
const store = useStore();
+ const { navigateSidePanel } = useNavigateSidePanel();
+ const isCommandMenuItemEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
+ );
const enterLayoutCustomizationMode = useCallback(() => {
const isLayoutCustomizationModeAlreadyEnabled = store.get(
@@ -26,10 +43,31 @@ export const useEnterLayoutCustomizationMode = () => {
);
store.set(navigationMenuItemsDraftState.atom, workspaceNavigationMenuItems);
+ const persistedCommandMenuItems = store.get(commandMenuItemsSelector.atom);
+ store.set(commandMenuItemsDraftState.atom, persistedCommandMenuItems);
+
store.set(activeCustomizationPageLayoutIdsState.atom, []);
store.set(isLayoutCustomizationModeEnabledState.atom, true);
- }, [store]);
+
+ const isSidePanelOpened = store.get(isSidePanelOpenedState.atom);
+ const currentSidePanelPage = store.get(sidePanelPageState.atom);
+
+ if (
+ isCommandMenuItemEnabled &&
+ isSidePanelOpened &&
+ currentSidePanelPage === SidePanelPages.CommandMenuDisplay
+ ) {
+ store.set(commandMenuItemEditSelectionModeState.atom, 'selection');
+
+ navigateSidePanel({
+ page: SidePanelPages.CommandMenuEdit,
+ pageTitle: t`Edit actions`,
+ pageIcon: IconPencil,
+ resetNavigationStack: true,
+ });
+ }
+ }, [isCommandMenuItemEnabled, navigateSidePanel, store]);
return { enterLayoutCustomizationMode };
};
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts
index 8700c501d4..1b641e5c01 100644
--- a/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts
@@ -1,3 +1,4 @@
+import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
@@ -25,6 +26,7 @@ export const useExitLayoutCustomizationMode = () => {
const exitLayoutCustomizationMode = useCallback(() => {
setNavigationMenuItemsDraft(null);
setSelectedNavigationMenuItemIdInEditMode(null);
+ store.set(commandMenuItemsDraftState.atom, null);
store.set(currentPageLayoutIdState.atom, null);
store.set(activeCustomizationPageLayoutIdsState.atom, []);
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts
index 82645a7799..f8c9abd8fd 100644
--- a/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts
@@ -1,3 +1,4 @@
+import { useCommandMenuItemsDraftState } from '@/command-menu-item/server-items/common/hooks/useCommandMenuItemsDraftState';
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
@@ -14,6 +15,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useIsLayoutCustomizationDirty = () => {
const { isDirty: isNavigationDirty } = useNavigationMenuItemsDraftState();
+ const { isDirty: isCommandMenuItemsDirty } = useCommandMenuItemsDraftState();
const isAnyPageLayoutDirtyAtom = useMemo(
() =>
@@ -93,5 +95,8 @@ export const useIsLayoutCustomizationDirty = () => {
const isAnyPageLayoutDirty = useAtomValue(isAnyPageLayoutDirtyAtom);
- return { isDirty: isNavigationDirty || isAnyPageLayoutDirty };
+ return {
+ isDirty:
+ isNavigationDirty || isAnyPageLayoutDirty || isCommandMenuItemsDirty,
+ };
};
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts
index 7ba51eced0..7eb705ccd4 100644
--- a/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts
@@ -1,3 +1,5 @@
+import { useSaveCommandMenuItemsDraft } from '@/command-menu-item/server-items/edit/hooks/useSaveCommandMenuItemsDraft';
+import { useCommandMenuItemsDraftState } from '@/command-menu-item/server-items/common/hooks/useCommandMenuItemsDraftState';
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
@@ -31,6 +33,8 @@ export const useSaveLayoutCustomization = () => {
const { t } = useLingui();
const { saveDraft } = useSaveNavigationMenuItemsDraft();
+ const { saveCommandMenuItemsDraft } = useSaveCommandMenuItemsDraft();
+ const { isDirty: isCommandMenuItemsDirty } = useCommandMenuItemsDraftState();
const { enqueueErrorSnackBar } = useSnackBar();
const { updatePageLayoutWithTabsAndWidgets } =
useUpdatePageLayoutWithTabsAndWidgets();
@@ -59,6 +63,10 @@ export const useSaveLayoutCustomization = () => {
await saveDraft();
}
+ if (isCommandMenuItemsDirty) {
+ await saveCommandMenuItemsDraft();
+ }
+
const activePageLayoutIds = store.get(
activeCustomizationPageLayoutIdsState.atom,
);
@@ -164,6 +172,8 @@ export const useSaveLayoutCustomization = () => {
}
}, [
saveDraft,
+ saveCommandMenuItemsDraft,
+ isCommandMenuItemsDirty,
updatePageLayoutWithTabsAndWidgets,
savePageLayoutWidgetsData,
exitLayoutCustomizationMode,
diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts
index badcf357b8..85da24bb04 100644
--- a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts
+++ b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts
@@ -12,6 +12,7 @@ import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
FindAllViewsDocument,
+ FindManyCommandMenuItemsDocument,
FindAllRecordPageLayoutsDocument,
FindFieldsWidgetViewsDocument,
FindManyLogicFunctionsDocument,
@@ -193,6 +194,23 @@ export const useLoadStaleMetadataEntities = () => {
);
}
+ if (staleEntityKeys.includes('commandMenuItems')) {
+ fetchPromises.push(
+ client
+ .query({
+ query: FindManyCommandMenuItemsDocument,
+ fetchPolicy: 'network-only',
+ })
+ .then((result) => {
+ if (!isDefined(result.data?.commandMenuItems)) {
+ return;
+ }
+
+ replaceDraft('commandMenuItems', result.data.commandMenuItems);
+ }),
+ );
+ }
+
await Promise.all(fetchPromises);
applyChanges();
},
diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatCommandMenuItem.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatCommandMenuItem.ts
index 72040d239d..15222e9021 100644
--- a/packages/twenty-front/src/modules/metadata-store/types/FlatCommandMenuItem.ts
+++ b/packages/twenty-front/src/modules/metadata-store/types/FlatCommandMenuItem.ts
@@ -1,3 +1,3 @@
-import { type CommandMenuItem } from '~/generated-metadata/graphql';
+import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
-export type FlatCommandMenuItem = Omit;
+export type FlatCommandMenuItem = CommandMenuItemFieldsFragment;
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx
index 0e7d919c8b..0b3b052e1e 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx
@@ -1,17 +1,22 @@
import { RecordIndexCommandMenu } from '@/command-menu-item/components/RecordIndexCommandMenu';
+import { CommandMenuItemMoreActionsButton } from '@/command-menu-item/server-items/display/components/CommandMenuItemMoreActionsButton';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { RecordIndexPageHeaderIcon } from '@/object-record/record-index/components/RecordIndexPageHeaderIcon';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { PageHeaderToggleSidePanelButton } from '@/ui/layout/page-header/components/PageHeaderToggleSidePanelButton';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledTitleWithSelectedRecords = styled.div`
display: flex;
@@ -61,6 +66,12 @@ export const RecordIndexPageHeader = () => {
contextStoreCurrentViewIdComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
+ );
+ const isCommandMenuItemEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
+ );
return (
{
{isDefined(contextStoreCurrentViewId) && (
<>
-
+ {isCommandMenuItemEnabled ? (
+ !isLayoutCustomizationModeEnabled && (
+
+ )
+ ) : (
+
+ )}
>
)}
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts
index 5b8e49e65a..baf0063e2f 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts
@@ -76,7 +76,7 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
return;
}
- setSidePanelPage(SidePanelPages.Root);
+ setSidePanelPage(SidePanelPages.CommandMenuDisplay);
closeSidePanelMenu();
},
[
diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelTopBar.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelTopBar.tsx
index fe2354c2ec..31edfee161 100644
--- a/packages/twenty-front/src/modules/side-panel/components/SidePanelTopBar.tsx
+++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelTopBar.tsx
@@ -164,11 +164,13 @@ export const SidePanelTopBar = () => {
)}
{lastChip &&
- sidePanelPage !== SidePanelPages.Root &&
+ sidePanelPage !== SidePanelPages.CommandMenuDisplay &&
+ sidePanelPage !== SidePanelPages.CommandMenuEdit &&
sidePanelPage !== SidePanelPages.SearchRecords && (
)}
- {(sidePanelPage === SidePanelPages.Root ||
+ {(sidePanelPage === SidePanelPages.CommandMenuDisplay ||
+ sidePanelPage === SidePanelPages.CommandMenuEdit ||
sidePanelPage === SidePanelPages.SearchRecords) && (
<>
{
if (
- sidePanelPage === SidePanelPages.Root ||
+ sidePanelPage === SidePanelPages.CommandMenuDisplay ||
sidePanelPage === SidePanelPages.SearchRecords
) {
inputRef.current?.focus();
diff --git a/packages/twenty-front/src/modules/side-panel/constants/SidePanelPagesConfig.tsx b/packages/twenty-front/src/modules/side-panel/constants/SidePanelPagesConfig.tsx
index 6501200565..362935f404 100644
--- a/packages/twenty-front/src/modules/side-panel/constants/SidePanelPagesConfig.tsx
+++ b/packages/twenty-front/src/modules/side-panel/constants/SidePanelPagesConfig.tsx
@@ -1,3 +1,5 @@
+import { SidePanelCommandMenuItemEditPage } from '@/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage';
+import { SidePanelCommandMenuItemDisplayPage } from '@/command-menu-item/server-items/display/components/SidePanelCommandMenuItemDisplayPage';
import { SidePanelAIChatThreadsPage } from '@/side-panel/pages/ai-chat-threads/components/SidePanelAIChatThreadsPage';
import { SidePanelAskAIPage } from '@/side-panel/pages/ask-ai/components/SidePanelAskAIPage';
import { SidePanelCalendarEventPage } from '@/side-panel/pages/calendar-event/components/SidePanelCalendarEventPage';
@@ -23,11 +25,28 @@ import { SidePanelWorkflowEditStepType } from '@/side-panel/pages/workflow/step/
import { SidePanelWorkflowRunViewStep } from '@/side-panel/pages/workflow/step/view-run/components/SidePanelWorkflowRunViewStep';
import { SidePanelWorkflowViewStep } from '@/side-panel/pages/workflow/step/view/components/SidePanelWorkflowViewStep';
import { SidePanelWorkflowSelectTriggerType } from '@/side-panel/pages/workflow/trigger-type/components/SidePanelWorkflowSelectTriggerType';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { SidePanelPages } from 'twenty-shared/types';
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
+
+const SidePanelCommandMenuDisplayPageSwitch = () => {
+ const isCommandMenuItemEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
+ );
+
+ if (isCommandMenuItemEnabled) {
+ return ;
+ }
+
+ return ;
+};
export const SIDE_PANEL_PAGES_CONFIG = new Map(
[
- [SidePanelPages.Root, ],
+ [
+ SidePanelPages.CommandMenuDisplay,
+ ,
+ ],
[SidePanelPages.ViewRecord, ],
[SidePanelPages.MergeRecords, ],
[SidePanelPages.UpdateRecords, ],
@@ -73,5 +92,6 @@ export const SIDE_PANEL_PAGES_CONFIG = new Map(
,
],
[SidePanelPages.NavigationMenuAddItem, ],
+ [SidePanelPages.CommandMenuEdit, ],
],
);
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useNavigateSidePanel.test.tsx b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useNavigateSidePanel.test.tsx
index 5844862695..1fc328bb97 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useNavigateSidePanel.test.tsx
+++ b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useNavigateSidePanel.test.tsx
@@ -65,8 +65,8 @@ describe('useNavigateSidePanel', () => {
act(() => {
result.current.navigateSidePanel({
- page: SidePanelPages.Root,
- pageTitle: 'Root',
+ page: SidePanelPages.CommandMenuDisplay,
+ pageTitle: 'Command menu',
pageIcon: Icon123,
pageIconColor: 'red',
pageId: 'mocked-uuid',
@@ -74,18 +74,20 @@ describe('useNavigateSidePanel', () => {
});
});
- expect(jotaiStore.get(sidePanelPageState.atom)).toBe(SidePanelPages.Root);
+ expect(jotaiStore.get(sidePanelPageState.atom)).toBe(
+ SidePanelPages.CommandMenuDisplay,
+ );
expect(jotaiStore.get(sidePanelNavigationStackState.atom)).toEqual([
{
- page: SidePanelPages.Root,
- pageTitle: 'Root',
+ page: SidePanelPages.CommandMenuDisplay,
+ pageTitle: 'Command menu',
pageIcon: Icon123,
pageIconColor: 'red',
pageId: 'mocked-uuid',
},
]);
expect(jotaiStore.get(sidePanelPageInfoState.atom)).toEqual({
- title: 'Root',
+ title: 'Command menu',
Icon: Icon123,
instanceId: 'mocked-uuid',
});
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useSidePanelCloseAnimationCompleteCleanup.test.tsx b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useSidePanelCloseAnimationCompleteCleanup.test.tsx
index 5988a8d976..6631814f70 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useSidePanelCloseAnimationCompleteCleanup.test.tsx
+++ b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useSidePanelCloseAnimationCompleteCleanup.test.tsx
@@ -136,7 +136,9 @@ describe('useSidePanelCloseAnimationCompleteCleanup', () => {
result.current.sidePanelCloseAnimationCompleteCleanup();
});
- expect(jotaiStore.get(sidePanelPageState.atom)).toBe(SidePanelPages.Root);
+ expect(jotaiStore.get(sidePanelPageState.atom)).toBe(
+ SidePanelPages.CommandMenuDisplay,
+ );
expect(jotaiStore.get(sidePanelPageInfoState.atom)).toEqual({
title: undefined,
Icon: undefined,
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useSidePanelHistory.test.tsx b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useSidePanelHistory.test.tsx
index 8f885ef79c..2a1e7238c3 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useSidePanelHistory.test.tsx
+++ b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useSidePanelHistory.test.tsx
@@ -104,7 +104,9 @@ describe('useSidePanelHistory', () => {
});
expect(jotaiStore.get(sidePanelNavigationStackState.atom)).toEqual([]);
- expect(jotaiStore.get(sidePanelPageState.atom)).toBe(SidePanelPages.Root);
+ expect(jotaiStore.get(sidePanelPageState.atom)).toBe(
+ SidePanelPages.CommandMenuDisplay,
+ );
expect(jotaiStore.get(sidePanelPageInfoState.atom)).toEqual({
title: undefined,
instanceId: '',
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useUpdateSidePanelPageInfo.test.tsx b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useUpdateSidePanelPageInfo.test.tsx
index 969539d242..ecb7d64934 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useUpdateSidePanelPageInfo.test.tsx
+++ b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useUpdateSidePanelPageInfo.test.tsx
@@ -16,7 +16,7 @@ const mockedPageInfo = {
const mockedNavigationStack = [
{
- page: SidePanelPages.Root,
+ page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Initial Title',
pageIcon: IconDotsVertical,
pageId: 'test-page-id',
@@ -65,7 +65,7 @@ describe('useUpdateSidePanelPageInfo', () => {
);
expect(sidePanelNavigationStack).toEqual([
{
- page: SidePanelPages.Root,
+ page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'New Title',
pageIcon: IconArrowDown,
pageId: 'test-page-id',
@@ -94,7 +94,7 @@ describe('useUpdateSidePanelPageInfo', () => {
);
expect(sidePanelNavigationStack).toEqual([
{
- page: SidePanelPages.Root,
+ page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'New Title',
pageIcon: IconDotsVertical,
pageId: 'test-page-id',
@@ -123,7 +123,7 @@ describe('useUpdateSidePanelPageInfo', () => {
);
expect(sidePanelNavigationStack).toEqual([
{
- page: SidePanelPages.Root,
+ page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Initial Title',
pageIcon: IconArrowDown,
pageId: 'test-page-id',
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup.ts b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup.ts
index 54ef3aae67..00a4f6393b 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup.ts
+++ b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup.ts
@@ -98,7 +98,7 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
}
store.set(viewableRecordIdState.atom, null);
- store.set(sidePanelPageState.atom, SidePanelPages.Root);
+ store.set(sidePanelPageState.atom, SidePanelPages.CommandMenuDisplay);
store.set(sidePanelPageInfoState.atom, {
title: undefined,
Icon: undefined,
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelContextChips.tsx b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelContextChips.tsx
index 1404e3b870..02d282accf 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelContextChips.tsx
+++ b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelContextChips.tsx
@@ -52,7 +52,7 @@ export const useSidePanelContextChips = () => {
const contextChips = useMemo(() => {
const filteredSidePanelNavigationStack = sidePanelNavigationStack.filter(
- (page) => page.page !== SidePanelPages.Root,
+ (page) => page.page !== SidePanelPages.CommandMenuDisplay,
);
return filteredSidePanelNavigationStack
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts
index f2e3bce0b7..ab9964571c 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts
+++ b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts
@@ -67,7 +67,7 @@ export const useSidePanelMenu = () => {
});
} else {
navigateSidePanel({
- page: SidePanelPages.Root,
+ page: SidePanelPages.CommandMenuDisplay,
pageTitle: t`Command Menu`,
pageIcon: IconDotsVertical,
resetNavigationStack: true,
diff --git a/packages/twenty-front/src/modules/side-panel/states/sidePanelPageState.ts b/packages/twenty-front/src/modules/side-panel/states/sidePanelPageState.ts
index 616cc5f089..b3055d4a8b 100644
--- a/packages/twenty-front/src/modules/side-panel/states/sidePanelPageState.ts
+++ b/packages/twenty-front/src/modules/side-panel/states/sidePanelPageState.ts
@@ -3,5 +3,5 @@ import { SidePanelPages } from 'twenty-shared/types';
export const sidePanelPageState = createAtomState({
key: 'side-panel/sidePanelPageState',
- defaultValue: SidePanelPages.Root,
+ defaultValue: SidePanelPages.CommandMenuDisplay,
});
diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx
index 4a4ea3654f..c1aa744f47 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx
@@ -6,16 +6,16 @@ import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawe
import { PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID } from '@/ui/layout/page/constants/PageActionContainerClickOutsideId';
import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { AnimatePresence } from 'framer-motion';
+import { isDefined } from 'twenty-shared/utils';
import {
type IconComponent,
IconX,
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
-import { isDefined } from 'twenty-shared/utils';
import {
MOBILE_VIEWPORT,
ThemeContext,
@@ -34,7 +34,7 @@ const StyledTopBarContainer = styled.div<{ isMobile: boolean }>`
min-height: ${PAGE_BAR_MIN_HEIGHT}px;
padding-bottom: ${themeCssVariables.spacing[3]};
padding-left: ${({ isMobile }) =>
- isMobile ? themeCssVariables.spacing[3] : 0};
+ isMobile ? themeCssVariables.spacing[3] : themeCssVariables.spacing[4]};
padding-right: ${themeCssVariables.spacing[3]};
padding-top: ${themeCssVariables.spacing[3]};
`;
@@ -42,10 +42,11 @@ const StyledTopBarContainer = styled.div<{ isMobile: boolean }>`
const StyledLeftContainer = styled.div`
align-items: center;
display: flex;
+ flex: 0 1 auto;
flex-direction: row;
gap: ${themeCssVariables.spacing[1]};
+ min-width: 0;
overflow-x: hidden;
- width: 100%;
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${themeCssVariables.spacing[1]};
}
@@ -71,9 +72,13 @@ const StyledTopBarIconStyledTitleContainer = styled.div`
`;
const StyledPageActionContainer = styled.div`
- display: inline-flex;
- flex: 1 0 auto;
+ align-items: center;
+ display: flex;
+ flex: 1 1 0;
gap: ${themeCssVariables.spacing[2]};
+
+ justify-content: flex-end;
+ min-width: 0;
`;
const StyledIconContainer = styled.div`
diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
index 7d9eddef27..2ad447824b 100644
--- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
+++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
@@ -1,10 +1,12 @@
import { useParams } from 'react-router-dom';
+import { CommandMenuItemMoreActionsButton } from '@/command-menu-item/server-items/display/components/CommandMenuItemMoreActionsButton';
import { RecordShowCommandMenu } from '@/command-menu-item/components/RecordShowCommandMenu';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { PageLayoutRecordPageRenderer } from '@/object-record/record-show/components/PageLayoutRecordPageRenderer';
@@ -13,10 +15,20 @@ import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordSh
import { computeRecordShowComponentInstanceId } from '@/object-record/record-show/utils/computeRecordShowComponentInstanceId';
import { PageHeaderToggleSidePanelButton } from '@/ui/layout/page-header/components/PageHeaderToggleSidePanelButton';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
import { RecordShowPageTitle } from '~/pages/object-record/RecordShowPageTitle';
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const RecordShowPage = () => {
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
+ );
+ const isCommandMenuItemEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
+ );
+
const parameters = useParams<{
objectNameSingular: string;
objectRecordId: string;
@@ -50,7 +62,13 @@ export const RecordShowPage = () => {
objectRecordId={objectRecordId}
>
-
+ {isCommandMenuItemEnabled ? (
+ !isLayoutCustomizationModeEnabled && (
+
+ )
+ ) : (
+
+ )}
= 2 and objectPermissions.canSoftDeleteObjectRecords and not hasAnySoftDeleteFilterOnView and numberOfSelectedRecords < 10000',
@@ -83,10 +83,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
restoreSingleRecord: {
universalIdentifier: '8b3a1cae-3e4d-43c1-a71f-48592b2e47ff',
label: 'Restore ${capitalize(objectMetadataItem.labelSingular)}',
- shortLabel: 'Restore',
icon: 'IconRefresh',
- position: 5,
isPinned: true,
+ position: 5,
+ shortLabel: 'Restore',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'numberOfSelectedRecords == 1 and everyDefined(selectedRecords, "deletedAt") and objectPermissions.canSoftDeleteObjectRecords and (pageType == "RECORD_PAGE" or hasAnySoftDeleteFilterOnView)',
@@ -98,10 +98,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
restoreMultipleRecords: {
universalIdentifier: '8b740c9d-d99a-45a8-812f-809caaf420ac',
label: 'Restore ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Restore',
icon: 'IconRefresh',
- position: 6,
isPinned: true,
+ position: 6,
+ shortLabel: 'Restore',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'numberOfSelectedRecords >= 2 and objectPermissions.canSoftDeleteObjectRecords and hasAnySoftDeleteFilterOnView and numberOfSelectedRecords < 10000',
@@ -114,10 +114,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
universalIdentifier: '44a78417-c394-4bc8-961f-98b503030ddb',
label:
'Permanently destroy ${capitalize(objectMetadataItem.labelSingular)}',
- shortLabel: 'Destroy',
icon: 'IconTrashX',
- position: 7,
isPinned: false,
+ position: 7,
+ shortLabel: 'Destroy',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'numberOfSelectedRecords == 1 and objectPermissions.canDestroyObjectRecords and everyDefined(selectedRecords, "deletedAt")',
@@ -129,10 +129,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
destroyMultipleRecords: {
universalIdentifier: 'c630b3fb-7920-40d1-9906-77d0aa797608',
label: 'Permanently destroy ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Destroy',
icon: 'IconTrashX',
- position: 8,
isPinned: false,
+ position: 8,
+ shortLabel: 'Destroy',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'numberOfSelectedRecords >= 2 and objectPermissions.canDestroyObjectRecords and hasAnySoftDeleteFilterOnView and numberOfSelectedRecords < 10000',
@@ -144,10 +144,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
addToFavorites: {
universalIdentifier: '38bf80c3-bd55-4753-80ba-38aa66429a03',
label: 'Add to Favorites',
- shortLabel: null,
icon: 'IconHeart',
- position: 9,
isPinned: true,
+ position: 9,
+ shortLabel: null,
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'arrayLength(favoriteRecordIds) < numberOfSelectedRecords and noneDefined(selectedRecords, "deletedAt") and not hasAnySoftDeleteFilterOnView',
@@ -159,10 +159,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
removeFromFavorites: {
universalIdentifier: '3ea42507-44fa-4895-a36d-cbfef7355a50',
label: 'Remove from Favorites',
- shortLabel: null,
icon: 'IconHeartOff',
- position: 10,
isPinned: true,
+ position: 10,
+ shortLabel: null,
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'arrayLength(favoriteRecordIds) == numberOfSelectedRecords and noneDefined(selectedRecords, "deletedAt") and not hasAnySoftDeleteFilterOnView',
@@ -174,10 +174,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
exportNoteToPdf: {
universalIdentifier: '86c8f3aa-9276-4c16-8cff-e295e34fbaf0',
label: 'Export to PDF',
- shortLabel: 'Export',
icon: 'IconFileExport',
- position: 11,
isPinned: false,
+ position: 11,
+ shortLabel: 'Export',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'pageType == "RECORD_PAGE" and (objectMetadataItem.nameSingular == "note" or objectMetadataItem.nameSingular == "task") and someNonEmptyString(selectedRecords, "bodyV2.blocknote")',
@@ -189,10 +189,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
exportFromRecordIndex: {
universalIdentifier: 'a934ba8a-ac8f-487d-9cd9-06dfdaec1f49',
label: 'Export',
- shortLabel: 'Export',
icon: 'IconFileExport',
- position: 12,
isPinned: false,
+ position: 12,
+ shortLabel: 'Export',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression: 'pageType == "INDEX_PAGE"',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -203,10 +203,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
exportFromRecordShow: {
universalIdentifier: 'ba339455-f3c2-4ed1-bf77-3e316d7d6a66',
label: 'Export',
- shortLabel: 'Export',
icon: 'IconFileExport',
- position: 13,
isPinned: false,
+ position: 13,
+ shortLabel: 'Export',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression: 'pageType == "RECORD_PAGE"',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -217,10 +217,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
updateMultipleRecords: {
universalIdentifier: '2e080651-f098-4a78-bea9-7a70002dc57c',
label: 'Update ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Update',
icon: 'IconEdit',
- position: 14,
isPinned: true,
+ position: 14,
+ shortLabel: 'Update',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'numberOfSelectedRecords >= 2 and objectPermissions.canUpdateObjectRecords',
@@ -232,10 +232,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
mergeMultipleRecords: {
universalIdentifier: '6c14eb04-8e7e-4d47-93c0-8ec4834e2e60',
label: 'Merge ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Merge',
icon: 'IconArrowMerge',
- position: 15,
isPinned: false,
+ position: 15,
+ shortLabel: 'Merge',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'numberOfSelectedRecords >= 2 and isDefined(objectMetadataItem.duplicateCriteria) and objectPermissions.canUpdateObjectRecords and objectPermissions.canDestroyObjectRecords and numberOfSelectedRecords <= 9',
@@ -247,10 +247,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
exportMultipleRecords: {
universalIdentifier: 'f71f68e5-7b6e-4c03-8161-c48434d7777c',
label: 'Export ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Export',
icon: 'IconFileExport',
- position: 16,
isPinned: false,
+ position: 16,
+ shortLabel: 'Export',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression: 'numberOfSelectedRecords >= 2',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -261,10 +261,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
importRecords: {
universalIdentifier: 'a2dc9de7-4798-422e-bb55-bfad7b9bdbe8',
label: 'Import ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Import',
icon: 'IconFileImport',
- position: 17,
isPinned: false,
+ position: 17,
+ shortLabel: 'Import',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'not hasAnySoftDeleteFilterOnView',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -275,10 +275,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
exportView: {
universalIdentifier: '80680f2a-c426-48b3-a839-c63a6183dc4b',
label: 'Export View',
- shortLabel: 'Export',
icon: 'IconFileExport',
- position: 18,
isPinned: false,
+ position: 18,
+ shortLabel: 'Export',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier: null,
@@ -289,10 +289,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeDeletedRecords: {
universalIdentifier: 'd63c21c3-9785-4750-be87-5f36269b8e0d',
label: 'See deleted ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Deleted ${capitalize(objectMetadataItem.labelPlural)}',
icon: 'IconRotate2',
- position: 19,
isPinned: false,
+ position: 19,
+ shortLabel: 'Deleted ${capitalize(objectMetadataItem.labelPlural)}',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'not hasAnySoftDeleteFilterOnView',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -303,10 +303,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
createNewView: {
universalIdentifier: '6ec7c339-e167-431d-bec6-d1c737df677c',
label: 'Create View',
- shortLabel: 'Create View',
icon: 'IconLayout',
- position: 20,
isPinned: false,
+ position: 20,
+ shortLabel: 'Create View',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'not hasAnySoftDeleteFilterOnView',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -317,10 +317,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
hideDeletedRecords: {
universalIdentifier: '1420db7f-0fba-49e2-b23e-4b7caa0fafa0',
label: 'Hide deleted ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Hide deleted',
icon: 'IconEyeOff',
- position: 21,
isPinned: false,
+ position: 21,
+ shortLabel: 'Hide deleted',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'hasAnySoftDeleteFilterOnView',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -331,10 +331,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToPeople: {
universalIdentifier: 'dfe5fef8-d42c-40f0-941f-8e3b5eb01daa',
label: 'Go to People',
- shortLabel: 'People',
icon: 'IconUser',
- position: 23,
isPinned: false,
+ position: 23,
+ shortLabel: 'People',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.person',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -345,10 +345,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToCompanies: {
universalIdentifier: '196e4eec-bfdd-48a6-bcbb-6707ef11951a',
label: 'Go to Companies',
- shortLabel: 'Companies',
icon: 'IconBuildingSkyscraper',
- position: 24,
isPinned: false,
+ position: 24,
+ shortLabel: 'Companies',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.company',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -359,10 +359,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToDashboards: {
universalIdentifier: '11dc07d1-21b2-4f86-af8c-6a664c02f00c',
label: 'Go to Dashboards',
- shortLabel: 'Dashboards',
icon: 'IconLayoutDashboard',
- position: 25,
isPinned: false,
+ position: 25,
+ shortLabel: 'Dashboards',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.dashboard',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -373,10 +373,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToOpportunities: {
universalIdentifier: 'f04f5e00-a208-422f-acf2-ff189769510d',
label: 'Go to Opportunities',
- shortLabel: 'Opportunities',
icon: 'IconTargetArrow',
- position: 26,
isPinned: false,
+ position: 26,
+ shortLabel: 'Opportunities',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression:
'targetObjectReadPermissions.opportunity',
@@ -388,10 +388,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToSettings: {
universalIdentifier: 'ef9aba44-0068-453e-930a-f8c182af18ee',
label: 'Go to Settings',
- shortLabel: 'Settings',
icon: 'IconSettings',
- position: 27,
isPinned: false,
+ position: 27,
+ shortLabel: 'Settings',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier: null,
@@ -402,10 +402,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToTasks: {
universalIdentifier: 'e8e3bd0b-5ce9-4577-bb61-588c0b1ad063',
label: 'Go to Tasks',
- shortLabel: 'Tasks',
icon: 'IconCheckbox',
- position: 28,
isPinned: false,
+ position: 28,
+ shortLabel: 'Tasks',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.task',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -416,10 +416,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToNotes: {
universalIdentifier: '08e0f0cc-ac2d-46cd-9bca-39a409b9addf',
label: 'Go to Notes',
- shortLabel: 'Notes',
icon: 'IconCheckbox',
- position: 29,
isPinned: false,
+ position: 29,
+ shortLabel: 'Notes',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.note',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -430,10 +430,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
editRecordPageLayout: {
universalIdentifier: 'd9794c67-1799-424f-8871-5ea771dd4a6d',
label: 'Edit Page Layout',
- shortLabel: 'Edit Layout',
icon: 'IconPencil',
- position: 30,
isPinned: false,
+ position: 30,
+ shortLabel: 'Edit Layout',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'pageType == "RECORD_PAGE" and featureFlags.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED and noneDefined(selectedRecords, "deletedAt") and objectPermissions.canUpdateObjectRecords and objectMetadataItem.nameSingular != "dashboard"',
@@ -445,10 +445,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
editDashboardLayout: {
universalIdentifier: 'b9b53bbc-3129-4eb9-8344-c3f9628ffa7d',
label: 'Edit Dashboard',
- shortLabel: 'Edit',
icon: 'IconPencil',
- position: 33,
isPinned: true,
+ position: 33,
+ shortLabel: 'Edit',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'pageType == "RECORD_PAGE" and not isPageInEditMode and noneDefined(selectedRecords, "deletedAt") and everyDefined(selectedRecords, "pageLayoutId") and objectPermissions.canUpdateObjectRecords',
@@ -461,10 +461,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
saveDashboardLayout: {
universalIdentifier: '18b23908-f816-42ab-bc0a-eb5fae29c695',
label: 'Save Dashboard',
- shortLabel: 'Save',
icon: 'IconDeviceFloppy',
- position: 34,
isPinned: true,
+ position: 34,
+ shortLabel: 'Save',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'pageType == "RECORD_PAGE" and isPageInEditMode and noneDefined(selectedRecords, "deletedAt") and everyDefined(selectedRecords, "pageLayoutId") and objectPermissions.canUpdateObjectRecords',
@@ -477,10 +477,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
cancelDashboardLayout: {
universalIdentifier: '030ecd01-0aaf-4e6d-8400-105996548887',
label: 'Cancel Edition',
- shortLabel: 'Cancel',
icon: 'IconCancel',
- position: 35,
isPinned: true,
+ position: 35,
+ shortLabel: 'Cancel',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'pageType == "RECORD_PAGE" and isPageInEditMode and noneDefined(selectedRecords, "deletedAt") and everyDefined(selectedRecords, "pageLayoutId") and objectPermissions.canUpdateObjectRecords',
@@ -493,10 +493,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
duplicateDashboard: {
universalIdentifier: '2ee07307-60ce-41ef-bfee-7c718f67557e',
label: 'Duplicate Dashboard',
- shortLabel: 'Duplicate',
icon: 'IconCopyPlus',
- position: 36,
isPinned: false,
+ position: 36,
+ shortLabel: 'Duplicate',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'noneDefined(selectedRecords, "deletedAt") and objectPermissions.canUpdateObjectRecords',
@@ -509,10 +509,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToWorkflows: {
universalIdentifier: '4fa778f9-7931-4d18-b895-929e1ef9c31f',
label: 'Go to Workflows',
- shortLabel: 'Workflows',
icon: 'IconSettingsAutomation',
- position: 41,
isPinned: false,
+ position: 41,
+ shortLabel: 'Workflows',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.workflow',
availabilityObjectMetadataUniversalIdentifier: null,
@@ -523,10 +523,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
activateWorkflow: {
universalIdentifier: '44f19c85-0fd0-482f-a14e-da513c60b1b3',
label: 'Activate Workflow',
- shortLabel: 'Activate',
icon: 'IconPower',
- position: 42,
isPinned: true,
+ position: 42,
+ shortLabel: 'Activate',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'everyDefined(selectedRecords, "currentVersion.trigger") and everyDefined(selectedRecords, "currentVersion.steps") and every(selectedRecords, "currentVersion.steps.length") and (everyEquals(selectedRecords, "currentVersion.status", "DRAFT") or includesNone(selectedRecords, "statuses", "ACTIVE")) and noneDefined(selectedRecords, "deletedAt")',
@@ -539,10 +539,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
deactivateWorkflow: {
universalIdentifier: '57f21a06-a17a-47b1-a123-90d90dbdf0b7',
label: 'Deactivate Workflow',
- shortLabel: 'Deactivate',
icon: 'IconPlayerPause',
- position: 43,
isPinned: true,
+ position: 43,
+ shortLabel: 'Deactivate',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'everyEquals(selectedRecords, "currentVersion.status", "ACTIVE") and noneDefined(selectedRecords, "deletedAt")',
@@ -555,10 +555,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
discardDraftWorkflow: {
universalIdentifier: '4c227f2e-03bb-4a66-9b13-49f263264f4a',
label: 'Discard Draft',
- shortLabel: 'Discard Draft',
icon: 'IconNoteOff',
- position: 44,
isPinned: true,
+ position: 44,
+ shortLabel: 'Discard Draft',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'every(selectedRecords, "versions.length") and everyEquals(selectedRecords, "currentVersion.status", "DRAFT") and noneDefined(selectedRecords, "deletedAt")',
@@ -571,10 +571,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
testWorkflow: {
universalIdentifier: 'f85d552a-87a3-4667-99f7-71b47917539c',
label: 'Test Workflow',
- shortLabel: 'Test',
icon: 'IconPlayerPlay',
- position: 45,
isPinned: true,
+ position: 45,
+ shortLabel: 'Test',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'everyDefined(selectedRecords, "currentVersion.trigger") and everyDefined(selectedRecords, "currentVersion.steps") and every(selectedRecords, "currentVersion.steps.length") and ((everyEquals(selectedRecords, "currentVersion.trigger.type", "MANUAL") and noneDefined(selectedRecords, "currentVersion.trigger.settings.objectType")) or everyEquals(selectedRecords, "currentVersion.trigger.type", "WEBHOOK") or everyEquals(selectedRecords, "currentVersion.trigger.type", "CRON")) and noneDefined(selectedRecords, "deletedAt")',
@@ -587,10 +587,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeActiveVersionWorkflow: {
universalIdentifier: '31790508-75ff-4e4c-a768-83bd1b0718e0',
label: 'See Active Version',
- shortLabel: 'See Active Version',
icon: 'IconVersions',
- position: 46,
isPinned: false,
+ position: 46,
+ shortLabel: 'See Active Version',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'includesEvery(selectedRecords, "statuses", "ACTIVE") and includesEvery(selectedRecords, "statuses", "DRAFT") and noneDefined(selectedRecords, "deletedAt")',
@@ -603,10 +603,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeRunsWorkflow: {
universalIdentifier: 'e57efc2d-00a2-493a-b76c-f2dabd23a5eb',
label: 'See Runs',
- shortLabel: 'See Runs',
icon: 'IconHistoryToggle',
- position: 47,
isPinned: true,
+ position: 47,
+ shortLabel: 'See Runs',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'noneDefined(selectedRecords, "deletedAt")',
@@ -619,10 +619,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeVersionsWorkflow: {
universalIdentifier: '92781d24-b875-4282-8cdb-d127f04a5c7d',
label: 'See Versions History',
- shortLabel: 'See Versions',
icon: 'IconVersions',
- position: 48,
isPinned: false,
+ position: 48,
+ shortLabel: 'See Versions',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'noneDefined(selectedRecords, "deletedAt")',
@@ -635,10 +635,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
addNodeWorkflow: {
universalIdentifier: '818117fa-6cad-4ebc-83c1-40f4afc28d94',
label: 'Add a Node',
- shortLabel: 'Add a Node',
icon: 'IconPlus',
- position: 49,
isPinned: true,
+ position: 49,
+ shortLabel: 'Add a Node',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'pageType == "RECORD_PAGE" and everyDefined(selectedRecords, "currentVersion.trigger") and everyDefined(selectedRecords, "currentVersion.steps") and every(selectedRecords, "currentVersion.steps.length") and noneDefined(selectedRecords, "deletedAt")',
@@ -651,10 +651,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
tidyUpWorkflow: {
universalIdentifier: '1f3a3cab-161a-4775-af47-11be4d0bf411',
label: 'Tidy up Workflow',
- shortLabel: 'Tidy up',
icon: 'IconReorder',
- position: 50,
isPinned: false,
+ position: 50,
+ shortLabel: 'Tidy up',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'pageType == "RECORD_PAGE" and everyDefined(selectedRecords, "currentVersion.trigger") and everyDefined(selectedRecords, "currentVersion.steps") and every(selectedRecords, "currentVersion.steps.length") and noneDefined(selectedRecords, "deletedAt")',
@@ -667,10 +667,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
duplicateWorkflow: {
universalIdentifier: '91094438-b4c2-46ad-a23b-8af4b23ba514',
label: 'Duplicate Workflow',
- shortLabel: 'Duplicate',
icon: 'IconCopy',
- position: 51,
isPinned: false,
+ position: 51,
+ shortLabel: 'Duplicate',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'everyDefined(selectedRecords, "currentVersion") and noneDefined(selectedRecords, "deletedAt")',
@@ -683,10 +683,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
goToRuns: {
universalIdentifier: '1ba959da-ff49-4c1f-a517-2b78ee200508',
label: 'Go to Runs',
- shortLabel: 'See Runs',
icon: 'IconHistoryToggle',
- position: 52,
isPinned: false,
+ position: 52,
+ shortLabel: 'See Runs',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression:
'not hasAnySoftDeleteFilterOnView and objectMetadataItem.nameSingular != "workflowRun" and objectMetadataItem.nameSingular != "workflowVersion" and objectMetadataItem.nameSingular != "workflow"',
@@ -698,10 +698,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeVersionWorkflowRun: {
universalIdentifier: 'cc3a065c-c89e-40ac-9449-4272c55b1bb8',
label: 'See Version',
- shortLabel: 'See Version',
icon: 'IconVersions',
- position: 53,
isPinned: true,
+ position: 53,
+ shortLabel: 'See Version',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier:
@@ -713,10 +713,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeWorkflowWorkflowRun: {
universalIdentifier: '9d9cc62d-3543-45c3-93f3-23d2d8979f2b',
label: 'See Workflow',
- shortLabel: 'See Workflow',
icon: 'IconSettingsAutomation',
- position: 54,
isPinned: true,
+ position: 54,
+ shortLabel: 'See Workflow',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier:
@@ -728,10 +728,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
stopWorkflowRun: {
universalIdentifier: '4c186606-9515-4561-a1eb-9a072b4f5e58',
label: 'Stop',
- shortLabel: 'Stop',
icon: 'IconPlayerStop',
- position: 55,
isPinned: true,
+ position: 55,
+ shortLabel: 'Stop',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'isSelectAll or someEquals(selectedRecords, "status", "NOT_STARTED") or someEquals(selectedRecords, "status", "ENQUEUED") or someEquals(selectedRecords, "status", "RUNNING")',
@@ -744,10 +744,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeRunsWorkflowVersion: {
universalIdentifier: '44e305c7-4f0a-45ec-803f-6471b56455cb',
label: 'See Runs',
- shortLabel: 'See Runs',
icon: 'IconHistoryToggle',
- position: 56,
isPinned: true,
+ position: 56,
+ shortLabel: 'See Runs',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'everyDefined(selectedRecords, "workflow")',
@@ -760,10 +760,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeWorkflowWorkflowVersion: {
universalIdentifier: 'b43052db-023e-4083-9b63-2c2dfbfd1320',
label: 'See Workflow',
- shortLabel: 'See Workflow',
icon: 'IconSettingsAutomation',
- position: 57,
isPinned: true,
+ position: 57,
+ shortLabel: 'See Workflow',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'everyDefined(selectedRecords, "workflow.id")',
@@ -776,10 +776,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
useAsDraftWorkflowVersion: {
universalIdentifier: '483c0c1d-ea4d-4a4d-8a59-2dcf9f8e38f6',
label: 'Use as Draft',
- shortLabel: 'Use as Draft',
icon: 'IconPencil',
- position: 58,
isPinned: true,
+ position: 58,
+ shortLabel: 'Use as Draft',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'noneEquals(selectedRecords, "status", "DRAFT")',
@@ -792,10 +792,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
seeVersionsWorkflowVersion: {
universalIdentifier: '1d4abeb7-2750-4af7-9a92-fbadd2a9e4ba',
label: 'See Versions History',
- shortLabel: 'See Versions',
icon: 'IconVersions',
- position: 59,
isPinned: false,
+ position: 59,
+ shortLabel: 'See Versions',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'everyDefined(selectedRecords, "workflow")',
@@ -808,10 +808,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
searchRecords: {
universalIdentifier: 'fa24e25e-68f8-4548-82ff-c7b5168b7c7d',
label: 'Search ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Search',
icon: 'IconSearch',
- position: 60,
isPinned: false,
+ position: 60,
+ shortLabel: 'Search',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier: null,
@@ -822,10 +822,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
searchRecordsFallback: {
universalIdentifier: 'c659890c-7266-46c9-bfe1-75cefff8b6d0',
label: 'Search ${capitalize(objectMetadataItem.labelPlural)}',
- shortLabel: 'Search',
icon: 'IconSearch',
- position: 61,
isPinned: false,
+ position: 61,
+ shortLabel: 'Search',
availabilityType: CommandMenuItemAvailabilityType.FALLBACK,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier: null,
@@ -836,10 +836,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
askAi: {
universalIdentifier: 'ce5fb54d-2b19-4dd1-b7b4-9532a1761a41',
label: 'Ask AI',
- shortLabel: 'Ask AI',
icon: 'IconSparkles',
- position: 62,
isPinned: false,
+ position: 62,
+ shortLabel: 'Ask AI',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier: null,
@@ -850,10 +850,10 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
viewPreviousAiChats: {
universalIdentifier: '3084c3c9-cc23-4dad-9e00-92025f5cba7a',
label: 'View Previous AI Chats',
- shortLabel: 'Previous AI Chats',
icon: 'IconHistory',
- position: 63,
isPinned: false,
+ position: 63,
+ shortLabel: 'Previous AI Chats',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier: null,
diff --git a/packages/twenty-shared/src/types/SidePanelPages.ts b/packages/twenty-shared/src/types/SidePanelPages.ts
index 358c8d784d..abaac379b1 100644
--- a/packages/twenty-shared/src/types/SidePanelPages.ts
+++ b/packages/twenty-shared/src/types/SidePanelPages.ts
@@ -1,5 +1,5 @@
export enum SidePanelPages {
- Root = 'root',
+ CommandMenuDisplay = 'command-menu-display',
ViewRecord = 'view-record',
MergeRecords = 'merge-records',
UpdateRecords = 'update-records',
@@ -25,4 +25,5 @@ export enum SidePanelPages {
ViewFrontComponent = 'view-front-component',
NavigationMenuItemEdit = 'navigation-menu-item-edit',
NavigationMenuAddItem = 'navigation-menu-add-item',
+ CommandMenuEdit = 'command-menu-edit',
}
diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
index 8b29182def..5629fa6ace 100644
--- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
+++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
@@ -238,6 +238,7 @@ export {
IconLayoutList,
IconLayoutNavbar,
IconLayoutSidebarLeftCollapse,
+ IconLayoutSidebarLeftExpand,
IconLayoutSidebarRight,
IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand,
@@ -290,6 +291,7 @@ export {
IconPhoto,
IconPhotoUp,
IconPilcrow,
+ IconPin,
IconPinned,
IconPinnedOff,
IconPlayerPause,
@@ -351,6 +353,7 @@ export {
IconSpy,
IconSql,
IconSquare,
+ IconSquareCheck,
IconSquareKey,
IconSquareNumber1,
IconSquareNumber2,
@@ -364,6 +367,7 @@ export {
IconSquarePlus,
IconSquareRoundedCheck,
IconSquareRoundedX,
+ IconSquareX,
IconStack2,
IconStatusChange,
IconStepInto,
diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts
index fa9335a0a4..6b9a917260 100644
--- a/packages/twenty-ui/src/display/index.ts
+++ b/packages/twenty-ui/src/display/index.ts
@@ -316,6 +316,7 @@ export {
IconLayoutList,
IconLayoutNavbar,
IconLayoutSidebarLeftCollapse,
+ IconLayoutSidebarLeftExpand,
IconLayoutSidebarRight,
IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand,
@@ -368,6 +369,7 @@ export {
IconPhoto,
IconPhotoUp,
IconPilcrow,
+ IconPin,
IconPinned,
IconPinnedOff,
IconPlayerPause,
@@ -429,6 +431,7 @@ export {
IconSpy,
IconSql,
IconSquare,
+ IconSquareCheck,
IconSquareKey,
IconSquareNumber1,
IconSquareNumber2,
@@ -442,6 +445,7 @@ export {
IconSquarePlus,
IconSquareRoundedCheck,
IconSquareRoundedX,
+ IconSquareX,
IconStack2,
IconStatusChange,
IconStepInto,
diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.tsx
new file mode 100644
index 0000000000..905790ec63
--- /dev/null
+++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.tsx
@@ -0,0 +1,69 @@
+import { styled } from '@linaria/react';
+import { motion } from 'framer-motion';
+import { useContext } from 'react';
+import { type IconComponent } from '@ui/display';
+import { ThemeContext } from '@ui/theme-constants';
+
+const StyledContainer = styled.div<{ size: number }>`
+ height: ${({ size }) => size}px;
+ overflow: hidden;
+ position: relative;
+ width: ${({ size }) => size}px;
+`;
+
+const StyledLayer = styled(motion.div)`
+ align-items: center;
+ display: flex;
+ inset: 0;
+ justify-content: center;
+ position: absolute;
+`;
+
+type AnimatedIconCrossfadeProps = {
+ isActive: boolean;
+ ActiveIcon: IconComponent;
+ InactiveIcon: IconComponent;
+ size?: number;
+};
+
+export const AnimatedIconCrossfade = ({
+ isActive,
+ ActiveIcon,
+ InactiveIcon,
+ size,
+}: AnimatedIconCrossfadeProps) => {
+ const { theme } = useContext(ThemeContext);
+
+ const iconSize = size ?? theme.icon.size.sm;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-ui/src/utilities/animation/index.ts b/packages/twenty-ui/src/utilities/animation/index.ts
index cba279289a..1754312639 100644
--- a/packages/twenty-ui/src/utilities/animation/index.ts
+++ b/packages/twenty-ui/src/utilities/animation/index.ts
@@ -1,7 +1,8 @@
+export * from './components/AnimatedCircleLoading';
export * from './components/AnimatedContainer';
export * from './components/AnimatedEaseIn';
export * from './components/AnimatedEaseInOut';
export * from './components/AnimatedFadeOut';
+export * from './components/AnimatedIconCrossfade';
export * from './components/AnimatedTextWord';
export * from './components/AnimatedTranslation';
-export * from './components/AnimatedCircleLoading';
diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts
index 1c5549a4a5..71c09cc407 100644
--- a/packages/twenty-ui/src/utilities/index.ts
+++ b/packages/twenty-ui/src/utilities/index.ts
@@ -12,6 +12,7 @@ export { AnimatedContainer } from './animation/components/AnimatedContainer';
export { AnimatedEaseIn } from './animation/components/AnimatedEaseIn';
export { AnimatedEaseInOut } from './animation/components/AnimatedEaseInOut';
export { AnimatedFadeOut } from './animation/components/AnimatedFadeOut';
+export { AnimatedIconCrossfade } from './animation/components/AnimatedIconCrossfade';
export { AnimatedRotate } from './animation/components/AnimatedRotate';
export { AnimatedTextWord } from './animation/components/AnimatedTextWord';
export { AnimatedTranslation } from './animation/components/AnimatedTranslation';