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