8978 add navigation inside the command menu for showpage (#9103)

Closes #8978

- Added new options in the actions config files: `shortLabel`,
`availableOn`
- Added two actions: Navigate to previous records and Navigate to next
records
- Modified `useRecordShowPagePagination` to loop on records when we are
on first record and we hit previous or when we are on last record and we
hit next
- Introduced a new component state
`contextStoreCurrentViewTypeComponentState`
This commit is contained in:
Raphaël Bosi
2024-12-17 17:48:12 +01:00
committed by GitHub
parent bb8c763f9c
commit b033a50d7c
22 changed files with 529 additions and 147 deletions

View File

@ -1,9 +1,12 @@
import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect';
import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect';
import { ShowPageSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/ShowPageSingleRecordActionMenuEntrySetterEffect';
import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect';
import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -46,6 +49,10 @@ const ActionEffects = ({
contextStoreTargetedRecordsRuleComponentState,
);
const contextStoreCurrentViewType = useRecoilComponentValueV2(
contextStoreCurrentViewTypeComponentState,
);
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
return (
@ -59,9 +66,17 @@ const ActionEffects = ({
{contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
<>
<SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
{contextStoreCurrentViewType === ContextStoreViewType.ShowPage && (
<ShowPageSingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
{(contextStoreCurrentViewType === ContextStoreViewType.Table ||
contextStoreCurrentViewType === ContextStoreViewType.Kanban) && (
<SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
{isWorkflowEnabled && (
<WorkflowRunRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}

View File

@ -0,0 +1,76 @@
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useEffect } from 'react';
import { isDefined } from 'twenty-ui';
export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const isPageHeaderV2Enabled = useIsFeatureEnabled(
'IS_PAGE_HEADER_V2_ENABLED',
);
const actionConfig = getActionConfig(
objectMetadataItem,
isPageHeaderV2Enabled,
);
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const selectedRecordId =
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
: undefined;
if (!isDefined(selectedRecordId)) {
throw new Error('Selected record ID is required');
}
const actionMenuEntries = Object.values(actionConfig ?? {})
.filter((action) =>
action.availableOn?.includes(ActionAvailableOn.SHOW_PAGE),
)
.map((action) => {
const { shouldBeRegistered, onClick, ConfirmationModal } =
action.actionHook({
recordId: selectedRecordId,
objectMetadataItem,
});
if (shouldBeRegistered) {
return {
...action,
onClick,
ConfirmationModal,
};
}
return undefined;
})
.filter(isDefined);
useEffect(() => {
for (const action of actionMenuEntries) {
addActionMenuEntry(action);
}
return () => {
for (const action of actionMenuEntries) {
removeActionMenuEntry(action.key);
}
};
}, [actionMenuEntries, addActionMenuEntry, removeActionMenuEntry]);
return null;
};

View File

@ -1,4 +1,5 @@
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@ -37,6 +38,11 @@ export const SingleRecordActionMenuEntrySetterEffect = ({
}
const actionMenuEntries = Object.values(actionConfig ?? {})
.filter((action) =>
action.availableOn?.includes(
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
),
)
.map((action) => {
const { shouldBeRegistered, onClick, ConfirmationModal } =
action.actionHook({

View File

@ -1,6 +1,7 @@
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction';
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import {
ActionMenuEntry,
@ -22,6 +23,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
label: 'Add to favorites',
position: 0,
Icon: IconHeart,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
@ -31,6 +36,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
label: 'Remove from favorites',
position: 1,
Icon: IconHeartOff,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useRemoveFromFavoritesSingleRecordAction,
},
deleteSingleRecord: {
@ -42,6 +51,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
Icon: IconTrash,
accent: 'danger',
isPinned: true,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDeleteSingleRecordAction,
},
};

View File

@ -1,13 +1,22 @@
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction';
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { IconHeart, IconHeartOff, IconTrash } from 'twenty-ui';
import {
IconChevronDown,
IconChevronUp,
IconHeart,
IconHeartOff,
IconTrash,
} from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
string,
@ -20,9 +29,14 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
scope: ActionMenuEntryScope.RecordSelection,
key: 'add-to-favorites-single-record',
label: 'Add to favorites',
shortLabel: 'Add to favorites',
position: 0,
isPinned: true,
Icon: IconHeart,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
@ -30,20 +44,54 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
scope: ActionMenuEntryScope.RecordSelection,
key: 'remove-from-favorites-single-record',
label: 'Remove from favorites',
shortLabel: 'Remove from favorites',
isPinned: true,
position: 1,
Icon: IconHeartOff,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useRemoveFromFavoritesSingleRecordAction,
},
deleteSingleRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'delete-single-record',
label: 'Delete',
label: 'Delete record',
shortLabel: 'Delete',
position: 2,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useDeleteSingleRecordAction,
},
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-previous-record',
label: 'Navigate to previous record',
shortLabel: '',
position: 3,
isPinned: true,
Icon: IconChevronUp,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-next-record',
label: 'Navigate to next record',
shortLabel: '',
position: 4,
isPinned: true,
Icon: IconChevronDown,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
},
};

View File

@ -0,0 +1,15 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
export const useNavigateToNextRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const { navigateToNextRecord } = useRecordShowPagePagination(
objectMetadataItem.nameSingular,
recordId,
);
return {
shouldBeRegistered: true,
onClick: navigateToNextRecord,
};
};

View File

@ -0,0 +1,15 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
export const useNavigateToPreviousRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const { navigateToPreviousRecord } = useRecordShowPagePagination(
objectMetadataItem.nameSingular,
recordId,
);
return {
shouldBeRegistered: true,
onClick: navigateToPreviousRecord,
};
};

View File

@ -1,3 +1,5 @@
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
import { useActivateDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateDraftWorkflowSingleRecordAction';
import { useActivateLastPublishedVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateLastPublishedVersionWorkflowSingleRecordAction';
import { useDeactivateWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowSingleRecordAction';
@ -6,6 +8,7 @@ import { useSeeActiveVersionWorkflowSingleRecordAction } from '@/action-menu/act
import { useSeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeRunsWorkflowSingleRecordAction';
import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction';
import { useTestWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import {
ActionMenuEntry,
@ -13,6 +16,8 @@ import {
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import {
IconChevronDown,
IconChevronUp,
IconHistory,
IconHistoryToggle,
IconPlayerPause,
@ -30,81 +35,143 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
activateWorkflowDraftSingleRecord: {
key: 'activate-workflow-draft-single-record',
label: 'Activate Draft',
shortLabel: 'Activate Draft',
isPinned: true,
position: 1,
Icon: IconPower,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateDraftWorkflowSingleRecordAction,
},
activateWorkflowLastPublishedVersionSingleRecord: {
key: 'activate-workflow-last-published-version-single-record',
label: 'Activate last published version',
shortLabel: 'Activate last version',
isPinned: true,
position: 2,
Icon: IconPower,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
},
deactivateWorkflowSingleRecord: {
key: 'deactivate-workflow-single-record',
label: 'Deactivate Workflow',
shortLabel: 'Deactivate',
isPinned: true,
position: 3,
Icon: IconPlayerPause,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDeactivateWorkflowSingleRecordAction,
},
discardWorkflowDraftSingleRecord: {
key: 'discard-workflow-draft-single-record',
label: 'Discard Draft',
shortLabel: 'Discard Draft',
isPinned: true,
position: 4,
Icon: IconTrash,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDiscardDraftWorkflowSingleRecordAction,
},
seeWorkflowActiveVersionSingleRecord: {
key: 'see-workflow-active-version-single-record',
label: 'See active version',
shortLabel: 'See active version',
isPinned: false,
position: 5,
Icon: IconHistory,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
},
seeWorkflowRunsSingleRecord: {
key: 'see-workflow-runs-single-record',
label: 'See runs',
shortLabel: 'See runs',
isPinned: false,
position: 6,
Icon: IconHistoryToggle,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeRunsWorkflowSingleRecordAction,
},
seeWorkflowVersionsHistorySingleRecord: {
key: 'see-workflow-versions-history-single-record',
label: 'See versions history',
shortLabel: 'See versions',
isPinned: false,
position: 7,
Icon: IconHistory,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeVersionsWorkflowSingleRecordAction,
},
testWorkflowSingleRecord: {
key: 'test-workflow-single-record',
label: 'Test Workflow',
shortLabel: 'Test',
isPinned: true,
position: 8,
Icon: IconPlayerPlay,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useTestWorkflowSingleRecordAction,
},
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-previous-record',
label: 'Navigate to previous workflow',
shortLabel: '',
position: 9,
Icon: IconChevronUp,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-next-record',
label: 'Navigate to next workflow',
shortLabel: '',
position: 10,
Icon: IconChevronDown,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
},
};

View File

@ -1,13 +1,22 @@
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
import { useSeeExecutionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeExecutionsWorkflowVersionSingleRecordAction';
import { useSeeVersionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeVersionsWorkflowVersionSingleRecordAction';
import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { IconHistory, IconHistoryToggle, IconPencil } from 'twenty-ui';
import {
IconChevronDown,
IconChevronUp,
IconHistory,
IconHistoryToggle,
IconPencil,
} from 'twenty-ui';
export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
string,
@ -23,6 +32,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconPencil,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
},
seeWorkflowExecutionsSingleRecord: {
@ -32,6 +45,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistoryToggle,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeExecutionsWorkflowVersionSingleRecordAction,
},
seeWorkflowVersionsHistorySingleRecord: {
@ -41,6 +58,32 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistory,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction,
},
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-previous-record',
label: 'Navigate to previous version',
shortLabel: '',
position: 9,
Icon: IconChevronUp,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-next-record',
label: 'Navigate to next version',
shortLabel: '',
position: 10,
Icon: IconChevronDown,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
},
};

View File

@ -0,0 +1,6 @@
export enum ActionAvailableOn {
INDEX_PAGE_BULK_SELECTION = 'INDEX_PAGE_BULK_SELECTION',
INDEX_PAGE_SINGLE_RECORD_SELECTION = 'INDEX_PAGE_SINGLE_RECORD_SELECTION',
INDEX_PAGE_NO_SELECTION = 'INDEX_PAGE_NO_SELECTION',
SHOW_PAGE = 'SHOW_PAGE',
}

View File

@ -1,7 +1,7 @@
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Button, useIsMobile } from 'twenty-ui';
import { Button, IconButton, useIsMobile } from 'twenty-ui';
export const RecordShowActionMenuButtons = () => {
const actionMenuEntries = useRecoilComponentValueV2(
@ -15,18 +15,29 @@ export const RecordShowActionMenuButtons = () => {
return (
<>
{!isMobile &&
pinnedEntries.map((entry, index) => (
<Button
key={index}
Icon={entry.Icon}
size="small"
variant="secondary"
accent="default"
title={entry.label}
onClick={() => entry.onClick?.()}
ariaLabel={entry.label}
/>
))}
pinnedEntries.map((entry, index) =>
entry.shortLabel ? (
<Button
key={index}
Icon={entry.Icon}
size="small"
variant="secondary"
accent="default"
title={entry.shortLabel}
onClick={() => entry.onClick?.()}
ariaLabel={entry.label}
/>
) : (
<IconButton
Icon={entry.Icon}
size="small"
variant="secondary"
accent="default"
onClick={() => entry.onClick?.()}
ariaLabel={entry.label}
/>
),
)}
<PageHeaderOpenCommandMenuButton key="more" />
</>
);

View File

@ -1,3 +1,4 @@
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { MouseEvent, ReactElement } from 'react';
import { IconComponent, MenuItemAccent } from 'twenty-ui';
@ -16,10 +17,12 @@ export type ActionMenuEntry = {
scope: ActionMenuEntryScope;
key: string;
label: string;
shortLabel?: string;
position: number;
Icon: IconComponent;
isPinned?: boolean;
accent?: MenuItemAccent;
availableOn?: ActionAvailableOn[];
onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactElement;
};

View File

@ -11,6 +11,7 @@ import { isDefined } from '~/utils/isDefined';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
@ -108,6 +109,21 @@ export const useCommandMenu = () => {
}),
contextStoreCurrentViewId,
);
const contextStoreCurrentViewType = snapshot
.getLoadable(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreCurrentViewType,
);
}
setIsCommandMenuOpened(true);
@ -165,6 +181,13 @@ export const useCommandMenu = () => {
null,
);
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
null,
);
if (isCommandMenuOpened) {
setIsCommandMenuOpened(false);
resetSelectedItem();

View File

@ -0,0 +1,24 @@
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect } from 'react';
export const ContextStoreCurrentViewTypeEffect = ({
viewType,
}: {
viewType: ContextStoreViewType | null;
}) => {
const setContextStoreCurrentViewType = useSetRecoilComponentStateV2(
contextStoreCurrentViewTypeComponentState,
);
useEffect(() => {
setContextStoreCurrentViewType(viewType);
return () => {
setContextStoreCurrentViewType(null);
};
}, [setContextStoreCurrentViewType, viewType]);
return null;
};

View File

@ -0,0 +1,10 @@
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const contextStoreCurrentViewTypeComponentState =
createComponentStateV2<ContextStoreViewType | null>({
key: 'contextStoreCurrentViewTypeComponentState',
defaultValue: null,
componentInstanceContext: ContextStoreComponentInstanceContext,
});

View File

@ -0,0 +1,5 @@
export enum ContextStoreViewType {
Table = 'table',
Kanban = 'kanban',
ShowPage = 'show-page',
}

View File

@ -23,7 +23,9 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
import { ContextStoreCurrentViewTypeEffect } from '@/context-store/components/ContextStoreCurrentViewTypeEffect';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
@ -164,89 +166,98 @@ export const RecordIndexContainer = () => {
);
return (
<StyledContainer>
<InformationBannerWrapper />
<RecordFieldValueSelectorContextProvider>
<SpreadsheetImportProvider>
<ViewBar
viewBarId={recordIndexId}
optionsDropdownButton={
<ObjectOptionsDropdown
recordIndexId={recordIndexId}
objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.Table}
/>
}
onCurrentViewChange={(view) => {
if (!view) {
return;
<>
<ContextStoreCurrentViewTypeEffect
viewType={
recordIndexViewType === ViewType.Table
? ContextStoreViewType.Table
: ContextStoreViewType.Kanban
}
/>
<StyledContainer>
<InformationBannerWrapper />
<RecordFieldValueSelectorContextProvider>
<SpreadsheetImportProvider>
<ViewBar
viewBarId={recordIndexId}
optionsDropdownButton={
<ObjectOptionsDropdown
recordIndexId={recordIndexId}
objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.Table}
/>
}
onCurrentViewChange={(view) => {
if (!view) {
return;
}
onViewFieldsChange(view.viewFields);
onViewGroupsChange(view.viewGroups);
setTableViewFilterGroups(view.viewFilterGroups ?? []);
setTableFilters(
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
);
setRecordIndexFilters(
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
);
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
setContextStoreTargetedRecordsRule((prev) => ({
...prev,
filters: mapViewFiltersToFilters(
view.viewFilters,
filterDefinitions,
),
}));
setTableSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexViewType(view.type);
setRecordIndexViewKanbanFieldMetadataIdState(
view.kanbanFieldMetadataId,
);
setRecordIndexViewKanbanAggregateOperationState({
operation: view.kanbanAggregateOperation,
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
});
setRecordIndexIsCompactModeActive(view.isCompact);
}}
/>
<RecordIndexViewBarEffect
objectNamePlural={objectNamePlural}
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.Table && (
<>
<RecordIndexTableContainer
recordTableId={recordIndexId}
onViewFieldsChange(view.viewFields);
onViewGroupsChange(view.viewGroups);
setTableViewFilterGroups(view.viewFilterGroups ?? []);
setTableFilters(
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
);
setRecordIndexFilters(
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
);
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
setContextStoreTargetedRecordsRule((prev) => ({
...prev,
filters: mapViewFiltersToFilters(
view.viewFilters,
filterDefinitions,
),
}));
setTableSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexViewType(view.type);
setRecordIndexViewKanbanFieldMetadataIdState(
view.kanbanFieldMetadataId,
);
setRecordIndexViewKanbanAggregateOperationState({
operation: view.kanbanAggregateOperation,
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
});
setRecordIndexIsCompactModeActive(view.isCompact);
}}
/>
<RecordIndexViewBarEffect
objectNamePlural={objectNamePlural}
viewBarId={recordIndexId}
/>
<RecordIndexTableContainerEffect />
</>
)}
{recordIndexViewType === ViewType.Kanban && (
<StyledContainerWithPadding>
<RecordIndexBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
<RecordIndexBoardDataLoader
objectNameSingular={objectNameSingular}
recordBoardId={recordIndexId}
/>
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
</StyledContainerWithPadding>
)}
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />}
</RecordFieldValueSelectorContextProvider>
</StyledContainer>
</SpreadsheetImportProvider>
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.Table && (
<>
<RecordIndexTableContainer
recordTableId={recordIndexId}
viewBarId={recordIndexId}
/>
<RecordIndexTableContainerEffect />
</>
)}
{recordIndexViewType === ViewType.Kanban && (
<StyledContainerWithPadding>
<RecordIndexBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
<RecordIndexBoardDataLoader
objectNameSingular={objectNameSingular}
recordBoardId={recordIndexId}
/>
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
</StyledContainerWithPadding>
)}
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />}
</RecordFieldValueSelectorContextProvider>
</StyledContainer>
</>
);
};

View File

@ -1,4 +1,3 @@
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
@ -11,6 +10,7 @@ import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-s
import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL';
import { buildIndexTablePageURL } from '@/object-record/record-table/utils/buildIndexTableURL';
import { useQueryVariablesFromActiveFieldsOfViewOrDefaultView } from '@/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView';
import { isDefined } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize';
export const useRecordShowPagePagination = (
@ -100,22 +100,43 @@ export const useRecordShowPagePagination = (
const loading = loadingRecordAfter || loadingRecordBefore || loadingCursor;
const isThereARecordBefore = recordsBefore.length > 0;
const isThereARecordAfter = recordsAfter.length > 0;
const recordBefore = recordsBefore[0];
const recordAfter = recordsAfter[0];
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
objectNamePlural: objectMetadataItem.namePlural,
fieldVariables: {
filter,
orderBy,
},
});
const navigateToPreviousRecord = () => {
navigate(
buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam),
);
if (isDefined(recordBefore)) {
navigate(
buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam),
);
}
if (!loadingRecordBefore && !isDefined(recordBefore)) {
const firstRecordId = recordIdsInCache[recordIdsInCache.length - 1];
navigate(
buildShowPageURL(objectNameSingular, firstRecordId, viewIdQueryParam),
);
}
};
const navigateToNextRecord = () => {
navigate(
buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam),
);
if (isDefined(recordAfter)) {
navigate(
buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam),
);
}
if (!loadingRecordAfter && !isDefined(recordAfter)) {
const lastRecordId = recordIdsInCache[0];
navigate(
buildShowPageURL(objectNameSingular, lastRecordId, viewIdQueryParam),
);
}
};
const navigateToIndexView = () => {
@ -129,31 +150,21 @@ export const useRecordShowPagePagination = (
navigate(indexTableURL);
};
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
objectNamePlural: objectMetadataItem.namePlural,
fieldVariables: {
filter,
orderBy,
},
});
const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId);
const rankFoundInFiew = rankInView > -1;
const rankFoundInView = rankInView > -1;
const objectLabel = capitalize(objectMetadataItem.labelPlural);
const totalCount = Math.max(1, totalCountBefore, totalCountAfter);
const viewNameWithCount = rankFoundInFiew
const viewNameWithCount = rankFoundInView
? `${rankInView + 1} of ${totalCount} in ${objectLabel}`
: `${objectLabel} (${totalCount})`;
return {
viewName: viewNameWithCount,
hasPreviousRecord: isThereARecordBefore,
isLoadingPagination: loading,
hasNextRecord: isThereARecordAfter,
navigateToPreviousRecord,
navigateToNextRecord,
navigateToIndexView,

View File

@ -99,8 +99,6 @@ export const PageHeader = ({
hasClosePageButton,
onClosePage,
hasPaginationButtons,
hasPreviousRecord,
hasNextRecord,
navigateToPreviousRecord,
navigateToNextRecord,
Icon,
@ -140,14 +138,12 @@ export const PageHeader = ({
Icon={IconChevronUp}
size="small"
variant="secondary"
disabled={!hasPreviousRecord}
onClick={() => navigateToPreviousRecord?.()}
/>
<IconButton
Icon={IconChevronDown}
size="small"
variant="secondary"
disabled={!hasNextRecord}
onClick={() => navigateToNextRecord?.()}
/>
</>
@ -166,24 +162,6 @@ export const PageHeader = ({
</StyledLeftContainer>
<StyledPageActionContainer className="page-action-container">
{isPageHeaderV2Enabled && hasPaginationButtons && (
<>
<IconButton
Icon={IconChevronUp}
size={isMobile ? 'medium' : 'small'}
variant="secondary"
disabled={!hasPreviousRecord}
onClick={() => navigateToPreviousRecord?.()}
/>
<IconButton
Icon={IconChevronDown}
size={isMobile ? 'medium' : 'small'}
variant="secondary"
disabled={!hasNextRecord}
onClick={() => navigateToNextRecord?.()}
/>
</>
)}
{children}
</StyledPageActionContainer>
</StyledTopBarContainer>

View File

@ -86,6 +86,7 @@ export const RecordIndexPage = () => {
<RecordIndexContainerContextStoreObjectMetadataEffect />
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<MainContextStoreComponentInstanceIdSetterEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>

View File

@ -3,7 +3,9 @@ import { useParams } from 'react-router-dom';
import { RecordShowActionMenu } from '@/action-menu/components/RecordShowActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { ContextStoreCurrentViewTypeEffect } from '@/context-store/components/ContextStoreCurrentViewTypeEffect';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
@ -54,6 +56,9 @@ export const RecordShowPage = () => {
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordValueSetterEffect recordId={objectRecordId} />
<ContextStoreCurrentViewTypeEffect
viewType={ContextStoreViewType.ShowPage}
/>
<PageContainer>
<PageTitle title={pageTitle} />
<RecordShowPageHeader

View File

@ -14,8 +14,6 @@ export const RecordShowPageHeader = ({
}) => {
const {
viewName,
hasPreviousRecord,
hasNextRecord,
navigateToPreviousRecord,
navigateToNextRecord,
navigateToIndexView,
@ -29,9 +27,7 @@ export const RecordShowPageHeader = ({
hasPaginationButtons
hasClosePageButton
onClosePage={navigateToIndexView}
hasPreviousRecord={hasPreviousRecord}
navigateToPreviousRecord={navigateToPreviousRecord}
hasNextRecord={hasNextRecord}
navigateToNextRecord={navigateToNextRecord}
Icon={headerIcon}
>