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 { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect';
import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect'; 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 { 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 { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -46,6 +49,10 @@ const ActionEffects = ({
contextStoreTargetedRecordsRuleComponentState, contextStoreTargetedRecordsRuleComponentState,
); );
const contextStoreCurrentViewType = useRecoilComponentValueV2(
contextStoreCurrentViewTypeComponentState,
);
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
return ( return (
@ -59,9 +66,17 @@ const ActionEffects = ({
{contextStoreTargetedRecordsRule.mode === 'selection' && {contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && ( contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
<> <>
<SingleRecordActionMenuEntrySetterEffect {contextStoreCurrentViewType === ContextStoreViewType.ShowPage && (
objectMetadataItem={objectMetadataItem} <ShowPageSingleRecordActionMenuEntrySetterEffect
/> objectMetadataItem={objectMetadataItem}
/>
)}
{(contextStoreCurrentViewType === ContextStoreViewType.Table ||
contextStoreCurrentViewType === ContextStoreViewType.Kanban) && (
<SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
{isWorkflowEnabled && ( {isWorkflowEnabled && (
<WorkflowRunRecordActionMenuEntrySetterEffect <WorkflowRunRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem} 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 { 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 { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@ -37,6 +38,11 @@ export const SingleRecordActionMenuEntrySetterEffect = ({
} }
const actionMenuEntries = Object.values(actionConfig ?? {}) const actionMenuEntries = Object.values(actionConfig ?? {})
.filter((action) =>
action.availableOn?.includes(
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
),
)
.map((action) => { .map((action) => {
const { shouldBeRegistered, onClick, ConfirmationModal } = const { shouldBeRegistered, onClick, ConfirmationModal } =
action.actionHook({ action.actionHook({

View File

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

View File

@ -1,13 +1,22 @@
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction'; 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 { 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 { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
ActionMenuEntryScope, ActionMenuEntryScope,
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } 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< export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
string, string,
@ -20,9 +29,14 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: 'add-to-favorites-single-record', key: 'add-to-favorites-single-record',
label: 'Add to favorites', label: 'Add to favorites',
shortLabel: 'Add to favorites',
position: 0, position: 0,
isPinned: true, isPinned: true,
Icon: IconHeart, Icon: IconHeart,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useAddToFavoritesSingleRecordAction, actionHook: useAddToFavoritesSingleRecordAction,
}, },
removeFromFavoritesSingleRecord: { removeFromFavoritesSingleRecord: {
@ -30,20 +44,54 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: 'remove-from-favorites-single-record', key: 'remove-from-favorites-single-record',
label: 'Remove from favorites', label: 'Remove from favorites',
shortLabel: 'Remove from favorites',
isPinned: true, isPinned: true,
position: 1, position: 1,
Icon: IconHeartOff, Icon: IconHeartOff,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useRemoveFromFavoritesSingleRecordAction, actionHook: useRemoveFromFavoritesSingleRecordAction,
}, },
deleteSingleRecord: { deleteSingleRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: 'delete-single-record', key: 'delete-single-record',
label: 'Delete', label: 'Delete record',
shortLabel: 'Delete',
position: 2, position: 2,
Icon: IconTrash, Icon: IconTrash,
accent: 'danger', accent: 'danger',
isPinned: true, isPinned: true,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useDeleteSingleRecordAction, 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 { 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 { 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'; 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 { 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 { 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 { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
@ -13,6 +16,8 @@ import {
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } from '@/action-menu/types/ActionMenuEntry';
import { import {
IconChevronDown,
IconChevronUp,
IconHistory, IconHistory,
IconHistoryToggle, IconHistoryToggle,
IconPlayerPause, IconPlayerPause,
@ -30,81 +35,143 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
activateWorkflowDraftSingleRecord: { activateWorkflowDraftSingleRecord: {
key: 'activate-workflow-draft-single-record', key: 'activate-workflow-draft-single-record',
label: 'Activate Draft', label: 'Activate Draft',
shortLabel: 'Activate Draft',
isPinned: true, isPinned: true,
position: 1, position: 1,
Icon: IconPower, Icon: IconPower,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateDraftWorkflowSingleRecordAction, actionHook: useActivateDraftWorkflowSingleRecordAction,
}, },
activateWorkflowLastPublishedVersionSingleRecord: { activateWorkflowLastPublishedVersionSingleRecord: {
key: 'activate-workflow-last-published-version-single-record', key: 'activate-workflow-last-published-version-single-record',
label: 'Activate last published version', label: 'Activate last published version',
shortLabel: 'Activate last version',
isPinned: true, isPinned: true,
position: 2, position: 2,
Icon: IconPower, Icon: IconPower,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction, actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
}, },
deactivateWorkflowSingleRecord: { deactivateWorkflowSingleRecord: {
key: 'deactivate-workflow-single-record', key: 'deactivate-workflow-single-record',
label: 'Deactivate Workflow', label: 'Deactivate Workflow',
shortLabel: 'Deactivate',
isPinned: true, isPinned: true,
position: 3, position: 3,
Icon: IconPlayerPause, Icon: IconPlayerPause,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDeactivateWorkflowSingleRecordAction, actionHook: useDeactivateWorkflowSingleRecordAction,
}, },
discardWorkflowDraftSingleRecord: { discardWorkflowDraftSingleRecord: {
key: 'discard-workflow-draft-single-record', key: 'discard-workflow-draft-single-record',
label: 'Discard Draft', label: 'Discard Draft',
shortLabel: 'Discard Draft',
isPinned: true, isPinned: true,
position: 4, position: 4,
Icon: IconTrash, Icon: IconTrash,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDiscardDraftWorkflowSingleRecordAction, actionHook: useDiscardDraftWorkflowSingleRecordAction,
}, },
seeWorkflowActiveVersionSingleRecord: { seeWorkflowActiveVersionSingleRecord: {
key: 'see-workflow-active-version-single-record', key: 'see-workflow-active-version-single-record',
label: 'See active version', label: 'See active version',
shortLabel: 'See active version',
isPinned: false, isPinned: false,
position: 5, position: 5,
Icon: IconHistory, Icon: IconHistory,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeActiveVersionWorkflowSingleRecordAction, actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
}, },
seeWorkflowRunsSingleRecord: { seeWorkflowRunsSingleRecord: {
key: 'see-workflow-runs-single-record', key: 'see-workflow-runs-single-record',
label: 'See runs', label: 'See runs',
shortLabel: 'See runs',
isPinned: false, isPinned: false,
position: 6, position: 6,
Icon: IconHistoryToggle, Icon: IconHistoryToggle,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeRunsWorkflowSingleRecordAction, actionHook: useSeeRunsWorkflowSingleRecordAction,
}, },
seeWorkflowVersionsHistorySingleRecord: { seeWorkflowVersionsHistorySingleRecord: {
key: 'see-workflow-versions-history-single-record', key: 'see-workflow-versions-history-single-record',
label: 'See versions history', label: 'See versions history',
shortLabel: 'See versions',
isPinned: false, isPinned: false,
position: 7, position: 7,
Icon: IconHistory, Icon: IconHistory,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeVersionsWorkflowSingleRecordAction, actionHook: useSeeVersionsWorkflowSingleRecordAction,
}, },
testWorkflowSingleRecord: { testWorkflowSingleRecord: {
key: 'test-workflow-single-record', key: 'test-workflow-single-record',
label: 'Test Workflow', label: 'Test Workflow',
shortLabel: 'Test',
isPinned: true, isPinned: true,
position: 8, position: 8,
Icon: IconPlayerPlay, Icon: IconPlayerPlay,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useTestWorkflowSingleRecordAction, 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 { 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 { 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 { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
ActionMenuEntryScope, ActionMenuEntryScope,
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } 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< export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
string, string,
@ -23,6 +32,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
Icon: IconPencil, Icon: IconPencil,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction, actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
}, },
seeWorkflowExecutionsSingleRecord: { seeWorkflowExecutionsSingleRecord: {
@ -32,6 +45,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistoryToggle, Icon: IconHistoryToggle,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeExecutionsWorkflowVersionSingleRecordAction, actionHook: useSeeExecutionsWorkflowVersionSingleRecordAction,
}, },
seeWorkflowVersionsHistorySingleRecord: { seeWorkflowVersionsHistorySingleRecord: {
@ -41,6 +58,32 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistory, Icon: IconHistory,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction, 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 { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton'; import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; 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 = () => { export const RecordShowActionMenuButtons = () => {
const actionMenuEntries = useRecoilComponentValueV2( const actionMenuEntries = useRecoilComponentValueV2(
@ -15,18 +15,29 @@ export const RecordShowActionMenuButtons = () => {
return ( return (
<> <>
{!isMobile && {!isMobile &&
pinnedEntries.map((entry, index) => ( pinnedEntries.map((entry, index) =>
<Button entry.shortLabel ? (
key={index} <Button
Icon={entry.Icon} key={index}
size="small" Icon={entry.Icon}
variant="secondary" size="small"
accent="default" variant="secondary"
title={entry.label} accent="default"
onClick={() => entry.onClick?.()} title={entry.shortLabel}
ariaLabel={entry.label} 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" /> <PageHeaderOpenCommandMenuButton key="more" />
</> </>
); );

View File

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

View File

@ -11,6 +11,7 @@ import { isDefined } from '~/utils/isDefined';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
@ -108,6 +109,21 @@ export const useCommandMenu = () => {
}), }),
contextStoreCurrentViewId, contextStoreCurrentViewId,
); );
const contextStoreCurrentViewType = snapshot
.getLoadable(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreCurrentViewType,
);
} }
setIsCommandMenuOpened(true); setIsCommandMenuOpened(true);
@ -165,6 +181,13 @@ export const useCommandMenu = () => {
null, null,
); );
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
null,
);
if (isCommandMenuOpened) { if (isCommandMenuOpened) {
setIsCommandMenuOpened(false); setIsCommandMenuOpened(false);
resetSelectedItem(); 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 { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu'; import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
import { ContextStoreCurrentViewTypeEffect } from '@/context-store/components/ContextStoreCurrentViewTypeEffect';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup'; import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect'; import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
@ -164,89 +166,98 @@ export const RecordIndexContainer = () => {
); );
return ( return (
<StyledContainer> <>
<InformationBannerWrapper /> <ContextStoreCurrentViewTypeEffect
<RecordFieldValueSelectorContextProvider> viewType={
<SpreadsheetImportProvider> recordIndexViewType === ViewType.Table
<ViewBar ? ContextStoreViewType.Table
viewBarId={recordIndexId} : ContextStoreViewType.Kanban
optionsDropdownButton={ }
<ObjectOptionsDropdown />
recordIndexId={recordIndexId} <StyledContainer>
objectMetadataItem={objectMetadataItem} <InformationBannerWrapper />
viewType={recordIndexViewType ?? ViewType.Table} <RecordFieldValueSelectorContextProvider>
/> <SpreadsheetImportProvider>
} <ViewBar
onCurrentViewChange={(view) => { viewBarId={recordIndexId}
if (!view) { optionsDropdownButton={
return; <ObjectOptionsDropdown
recordIndexId={recordIndexId}
objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.Table}
/>
} }
onCurrentViewChange={(view) => {
if (!view) {
return;
}
onViewFieldsChange(view.viewFields); onViewFieldsChange(view.viewFields);
onViewGroupsChange(view.viewGroups); onViewGroupsChange(view.viewGroups);
setTableViewFilterGroups(view.viewFilterGroups ?? []); setTableViewFilterGroups(view.viewFilterGroups ?? []);
setTableFilters( setTableFilters(
mapViewFiltersToFilters(view.viewFilters, filterDefinitions), mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
); );
setRecordIndexFilters( setRecordIndexFilters(
mapViewFiltersToFilters(view.viewFilters, filterDefinitions), mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
); );
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []); setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
setContextStoreTargetedRecordsRule((prev) => ({ setContextStoreTargetedRecordsRule((prev) => ({
...prev, ...prev,
filters: mapViewFiltersToFilters( filters: mapViewFiltersToFilters(
view.viewFilters, view.viewFilters,
filterDefinitions, filterDefinitions,
), ),
})); }));
setTableSorts( setTableSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions), mapViewSortsToSorts(view.viewSorts, sortDefinitions),
); );
setRecordIndexSorts( setRecordIndexSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions), mapViewSortsToSorts(view.viewSorts, sortDefinitions),
); );
setRecordIndexViewType(view.type); setRecordIndexViewType(view.type);
setRecordIndexViewKanbanFieldMetadataIdState( setRecordIndexViewKanbanFieldMetadataIdState(
view.kanbanFieldMetadataId, view.kanbanFieldMetadataId,
); );
setRecordIndexViewKanbanAggregateOperationState({ setRecordIndexViewKanbanAggregateOperationState({
operation: view.kanbanAggregateOperation, operation: view.kanbanAggregateOperation,
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId, fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
}); });
setRecordIndexIsCompactModeActive(view.isCompact); setRecordIndexIsCompactModeActive(view.isCompact);
}} }}
/> />
<RecordIndexViewBarEffect <RecordIndexViewBarEffect
objectNamePlural={objectNamePlural} objectNamePlural={objectNamePlural}
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.Table && (
<>
<RecordIndexTableContainer
recordTableId={recordIndexId}
viewBarId={recordIndexId} viewBarId={recordIndexId}
/> />
<RecordIndexTableContainerEffect /> </SpreadsheetImportProvider>
</> <RecordIndexFiltersToContextStoreEffect />
)} {recordIndexViewType === ViewType.Table && (
{recordIndexViewType === ViewType.Kanban && ( <>
<StyledContainerWithPadding> <RecordIndexTableContainer
<RecordIndexBoardContainer recordTableId={recordIndexId}
recordBoardId={recordIndexId} viewBarId={recordIndexId}
viewBarId={recordIndexId} />
objectNameSingular={objectNameSingular} <RecordIndexTableContainerEffect />
/> </>
<RecordIndexBoardDataLoader )}
objectNameSingular={objectNameSingular} {recordIndexViewType === ViewType.Kanban && (
recordBoardId={recordIndexId} <StyledContainerWithPadding>
/> <RecordIndexBoardContainer
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} /> recordBoardId={recordIndexId}
</StyledContainerWithPadding> viewBarId={recordIndexId}
)} objectNameSingular={objectNameSingular}
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />} />
</RecordFieldValueSelectorContextProvider> <RecordIndexBoardDataLoader
</StyledContainer> 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 { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; 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 { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL';
import { buildIndexTablePageURL } from '@/object-record/record-table/utils/buildIndexTableURL'; import { buildIndexTablePageURL } from '@/object-record/record-table/utils/buildIndexTableURL';
import { useQueryVariablesFromActiveFieldsOfViewOrDefaultView } from '@/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView'; import { useQueryVariablesFromActiveFieldsOfViewOrDefaultView } from '@/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView';
import { isDefined } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
export const useRecordShowPagePagination = ( export const useRecordShowPagePagination = (
@ -100,22 +100,43 @@ export const useRecordShowPagePagination = (
const loading = loadingRecordAfter || loadingRecordBefore || loadingCursor; const loading = loadingRecordAfter || loadingRecordBefore || loadingCursor;
const isThereARecordBefore = recordsBefore.length > 0;
const isThereARecordAfter = recordsAfter.length > 0;
const recordBefore = recordsBefore[0]; const recordBefore = recordsBefore[0];
const recordAfter = recordsAfter[0]; const recordAfter = recordsAfter[0];
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
objectNamePlural: objectMetadataItem.namePlural,
fieldVariables: {
filter,
orderBy,
},
});
const navigateToPreviousRecord = () => { const navigateToPreviousRecord = () => {
navigate( if (isDefined(recordBefore)) {
buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam), navigate(
); buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam),
);
}
if (!loadingRecordBefore && !isDefined(recordBefore)) {
const firstRecordId = recordIdsInCache[recordIdsInCache.length - 1];
navigate(
buildShowPageURL(objectNameSingular, firstRecordId, viewIdQueryParam),
);
}
}; };
const navigateToNextRecord = () => { const navigateToNextRecord = () => {
navigate( if (isDefined(recordAfter)) {
buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam), navigate(
); buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam),
);
}
if (!loadingRecordAfter && !isDefined(recordAfter)) {
const lastRecordId = recordIdsInCache[0];
navigate(
buildShowPageURL(objectNameSingular, lastRecordId, viewIdQueryParam),
);
}
}; };
const navigateToIndexView = () => { const navigateToIndexView = () => {
@ -129,31 +150,21 @@ export const useRecordShowPagePagination = (
navigate(indexTableURL); navigate(indexTableURL);
}; };
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
objectNamePlural: objectMetadataItem.namePlural,
fieldVariables: {
filter,
orderBy,
},
});
const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId); const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId);
const rankFoundInFiew = rankInView > -1; const rankFoundInView = rankInView > -1;
const objectLabel = capitalize(objectMetadataItem.labelPlural); const objectLabel = capitalize(objectMetadataItem.labelPlural);
const totalCount = Math.max(1, totalCountBefore, totalCountAfter); const totalCount = Math.max(1, totalCountBefore, totalCountAfter);
const viewNameWithCount = rankFoundInFiew const viewNameWithCount = rankFoundInView
? `${rankInView + 1} of ${totalCount} in ${objectLabel}` ? `${rankInView + 1} of ${totalCount} in ${objectLabel}`
: `${objectLabel} (${totalCount})`; : `${objectLabel} (${totalCount})`;
return { return {
viewName: viewNameWithCount, viewName: viewNameWithCount,
hasPreviousRecord: isThereARecordBefore,
isLoadingPagination: loading, isLoadingPagination: loading,
hasNextRecord: isThereARecordAfter,
navigateToPreviousRecord, navigateToPreviousRecord,
navigateToNextRecord, navigateToNextRecord,
navigateToIndexView, navigateToIndexView,

View File

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

View File

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

View File

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

View File

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