Refactor command menu items deprecated code (#19508)
- Removes the intermediate `CommandMenuItemConfig` / `CommandConfigContext` / `CommandMenuItemDisplay` abstraction layers, replacing them with a single `CommandMenuItemRenderer` that renders directly from the command menu items from the backend - Eliminates the server-items/ subdirectory by moving its contents (hooks/, contexts/, states/, display/, edit/) up into the parent command-menu-item/ module, removing an unnecessary nesting level.
This commit is contained in:
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { PinnedCommandMenuItemButtons } from '@/command-menu-item/server-items/display/components/PinnedCommandMenuItemButtons';
|
||||
import { PinnedCommandMenuItemButtons } from '@/command-menu-item/display/components/PinnedCommandMenuItemButtons';
|
||||
import { RecordIndexCommandMenuDropdown } from '@/command-menu-item/components/RecordIndexCommandMenuDropdown';
|
||||
import { CommandMenuContextProvider } from '@/command-menu-item/contexts/CommandMenuContextProvider';
|
||||
import { CommandMenuItemEditButton } from '@/command-menu-item/server-items/edit/components/CommandMenuItemEditButton';
|
||||
import { PinnedCommandMenuItemButtonsEditMode } from '@/command-menu-item/server-items/edit/components/PinnedCommandMenuItemButtonsEditMode';
|
||||
import { CommandMenuItemEditButton } from '@/command-menu-item/edit/components/CommandMenuItemEditButton';
|
||||
import { PinnedCommandMenuItemButtonsEditMode } from '@/command-menu-item/edit/components/PinnedCommandMenuItemButtonsEditMode';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
|
||||
+10
-11
@@ -1,11 +1,9 @@
|
||||
import { CommandMenuItemComponent } from '@/command-menu-item/display/components/CommandMenuItemComponent';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { COMMAND_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/command-menu-item/constants/CommandMenuDropdownClickOutsideId';
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { CommandMenuItemRenderer } from '@/command-menu-item/display/components/CommandMenuItemRenderer';
|
||||
import { recordIndexCommandMenuDropdownPositionComponentState } from '@/command-menu-item/states/recordIndexCommandMenuDropdownPositionComponentState';
|
||||
import { getCommandMenuDropdownIdFromCommandMenuId } from '@/command-menu-item/utils/getCommandMenuDropdownIdFromCommandMenuId';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -21,6 +19,7 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { IconLayoutSidebarRightExpand } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledDropdownMenuContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -36,10 +35,10 @@ export const RecordIndexCommandMenuDropdown = () => {
|
||||
const { t } = useLingui();
|
||||
const { commandMenuItems } = useContext(CommandMenuContext);
|
||||
|
||||
const recordIndexActions = commandMenuItems.filter(
|
||||
(action) =>
|
||||
action.type === CommandMenuItemType.Standard &&
|
||||
action.scope === CommandMenuItemScope.RecordSelection,
|
||||
const recordIndexCommandMenuItems = commandMenuItems.filter(
|
||||
(item) =>
|
||||
item.availabilityType ===
|
||||
CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
);
|
||||
|
||||
const commandMenuId = useAvailableComponentInstanceIdOrThrow(
|
||||
@@ -57,7 +56,7 @@ export const RecordIndexCommandMenuDropdown = () => {
|
||||
const { openSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const selectedItemIdArray = [
|
||||
...recordIndexActions.map((action) => action.key),
|
||||
...recordIndexCommandMenuItems.map((item) => item.id),
|
||||
'more-actions',
|
||||
];
|
||||
|
||||
@@ -87,8 +86,8 @@ export const RecordIndexCommandMenuDropdown = () => {
|
||||
selectableItemIdArray={selectedItemIdArray}
|
||||
selectableListInstanceId={dropdownId}
|
||||
>
|
||||
{recordIndexActions.map((action) => (
|
||||
<CommandMenuItemComponent action={action} key={action.key} />
|
||||
{recordIndexCommandMenuItems.map((item) => (
|
||||
<CommandMenuItemRenderer item={item} key={item.id} />
|
||||
))}
|
||||
<SelectableListItem
|
||||
itemId="more-actions"
|
||||
|
||||
+34
-23
@@ -1,16 +1,16 @@
|
||||
import { CommandMenuItemComponent } from '@/command-menu-item/display/components/CommandMenuItemComponent';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { CommandMenuItemRenderer } from '@/command-menu-item/display/components/CommandMenuItemRenderer';
|
||||
import { getSidePanelCommandMenuDropdownIdFromCommandMenuId } from '@/command-menu-item/utils/getSidePanelCommandMenuDropdownIdFromCommandMenuId';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { OptionsDropdownMenu } from '@/ui/layout/dropdown/components/OptionsDropdownMenu';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { sidePanelWidgetFooterActionsState } from '@/ui/layout/side-panel/states/sidePanelWidgetFooterActionsState';
|
||||
import { sidePanelWidgetFooterCommandMenuItemsState } from '@/ui/layout/side-panel/states/sidePanelWidgetFooterCommandMenuItemsState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { HorizontalSeparator } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const RecordPageSidePanelCommandMenuDropdown = () => {
|
||||
const { commandMenuItems } = useContext(CommandMenuContext);
|
||||
@@ -24,21 +24,30 @@ export const RecordPageSidePanelCommandMenuDropdown = () => {
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const sidePanelWidgetFooterActions = useAtomStateValue(
|
||||
sidePanelWidgetFooterActionsState,
|
||||
const sidePanelWidgetFooterCommandMenuItems = useAtomStateValue(
|
||||
sidePanelWidgetFooterCommandMenuItemsState,
|
||||
);
|
||||
|
||||
const dropdownWidgetActions = sidePanelWidgetFooterActions.filter(
|
||||
(action) => action.isPinned === false,
|
||||
);
|
||||
const dropdownWidgetCommandMenuItems =
|
||||
sidePanelWidgetFooterCommandMenuItems.filter(
|
||||
(commandMenuItem) => commandMenuItem.isPinned === false,
|
||||
);
|
||||
|
||||
const recordSelectionActions = commandMenuItems.filter(
|
||||
(action) => action.scope === CommandMenuItemScope.RecordSelection,
|
||||
const recordSelectionCommandMenuItems = useMemo(
|
||||
() =>
|
||||
commandMenuItems.filter(
|
||||
(item) =>
|
||||
item.availabilityType ===
|
||||
CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
),
|
||||
[commandMenuItems],
|
||||
);
|
||||
|
||||
const selectableItemIdArray = [
|
||||
...dropdownWidgetActions.map((action) => action.key),
|
||||
...recordSelectionActions.map((action) => action.key),
|
||||
...dropdownWidgetCommandMenuItems.map(
|
||||
(commandMenuItem) => commandMenuItem.id,
|
||||
),
|
||||
...recordSelectionCommandMenuItems.map((item) => item.id),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -47,21 +56,23 @@ export const RecordPageSidePanelCommandMenuDropdown = () => {
|
||||
selectableListId={commandMenuId}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
>
|
||||
{dropdownWidgetActions.map((action) => (
|
||||
{dropdownWidgetCommandMenuItems.map((commandMenuItem) => (
|
||||
<MenuItem
|
||||
key={action.key}
|
||||
text={action.label}
|
||||
LeftIcon={action.Icon}
|
||||
key={commandMenuItem.id}
|
||||
text={commandMenuItem.label}
|
||||
LeftIcon={commandMenuItem.Icon}
|
||||
onClick={() => {
|
||||
closeDropdown(dropdownId);
|
||||
action.onClick();
|
||||
commandMenuItem.onClick();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{dropdownWidgetActions.length > 0 &&
|
||||
recordSelectionActions.length > 0 && <HorizontalSeparator noMargin />}
|
||||
{recordSelectionActions.map((action) => (
|
||||
<CommandMenuItemComponent action={action} key={action.key} />
|
||||
{dropdownWidgetCommandMenuItems.length > 0 &&
|
||||
recordSelectionCommandMenuItems.length > 0 && (
|
||||
<HorizontalSeparator noMargin />
|
||||
)}
|
||||
{recordSelectionCommandMenuItems.map((item) => (
|
||||
<CommandMenuItemRenderer item={item} key={item.id} />
|
||||
))}
|
||||
</OptionsDropdownMenu>
|
||||
);
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { CommandMenuContextProvider } from '@/command-menu-item/contexts/CommandMenuContextProvider';
|
||||
import { PinnedCommandMenuItemButtons } from '@/command-menu-item/server-items/display/components/PinnedCommandMenuItemButtons';
|
||||
import { CommandMenuItemEditButton } from '@/command-menu-item/server-items/edit/components/CommandMenuItemEditButton';
|
||||
import { PinnedCommandMenuItemButtons } from '@/command-menu-item/display/components/PinnedCommandMenuItemButtons';
|
||||
import { CommandMenuItemEditButton } from '@/command-menu-item/edit/components/CommandMenuItemEditButton';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
|
||||
+13
-25
@@ -1,22 +1,20 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import * as test from 'storybook/test';
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
import { expect, within } from 'storybook/test';
|
||||
|
||||
import { RecordIndexCommandMenuDropdown } from '@/command-menu-item/components/RecordIndexCommandMenuDropdown';
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { recordIndexCommandMenuDropdownPositionComponentState } from '@/command-menu-item/states/recordIndexCommandMenuDropdownPositionComponentState';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
|
||||
import { EMPTY_COMMAND_MENU_CONTEXT_API } from '@/command-menu-item/constants/EmptyCommandMenuContextApi';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { RouterDecorator } from 'twenty-ui/testing';
|
||||
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
|
||||
|
||||
const deleteMock = test.fn();
|
||||
const addToFavoritesMock = test.fn();
|
||||
const exportMock = test.fn();
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
|
||||
const meta: Meta<typeof RecordIndexCommandMenuDropdown> = {
|
||||
title: 'Modules/CommandMenu/RecordIndexCommandMenuDropdown',
|
||||
@@ -43,14 +41,10 @@ const meta: Meta<typeof RecordIndexCommandMenuDropdown> = {
|
||||
>
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
isInSidePanel: true,
|
||||
displayType: 'dropdownItem',
|
||||
containerType: 'index-page-dropdown',
|
||||
commandMenuItems: createMockCommandMenuItems({
|
||||
deleteMock,
|
||||
addToFavoritesMock,
|
||||
exportMock,
|
||||
}),
|
||||
commandMenuItems: createMockCommandMenuItems(),
|
||||
commandMenuContextApi: EMPTY_COMMAND_MENU_CONTEXT_API,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
@@ -60,6 +54,8 @@ const meta: Meta<typeof RecordIndexCommandMenuDropdown> = {
|
||||
);
|
||||
},
|
||||
ContextStoreDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
RouterDecorator,
|
||||
],
|
||||
};
|
||||
@@ -82,21 +78,13 @@ export const WithInteractions: Story = {
|
||||
const canvas = within(canvasElement.ownerDocument.body);
|
||||
|
||||
const deleteButton = await canvas.findByText('Delete');
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
const addToFavoritesButton = await canvas.findByText('Add to favorites');
|
||||
await userEvent.click(addToFavoritesButton);
|
||||
|
||||
const exportButton = await canvas.findByText('Export');
|
||||
await userEvent.click(exportButton);
|
||||
|
||||
const moreActionsButton = await canvas.findByText('More actions');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteMock).toHaveBeenCalled();
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
expect(exportMock).toHaveBeenCalled();
|
||||
expect(moreActionsButton).toBeInTheDocument();
|
||||
});
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
expect(addToFavoritesButton).toBeInTheDocument();
|
||||
expect(exportButton).toBeInTheDocument();
|
||||
expect(moreActionsButton).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
+8
-18
@@ -1,7 +1,6 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import * as test from 'storybook/test';
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
import { userEvent, within } from 'storybook/test';
|
||||
|
||||
import { RecordPageSidePanelCommandMenuDropdown } from '@/command-menu-item/components/RecordPageSidePanelCommandMenuDropdown';
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
@@ -9,15 +8,13 @@ import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-men
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { EMPTY_COMMAND_MENU_CONTEXT_API } from '@/command-menu-item/constants/EmptyCommandMenuContextApi';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
|
||||
const deleteMock = test.fn();
|
||||
const addToFavoritesMock = test.fn();
|
||||
const exportMock = test.fn();
|
||||
|
||||
const meta: Meta<typeof RecordPageSidePanelCommandMenuDropdown> = {
|
||||
title: 'Modules/CommandMenu/RecordPageSidePanelCommandMenuDropdown',
|
||||
@@ -40,14 +37,13 @@ const meta: Meta<typeof RecordPageSidePanelCommandMenuDropdown> = {
|
||||
>
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
isInSidePanel: true,
|
||||
displayType: 'dropdownItem',
|
||||
containerType: 'command-menu-show-page-dropdown',
|
||||
commandMenuItems: createMockCommandMenuItems({
|
||||
deleteMock,
|
||||
addToFavoritesMock,
|
||||
exportMock,
|
||||
}),
|
||||
commandMenuItems: createMockCommandMenuItems(),
|
||||
commandMenuContextApi: {
|
||||
...EMPTY_COMMAND_MENU_CONTEXT_API,
|
||||
isInSidePanel: true,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
@@ -102,11 +98,5 @@ export const WithButtonClicks: Story = {
|
||||
|
||||
const exportButton = await canvas.findByText('Export');
|
||||
await userEvent.click(exportButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteMock).toHaveBeenCalled();
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
expect(exportMock).toHaveBeenCalled();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
CommandMenuContextApiPageType,
|
||||
type CommandMenuContextApi,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const EMPTY_COMMAND_MENU_CONTEXT_API: CommandMenuContextApi = {
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
isInSidePanel: false,
|
||||
isPageInEditMode: false,
|
||||
favoriteRecordIds: [],
|
||||
isSelectAll: false,
|
||||
hasAnySoftDeleteFilterOnView: false,
|
||||
numberOfSelectedRecords: 0,
|
||||
objectPermissions: {
|
||||
canReadObjectRecords: false,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
objectMetadataId: '',
|
||||
rowLevelPermissionPredicates: [],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
},
|
||||
selectedRecords: [],
|
||||
featureFlags: {},
|
||||
targetObjectReadPermissions: {},
|
||||
targetObjectWritePermissions: {},
|
||||
objectMetadataItem: {},
|
||||
objectMetadataLabel: '',
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const CommandConfigContext = createContext<CommandMenuItemConfig | null>(
|
||||
null,
|
||||
);
|
||||
@@ -1,17 +1,19 @@
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
import { EMPTY_COMMAND_MENU_CONTEXT_API } from '@/command-menu-item/constants/EmptyCommandMenuContextApi';
|
||||
import { type CommandMenuItemContainerType } from '@/command-menu-item/types/CommandMenuItemContainerType';
|
||||
import { createContext } from 'react';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
export type CommandMenuContextType = {
|
||||
isInSidePanel: boolean;
|
||||
displayType: 'button' | 'listItem' | 'dropdownItem';
|
||||
containerType: CommandMenuItemContainerType;
|
||||
commandMenuItems: CommandMenuItemConfig[];
|
||||
commandMenuItems: CommandMenuItemFieldsFragment[];
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
export const CommandMenuContext = createContext<CommandMenuContextType>({
|
||||
isInSidePanel: false,
|
||||
containerType: 'command-menu-list',
|
||||
displayType: 'button',
|
||||
commandMenuItems: [],
|
||||
commandMenuContextApi: EMPTY_COMMAND_MENU_CONTEXT_API,
|
||||
});
|
||||
|
||||
+50
-8
@@ -1,21 +1,63 @@
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CommandMenuContextType } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { CommandMenuContextProviderServerItems } from '@/command-menu-item/server-items/common/contexts/CommandMenuContextProviderServerItems';
|
||||
import { useCommandMenuContextApi } from '@/command-menu-item/hooks/useCommandMenuContextApi';
|
||||
|
||||
import { CommandMenuContextProviderContent } from './CommandMenuContextProviderContent';
|
||||
import { CommandMenuContextProviderWithWorkflowEnrichment } from './CommandMenuContextProviderWithWorkflowEnrichment';
|
||||
|
||||
type CommandMenuContextProviderProps = {
|
||||
isInSidePanel: boolean;
|
||||
displayType: CommandMenuContextType['displayType'];
|
||||
containerType: CommandMenuContextType['containerType'];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuContextProvider = ({
|
||||
children,
|
||||
isInSidePanel,
|
||||
displayType,
|
||||
containerType,
|
||||
}: Omit<CommandMenuContextType, 'commandMenuItems'> & {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
children,
|
||||
}: CommandMenuContextProviderProps) => {
|
||||
const commandMenuContextApiFromHook = useCommandMenuContextApi();
|
||||
|
||||
const commandMenuContextApi = isInSidePanel
|
||||
? { ...commandMenuContextApiFromHook, isInSidePanel: true }
|
||||
: commandMenuContextApiFromHook;
|
||||
|
||||
const currentObjectNameSingular =
|
||||
commandMenuContextApi.objectMetadataItem.nameSingular;
|
||||
|
||||
const isWorkflow =
|
||||
currentObjectNameSingular === CoreObjectNameSingular.Workflow;
|
||||
|
||||
const selectedWorkflowRecordIds = isWorkflow
|
||||
? commandMenuContextApi.selectedRecords
|
||||
.map((record) => record.id)
|
||||
.filter(isDefined)
|
||||
: [];
|
||||
|
||||
if (selectedWorkflowRecordIds.length > 0) {
|
||||
return (
|
||||
<CommandMenuContextProviderWithWorkflowEnrichment
|
||||
displayType={displayType}
|
||||
containerType={containerType}
|
||||
commandMenuContextApi={commandMenuContextApi}
|
||||
selectedWorkflowRecordIds={selectedWorkflowRecordIds}
|
||||
>
|
||||
{children}
|
||||
</CommandMenuContextProviderWithWorkflowEnrichment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuContextProviderServerItems
|
||||
isInSidePanel={isInSidePanel}
|
||||
<CommandMenuContextProviderContent
|
||||
displayType={displayType}
|
||||
containerType={containerType}
|
||||
commandMenuContextApi={commandMenuContextApi}
|
||||
>
|
||||
{children}
|
||||
</CommandMenuContextProviderServerItems>
|
||||
</CommandMenuContextProviderContent>
|
||||
);
|
||||
};
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
CommandMenuContext,
|
||||
type CommandMenuContextType,
|
||||
} from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMemo } from 'react';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
import { evaluateConditionalAvailabilityExpression } from 'twenty-shared/utils';
|
||||
|
||||
type CommandMenuContextProviderContentProps = {
|
||||
displayType: CommandMenuContextType['displayType'];
|
||||
containerType: CommandMenuContextType['containerType'];
|
||||
children: React.ReactNode;
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
export const CommandMenuContextProviderContent = ({
|
||||
displayType,
|
||||
containerType,
|
||||
children,
|
||||
commandMenuContextApi,
|
||||
}: CommandMenuContextProviderContentProps) => {
|
||||
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
|
||||
|
||||
const filteredCommandMenuItems = useMemo(() => {
|
||||
const currentObjectMetadataItemId =
|
||||
commandMenuContextApi.objectMetadataItem.id;
|
||||
|
||||
return commandMenuItems
|
||||
.filter(
|
||||
doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
|
||||
)
|
||||
.filter((item) =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
commandMenuContextApi,
|
||||
),
|
||||
)
|
||||
.sort(
|
||||
(firstItem, secondItem) => firstItem.position - secondItem.position,
|
||||
);
|
||||
}, [commandMenuItems, commandMenuContextApi]);
|
||||
|
||||
return (
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
displayType,
|
||||
containerType,
|
||||
commandMenuItems: filteredCommandMenuItems,
|
||||
commandMenuContextApi,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CommandMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
+9
-13
@@ -2,28 +2,25 @@ import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CommandMenuContextType } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { useWorkflowsWithCurrentVersions } from '@/command-menu-item/server-items/common/hooks/useWorkflowsWithCurrentVersions';
|
||||
import { useWorkflowsWithCurrentVersions } from '@/command-menu-item/hooks/useWorkflowsWithCurrentVersions';
|
||||
|
||||
import { CommandMenuContextProviderServerItemsContent } from './CommandMenuContextProviderServerItemsContent';
|
||||
import { CommandMenuContextProviderContent } from './CommandMenuContextProviderContent';
|
||||
|
||||
type CommandMenuContextProviderServerItemsWithWorkflowEnrichmentProps = {
|
||||
isInSidePanel: CommandMenuContextType['isInSidePanel'];
|
||||
type CommandMenuContextProviderWithWorkflowEnrichmentProps = {
|
||||
displayType: CommandMenuContextType['displayType'];
|
||||
containerType: CommandMenuContextType['containerType'];
|
||||
children: React.ReactNode;
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
selectedWorkflowRecordIds: string[];
|
||||
};
|
||||
|
||||
export const CommandMenuContextProviderServerItemsWithWorkflowEnrichment = ({
|
||||
isInSidePanel,
|
||||
export const CommandMenuContextProviderWithWorkflowEnrichment = ({
|
||||
displayType,
|
||||
containerType,
|
||||
children,
|
||||
commandMenuContextApi,
|
||||
selectedWorkflowRecordIds,
|
||||
}: CommandMenuContextProviderServerItemsWithWorkflowEnrichmentProps & {
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
selectedWorkflowRecordIds: string[];
|
||||
}) => {
|
||||
}: CommandMenuContextProviderWithWorkflowEnrichmentProps) => {
|
||||
const workflowsWithCurrentVersions = useWorkflowsWithCurrentVersions(
|
||||
selectedWorkflowRecordIds,
|
||||
);
|
||||
@@ -53,13 +50,12 @@ export const CommandMenuContextProviderServerItemsWithWorkflowEnrichment = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandMenuContextProviderServerItemsContent
|
||||
isInSidePanel={isInSidePanel}
|
||||
<CommandMenuContextProviderContent
|
||||
displayType={displayType}
|
||||
containerType={containerType}
|
||||
commandMenuContextApi={enrichedCommandMenuContextApi}
|
||||
>
|
||||
{children}
|
||||
</CommandMenuContextProviderServerItemsContent>
|
||||
</CommandMenuContextProviderContent>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { CommandMenuItemDisplay } from '@/command-menu-item/display/components/CommandMenuItemDisplay';
|
||||
import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const Command = ({
|
||||
onClick,
|
||||
closeSidePanelOnShowPageOptionsExecution = false,
|
||||
closeSidePanelOnCommandMenuListExecution = true,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
closeSidePanelOnShowPageOptionsExecution?: boolean;
|
||||
closeSidePanelOnCommandMenuListExecution?: boolean;
|
||||
}) => {
|
||||
const commandMenuItemConfig = useContext(CommandConfigContext);
|
||||
|
||||
const { closeCommandMenu } = useCloseCommandMenu({
|
||||
closeSidePanelOnShowPageOptionsExecution,
|
||||
closeSidePanelOnCommandMenuListExecution,
|
||||
});
|
||||
|
||||
if (!commandMenuItemConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
closeCommandMenu();
|
||||
onClick();
|
||||
};
|
||||
|
||||
return <CommandMenuItemDisplay onClick={handleClick} />;
|
||||
};
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import { type CommandMenuItemDisplayProps } from '@/command-menu-item/display/components/CommandMenuItemDisplay';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
export const CommandDropdownItem = ({
|
||||
action,
|
||||
onClick,
|
||||
to,
|
||||
disabled = false,
|
||||
}: {
|
||||
action: CommandMenuItemDisplayProps;
|
||||
onClick?: () => void;
|
||||
to?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.();
|
||||
if (isDefined(to)) {
|
||||
navigate(to);
|
||||
}
|
||||
};
|
||||
|
||||
const selectableListInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
SelectableListComponentInstanceContext,
|
||||
);
|
||||
|
||||
const isSelectedItemId = useAtomComponentFamilyStateValue(
|
||||
isSelectedItemIdComponentFamilyState,
|
||||
action.key,
|
||||
selectableListInstanceId,
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectableListItem itemId={action.key} onEnter={handleClick}>
|
||||
<MenuItem
|
||||
focused={isSelectedItemId}
|
||||
key={action.key}
|
||||
LeftIcon={action.Icon}
|
||||
onClick={handleClick}
|
||||
text={getCommandMenuItemLabel(action.label)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { CommandMenuItemDisplay } from '@/command-menu-item/display/components/CommandMenuItemDisplay';
|
||||
import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
|
||||
import { type PathParam } from 'react-router-dom';
|
||||
import { type AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandLink = <T extends AppPath>({
|
||||
to,
|
||||
params,
|
||||
queryParams,
|
||||
}: {
|
||||
to: T;
|
||||
params?: { [key in PathParam<T>]: string | null };
|
||||
queryParams?: Record<string, any>;
|
||||
}) => {
|
||||
const { closeCommandMenu } = useCloseCommandMenu();
|
||||
|
||||
const path = getAppPath(to, params, queryParams);
|
||||
|
||||
return <CommandMenuItemDisplay onClick={closeCommandMenu} to={path} />;
|
||||
};
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { type CommandMenuItemDisplayProps } from '@/command-menu-item/display/components/CommandMenuItemDisplay';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
import { CommandListItemLoader } from './CommandListItemLoader';
|
||||
|
||||
export const CommandListItem = ({
|
||||
action,
|
||||
onClick,
|
||||
to,
|
||||
disabled = false,
|
||||
progress,
|
||||
showDisabledLoader = false,
|
||||
}: {
|
||||
action: CommandMenuItemDisplayProps;
|
||||
onClick?: () => void;
|
||||
to?: string;
|
||||
disabled?: boolean;
|
||||
progress?: number;
|
||||
showDisabledLoader?: boolean;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const handleClick = () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.();
|
||||
if (isDefined(to)) {
|
||||
navigate(to);
|
||||
}
|
||||
};
|
||||
|
||||
const loaderComponent =
|
||||
disabled && showDisabledLoader ? (
|
||||
isDefined(progress) ? (
|
||||
<CommandListItemLoader progress={progress} />
|
||||
) : (
|
||||
<Loader />
|
||||
)
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<SelectableListItem itemId={action.key} onEnter={handleClick}>
|
||||
<CommandMenuItem
|
||||
id={action.key}
|
||||
Icon={action.Icon}
|
||||
label={getCommandMenuItemLabel(action.label)}
|
||||
description={getCommandMenuItemLabel(action.description)}
|
||||
to={to}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
hotKeys={action.hotKeys}
|
||||
disabled={disabled}
|
||||
RightComponent={loaderComponent}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { CommandMenuButton } from '@/command-menu/components/CommandMenuButton';
|
||||
import { type CommandMenuItemDisplayProps } from '@/command-menu-item/display/components/CommandMenuItemDisplay';
|
||||
|
||||
export const CommandMenuItemButton = ({
|
||||
action,
|
||||
onClick,
|
||||
to,
|
||||
disabled = false,
|
||||
}: {
|
||||
action: CommandMenuItemDisplayProps;
|
||||
onClick?: (event?: React.MouseEvent<HTMLElement>) => void;
|
||||
to?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<CommandMenuButton
|
||||
command={action}
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
|
||||
export const CommandMenuItemComponent = ({
|
||||
action,
|
||||
}: {
|
||||
action: CommandMenuItemConfig;
|
||||
}) => {
|
||||
return (
|
||||
<CommandConfigContext.Provider value={action}>
|
||||
{action.component}
|
||||
</CommandConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { CommandMenuItemButton } from '@/command-menu-item/display/components/CommandMenuItemButton';
|
||||
import { CommandDropdownItem } from '@/command-menu-item/display/components/CommandDropdownItem';
|
||||
import { CommandListItem } from '@/command-menu-item/display/components/CommandListItem';
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { useIsCommandBlockedByGlobalLayoutCustomization } from '@/command-menu-item/hooks/useIsCommandBlockedByGlobalLayoutCustomization';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { useContext } from 'react';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { type MenuItemAccent } from 'twenty-ui/navigation';
|
||||
|
||||
export type CommandMenuItemDisplayProps = {
|
||||
key: string;
|
||||
label: Nullable<MessageDescriptor | string>;
|
||||
shortLabel?: Nullable<MessageDescriptor | string>;
|
||||
description?: MessageDescriptor | string;
|
||||
Icon: IconComponent;
|
||||
isPrimaryCTA?: boolean;
|
||||
accent?: MenuItemAccent;
|
||||
hotKeys?: Nullable<string[]>;
|
||||
};
|
||||
|
||||
export const CommandMenuItemDisplay = ({
|
||||
onClick,
|
||||
to,
|
||||
disabled,
|
||||
progress,
|
||||
showDisabledLoader = false,
|
||||
}: {
|
||||
onClick?: (event?: React.MouseEvent<HTMLElement>) => void;
|
||||
to?: string;
|
||||
disabled?: boolean;
|
||||
progress?: number;
|
||||
showDisabledLoader?: boolean;
|
||||
}) => {
|
||||
const action = useContext(CommandConfigContext);
|
||||
const { displayType } = useContext(CommandMenuContext);
|
||||
const isBlockedByGlobalLayoutCustomization =
|
||||
useIsCommandBlockedByGlobalLayoutCustomization(action);
|
||||
|
||||
if (!action) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDisabled =
|
||||
disabled === true || isBlockedByGlobalLayoutCustomization === true;
|
||||
|
||||
const onClickWhenEnabled = isDisabled ? undefined : onClick;
|
||||
const toWhenEnabled = isDisabled ? undefined : to;
|
||||
|
||||
if (displayType === 'button') {
|
||||
return (
|
||||
<CommandMenuItemButton
|
||||
action={action}
|
||||
onClick={onClickWhenEnabled}
|
||||
to={toWhenEnabled}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (displayType === 'listItem') {
|
||||
return (
|
||||
<CommandListItem
|
||||
action={action}
|
||||
onClick={onClickWhenEnabled}
|
||||
to={toWhenEnabled}
|
||||
disabled={isDisabled}
|
||||
progress={progress}
|
||||
showDisabledLoader={showDisabledLoader}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (displayType === 'dropdownItem') {
|
||||
return (
|
||||
<CommandDropdownItem
|
||||
action={action}
|
||||
onClick={onClickWhenEnabled}
|
||||
to={toWhenEnabled}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return assertUnreachable(displayType, 'Unsupported display type');
|
||||
};
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { CommandListItemLoader } from '@/command-menu-item/display/components/CommandListItemLoader';
|
||||
import { interpolateCommandMenuItemFields } from '@/command-menu-item/display/utils/interpolateCommandMenuItemFields';
|
||||
import { useCommandMenuItemClick } from '@/command-menu-item/hooks/useCommandMenuItemClick';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { CommandMenuButton } from '@/command-menu/components/CommandMenuButton';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||
import { useContext } from 'react';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
type CommandMenuItemRendererProps = {
|
||||
item: CommandMenuItemFieldsFragment;
|
||||
};
|
||||
|
||||
type CommandMenuItemButtonRendererProps = CommandMenuItemRendererProps;
|
||||
|
||||
const CommandMenuItemButtonRenderer = ({
|
||||
item,
|
||||
}: CommandMenuItemButtonRendererProps) => {
|
||||
const { commandMenuContextApi } = useContext(CommandMenuContext);
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { iconKey, label, shortLabel } = interpolateCommandMenuItemFields(
|
||||
item,
|
||||
commandMenuContextApi,
|
||||
);
|
||||
|
||||
const Icon = getIcon(iconKey, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
const { handleClick, disabled } = useCommandMenuItemClick({
|
||||
item,
|
||||
Icon,
|
||||
label,
|
||||
});
|
||||
|
||||
return (
|
||||
<CommandMenuButton
|
||||
command={{
|
||||
key: item.id,
|
||||
label,
|
||||
shortLabel,
|
||||
Icon,
|
||||
}}
|
||||
onClick={disabled ? undefined : handleClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandMenuItemSelectableRenderer = ({
|
||||
item,
|
||||
displayType,
|
||||
}: CommandMenuItemRendererProps & {
|
||||
displayType: 'listItem' | 'dropdownItem';
|
||||
}) => {
|
||||
const { commandMenuContextApi } = useContext(CommandMenuContext);
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { iconKey, label } = interpolateCommandMenuItemFields(
|
||||
item,
|
||||
commandMenuContextApi,
|
||||
);
|
||||
|
||||
const Icon = getIcon(iconKey, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
const { handleClick, disabled, progress, showDisabledLoader } =
|
||||
useCommandMenuItemClick({ item, Icon, label });
|
||||
|
||||
const selectableListInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
SelectableListComponentInstanceContext,
|
||||
);
|
||||
|
||||
const isSelectedItemId = useAtomComponentFamilyStateValue(
|
||||
isSelectedItemIdComponentFamilyState,
|
||||
item.id,
|
||||
selectableListInstanceId,
|
||||
);
|
||||
|
||||
const onItemClick = () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
handleClick();
|
||||
};
|
||||
|
||||
if (displayType === 'listItem') {
|
||||
const loaderComponent =
|
||||
disabled && showDisabledLoader ? (
|
||||
isDefined(progress) ? (
|
||||
<CommandListItemLoader progress={progress} />
|
||||
) : (
|
||||
<Loader />
|
||||
)
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<SelectableListItem itemId={item.id} onEnter={onItemClick}>
|
||||
<CommandMenuItem
|
||||
id={item.id}
|
||||
Icon={Icon}
|
||||
label={getCommandMenuItemLabel(label)}
|
||||
onClick={disabled ? undefined : handleClick}
|
||||
hotKeys={item.hotKeys}
|
||||
disabled={disabled}
|
||||
RightComponent={loaderComponent}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectableListItem itemId={item.id} onEnter={onItemClick}>
|
||||
<MenuItem
|
||||
focused={isSelectedItemId}
|
||||
LeftIcon={Icon}
|
||||
onClick={onItemClick}
|
||||
text={getCommandMenuItemLabel(label)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
|
||||
// oxlint-disable-next-line twenty/effect-components
|
||||
export const CommandMenuItemRenderer = ({
|
||||
item,
|
||||
}: CommandMenuItemRendererProps) => {
|
||||
const { displayType } = useContext(CommandMenuContext);
|
||||
|
||||
if (displayType === 'button') {
|
||||
return <CommandMenuItemButtonRenderer item={item} />;
|
||||
}
|
||||
|
||||
if (displayType === 'listItem' || displayType === 'dropdownItem') {
|
||||
return (
|
||||
<CommandMenuItemSelectableRenderer
|
||||
item={item}
|
||||
displayType={displayType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return assertUnreachable(displayType, 'Unsupported display type');
|
||||
};
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { CommandMenuItemDisplay } from '@/command-menu-item/display/components/CommandMenuItemDisplay';
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { type ButtonAccent } from 'twenty-ui/input';
|
||||
|
||||
export type CommandModalProps = {
|
||||
title: string;
|
||||
subtitle: ReactNode;
|
||||
onConfirmClick: () => void | Promise<void>;
|
||||
confirmButtonText?: string;
|
||||
confirmButtonAccent?: ButtonAccent;
|
||||
isLoading?: boolean;
|
||||
closeSidePanelOnShowPageOptionsExecution?: boolean;
|
||||
closeSidePanelOnCommandMenuListExecution?: boolean;
|
||||
};
|
||||
|
||||
export const CommandModal = ({
|
||||
title,
|
||||
subtitle,
|
||||
onConfirmClick,
|
||||
confirmButtonText = t`Confirm`,
|
||||
confirmButtonAccent = 'danger',
|
||||
isLoading = false,
|
||||
closeSidePanelOnShowPageOptionsExecution,
|
||||
closeSidePanelOnCommandMenuListExecution,
|
||||
}: CommandModalProps) => {
|
||||
const { openModal } = useModal();
|
||||
|
||||
const { closeCommandMenu } = useCloseCommandMenu({
|
||||
closeSidePanelOnShowPageOptionsExecution,
|
||||
closeSidePanelOnCommandMenuListExecution,
|
||||
});
|
||||
|
||||
const handleConfirmClick = async () => {
|
||||
await onConfirmClick();
|
||||
closeCommandMenu();
|
||||
};
|
||||
|
||||
const commandMenuItemConfig = useContext(CommandConfigContext);
|
||||
const { containerType } = useContext(CommandMenuContext);
|
||||
|
||||
const modalId = `${commandMenuItemConfig?.key}-command-menu-item-modal-${containerType}`;
|
||||
|
||||
const isModalOpened = useAtomComponentStateValue(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
|
||||
if (!commandMenuItemConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = () => openModal(modalId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandMenuItemDisplay onClick={handleClick} />
|
||||
{isModalOpened && (
|
||||
<ConfirmationModal
|
||||
modalInstanceId={modalId}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onConfirmClick={handleConfirmClick}
|
||||
confirmButtonText={confirmButtonText}
|
||||
confirmButtonAccent={confirmButtonAccent}
|
||||
loading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const FrontComponentCommandMenuItem = ({
|
||||
frontComponentId,
|
||||
}: {
|
||||
frontComponentId: string;
|
||||
}) => {
|
||||
const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
|
||||
const commandMenuItemConfig = useContext(CommandConfigContext);
|
||||
|
||||
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
const currentObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === contextStoreCurrentObjectMetadataItemId,
|
||||
);
|
||||
|
||||
const selectedRecordIds =
|
||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||
? contextStoreTargetedRecordsRule.selectedRecordIds
|
||||
: [];
|
||||
|
||||
const recordId =
|
||||
selectedRecordIds.length === 1 ? selectedRecordIds[0] : undefined;
|
||||
|
||||
const objectNameSingular = currentObjectMetadataItem?.nameSingular;
|
||||
|
||||
const displayLabel =
|
||||
typeof commandMenuItemConfig?.label === 'string'
|
||||
? commandMenuItemConfig.label
|
||||
: '';
|
||||
|
||||
const Icon = commandMenuItemConfig?.Icon;
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isDefined(Icon)) {
|
||||
return;
|
||||
}
|
||||
|
||||
openFrontComponentInSidePanel({
|
||||
frontComponentId,
|
||||
pageTitle: displayLabel,
|
||||
pageIcon: Icon,
|
||||
recordContext:
|
||||
isDefined(recordId) && isDefined(objectNameSingular)
|
||||
? { recordId, objectNameSingular }
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return <Command onClick={handleClick} />;
|
||||
};
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { useMountCommand } from '@/command-menu-item/engine-command/hooks/useMountCommand';
|
||||
import { isEngineCommandMountedFamilySelector } from '@/command-menu-item/engine-command/selectors/isEngineCommandMountedFamilySelector';
|
||||
import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
|
||||
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
import { CommandMenuItemDisplay } from './CommandMenuItemDisplay';
|
||||
|
||||
export const HeadlessCommandMenuItem = ({
|
||||
item,
|
||||
}: {
|
||||
item: CommandMenuItemFieldsFragment;
|
||||
}) => {
|
||||
const commandMenuItemConfig = useContext(CommandConfigContext);
|
||||
const mountCommand = useMountCommand();
|
||||
|
||||
const contextStoreInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
ContextStoreComponentInstanceContext,
|
||||
);
|
||||
|
||||
const isMounted = useAtomFamilySelectorValue(
|
||||
isEngineCommandMountedFamilySelector,
|
||||
item.id,
|
||||
);
|
||||
|
||||
const { closeCommandMenu } = useCloseCommandMenu({
|
||||
closeSidePanelOnShowPageOptionsExecution: false,
|
||||
closeSidePanelOnCommandMenuListExecution: false,
|
||||
});
|
||||
|
||||
const commandMenuItemProgress = useAtomFamilyStateValue(
|
||||
commandMenuItemProgressFamilyState,
|
||||
item.id,
|
||||
);
|
||||
|
||||
if (!isDefined(commandMenuItemConfig)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = async () => {
|
||||
if (isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeCommandMenu();
|
||||
|
||||
await mountCommand({
|
||||
engineCommandId: item.id,
|
||||
contextStoreInstanceId,
|
||||
engineComponentKey: item.engineComponentKey,
|
||||
frontComponentId: item.frontComponentId ?? undefined,
|
||||
workflowVersionId: item.workflowVersionId ?? undefined,
|
||||
availabilityType: item.availabilityType,
|
||||
availabilityObjectMetadataId: item.availabilityObjectMetadataId,
|
||||
payload: item.payload ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandMenuItemDisplay
|
||||
onClick={handleClick}
|
||||
disabled={isMounted}
|
||||
progress={commandMenuItemProgress}
|
||||
showDisabledLoader={isMounted}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+21
-28
@@ -1,15 +1,15 @@
|
||||
import { CommandMenuItemComponent } from '@/command-menu-item/display/components/CommandMenuItemComponent';
|
||||
import { CommandMenuItemRenderer } from '@/command-menu-item/display/components/CommandMenuItemRenderer';
|
||||
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 { PinnedCommandMenuItemsInlineMeasurements } from '@/command-menu-item/display/components/PinnedCommandMenuItemsInlineMeasurements';
|
||||
import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/display/constants/PinnedCommandMenuItemsGap';
|
||||
import { usePinnedCommandMenuItemsInlineLayout } from '@/command-menu-item/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)`
|
||||
const StyledCommandMenuItemContainer = styled(motion.div)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -40,10 +40,7 @@ export const PinnedCommandMenuItemButtons = () => {
|
||||
const { commandMenuItems } = useContext(CommandMenuContext);
|
||||
|
||||
const pinnedCommandMenuItems = useMemo(
|
||||
() =>
|
||||
commandMenuItems.filter(
|
||||
(commandMenuItem) => commandMenuItem.isPinned === true,
|
||||
),
|
||||
() => commandMenuItems.filter((item) => item.isPinned === true),
|
||||
[commandMenuItems],
|
||||
);
|
||||
|
||||
@@ -71,25 +68,21 @@ export const PinnedCommandMenuItemButtons = () => {
|
||||
<NodeDimension onDimensionChange={onContainerDimensionChange}>
|
||||
<StyledContainer>
|
||||
<StyledItemsContainer>
|
||||
{pinnedInlineCommandMenuItems.map(
|
||||
(pinnedInlineCommandMenuItem) => (
|
||||
<StyledActionContainer
|
||||
key={pinnedInlineCommandMenuItem.key}
|
||||
layout
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'unset', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
<CommandMenuItemComponent
|
||||
action={pinnedInlineCommandMenuItem}
|
||||
/>
|
||||
</StyledActionContainer>
|
||||
),
|
||||
)}
|
||||
{pinnedInlineCommandMenuItems.map((item) => (
|
||||
<StyledCommandMenuItemContainer
|
||||
key={item.id}
|
||||
layout
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'unset', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
<CommandMenuItemRenderer item={item} />
|
||||
</StyledCommandMenuItemContainer>
|
||||
))}
|
||||
</StyledItemsContainer>
|
||||
</StyledContainer>
|
||||
</NodeDimension>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/display/constants/PinnedCommandMenuItemsGap';
|
||||
import { interpolateCommandMenuItemFields } from '@/command-menu-item/display/utils/interpolateCommandMenuItemFields';
|
||||
import { CommandMenuButton } from '@/command-menu/components/CommandMenuButton';
|
||||
import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
|
||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
type ElementDimensions = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type PinnedCommandMenuItemsInlineMeasurementsProps = {
|
||||
pinnedCommandMenuItems: CommandMenuItemFieldsFragment[];
|
||||
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) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { commandMenuContextApi } = useContext(CommandMenuContext);
|
||||
|
||||
return (
|
||||
<StyledHiddenMeasurementsContainer>
|
||||
{pinnedCommandMenuItems.map((item) => {
|
||||
const { iconKey, label, shortLabel } = interpolateCommandMenuItemFields(
|
||||
item,
|
||||
commandMenuContextApi,
|
||||
);
|
||||
|
||||
const Icon = getIcon(iconKey, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
return (
|
||||
<NodeDimension
|
||||
key={item.id}
|
||||
onDimensionChange={onPinnedCommandMenuItemDimensionChange(item.id)}
|
||||
>
|
||||
<CommandMenuButton
|
||||
command={{
|
||||
key: `${item.id}-inline-measurement`,
|
||||
label,
|
||||
shortLabel,
|
||||
Icon,
|
||||
}}
|
||||
/>
|
||||
</NodeDimension>
|
||||
);
|
||||
})}
|
||||
</StyledHiddenMeasurementsContainer>
|
||||
);
|
||||
};
|
||||
+62
-59
@@ -1,64 +1,64 @@
|
||||
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 { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { CommandMenuItemRenderer } from '@/command-menu-item/display/components/CommandMenuItemRenderer';
|
||||
import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/display/constants/PinnedCommandMenuItemsGap';
|
||||
import { commandMenuPinnedInlineLayoutState } from '@/command-menu-item/display/states/commandMenuPinnedInlineLayoutState';
|
||||
import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
|
||||
import { groupCommandMenuItems } from '@/command-menu-item/utils/groupCommandMenuItems';
|
||||
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 { useFilterCommandMenuItemsWithSidePanelSearch } from '@/side-panel/pages/root/hooks/useFilterCommandMenuItemsWithSidePanelSearch';
|
||||
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 { isNumber } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const SidePanelCommandMenuItemDisplayPage = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const sidePanelSearch = useAtomStateValue(sidePanelSearchState);
|
||||
const { commandMenuItems } = useContext(CommandMenuContext);
|
||||
const { commandMenuItems, commandMenuContextApi } =
|
||||
useContext(CommandMenuContext);
|
||||
const commandMenuPinnedInlineLayout = useAtomStateValue(
|
||||
commandMenuPinnedInlineLayoutState,
|
||||
);
|
||||
|
||||
const { filterActionsWithSidePanelSearch } =
|
||||
useFilterActionsWithSidePanelSearch({
|
||||
const { filterCommandMenuItemsWithSidePanelSearch } =
|
||||
useFilterCommandMenuItemsWithSidePanelSearch({
|
||||
sidePanelSearch,
|
||||
commandMenuContextApi,
|
||||
});
|
||||
|
||||
const pinnedCommandMenuItems = commandMenuItems
|
||||
.filter((commandMenuItem) => commandMenuItem.isPinned === true)
|
||||
.sort(
|
||||
(firstPinnedCommandMenuItem, secondPinnedCommandMenuItem) =>
|
||||
firstPinnedCommandMenuItem.position -
|
||||
secondPinnedCommandMenuItem.position,
|
||||
);
|
||||
const { pinned: pinnedCommandMenuItems, other: nonPinnedCommandMenuItems } =
|
||||
useMemo(() => groupCommandMenuItems(commandMenuItems), [commandMenuItems]);
|
||||
|
||||
const unpinnedCommandMenuItems = commandMenuItems
|
||||
.filter(
|
||||
(commandMenuItem) =>
|
||||
commandMenuItem.isPinned !== true &&
|
||||
commandMenuItem.type !== CommandMenuItemType.Fallback,
|
||||
)
|
||||
.sort(
|
||||
(firstUnpinnedCommandMenuItem, secondUnpinnedCommandMenuItem) =>
|
||||
firstUnpinnedCommandMenuItem.position -
|
||||
secondUnpinnedCommandMenuItem.position,
|
||||
);
|
||||
const unpinnedCommandMenuItems = useMemo(
|
||||
() =>
|
||||
nonPinnedCommandMenuItems.filter(
|
||||
(item) =>
|
||||
item.availabilityType !== CommandMenuItemAvailabilityType.FALLBACK,
|
||||
),
|
||||
[nonPinnedCommandMenuItems],
|
||||
);
|
||||
|
||||
const fallbackCommandMenuItems = commandMenuItems.filter(
|
||||
(commandMenuItem) => commandMenuItem.type === CommandMenuItemType.Fallback,
|
||||
const fallbackCommandMenuItems = useMemo(
|
||||
() =>
|
||||
nonPinnedCommandMenuItems.filter(
|
||||
(item) =>
|
||||
item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK,
|
||||
),
|
||||
[nonPinnedCommandMenuItems],
|
||||
);
|
||||
|
||||
const pinnedCommandMenuItemKeysInDisplayOrder = pinnedCommandMenuItems.map(
|
||||
(pinnedCommandMenuItem) => pinnedCommandMenuItem.key,
|
||||
(item) => item.id,
|
||||
);
|
||||
|
||||
const visiblePinnedCommandMenuItemCount =
|
||||
@@ -74,11 +74,9 @@ export const SidePanelCommandMenuItemDisplayPage = () => {
|
||||
|
||||
const hasKnownPinnedInlineLayout =
|
||||
commandMenuPinnedInlineLayout.containerWidth > 0 &&
|
||||
pinnedCommandMenuItemKeysInDisplayOrder.every((commandMenuItemKey) =>
|
||||
pinnedCommandMenuItemKeysInDisplayOrder.every((itemKey) =>
|
||||
isNumber(
|
||||
commandMenuPinnedInlineLayout.commandMenuItemWidthsByKey[
|
||||
commandMenuItemKey
|
||||
],
|
||||
commandMenuPinnedInlineLayout.commandMenuItemWidthsByKey[itemKey],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -86,32 +84,20 @@ export const SidePanelCommandMenuItemDisplayPage = () => {
|
||||
? pinnedCommandMenuItems.slice(visiblePinnedCommandMenuItemCount)
|
||||
: pinnedCommandMenuItems;
|
||||
|
||||
const matchingPinnedItems = filterActionsWithSidePanelSearch(
|
||||
const matchingPinnedItems = filterCommandMenuItemsWithSidePanelSearch(
|
||||
pinnedOverflowCommandMenuItems,
|
||||
);
|
||||
const matchingOtherItems = filterActionsWithSidePanelSearch(
|
||||
const matchingOtherItems = filterCommandMenuItemsWithSidePanelSearch(
|
||||
unpinnedCommandMenuItems,
|
||||
);
|
||||
|
||||
const noResults = !matchingPinnedItems.length && !matchingOtherItems.length;
|
||||
|
||||
const commandGroups: SidePanelCommandMenuItemGroupConfig[] = [
|
||||
{
|
||||
heading: t`Pinned`,
|
||||
items: matchingPinnedItems,
|
||||
},
|
||||
{
|
||||
heading: t`Other`,
|
||||
items: matchingOtherItems,
|
||||
},
|
||||
{
|
||||
heading: t`Fallback`,
|
||||
items: noResults ? fallbackCommandMenuItems : [],
|
||||
},
|
||||
];
|
||||
|
||||
const selectableItems = commandGroups.flatMap((group) => group.items ?? []);
|
||||
const selectableItemIds = selectableItems.map((item) => item.key);
|
||||
const selectableItemIds = [
|
||||
...matchingPinnedItems,
|
||||
...matchingOtherItems,
|
||||
...(noResults ? fallbackCommandMenuItems : []),
|
||||
].map((item) => item.id);
|
||||
|
||||
// oxlint-disable-next-line twenty/matching-state-variable
|
||||
const previousContextStoreCurrentObjectMetadataItemId =
|
||||
@@ -125,16 +111,33 @@ export const SidePanelCommandMenuItemDisplayPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanelList
|
||||
commandGroups={commandGroups}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={noResults}
|
||||
>
|
||||
<SidePanelList selectableItemIds={selectableItemIds} noResults={noResults}>
|
||||
{isDefined(previousContextStoreCurrentObjectMetadataItemId) && (
|
||||
<SidePanelGroup heading={t`Context`}>
|
||||
<SidePanelResetContextToSelectionButton />
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
{matchingPinnedItems.length > 0 && (
|
||||
<SidePanelGroup heading={t`Pinned`}>
|
||||
{matchingPinnedItems.map((item) => (
|
||||
<CommandMenuItemRenderer item={item} key={item.id} />
|
||||
))}
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
{matchingOtherItems.length > 0 && (
|
||||
<SidePanelGroup heading={t`Other`}>
|
||||
{matchingOtherItems.map((item) => (
|
||||
<CommandMenuItemRenderer item={item} key={item.id} />
|
||||
))}
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
{noResults && fallbackCommandMenuItems.length > 0 && (
|
||||
<SidePanelGroup heading={t`Fallback`}>
|
||||
{fallbackCommandMenuItems.map((item) => (
|
||||
<CommandMenuItemRenderer item={item} key={item.id} />
|
||||
))}
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
</SidePanelList>
|
||||
);
|
||||
};
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, within } from 'storybook/test';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||
|
||||
import { CommandMenuItemButton } from '@/command-menu-item/display/components/CommandMenuItemButton';
|
||||
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const meta: Meta<typeof CommandMenuItemButton> = {
|
||||
title: 'Modules/CommandMenuItem/Display/CommandMenuItemButton',
|
||||
component: CommandMenuItemButton,
|
||||
decorators: [ComponentDecorator, RouterDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof CommandMenuItemButton>;
|
||||
|
||||
const deleteMock = fn();
|
||||
const addToFavoritesMock = fn();
|
||||
|
||||
const mockActions = createMockCommandMenuItems({
|
||||
deleteMock,
|
||||
addToFavoritesMock,
|
||||
});
|
||||
|
||||
const addToFavoritesCommandMenuItem = mockActions.find(
|
||||
(action) => action.key === EngineComponentKey.ADD_TO_FAVORITES,
|
||||
);
|
||||
|
||||
const goToPeopleCommandMenuItem = mockActions.find(
|
||||
(action) => action.key === EngineComponentKey.GO_TO_PEOPLE,
|
||||
);
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
action: addToFavoritesCommandMenuItem,
|
||||
onClick: addToFavoritesMock,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.shortLabel),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLink: Story = {
|
||||
args: {
|
||||
action: goToPeopleCommandMenuItem,
|
||||
to: '/objects/people',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const menuItem = await canvas.findByText(
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.shortLabel),
|
||||
);
|
||||
expect(menuItem).toBeVisible();
|
||||
expect(canvas.getByRole('link')).toHaveAttribute('href', '/objects/people');
|
||||
},
|
||||
};
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, within } from 'storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { CommandMenuItemComponent } from '@/command-menu-item/display/components/CommandMenuItemComponent';
|
||||
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const mockActions = createMockCommandMenuItems({});
|
||||
|
||||
const addToFavoritesCommandMenuItem = mockActions.find(
|
||||
(action) => action.key === EngineComponentKey.ADD_TO_FAVORITES,
|
||||
);
|
||||
|
||||
if (!addToFavoritesCommandMenuItem) {
|
||||
throw new Error('Add to favorites action not found');
|
||||
}
|
||||
|
||||
const meta: Meta<typeof CommandMenuItemComponent> = {
|
||||
title: 'Modules/CommandMenuItem/Display/CommandMenuItemComponent',
|
||||
component: CommandMenuItemComponent,
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
(Story) => (
|
||||
<CommandMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'story' }}
|
||||
>
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
isInSidePanel: false,
|
||||
containerType: 'index-page-header',
|
||||
displayType: 'button',
|
||||
commandMenuItems: [addToFavoritesCommandMenuItem],
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</CommandMenuContext.Provider>
|
||||
</CommandMenuComponentInstanceContext.Provider>
|
||||
),
|
||||
],
|
||||
args: {
|
||||
action: addToFavoritesCommandMenuItem,
|
||||
},
|
||||
parameters: {
|
||||
container: {
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommandMenuItemComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.shortLabel),
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
};
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, within } from 'storybook/test';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { CommandMenuItemDisplay } from '@/command-menu-item/display/components/CommandMenuItemDisplay';
|
||||
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
type Story = StoryObj<typeof CommandMenuItemDisplay>;
|
||||
|
||||
const deleteMock = fn();
|
||||
const addToFavoritesMock = fn();
|
||||
|
||||
const mockActions = createMockCommandMenuItems({
|
||||
deleteMock,
|
||||
addToFavoritesMock,
|
||||
});
|
||||
|
||||
const addToFavoritesCommandMenuItem = mockActions.find(
|
||||
(action) => action.key === EngineComponentKey.ADD_TO_FAVORITES,
|
||||
);
|
||||
|
||||
if (!addToFavoritesCommandMenuItem) {
|
||||
throw new Error('addToFavoritesCommandMenuItem not found');
|
||||
}
|
||||
|
||||
const meta: Meta<typeof CommandMenuItemDisplay> = {
|
||||
title: 'Modules/CommandMenuItem/Display/CommandMenuItemDisplay',
|
||||
component: CommandMenuItemDisplay,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<CommandConfigContext.Provider value={addToFavoritesCommandMenuItem}>
|
||||
<Story />
|
||||
</CommandConfigContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
RouterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const AsButton: Story = {
|
||||
args: {
|
||||
onClick: addToFavoritesMock,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
isInSidePanel: false,
|
||||
containerType: 'command-menu-list',
|
||||
displayType: 'button',
|
||||
commandMenuItems: [],
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</CommandMenuContext.Provider>
|
||||
),
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.shortLabel),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const AsListItem: Story = {
|
||||
args: {
|
||||
onClick: addToFavoritesMock,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<SelectableListComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'story' }}
|
||||
>
|
||||
<Story />
|
||||
</SelectableListComponentInstanceContext.Provider>
|
||||
),
|
||||
(Story) => (
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
isInSidePanel: false,
|
||||
containerType: 'command-menu-list',
|
||||
displayType: 'listItem',
|
||||
commandMenuItems: [],
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</CommandMenuContext.Provider>
|
||||
),
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const AsDropdownItem: Story = {
|
||||
args: {
|
||||
onClick: addToFavoritesMock,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<SelectableListComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'story' }}
|
||||
>
|
||||
<Story />
|
||||
</SelectableListComponentInstanceContext.Provider>
|
||||
),
|
||||
(Story) => (
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
isInSidePanel: false,
|
||||
containerType: 'command-menu-list',
|
||||
displayType: 'dropdownItem',
|
||||
commandMenuItems: [],
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</CommandMenuContext.Provider>
|
||||
),
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, within } from 'storybook/test';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||
|
||||
import { CommandDropdownItem } from '@/command-menu-item/display/components/CommandDropdownItem';
|
||||
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const meta: Meta<typeof CommandDropdownItem> = {
|
||||
title: 'Modules/CommandMenuItem/Display/CommandDropdownItem',
|
||||
component: CommandDropdownItem,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<SelectableListComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'story' }}
|
||||
>
|
||||
<Story />
|
||||
</SelectableListComponentInstanceContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
RouterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof CommandDropdownItem>;
|
||||
|
||||
const deleteMock = fn();
|
||||
const addToFavoritesMock = fn();
|
||||
|
||||
const mockActions = createMockCommandMenuItems({
|
||||
deleteMock,
|
||||
addToFavoritesMock,
|
||||
});
|
||||
|
||||
const addToFavoritesCommandMenuItem = mockActions.find(
|
||||
(action) => action.key === EngineComponentKey.ADD_TO_FAVORITES,
|
||||
);
|
||||
|
||||
const goToPeopleCommandMenuItem = mockActions.find(
|
||||
(action) => action.key === EngineComponentKey.GO_TO_PEOPLE,
|
||||
);
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
action: addToFavoritesCommandMenuItem,
|
||||
onClick: addToFavoritesMock,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLink: Story = {
|
||||
args: {
|
||||
action: goToPeopleCommandMenuItem,
|
||||
to: '/objects/people',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const dropdownItem = await canvas.findByText(
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.label),
|
||||
);
|
||||
expect(dropdownItem).toBeVisible();
|
||||
},
|
||||
};
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, within } from 'storybook/test';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||
|
||||
import { CommandListItem } from '@/command-menu-item/display/components/CommandListItem';
|
||||
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
type Story = StoryObj<typeof CommandListItem>;
|
||||
|
||||
const deleteMock = fn();
|
||||
const addToFavoritesMock = fn();
|
||||
|
||||
const mockActions = createMockCommandMenuItems({
|
||||
deleteMock,
|
||||
addToFavoritesMock,
|
||||
});
|
||||
|
||||
const addToFavoritesCommandMenuItem = mockActions.find(
|
||||
(action) => action.key === EngineComponentKey.ADD_TO_FAVORITES,
|
||||
);
|
||||
|
||||
const goToPeopleCommandMenuItem = mockActions.find(
|
||||
(action) => action.key === EngineComponentKey.GO_TO_PEOPLE,
|
||||
);
|
||||
|
||||
const meta: Meta<typeof CommandListItem> = {
|
||||
title: 'Modules/CommandMenuItem/Display/CommandListItem',
|
||||
component: CommandListItem,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<SelectableListComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'story' }}
|
||||
>
|
||||
<Story />
|
||||
</SelectableListComponentInstanceContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
RouterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
action: addToFavoritesCommandMenuItem,
|
||||
onClick: addToFavoritesMock,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLink: Story = {
|
||||
args: {
|
||||
action: goToPeopleCommandMenuItem,
|
||||
to: '/objects/people',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const listItem = await canvas.findByText(
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.label),
|
||||
);
|
||||
expect(listItem).toBeVisible();
|
||||
},
|
||||
};
|
||||
+11
-31
@@ -1,9 +1,9 @@
|
||||
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 { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/display/constants/PinnedCommandMenuItemsGap';
|
||||
import { commandMenuPinnedInlineLayoutState } from '@/command-menu-item/display/states/commandMenuPinnedInlineLayoutState';
|
||||
import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
type ElementDimensions = {
|
||||
width: number;
|
||||
@@ -11,7 +11,7 @@ type ElementDimensions = {
|
||||
};
|
||||
|
||||
type UsePinnedCommandMenuItemsInlineLayoutParams = {
|
||||
pinnedCommandMenuItems: CommandMenuItemConfig[];
|
||||
pinnedCommandMenuItems: CommandMenuItemFieldsFragment[];
|
||||
};
|
||||
|
||||
export const usePinnedCommandMenuItemsInlineLayout = ({
|
||||
@@ -20,22 +20,9 @@ export const usePinnedCommandMenuItemsInlineLayout = ({
|
||||
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],
|
||||
() => pinnedCommandMenuItems.map((item) => item.id),
|
||||
[pinnedCommandMenuItems],
|
||||
);
|
||||
|
||||
const visiblePinnedCommandMenuItemCount = useMemo(
|
||||
@@ -53,20 +40,13 @@ export const usePinnedCommandMenuItemsInlineLayout = ({
|
||||
);
|
||||
|
||||
const pinnedInlineCommandMenuItems = useMemo(
|
||||
() =>
|
||||
pinnedCommandMenuItemsSortedByPosition.slice(
|
||||
0,
|
||||
visiblePinnedCommandMenuItemCount,
|
||||
),
|
||||
[pinnedCommandMenuItemsSortedByPosition, visiblePinnedCommandMenuItemCount],
|
||||
() => pinnedCommandMenuItems.slice(0, visiblePinnedCommandMenuItemCount),
|
||||
[pinnedCommandMenuItems, visiblePinnedCommandMenuItemCount],
|
||||
);
|
||||
|
||||
const pinnedOverflowCommandMenuItems = useMemo(
|
||||
() =>
|
||||
pinnedCommandMenuItemsSortedByPosition.slice(
|
||||
visiblePinnedCommandMenuItemCount,
|
||||
),
|
||||
[pinnedCommandMenuItemsSortedByPosition, visiblePinnedCommandMenuItemCount],
|
||||
() => pinnedCommandMenuItems.slice(visiblePinnedCommandMenuItemCount),
|
||||
[pinnedCommandMenuItems, visiblePinnedCommandMenuItemCount],
|
||||
);
|
||||
|
||||
const onContainerDimensionChange = useCallback(
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { type CommandMenuPinnedInlineLayout } from '@/command-menu-item/server-items/display/types/CommandMenuPinnedInlineLayout';
|
||||
import { type CommandMenuPinnedInlineLayout } from '@/command-menu-item/display/types/CommandMenuPinnedInlineLayout';
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const commandMenuPinnedInlineLayoutState =
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/server-items/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
|
||||
import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
|
||||
|
||||
describe('getVisibleCommandMenuItemCountForContainerWidth', () => {
|
||||
it('should return all items when container width is zero', () => {
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { type CommandMenuContextApi, type Nullable } from 'twenty-shared/types';
|
||||
import { interpolateCommandMenuItemTemplate } from 'twenty-shared/utils';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
type InterpolatedCommandMenuItemFields = {
|
||||
iconKey: Nullable<string>;
|
||||
label: string;
|
||||
shortLabel: Nullable<string>;
|
||||
};
|
||||
|
||||
export const interpolateCommandMenuItemFields = (
|
||||
item: CommandMenuItemFieldsFragment,
|
||||
commandMenuContextApi: CommandMenuContextApi,
|
||||
): InterpolatedCommandMenuItemFields => {
|
||||
const iconKey = interpolateCommandMenuItemTemplate({
|
||||
label: item.icon,
|
||||
context: commandMenuContextApi,
|
||||
});
|
||||
|
||||
const label =
|
||||
interpolateCommandMenuItemTemplate({
|
||||
label: item.label,
|
||||
context: commandMenuContextApi,
|
||||
}) ?? item.label;
|
||||
|
||||
const shortLabel = interpolateCommandMenuItemTemplate({
|
||||
label: item.shortLabel,
|
||||
context: commandMenuContextApi,
|
||||
});
|
||||
|
||||
return { iconKey, label, shortLabel };
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { COMMAND_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/command-menu-item/constants/CommandMenuDropdownClickOutsideId';
|
||||
import { useSelectFirstRecordForEditMode } from '@/command-menu-item/server-items/edit/hooks/useSelectFirstRecordForEditMode';
|
||||
import { useSelectFirstRecordForEditMode } from '@/command-menu-item/edit/hooks/useSelectFirstRecordForEditMode';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { mainContextStoreHasSelectedRecordsSelector } from '@/context-store/states/selectors/mainContextStoreHasSelectedRecordsSelector';
|
||||
import { useResetRecordIndexSelection } from '@/object-record/record-index/hooks/useResetRecordIndexSelection';
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/server-items/edit/hooks/useUpdateCommandMenuItemInDraft';
|
||||
import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/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';
|
||||
+36
-66
@@ -1,26 +1,23 @@
|
||||
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 { mainContextStoreHasSelectedRecordsSelector } from '@/context-store/states/selectors/mainContextStoreHasSelectedRecordsSelector';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { PinnedCommandMenuItemsInlineMeasurements } from '@/command-menu-item/display/components/PinnedCommandMenuItemsInlineMeasurements';
|
||||
import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/display/constants/PinnedCommandMenuItemsGap';
|
||||
import { usePinnedCommandMenuItemsInlineLayout } from '@/command-menu-item/display/hooks/usePinnedCommandMenuItemsInlineLayout';
|
||||
import { interpolateCommandMenuItemFields } from '@/command-menu-item/display/utils/interpolateCommandMenuItemFields';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { useCommandMenuContextApi } from '@/command-menu-item/hooks/useCommandMenuContextApi';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { CommandMenuButton } from '@/command-menu/components/CommandMenuButton';
|
||||
import { mainContextStoreHasSelectedRecordsSelector } from '@/context-store/states/selectors/mainContextStoreHasSelectedRecordsSelector';
|
||||
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 { interpolateCommandMenuItemTemplate } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledActionContainer = styled(motion.div)`
|
||||
const StyledCommandMenuItemContainer = styled(motion.div)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -72,52 +69,12 @@ export const PinnedCommandMenuItemButtonsEditMode = () => {
|
||||
[mainContextStoreHasSelectedRecords],
|
||||
);
|
||||
|
||||
const interpolateLabel = (rawLabel: string | null | undefined) =>
|
||||
interpolateCommandMenuItemTemplate({
|
||||
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: (
|
||||
<CommandMenuButton
|
||||
command={{ key, label, shortLabel, Icon }}
|
||||
disabled
|
||||
/>
|
||||
),
|
||||
};
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
currentObjectMetadataItemId,
|
||||
allowedAvailabilityTypes,
|
||||
commandMenuContextApi,
|
||||
getIcon,
|
||||
],
|
||||
);
|
||||
const pinnedCommandMenuItems = commandMenuItemsDraft
|
||||
.filter(
|
||||
doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
|
||||
)
|
||||
.filter((item) => allowedAvailabilityTypes.has(item.availabilityType))
|
||||
.filter((item) => item.isPinned);
|
||||
|
||||
const {
|
||||
pinnedInlineCommandMenuItems,
|
||||
@@ -143,10 +100,15 @@ export const PinnedCommandMenuItemButtonsEditMode = () => {
|
||||
<NodeDimension onDimensionChange={onContainerDimensionChange}>
|
||||
<StyledContainer>
|
||||
<StyledItemsContainer>
|
||||
{pinnedInlineCommandMenuItems.map(
|
||||
(pinnedInlineCommandMenuItem) => (
|
||||
<StyledActionContainer
|
||||
key={pinnedInlineCommandMenuItem.key}
|
||||
{pinnedInlineCommandMenuItems.map((item) => {
|
||||
const { iconKey, label, shortLabel } =
|
||||
interpolateCommandMenuItemFields(item, commandMenuContextApi);
|
||||
|
||||
const Icon = getIcon(iconKey, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
return (
|
||||
<StyledCommandMenuItemContainer
|
||||
key={item.id}
|
||||
layout
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'unset', opacity: 1 }}
|
||||
@@ -156,10 +118,18 @@ export const PinnedCommandMenuItemButtonsEditMode = () => {
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
{pinnedInlineCommandMenuItem.component}
|
||||
</StyledActionContainer>
|
||||
),
|
||||
)}
|
||||
<CommandMenuButton
|
||||
command={{
|
||||
key: item.id,
|
||||
label,
|
||||
shortLabel,
|
||||
Icon,
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</StyledCommandMenuItemContainer>
|
||||
);
|
||||
})}
|
||||
</StyledItemsContainer>
|
||||
</StyledContainer>
|
||||
</NodeDimension>
|
||||
+15
-25
@@ -1,12 +1,13 @@
|
||||
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 { 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 { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/server-items/edit/hooks/useUpdateCommandMenuItemInDraft';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
|
||||
import { useCommandMenuContextApi } from '@/command-menu-item/hooks/useCommandMenuContextApi';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { groupCommandMenuItems } from '@/command-menu-item/utils/groupCommandMenuItems';
|
||||
import { CommandMenuItemEditRecordSelectionDropdown } from '@/command-menu-item/edit/components/CommandMenuItemEditRecordSelectionDropdown';
|
||||
import { CommandMenuItemOptionsDropdown } from '@/command-menu-item/edit/components/CommandMenuItemOptionsDropdown';
|
||||
import { useReorderCommandMenuItemsInDraft } from '@/command-menu-item/edit/hooks/useReorderCommandMenuItemsInDraft';
|
||||
import { useResetCommandMenuItemsDraft } from '@/command-menu-item/edit/hooks/useResetCommandMenuItemsDraft';
|
||||
import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/edit/hooks/useUpdateCommandMenuItemInDraft';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { mainContextStoreHasSelectedRecordsSelector } from '@/context-store/states/selectors/mainContextStoreHasSelectedRecordsSelector';
|
||||
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
@@ -41,17 +42,6 @@ import {
|
||||
} 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;
|
||||
@@ -112,7 +102,8 @@ export const SidePanelCommandMenuItemEditPage = () => {
|
||||
.filter(
|
||||
doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
|
||||
)
|
||||
.filter((item) => allowedAvailabilityTypes.has(item.availabilityType));
|
||||
.filter((item) => allowedAvailabilityTypes.has(item.availabilityType))
|
||||
.sort((firstItem, secondItem) => firstItem.position - secondItem.position);
|
||||
|
||||
const filteredCommandMenuItemIds = new Set(
|
||||
filteredCommandMenuItems.map((item) => item.id),
|
||||
@@ -124,9 +115,8 @@ export const SidePanelCommandMenuItemEditPage = () => {
|
||||
context: commandMenuContextApi,
|
||||
}) ?? item.label;
|
||||
|
||||
const { pinned: allPinnedItems, other: allOtherItems } = partitionByPinned(
|
||||
filteredCommandMenuItems,
|
||||
);
|
||||
const { pinned: allPinnedItems, other: allOtherItems } =
|
||||
groupCommandMenuItems(filteredCommandMenuItems);
|
||||
|
||||
const normalizedSearch =
|
||||
sidePanelSearch.length > 0
|
||||
@@ -247,7 +237,7 @@ export const SidePanelCommandMenuItemEditPage = () => {
|
||||
/>
|
||||
</StyledViewbar>
|
||||
<StyledContent>
|
||||
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
<SidePanelList selectableItemIds={selectableItemIds}>
|
||||
<SidePanelGroup heading={t`Pinned`}>
|
||||
<DraggableList
|
||||
onDragEnd={handlePinnedDragEnd}
|
||||
+3
-3
@@ -2,9 +2,9 @@ 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';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { computeInsertPositionFromBounds } from '@/command-menu-item/edit/utils/computeInsertPositionFromBounds';
|
||||
import { getPositionBoundsAtInsertionPoint } from '@/command-menu-item/edit/utils/getPositionBoundsAtInsertionPoint';
|
||||
|
||||
export const useReorderCommandMenuItemsInDraft = () => {
|
||||
const store = useStore();
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
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';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
|
||||
// Resets the draft to the current server state, discarding all user edits.
|
||||
export const useResetCommandMenuItemsDraft = () => {
|
||||
+3
-3
@@ -3,9 +3,9 @@ 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 { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { UPDATE_COMMAND_MENU_ITEM } from '@/command-menu-item/graphql/mutations/updateCommandMenuItem';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type UpdateCommandMenuItemInput } from '~/generated-metadata/graphql';
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { getRecordIndexId } from '@/command-menu-item/server-items/edit/utils/getRecordIndexId';
|
||||
import { getRecordIndexId } from '@/command-menu-item/edit/utils/getRecordIndexId';
|
||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||
import { useResetRecordIndexSelection } from '@/object-record/record-index/hooks/useResetRecordIndexSelection';
|
||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||
+2
-2
@@ -2,8 +2,8 @@ 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';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { type CommandMenuItemEditableFields } from '@/command-menu-item/edit/types/CommandMenuItemEditableFields';
|
||||
|
||||
export const useUpdateCommandMenuItemInDraft = () => {
|
||||
const store = useStore();
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { computeInsertPositionFromBounds } from '@/command-menu-item/server-items/edit/utils/computeInsertPositionFromBounds';
|
||||
import { computeInsertPositionFromBounds } from '@/command-menu-item/edit/utils/computeInsertPositionFromBounds';
|
||||
|
||||
describe('computeInsertPositionFromBounds', () => {
|
||||
it('returns midpoint when both bounds are defined', () => {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { getPositionBoundsAtInsertionPoint } from '@/command-menu-item/server-items/edit/utils/getPositionBoundsAtInsertionPoint';
|
||||
import { getPositionBoundsAtInsertionPoint } from '@/command-menu-item/edit/utils/getPositionBoundsAtInsertionPoint';
|
||||
|
||||
const makeItems = (positions: number[]) =>
|
||||
positions.map((position, index) => ({
|
||||
+26
-1
@@ -3,6 +3,7 @@ import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMe
|
||||
import { type CommandMenuItemContainerType } from '@/command-menu-item/types/CommandMenuItemContainerType';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { CommandMenuContextApiPageType } from 'twenty-shared/types';
|
||||
|
||||
const TEST_COMMAND_MENU_ID = 'test-cmd-menu-1';
|
||||
|
||||
@@ -40,9 +41,33 @@ const getWrapper =
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
containerType,
|
||||
isInSidePanel,
|
||||
displayType: 'button',
|
||||
commandMenuItems: [],
|
||||
commandMenuContextApi: {
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
isInSidePanel,
|
||||
isPageInEditMode: false,
|
||||
favoriteRecordIds: [],
|
||||
isSelectAll: false,
|
||||
hasAnySoftDeleteFilterOnView: false,
|
||||
numberOfSelectedRecords: 0,
|
||||
objectPermissions: {
|
||||
canReadObjectRecords: false,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
objectMetadataId: '',
|
||||
rowLevelPermissionPredicates: [],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
},
|
||||
selectedRecords: [],
|
||||
featureFlags: {},
|
||||
targetObjectReadPermissions: {},
|
||||
targetObjectWritePermissions: {},
|
||||
objectMetadataItem: {},
|
||||
objectMetadataLabel: '',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { useIsCommandBlockedByGlobalLayoutCustomization } from '@/command-menu-item/hooks/useIsCommandBlockedByGlobalLayoutCustomization';
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { createStore, Provider as JotaiProvider } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import { CommandMenuItemViewType } from 'twenty-shared/types';
|
||||
import { Icon123 } from 'twenty-ui/display';
|
||||
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<JotaiProvider store={store}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
const buildCommandMenuItemConfig = (
|
||||
isAllowedDuringGlobalLayoutCustomization?: boolean,
|
||||
): CommandMenuItemConfig => ({
|
||||
type: CommandMenuItemType.Standard,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
key: 'test-command',
|
||||
label: 'Test Command',
|
||||
position: 1,
|
||||
Icon: Icon123,
|
||||
availableOn: [CommandMenuItemViewType.GLOBAL],
|
||||
shouldBeRegistered: () => true,
|
||||
component: null,
|
||||
isAllowedDuringGlobalLayoutCustomization,
|
||||
});
|
||||
|
||||
describe('useIsCommandBlockedByGlobalLayoutCustomization', () => {
|
||||
it('should not block commands when global layout customization is inactive', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
const commandMenuItemConfig = buildCommandMenuItemConfig(false);
|
||||
|
||||
store.set(isLayoutCustomizationModeEnabledState.atom, false);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useIsCommandBlockedByGlobalLayoutCustomization(commandMenuItemConfig),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should block commands by default when global layout customization is active', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
const commandMenuItemConfig = buildCommandMenuItemConfig();
|
||||
|
||||
store.set(isLayoutCustomizationModeEnabledState.atom, true);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useIsCommandBlockedByGlobalLayoutCustomization(commandMenuItemConfig),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow commands explicitly marked for global layout customization', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
const commandMenuItemConfig = buildCommandMenuItemConfig(true);
|
||||
|
||||
store.set(isLayoutCustomizationModeEnabledState.atom, true);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useIsCommandBlockedByGlobalLayoutCustomization(commandMenuItemConfig),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { getCommandMenuDropdownIdFromCommandMenuId } from '@/command-menu-item/utils/getCommandMenuDropdownIdFromCommandMenuId';
|
||||
import { getSidePanelCommandMenuDropdownIdFromCommandMenuId } from '@/command-menu-item/utils/getSidePanelCommandMenuDropdownIdFromCommandMenuId';
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCloseCommandMenu = ({
|
||||
closeSidePanelOnShowPageOptionsExecution = false,
|
||||
@@ -15,7 +14,9 @@ export const useCloseCommandMenu = ({
|
||||
closeSidePanelOnShowPageOptionsExecution?: boolean;
|
||||
closeSidePanelOnCommandMenuListExecution?: boolean;
|
||||
} = {}) => {
|
||||
const { containerType, isInSidePanel } = useContext(CommandMenuContext);
|
||||
const { containerType, commandMenuContextApi } =
|
||||
useContext(CommandMenuContext);
|
||||
const isInSidePanel = commandMenuContextApi.isInSidePanel;
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
@@ -31,10 +32,7 @@ export const useCloseCommandMenu = ({
|
||||
|
||||
const closeCommandMenu = () => {
|
||||
if (containerType === 'command-menu-list') {
|
||||
if (
|
||||
isDefined(closeSidePanelOnCommandMenuListExecution) &&
|
||||
!closeSidePanelOnCommandMenuListExecution
|
||||
) {
|
||||
if (!closeSidePanelOnCommandMenuListExecution) {
|
||||
return;
|
||||
}
|
||||
closeSidePanelMenu();
|
||||
@@ -49,7 +47,6 @@ export const useCloseCommandMenu = ({
|
||||
|
||||
if (
|
||||
containerType === 'command-menu-show-page-dropdown' &&
|
||||
isDefined(closeSidePanelOnShowPageOptionsExecution) &&
|
||||
closeSidePanelOnShowPageOptionsExecution
|
||||
) {
|
||||
closeSidePanelMenu();
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { useMountCommand } from '@/command-menu-item/engine-command/hooks/useMountCommand';
|
||||
import { isEngineCommandMountedFamilySelector } from '@/command-menu-item/engine-command/selectors/isEngineCommandMountedFamilySelector';
|
||||
import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
|
||||
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCommandMenuItemClick = ({
|
||||
item,
|
||||
Icon,
|
||||
label,
|
||||
}: {
|
||||
item: CommandMenuItemFieldsFragment;
|
||||
Icon: IconComponent;
|
||||
label: string;
|
||||
}) => {
|
||||
const { commandMenuContextApi } = useContext(CommandMenuContext);
|
||||
const mountCommand = useMountCommand();
|
||||
const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
|
||||
|
||||
const contextStoreInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
ContextStoreComponentInstanceContext,
|
||||
);
|
||||
|
||||
const isMounted = useAtomFamilySelectorValue(
|
||||
isEngineCommandMountedFamilySelector,
|
||||
item.id,
|
||||
);
|
||||
|
||||
const commandMenuItemProgress = useAtomFamilyStateValue(
|
||||
commandMenuItemProgressFamilyState,
|
||||
item.id,
|
||||
);
|
||||
|
||||
const isHeadless =
|
||||
isDefined(item.frontComponentId) &&
|
||||
item.frontComponent?.isHeadless === true;
|
||||
|
||||
const isEngineCommand =
|
||||
isDefined(item.engineComponentKey) && !isDefined(item.frontComponentId);
|
||||
|
||||
const isFrontComponent =
|
||||
isDefined(item.frontComponentId) &&
|
||||
item.frontComponent?.isHeadless !== true;
|
||||
|
||||
const shouldMountCommand = isHeadless || isEngineCommand;
|
||||
|
||||
const closeBehavior = shouldMountCommand
|
||||
? ({
|
||||
closeSidePanelOnShowPageOptionsExecution: false,
|
||||
closeSidePanelOnCommandMenuListExecution: false,
|
||||
} as const)
|
||||
: ({} as const);
|
||||
|
||||
const { closeCommandMenu } = useCloseCommandMenu(closeBehavior);
|
||||
|
||||
const disabled = shouldMountCommand ? isMounted : false;
|
||||
|
||||
const handleClick = async () => {
|
||||
if (shouldMountCommand) {
|
||||
if (isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeCommandMenu();
|
||||
|
||||
await mountCommand({
|
||||
engineCommandId: item.id,
|
||||
contextStoreInstanceId,
|
||||
engineComponentKey: item.engineComponentKey,
|
||||
frontComponentId: item.frontComponentId ?? undefined,
|
||||
workflowVersionId: item.workflowVersionId ?? undefined,
|
||||
availabilityType: item.availabilityType,
|
||||
availabilityObjectMetadataId: item.availabilityObjectMetadataId,
|
||||
payload: item.payload ?? undefined,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFrontComponent && isDefined(item.frontComponentId)) {
|
||||
const { selectedRecords, objectMetadataItem } = commandMenuContextApi;
|
||||
|
||||
const recordId =
|
||||
selectedRecords.length === 1 ? selectedRecords[0].id : undefined;
|
||||
|
||||
const objectNameSingular = objectMetadataItem.nameSingular as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
closeCommandMenu();
|
||||
|
||||
openFrontComponentInSidePanel({
|
||||
frontComponentId: item.frontComponentId,
|
||||
pageTitle: label,
|
||||
pageIcon: Icon,
|
||||
recordContext:
|
||||
isDefined(recordId) && isDefined(objectNameSingular)
|
||||
? { recordId, objectNameSingular }
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleClick,
|
||||
disabled,
|
||||
progress: shouldMountCommand ? commandMenuItemProgress : undefined,
|
||||
showDisabledLoader: shouldMountCommand ? isMounted : false,
|
||||
};
|
||||
};
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/server-items/common/states/commandMenuItemsSelector';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useIsCommandBlockedByGlobalLayoutCustomization = (
|
||||
commandMenuItemConfig: CommandMenuItemConfig | null,
|
||||
) => {
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
|
||||
if (!isLayoutCustomizationModeEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !commandMenuItemConfig?.isAllowedDuringGlobalLayoutCustomization;
|
||||
};
|
||||
+79
-96
@@ -1,98 +1,81 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { CommandMenuItemViewType, AppPath } from 'twenty-shared/types';
|
||||
import {
|
||||
IconFileExport,
|
||||
IconHeart,
|
||||
IconTrash,
|
||||
IconUser,
|
||||
} from 'twenty-ui/display';
|
||||
CommandMenuItemAvailabilityType,
|
||||
EngineComponentKey,
|
||||
type CommandMenuItemFieldsFragment,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { CommandLink } from '@/command-menu-item/display/components/CommandLink';
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const createMockCommandMenuItems = ({
|
||||
deleteMock = () => {},
|
||||
addToFavoritesMock = () => {},
|
||||
exportMock = () => {},
|
||||
}: {
|
||||
deleteMock?: () => void;
|
||||
addToFavoritesMock?: () => void;
|
||||
exportMock?: () => void;
|
||||
}): CommandMenuItemConfig[] => [
|
||||
{
|
||||
type: CommandMenuItemType.Standard,
|
||||
scope: CommandMenuItemScope.RecordSelection,
|
||||
key: EngineComponentKey.ADD_TO_FAVORITES,
|
||||
label: msg`Add to favorites`,
|
||||
shortLabel: msg`Add to favorites`,
|
||||
position: 2,
|
||||
isPinned: true,
|
||||
Icon: IconHeart,
|
||||
shouldBeRegistered: () => true,
|
||||
availableOn: [
|
||||
CommandMenuItemViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
CommandMenuItemViewType.SHOW_PAGE,
|
||||
],
|
||||
component: <Command onClick={addToFavoritesMock} />,
|
||||
},
|
||||
{
|
||||
type: CommandMenuItemType.Standard,
|
||||
scope: CommandMenuItemScope.RecordSelection,
|
||||
key: EngineComponentKey.EXPORT_FROM_RECORD_INDEX,
|
||||
label: msg`Export`,
|
||||
shortLabel: msg`Export`,
|
||||
position: 4,
|
||||
Icon: IconFileExport,
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
shouldBeRegistered: () => true,
|
||||
availableOn: [CommandMenuItemViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION],
|
||||
component: <Command onClick={exportMock} />,
|
||||
},
|
||||
{
|
||||
type: CommandMenuItemType.Standard,
|
||||
scope: CommandMenuItemScope.RecordSelection,
|
||||
key: EngineComponentKey.DELETE_SINGLE_RECORD,
|
||||
label: msg`Delete`,
|
||||
shortLabel: msg`Delete`,
|
||||
position: 7,
|
||||
Icon: IconTrash,
|
||||
accent: 'default',
|
||||
isPinned: true,
|
||||
shouldBeRegistered: () => true,
|
||||
availableOn: [
|
||||
CommandMenuItemViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
CommandMenuItemViewType.SHOW_PAGE,
|
||||
],
|
||||
component: <Command onClick={deleteMock} />,
|
||||
},
|
||||
{
|
||||
type: CommandMenuItemType.Navigation,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
key: EngineComponentKey.GO_TO_PEOPLE,
|
||||
label: msg`Go to People`,
|
||||
shortLabel: msg`People`,
|
||||
position: 19,
|
||||
Icon: IconUser,
|
||||
isPinned: false,
|
||||
availableOn: [
|
||||
CommandMenuItemViewType.INDEX_PAGE_NO_SELECTION,
|
||||
CommandMenuItemViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
CommandMenuItemViewType.INDEX_PAGE_BULK_SELECTION,
|
||||
CommandMenuItemViewType.SHOW_PAGE,
|
||||
],
|
||||
shouldBeRegistered: () => true,
|
||||
component: (
|
||||
<CommandLink
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.Person }}
|
||||
/>
|
||||
),
|
||||
hotKeys: ['G', 'P'],
|
||||
},
|
||||
];
|
||||
export const createMockCommandMenuItems =
|
||||
(): CommandMenuItemFieldsFragment[] => [
|
||||
{
|
||||
__typename: 'CommandMenuItem',
|
||||
id: 'mock-add-to-favorites',
|
||||
workflowVersionId: null,
|
||||
frontComponentId: null,
|
||||
frontComponent: null,
|
||||
engineComponentKey: EngineComponentKey.ADD_TO_FAVORITES,
|
||||
label: 'Add to favorites',
|
||||
icon: 'IconHeart',
|
||||
shortLabel: 'Add to favorites',
|
||||
position: 2,
|
||||
isPinned: true,
|
||||
hotKeys: null,
|
||||
conditionalAvailabilityExpression: null,
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
availabilityObjectMetadataId: null,
|
||||
payload: null,
|
||||
},
|
||||
{
|
||||
__typename: 'CommandMenuItem',
|
||||
id: 'mock-export',
|
||||
workflowVersionId: null,
|
||||
frontComponentId: null,
|
||||
frontComponent: null,
|
||||
engineComponentKey: EngineComponentKey.EXPORT_FROM_RECORD_INDEX,
|
||||
label: 'Export',
|
||||
icon: 'IconFileExport',
|
||||
shortLabel: 'Export',
|
||||
position: 4,
|
||||
isPinned: false,
|
||||
hotKeys: null,
|
||||
conditionalAvailabilityExpression: null,
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
availabilityObjectMetadataId: null,
|
||||
payload: null,
|
||||
},
|
||||
{
|
||||
__typename: 'CommandMenuItem',
|
||||
id: 'mock-delete',
|
||||
workflowVersionId: null,
|
||||
frontComponentId: null,
|
||||
frontComponent: null,
|
||||
engineComponentKey: EngineComponentKey.DELETE_SINGLE_RECORD,
|
||||
label: 'Delete',
|
||||
icon: 'IconTrash',
|
||||
shortLabel: 'Delete',
|
||||
position: 7,
|
||||
isPinned: true,
|
||||
hotKeys: null,
|
||||
conditionalAvailabilityExpression: null,
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
availabilityObjectMetadataId: null,
|
||||
payload: null,
|
||||
},
|
||||
{
|
||||
__typename: 'CommandMenuItem',
|
||||
id: 'mock-go-to-people',
|
||||
workflowVersionId: null,
|
||||
frontComponentId: null,
|
||||
frontComponent: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_PEOPLE,
|
||||
label: 'Go to People',
|
||||
icon: 'IconUser',
|
||||
shortLabel: 'People',
|
||||
position: 19,
|
||||
isPinned: false,
|
||||
hotKeys: ['G', 'P'],
|
||||
conditionalAvailabilityExpression: null,
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
availabilityObjectMetadataId: null,
|
||||
payload: null,
|
||||
},
|
||||
];
|
||||
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CommandMenuContextType } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { useCommandMenuContextApi } from '@/command-menu-item/server-items/common/hooks/useCommandMenuContextApi';
|
||||
|
||||
import { CommandMenuContextProviderServerItemsContent } from './CommandMenuContextProviderServerItemsContent';
|
||||
import { CommandMenuContextProviderServerItemsWithWorkflowEnrichment } from './CommandMenuContextProviderServerItemsWithWorkflowEnrichment';
|
||||
|
||||
type CommandMenuContextProviderServerItemsProps = {
|
||||
isInSidePanel: CommandMenuContextType['isInSidePanel'];
|
||||
displayType: CommandMenuContextType['displayType'];
|
||||
containerType: CommandMenuContextType['containerType'];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuContextProviderServerItems = ({
|
||||
isInSidePanel,
|
||||
displayType,
|
||||
containerType,
|
||||
children,
|
||||
}: CommandMenuContextProviderServerItemsProps) => {
|
||||
const commandMenuContextApiFromHook = useCommandMenuContextApi();
|
||||
|
||||
// SidePanelRecordPage shadows the outer ContextStore provider with a
|
||||
// per-page instance ID, so useCommandMenuContextApi derives isInSidePanel
|
||||
// as false. The explicit prop from the caller is the source of truth.
|
||||
const commandMenuContextApi = isInSidePanel
|
||||
? { ...commandMenuContextApiFromHook, isInSidePanel: true as const }
|
||||
: commandMenuContextApiFromHook;
|
||||
|
||||
const currentObjectNameSingular =
|
||||
commandMenuContextApi.objectMetadataItem.nameSingular;
|
||||
|
||||
const isWorkflow =
|
||||
currentObjectNameSingular === CoreObjectNameSingular.Workflow;
|
||||
|
||||
const selectedWorkflowRecordIds = isWorkflow
|
||||
? commandMenuContextApi.selectedRecords
|
||||
.map((record) => record.id)
|
||||
.filter(isDefined)
|
||||
: [];
|
||||
|
||||
if (selectedWorkflowRecordIds.length > 0) {
|
||||
return (
|
||||
<CommandMenuContextProviderServerItemsWithWorkflowEnrichment
|
||||
isInSidePanel={isInSidePanel}
|
||||
displayType={displayType}
|
||||
containerType={containerType}
|
||||
commandMenuContextApi={commandMenuContextApi}
|
||||
selectedWorkflowRecordIds={selectedWorkflowRecordIds}
|
||||
>
|
||||
{children}
|
||||
</CommandMenuContextProviderServerItemsWithWorkflowEnrichment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuContextProviderServerItemsContent
|
||||
isInSidePanel={isInSidePanel}
|
||||
displayType={displayType}
|
||||
containerType={containerType}
|
||||
commandMenuContextApi={commandMenuContextApi}
|
||||
>
|
||||
{children}
|
||||
</CommandMenuContextProviderServerItemsContent>
|
||||
);
|
||||
};
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
CommandMenuContext,
|
||||
type CommandMenuContextType,
|
||||
} from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { useCommandMenuItemsFromBackend } from '@/command-menu-item/server-items/common/hooks/useCommandMenuItemsFromBackend';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
|
||||
type CommandMenuContextProviderServerItemsContentProps = {
|
||||
isInSidePanel: CommandMenuContextType['isInSidePanel'];
|
||||
displayType: CommandMenuContextType['displayType'];
|
||||
containerType: CommandMenuContextType['containerType'];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuContextProviderServerItemsContent = ({
|
||||
isInSidePanel,
|
||||
displayType,
|
||||
containerType,
|
||||
children,
|
||||
commandMenuContextApi,
|
||||
}: CommandMenuContextProviderServerItemsContentProps & {
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
}) => {
|
||||
const commandMenuItems = useCommandMenuItemsFromBackend(
|
||||
commandMenuContextApi,
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
isInSidePanel,
|
||||
displayType,
|
||||
containerType,
|
||||
commandMenuItems,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CommandMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
import { useCommandMenuItemsFromBackend } from '@/command-menu-item/server-items/common/hooks/useCommandMenuItemsFromBackend';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import {
|
||||
CommandMenuContextApiPageType,
|
||||
type CommandMenuContextApi,
|
||||
} from 'twenty-shared/types';
|
||||
import { Icon123 } from 'twenty-ui/display';
|
||||
import {
|
||||
CommandMenuItemAvailabilityType,
|
||||
type CommandMenuItemFieldsFragment,
|
||||
EngineComponentKey,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
jest.mock('@/ui/utilities/state/jotai/hooks/useAtomStateValue');
|
||||
jest.mock('twenty-ui/display', () => ({
|
||||
...jest.requireActual('twenty-ui/display'),
|
||||
useIcons: () => ({
|
||||
getIcon: () => Icon123,
|
||||
}),
|
||||
}));
|
||||
jest.mock('twenty-shared/utils', () => {
|
||||
const actual = jest.requireActual('twenty-shared/utils');
|
||||
|
||||
return {
|
||||
...actual,
|
||||
interpolateCommandMenuItemTemplate: jest.fn(
|
||||
({ label }: { label?: string | null }) => label ?? null,
|
||||
),
|
||||
evaluateConditionalAvailabilityExpression: jest.fn(
|
||||
(expression?: string | null) => expression !== 'hide',
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const mockedUseAtomStateValue = jest.mocked(useAtomStateValue);
|
||||
|
||||
const getCommandMenuContextApi = (
|
||||
numberOfSelectedRecords: number,
|
||||
): CommandMenuContextApi => ({
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
isInSidePanel: false,
|
||||
isPageInEditMode: false,
|
||||
favoriteRecordIds: [],
|
||||
isSelectAll: false,
|
||||
hasAnySoftDeleteFilterOnView: false,
|
||||
objectMetadataItem: {
|
||||
id: 'company-id',
|
||||
},
|
||||
objectMetadataLabel: 'Company',
|
||||
numberOfSelectedRecords,
|
||||
objectPermissions: {
|
||||
objectMetadataId: 'company-id',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
rowLevelPermissionPredicates: [],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
},
|
||||
selectedRecords: [],
|
||||
featureFlags: {},
|
||||
targetObjectReadPermissions: {},
|
||||
targetObjectWritePermissions: {},
|
||||
});
|
||||
|
||||
const buildCommandMenuItem = ({
|
||||
id,
|
||||
position,
|
||||
availabilityType,
|
||||
expression,
|
||||
isPinned = true,
|
||||
}: {
|
||||
id: string;
|
||||
position: number;
|
||||
availabilityType: CommandMenuItemAvailabilityType;
|
||||
expression?: string;
|
||||
isPinned?: boolean;
|
||||
}): CommandMenuItemFieldsFragment => ({
|
||||
id,
|
||||
label: id,
|
||||
shortLabel: `${id}-short`,
|
||||
icon: 'Icon123',
|
||||
position,
|
||||
isPinned,
|
||||
hotKeys: null,
|
||||
engineComponentKey: EngineComponentKey.CREATE_NEW_RECORD,
|
||||
frontComponentId: null,
|
||||
workflowVersionId: null,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId: 'company-id',
|
||||
conditionalAvailabilityExpression: expression ?? null,
|
||||
});
|
||||
|
||||
describe('useCommandMenuItemsFromBackend', () => {
|
||||
it('filters out items hidden by conditional availability expression', () => {
|
||||
mockedUseAtomStateValue.mockReturnValue([
|
||||
buildCommandMenuItem({
|
||||
id: 'global-visible',
|
||||
position: 1,
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
}),
|
||||
buildCommandMenuItem({
|
||||
id: 'global-hidden',
|
||||
position: 2,
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
expression: 'hide',
|
||||
}),
|
||||
buildCommandMenuItem({
|
||||
id: 'record-visible',
|
||||
position: 3,
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
}),
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommandMenuItemsFromBackend(getCommandMenuContextApi(0)),
|
||||
);
|
||||
|
||||
expect(result.current.map((item) => item.id)).toEqual(['global-visible']);
|
||||
});
|
||||
|
||||
it('maps global, record selection and fallback items to V2 output', () => {
|
||||
mockedUseAtomStateValue.mockReturnValue([
|
||||
buildCommandMenuItem({
|
||||
id: 'global-item',
|
||||
position: 2,
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
}),
|
||||
buildCommandMenuItem({
|
||||
id: 'record-item',
|
||||
position: 1,
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
}),
|
||||
buildCommandMenuItem({
|
||||
id: 'fallback-item',
|
||||
position: 3,
|
||||
availabilityType: CommandMenuItemAvailabilityType.FALLBACK,
|
||||
}),
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommandMenuItemsFromBackend(getCommandMenuContextApi(1)),
|
||||
);
|
||||
|
||||
expect(result.current.map((item) => item.id)).toEqual([
|
||||
'record-item',
|
||||
'global-item',
|
||||
'fallback-item',
|
||||
]);
|
||||
expect(result.current[2].isPinned).toBe(false);
|
||||
});
|
||||
});
|
||||
-242
@@ -1,242 +0,0 @@
|
||||
import { FrontComponentCommandMenuItem } from '@/command-menu-item/display/components/FrontComponentCommandMenuItem';
|
||||
import { HeadlessCommandMenuItem } from '@/command-menu-item/display/components/HeadlessCommandMenuItem';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/server-items/common/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/server-items/common/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
import {
|
||||
evaluateConditionalAvailabilityExpression,
|
||||
interpolateCommandMenuItemTemplate,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||
import {
|
||||
CommandMenuItemAvailabilityType,
|
||||
type CommandMenuItemFieldsFragment,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type CommandMenuItemWithFrontComponent = CommandMenuItemFieldsFragment & {
|
||||
frontComponentId: string;
|
||||
conditionalAvailabilityExpression?: string | null;
|
||||
};
|
||||
|
||||
type CommandMenuItemWithSource = CommandMenuItemFieldsFragment & {
|
||||
conditionalAvailabilityExpression?: string | null;
|
||||
};
|
||||
|
||||
type BuildCommandMenuItemFromFrontComponentParams = {
|
||||
item: CommandMenuItemWithFrontComponent;
|
||||
type?: CommandMenuItemType;
|
||||
scope: CommandMenuItemScope;
|
||||
isPinned: boolean;
|
||||
getIcon: ReturnType<typeof useIcons>['getIcon'];
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
const buildCommandMenuItemFromFrontComponent = ({
|
||||
item,
|
||||
type = CommandMenuItemType.FrontComponent,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
}: BuildCommandMenuItemFromFrontComponentParams): CommandMenuItemConfig => {
|
||||
const displayLabel = interpolateCommandMenuItemTemplate({
|
||||
label: item.label,
|
||||
context: commandMenuContextApi,
|
||||
});
|
||||
|
||||
const displayShortLabel = interpolateCommandMenuItemTemplate({
|
||||
label: item.shortLabel,
|
||||
context: commandMenuContextApi,
|
||||
});
|
||||
|
||||
const interpolatedIcon = interpolateCommandMenuItemTemplate({
|
||||
label: item.icon,
|
||||
context: commandMenuContextApi,
|
||||
});
|
||||
|
||||
const Icon = getIcon(interpolatedIcon, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
const isHeadless = item.frontComponent?.isHeadless === true;
|
||||
|
||||
return {
|
||||
type,
|
||||
key: `command-menu-item-front-component-${item.id}`,
|
||||
id: item.id,
|
||||
scope,
|
||||
label: displayLabel,
|
||||
shortLabel: displayShortLabel,
|
||||
position: item.position,
|
||||
isPinned,
|
||||
Icon,
|
||||
hotKeys: item.hotKeys,
|
||||
component: isHeadless ? (
|
||||
<HeadlessCommandMenuItem item={item} />
|
||||
) : (
|
||||
<FrontComponentCommandMenuItem frontComponentId={item.frontComponentId} />
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
type BuildCommandMenuItemFromStandardKeyParams = {
|
||||
item: CommandMenuItemWithSource;
|
||||
type?: CommandMenuItemType;
|
||||
scope: CommandMenuItemScope;
|
||||
isPinned: boolean;
|
||||
getIcon: ReturnType<typeof useIcons>['getIcon'];
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
const buildCommandItemFromEngineKey = ({
|
||||
item,
|
||||
type = CommandMenuItemType.Standard,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
}: BuildCommandMenuItemFromStandardKeyParams): CommandMenuItemConfig => {
|
||||
const interpolatedIcon = interpolateCommandMenuItemTemplate({
|
||||
label: item.icon,
|
||||
context: commandMenuContextApi,
|
||||
});
|
||||
const Icon = getIcon(interpolatedIcon, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
return {
|
||||
type,
|
||||
key: `command-menu-item-engine-${item.id}`,
|
||||
id: item.id,
|
||||
scope,
|
||||
label: interpolateCommandMenuItemTemplate({
|
||||
label: item.label,
|
||||
context: commandMenuContextApi,
|
||||
}),
|
||||
shortLabel: interpolateCommandMenuItemTemplate({
|
||||
label: item.shortLabel,
|
||||
context: commandMenuContextApi,
|
||||
}),
|
||||
position: item.position,
|
||||
isPinned,
|
||||
Icon,
|
||||
hotKeys: item.hotKeys,
|
||||
component: <HeadlessCommandMenuItem item={item} />,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCommandMenuItemsFromBackend = (
|
||||
commandMenuContextApi: CommandMenuContextApi,
|
||||
): CommandMenuItemConfig[] => {
|
||||
const { getIcon } = useIcons();
|
||||
const currentObjectMetadataItemId =
|
||||
commandMenuContextApi.objectMetadataItem.id;
|
||||
|
||||
const hasRecordSelection = commandMenuContextApi.numberOfSelectedRecords >= 1;
|
||||
|
||||
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
|
||||
|
||||
const itemsWithObjectMatches = commandMenuItems.filter(
|
||||
doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
|
||||
);
|
||||
const availableItems = itemsWithObjectMatches.filter((item) =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
commandMenuContextApi,
|
||||
),
|
||||
);
|
||||
|
||||
const buildCommandMenuItem = ({
|
||||
item,
|
||||
scope,
|
||||
isPinned,
|
||||
typeOverride,
|
||||
}: {
|
||||
item: CommandMenuItemFieldsFragment;
|
||||
scope: CommandMenuItemScope;
|
||||
isPinned: boolean;
|
||||
typeOverride?: CommandMenuItemType;
|
||||
}): CommandMenuItemConfig | null => {
|
||||
if (isDefined(item.frontComponentId)) {
|
||||
return buildCommandMenuItemFromFrontComponent({
|
||||
item: item as CommandMenuItemWithFrontComponent,
|
||||
type: typeOverride,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(item.engineComponentKey)) {
|
||||
return buildCommandItemFromEngineKey({
|
||||
item,
|
||||
type: typeOverride,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const globalItems = availableItems.filter(
|
||||
(item) => item.availabilityType === CommandMenuItemAvailabilityType.GLOBAL,
|
||||
);
|
||||
|
||||
const recordScopedItems = availableItems.filter(
|
||||
(item) =>
|
||||
item.availabilityType ===
|
||||
CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
);
|
||||
|
||||
const fallbackItems = availableItems.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,
|
||||
].sort((a, b) => a.position - b.position);
|
||||
};
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
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 (
|
||||
<StyledHiddenMeasurementsContainer>
|
||||
{pinnedCommandMenuItems.map((pinnedCommandMenuItem) => (
|
||||
<NodeDimension
|
||||
key={pinnedCommandMenuItem.key}
|
||||
onDimensionChange={onPinnedCommandMenuItemDimensionChange(
|
||||
pinnedCommandMenuItem.key,
|
||||
)}
|
||||
>
|
||||
<CommandMenuButton
|
||||
command={{
|
||||
key: `${pinnedCommandMenuItem.key}-inline-measurement`,
|
||||
label: pinnedCommandMenuItem.label,
|
||||
shortLabel: pinnedCommandMenuItem.shortLabel,
|
||||
Icon: pinnedCommandMenuItem.Icon,
|
||||
isPrimaryCTA: pinnedCommandMenuItem.isPrimaryCTA,
|
||||
}}
|
||||
/>
|
||||
</NodeDimension>
|
||||
))}
|
||||
</StyledHiddenMeasurementsContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { type CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { type CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import {
|
||||
type CommandMenuItemViewType,
|
||||
type Nullable,
|
||||
} from 'twenty-shared/types';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { type MenuItemAccent } from 'twenty-ui/navigation';
|
||||
import { type PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type CommandMenuItemConfig = {
|
||||
type: CommandMenuItemType;
|
||||
scope: CommandMenuItemScope;
|
||||
key: string;
|
||||
id?: string;
|
||||
label: Nullable<MessageDescriptor | string>;
|
||||
shortLabel?: Nullable<MessageDescriptor | string>;
|
||||
description?: MessageDescriptor | string;
|
||||
position: number;
|
||||
Icon: IconComponent;
|
||||
isPinned?: boolean;
|
||||
isPrimaryCTA?: boolean;
|
||||
accent?: MenuItemAccent;
|
||||
availableOn?: CommandMenuItemViewType[];
|
||||
shouldBeRegistered?: () => boolean;
|
||||
component: React.ReactNode;
|
||||
hotKeys?: Nullable<string[]>;
|
||||
requiredPermissionFlag?: PermissionFlagType;
|
||||
isAllowedDuringGlobalLayoutCustomization?: boolean;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum CommandMenuItemScope {
|
||||
Global = 'Global',
|
||||
RecordSelection = 'RecordSelection',
|
||||
CreateRelatedRecord = 'CreateRelatedRecord',
|
||||
Object = 'Object',
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export enum CommandMenuItemType {
|
||||
Standard = 'Standard',
|
||||
WorkflowRun = 'WorkflowRun',
|
||||
FrontComponent = 'FrontComponent',
|
||||
Fallback = 'Fallback',
|
||||
Navigation = 'Navigation',
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { getCommandMenuDropdownIdFromCommandMenuId } from '@/command-menu-item/utils/getCommandMenuDropdownIdFromCommandMenuId';
|
||||
|
||||
describe('getCommandMenuDropdownIdFromCommandMenuId', () => {
|
||||
it('should return the correct action menu dropdown id', () => {
|
||||
it('should return the correct command menu dropdown id', () => {
|
||||
expect(getCommandMenuDropdownIdFromCommandMenuId('command-menu-id')).toBe(
|
||||
'command-menu-dropdown-command-menu-id',
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { getCommandMenuIdFromRecordIndexId } from '@/command-menu-item/utils/getCommandMenuIdFromRecordIndexId';
|
||||
|
||||
describe('getCommandMenuIdFromRecordIndexId', () => {
|
||||
it('should return the correct action menu id', () => {
|
||||
it('should return the correct command menu id', () => {
|
||||
expect(getCommandMenuIdFromRecordIndexId('record-index-id')).toBe(
|
||||
'command-menu-record-index-record-index-id',
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { getSidePanelCommandMenuDropdownIdFromCommandMenuId } from '@/command-menu-item/utils/getSidePanelCommandMenuDropdownIdFromCommandMenuId';
|
||||
|
||||
describe('getSidePanelCommandMenuDropdownIdFromCommandMenuId', () => {
|
||||
it('should return the side panel action menu dropdown id', () => {
|
||||
it('should return the side panel command menu dropdown id', () => {
|
||||
expect(
|
||||
getSidePanelCommandMenuDropdownIdFromCommandMenuId('command-menu-id'),
|
||||
).toBe('side-panel-command-menu-dropdown-command-menu-id');
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
export const groupCommandMenuItems = (
|
||||
items: CommandMenuItemFieldsFragment[],
|
||||
) => {
|
||||
const pinned: CommandMenuItemFieldsFragment[] = [];
|
||||
const other: CommandMenuItemFieldsFragment[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.isPinned) {
|
||||
pinned.push(item);
|
||||
} else {
|
||||
other.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return { pinned, other };
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
export enum CommandType {
|
||||
Navigate = 'Navigate',
|
||||
Create = 'Create',
|
||||
StandardAction = 'StandardAction',
|
||||
WorkflowRun = 'WorkflowRun',
|
||||
Fallback = 'Fallback',
|
||||
}
|
||||
|
||||
export enum CommandScope {
|
||||
Global = 'Global',
|
||||
RecordSelection = 'RecordSelection',
|
||||
Object = 'Object',
|
||||
}
|
||||
|
||||
export type Command = {
|
||||
id: string;
|
||||
to?: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
type?: CommandType;
|
||||
scope?: CommandScope;
|
||||
Icon?: IconComponent;
|
||||
hotKeys?: string[];
|
||||
onCommandClick?: () => void;
|
||||
};
|
||||
+2
-2
@@ -4,8 +4,8 @@ import { useCallback } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconPencil } from 'twenty-ui/display';
|
||||
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/server-items/common/states/commandMenuItemsSelector';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/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';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { useCommandMenuItemsDraftState } from '@/command-menu-item/server-items/common/hooks/useCommandMenuItemsDraftState';
|
||||
import { useCommandMenuItemsDraftState } from '@/command-menu-item/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';
|
||||
|
||||
+2
-2
@@ -1,5 +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 { useSaveCommandMenuItemsDraft } from '@/command-menu-item/edit/hooks/useSaveCommandMenuItemsDraft';
|
||||
import { useCommandMenuItemsDraftState } from '@/command-menu-item/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';
|
||||
|
||||
-1
@@ -42,7 +42,6 @@ export const SidePanelEditFolderPickerSubPage = () => {
|
||||
onSearchChange={setSearchValue}
|
||||
>
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ export const SidePanelEditLinkItemView = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
<SidePanelList selectableItemIds={selectableItemIds}>
|
||||
<SidePanelGroup heading={t`Customize`}>
|
||||
<TextInput
|
||||
fullWidth
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ export const SidePanelEditObjectViewBase = ({
|
||||
: effectiveColor;
|
||||
|
||||
return (
|
||||
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
<SidePanelList selectableItemIds={selectableItemIds}>
|
||||
{showColorOption &&
|
||||
isDefined(selectedItem) &&
|
||||
objectMetadataItem?.isSystem !== true && (
|
||||
|
||||
-2
@@ -162,7 +162,6 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
case NavigationMenuItemType.FOLDER:
|
||||
return (
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={[
|
||||
ADD_ITEM_TO_FOLDER_ACTION_ID,
|
||||
...getOrganizeActionsSelectableItemIds(false),
|
||||
@@ -203,7 +202,6 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
default:
|
||||
return (
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={getOrganizeActionsSelectableItemIds(
|
||||
canMoveToOtherFolder,
|
||||
)}
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export const SidePanelNewSidebarItemMainMenu = ({
|
||||
return (
|
||||
<SidePanelAddToNavigationDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
<SidePanelList selectableItemIds={selectableItemIds}>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div ref={innerRef} {...droppableProps}>
|
||||
<SidePanelGroup heading={t`Data`}>
|
||||
|
||||
-1
@@ -101,7 +101,6 @@ export const SidePanelNewSidebarItemRecordSubPage = () => {
|
||||
<SidePanelAddToNavigationDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
loading={recordSearchLoading}
|
||||
noResults={isEmpty}
|
||||
|
||||
-1
@@ -48,7 +48,6 @@ export const SidePanelNewSidebarItemViewObjectPickerSubView = ({
|
||||
onSearchChange={onSearchChange}
|
||||
>
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
|
||||
-1
@@ -98,7 +98,6 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({
|
||||
<SidePanelAddToNavigationDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
|
||||
-1
@@ -47,7 +47,6 @@ export const SidePanelNewSidebarItemViewSystemSubView = ({
|
||||
onSearchChange={onSearchChange}
|
||||
>
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
|
||||
-2
@@ -90,7 +90,6 @@ export const SidePanelObjectPickerSubView = ({
|
||||
<SidePanelAddToNavigationDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
@@ -105,7 +104,6 @@ export const SidePanelObjectPickerSubView = ({
|
||||
</SidePanelAddToNavigationDroppable>
|
||||
) : (
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
|
||||
-2
@@ -74,7 +74,6 @@ export const SidePanelSystemObjectPickerSubView = ({
|
||||
<SidePanelAddToNavigationDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
@@ -89,7 +88,6 @@ export const SidePanelSystemObjectPickerSubView = ({
|
||||
</SidePanelAddToNavigationDroppable>
|
||||
) : (
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
import { RecordIndexCommandMenu } from '@/command-menu-item/components/RecordIndexCommandMenu';
|
||||
import { CommandMenuItemMoreActionsButton } from '@/command-menu-item/server-items/display/components/CommandMenuItemMoreActionsButton';
|
||||
import { SidePanelToggleButton } from '@/side-panel/components/SidePanelToggleButton';
|
||||
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';
|
||||
@@ -77,9 +77,7 @@ export const RecordIndexPageHeader = () => {
|
||||
{isDefined(contextStoreCurrentViewId) && (
|
||||
<>
|
||||
<RecordIndexCommandMenu />
|
||||
{!isLayoutCustomizationModeEnabled && (
|
||||
<CommandMenuItemMoreActionsButton />
|
||||
)}
|
||||
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
|
||||
</>
|
||||
)}
|
||||
</PageHeader>
|
||||
|
||||
+23
-19
@@ -1,7 +1,6 @@
|
||||
import { RecordPageSidePanelCommandMenu } from '@/command-menu-item/components/RecordPageSidePanelCommandMenu';
|
||||
import { RecordShowSidePanelOpenRecordButton } from '@/command-menu-item/components/RecordShowSidePanelOpenRecordButton';
|
||||
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreTargetedRecordsEffect';
|
||||
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
@@ -10,10 +9,11 @@ import { usePageLayoutIdForRecord } from '@/page-layout/hooks/usePageLayoutIdFor
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
|
||||
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
|
||||
import { sidePanelWidgetFooterActionsState } from '@/ui/layout/side-panel/states/sidePanelWidgetFooterActionsState';
|
||||
import { styled } from '@linaria/react';
|
||||
import { sidePanelWidgetFooterCommandMenuItemsState } from '@/ui/layout/side-panel/states/sidePanelWidgetFooterCommandMenuItemsState';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
@@ -58,15 +58,17 @@ export const PageLayoutRecordPageRenderer = ({
|
||||
targetObjectNameSingular: targetRecordIdentifier.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const sidePanelWidgetFooterActions = useAtomStateValue(
|
||||
sidePanelWidgetFooterActionsState,
|
||||
const sidePanelWidgetFooterCommandMenuItems = useAtomStateValue(
|
||||
sidePanelWidgetFooterCommandMenuItemsState,
|
||||
);
|
||||
|
||||
const pinnedWidgetActions = sidePanelWidgetFooterActions.filter(
|
||||
(action) => action.isPinned !== false,
|
||||
);
|
||||
const pinnedWidgetCommandMenuItems =
|
||||
sidePanelWidgetFooterCommandMenuItems.filter(
|
||||
(commandMenuItem) => commandMenuItem.isPinned !== false,
|
||||
);
|
||||
|
||||
const hasPinnedWidgetActions = pinnedWidgetActions.length > 0;
|
||||
const hasPinnedWidgetCommandMenuItems =
|
||||
pinnedWidgetCommandMenuItems.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -115,18 +117,20 @@ export const PageLayoutRecordPageRenderer = ({
|
||||
<SidePanelFooter
|
||||
actions={[
|
||||
<RecordPageSidePanelCommandMenu key="options" />,
|
||||
...(hasPinnedWidgetActions
|
||||
? pinnedWidgetActions.map((action) => (
|
||||
...(hasPinnedWidgetCommandMenuItems
|
||||
? pinnedWidgetCommandMenuItems.map((commandMenuItem) => (
|
||||
<Button
|
||||
key={action.key}
|
||||
key={commandMenuItem.id}
|
||||
size="small"
|
||||
variant={action.isPrimaryCTA ? 'primary' : 'secondary'}
|
||||
accent={action.isPrimaryCTA ? 'blue' : 'default'}
|
||||
title={action.label}
|
||||
Icon={action.Icon}
|
||||
hotkeys={action.hotkeys}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled}
|
||||
variant={
|
||||
commandMenuItem.isPrimaryCTA ? 'primary' : 'secondary'
|
||||
}
|
||||
accent={commandMenuItem.isPrimaryCTA ? 'blue' : 'default'}
|
||||
title={commandMenuItem.label}
|
||||
Icon={commandMenuItem.Icon}
|
||||
hotkeys={commandMenuItem.hotkeys}
|
||||
onClick={commandMenuItem.onClick}
|
||||
disabled={commandMenuItem.disabled}
|
||||
/>
|
||||
))
|
||||
: [
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { EMPTY_COMMAND_MENU_CONTEXT_API } from '@/command-menu-item/constants/EmptyCommandMenuContextApi';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
|
||||
@@ -78,7 +79,7 @@ const meta: Meta<typeof UpdateMultipleRecordsContainer> = {
|
||||
commandMenuItems: [],
|
||||
containerType: 'index-page-dropdown',
|
||||
displayType: 'dropdownItem',
|
||||
isInSidePanel: true,
|
||||
commandMenuContextApi: EMPTY_COMMAND_MENU_CONTEXT_API,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
|
||||
+11
-11
@@ -5,8 +5,8 @@ import { EmailComposerFields } from '@/activities/emails/components/EmailCompose
|
||||
import { useEmailComposerState } from '@/activities/emails/hooks/useEmailComposerState';
|
||||
import { type ReplyContextReady } from '@/activities/emails/hooks/useReplyContext';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { sidePanelWidgetFooterActionsState } from '@/ui/layout/side-panel/states/sidePanelWidgetFooterActionsState';
|
||||
import { type SidePanelFooterAction } from '@/ui/layout/side-panel/types/SidePanelFooterAction';
|
||||
import { sidePanelWidgetFooterCommandMenuItemsState } from '@/ui/layout/side-panel/states/sidePanelWidgetFooterCommandMenuItemsState';
|
||||
import { type SidePanelFooterCommandMenuItem } from '@/ui/layout/side-panel/types/SidePanelFooterCommandMenuItem';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -55,15 +55,15 @@ export const EmailThreadComposer = ({
|
||||
onSent: handleReplySent,
|
||||
});
|
||||
|
||||
const setSidePanelWidgetFooterActions = useSetAtomState(
|
||||
sidePanelWidgetFooterActionsState,
|
||||
const setSidePanelWidgetFooterCommandMenuItems = useSetAtomState(
|
||||
sidePanelWidgetFooterCommandMenuItemsState,
|
||||
);
|
||||
|
||||
const footerActions = useMemo((): SidePanelFooterAction[] => {
|
||||
const footerActions = useMemo((): SidePanelFooterCommandMenuItem[] => {
|
||||
if (!isComposerOpen) {
|
||||
return [
|
||||
{
|
||||
key: 'reply',
|
||||
id: 'reply',
|
||||
label: t`Reply`,
|
||||
Icon: IconArrowBackUp,
|
||||
isPrimaryCTA: true,
|
||||
@@ -74,14 +74,14 @@ export const EmailThreadComposer = ({
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'cancel-reply',
|
||||
id: 'cancel-reply',
|
||||
label: t`Cancel reply`,
|
||||
Icon: IconX,
|
||||
isPinned: false,
|
||||
onClick: () => setIsComposerOpen(false),
|
||||
},
|
||||
{
|
||||
key: 'send',
|
||||
id: 'send',
|
||||
label: t`Send`,
|
||||
Icon: IconSend,
|
||||
isPrimaryCTA: true,
|
||||
@@ -102,10 +102,10 @@ export const EmailThreadComposer = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setSidePanelWidgetFooterActions(footerActions);
|
||||
setSidePanelWidgetFooterCommandMenuItems(footerActions);
|
||||
|
||||
return () => setSidePanelWidgetFooterActions([]);
|
||||
}, [isInSidePanel, footerActions, setSidePanelWidgetFooterActions]);
|
||||
return () => setSidePanelWidgetFooterCommandMenuItems([]);
|
||||
}, [isInSidePanel, footerActions, setSidePanelWidgetFooterCommandMenuItems]);
|
||||
|
||||
const handleSendHotkey = useCallback(() => {
|
||||
if (isComposerOpen && composerState.canSend) {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { CommandMenuItemComponent } from '@/command-menu-item/display/components/CommandMenuItemComponent';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelDefaultSelectionEffect } from '@/side-panel/components/SidePanelDefaultSelectionEffect';
|
||||
import { SIDE_PANEL_SELECTABLE_LIST_ID } from '@/side-panel/constants/SidePanelSelectableListId';
|
||||
import { SIDE_PANEL_TOP_BAR_HEIGHT } from '@/side-panel/constants/SidePanelTopBarHeight';
|
||||
import { SIDE_PANEL_LIST_PADDING } from '@/side-panel/constants/SidePanelListPadding';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
|
||||
import { type SidePanelCommandMenuItemGroupConfig } from '@/side-panel/types/SidePanelCommandMenuItemGroupConfig';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
@@ -15,9 +12,8 @@ import { t } from '@lingui/core/macro';
|
||||
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export type SidePanelListProps = {
|
||||
commandGroups: SidePanelCommandMenuItemGroupConfig[];
|
||||
selectableItemIds: string[];
|
||||
children?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
loading?: boolean;
|
||||
noResults?: boolean;
|
||||
noResultsText?: string;
|
||||
@@ -54,7 +50,6 @@ const StyledEmpty = styled.div`
|
||||
`;
|
||||
|
||||
export const SidePanelList = ({
|
||||
commandGroups,
|
||||
selectableItemIds,
|
||||
children,
|
||||
loading = false,
|
||||
@@ -79,15 +74,6 @@ export const SidePanelList = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{commandGroups.map(({ heading, items }) =>
|
||||
items?.length ? (
|
||||
<SidePanelGroup heading={heading} key={heading}>
|
||||
{items.map((item) => (
|
||||
<CommandMenuItemComponent action={item} key={item.key} />
|
||||
))}
|
||||
</SidePanelGroup>
|
||||
) : null,
|
||||
)}
|
||||
{noResults && !loading && (
|
||||
<StyledEmpty>{noResultsText ?? t`No results found`}</StyledEmpty>
|
||||
)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user