9260 refactor multiple record actions and no selection actions (#9314)

Closes #9260

- Refactored multiple record actions and no selection record actions to
use config file
- Simplified actions registration logic
- Updated tests
This commit is contained in:
Raphaël Bosi
2025-01-02 13:15:27 +01:00
committed by GitHub
parent 306b45a038
commit e3f7a0572e
52 changed files with 854 additions and 1106 deletions

View File

@ -1,13 +1,10 @@
import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect'; import { RegisterRecordActionEffect } from '@/action-menu/actions/record-actions/components/RegisterRecordActionEffect';
import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect';
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 { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { getActionConfig } from '@/action-menu/actions/utils/getActionConfig';
import { getActionViewType } from '@/action-menu/actions/utils/getActionViewType';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; 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 { 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';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@ -19,33 +16,13 @@ export const RecordActionMenuEntriesSetter = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState, contextStoreCurrentObjectMetadataIdComponentState,
); );
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find( const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === contextStoreCurrentObjectMetadataId, (item) => item.id === contextStoreCurrentObjectMetadataId,
); );
if (
!isDefined(contextStoreCurrentObjectMetadataId) ||
!isDefined(objectMetadataItem)
) {
return null;
}
return (
<ActionEffects objectMetadataItemId={contextStoreCurrentObjectMetadataId} />
);
};
const ActionEffects = ({
objectMetadataItemId,
}: {
objectMetadataItemId: string;
}) => {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId,
});
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState, contextStoreTargetedRecordsRuleComponentState,
); );
@ -58,43 +35,52 @@ const ActionEffects = ({
FeatureFlagKey.IsWorkflowEnabled, FeatureFlagKey.IsWorkflowEnabled,
); );
const isPageHeaderV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled,
);
if (
!isDefined(contextStoreCurrentObjectMetadataId) ||
!isDefined(objectMetadataItem)
) {
return null;
}
const viewType = getActionViewType(
contextStoreCurrentViewType,
contextStoreTargetedRecordsRule,
);
const actionConfig = getActionConfig(
objectMetadataItem,
isPageHeaderV2Enabled,
);
const actionsToRegister = isDefined(viewType)
? Object.values(actionConfig ?? {}).filter((action) =>
action.availableOn?.includes(viewType),
)
: [];
return ( return (
<> <>
{contextStoreTargetedRecordsRule.mode === 'selection' && {actionsToRegister.map((action) => (
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0 && ( <RegisterRecordActionEffect
<NoSelectionActionMenuEntrySetterEffect key={action.key}
action={action}
objectMetadataItem={objectMetadataItem}
/>
))}
{isWorkflowEnabled &&
!(
contextStoreTargetedRecordsRule?.mode === 'selection' &&
contextStoreTargetedRecordsRule?.selectedRecordIds.length === 0
) && (
<WorkflowRunRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem} objectMetadataItem={objectMetadataItem}
/> />
)} )}
{contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
<>
{contextStoreCurrentViewType === ContextStoreViewType.ShowPage && (
<SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
viewType={ActionViewType.SHOW_PAGE}
/>
)}
{(contextStoreCurrentViewType === ContextStoreViewType.Table ||
contextStoreCurrentViewType === ContextStoreViewType.Kanban) && (
<SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
viewType={ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION}
/>
)}
{isWorkflowEnabled && (
<WorkflowRunRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
</>
)}
{(contextStoreTargetedRecordsRule.mode === 'exclusion' ||
contextStoreTargetedRecordsRule.selectedRecordIds.length > 1) && (
<MultipleRecordsActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
</> </>
); );
}; };

View File

@ -0,0 +1,55 @@
import { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useContext, useEffect } from 'react';
type RegisterRecordActionEffectProps = {
action: ActionMenuEntry & {
actionHook: ActionHook;
};
objectMetadataItem: ObjectMetadataItem;
};
export const RegisterRecordActionEffect = ({
action,
objectMetadataItem,
}: RegisterRecordActionEffectProps) => {
const { shouldBeRegistered, onClick, ConfirmationModal } = action.actionHook({
objectMetadataItem,
});
const { onActionStartedCallback, onActionExecutedCallback } =
useContext(ActionMenuContext);
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const wrappedAction = wrapActionInCallbacks({
action: {
...action,
onClick,
ConfirmationModal,
},
onActionStartedCallback,
onActionExecutedCallback,
});
useEffect(() => {
if (shouldBeRegistered) {
addActionMenuEntry(wrappedAction);
}
return () => {
removeActionMenuEntry(wrappedAction.key);
};
}, [
addActionMenuEntry,
removeActionMenuEntry,
shouldBeRegistered,
wrappedAction,
]);
return null;
};

View File

@ -1,20 +1,29 @@
import { useDeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction';
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
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 { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
import { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
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 {
IconDatabaseExport,
IconHeart,
IconHeartOff,
IconTrash,
} from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record< export const DEFAULT_ACTIONS_CONFIG_V1: Record<
string, string,
ActionMenuEntry & { ActionMenuEntry & {
actionHook: SingleRecordActionHook; actionHook: ActionHook;
} }
> = { > = {
addToFavoritesSingleRecord: { addToFavoritesSingleRecord: {
@ -58,4 +67,43 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
], ],
actionHook: useDeleteSingleRecordAction, actionHook: useDeleteSingleRecordAction,
}, },
deleteMultipleRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.DELETE,
label: 'Delete records',
shortLabel: 'Delete',
position: 3,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useDeleteMultipleRecordsAction,
},
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.EXPORT,
label: 'Export records',
shortLabel: 'Export',
position: 4,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
label: 'Export view',
shortLabel: 'Export',
position: 5,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
}; };

View File

@ -1,3 +1,7 @@
import { useDeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction';
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
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 { useDestroySingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction'; import { useDestroySingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction';
@ -6,8 +10,8 @@ import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction'; 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 { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
import { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
ActionMenuEntryScope, ActionMenuEntryScope,
@ -16,6 +20,7 @@ import {
import { import {
IconChevronDown, IconChevronDown,
IconChevronUp, IconChevronUp,
IconDatabaseExport,
IconFileExport, IconFileExport,
IconHeart, IconHeart,
IconHeartOff, IconHeartOff,
@ -23,10 +28,10 @@ import {
IconTrashX, IconTrashX,
} from 'twenty-ui'; } from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< export const DEFAULT_ACTIONS_CONFIG_V2: Record<
string, string,
ActionMenuEntry & { ActionMenuEntry & {
actionHook: SingleRecordActionHook; actionHook: ActionHook;
} }
> = { > = {
exportNoteToPdf: { exportNoteToPdf: {
@ -87,13 +92,52 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
], ],
actionHook: useDeleteSingleRecordAction, actionHook: useDeleteSingleRecordAction,
}, },
deleteMultipleRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.DELETE,
label: 'Delete records',
shortLabel: 'Delete',
position: 4,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useDeleteMultipleRecordsAction,
},
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.EXPORT,
label: 'Export records',
shortLabel: 'Export',
position: 5,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
label: 'Export view',
shortLabel: 'Export',
position: 6,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
destroySingleRecord: { destroySingleRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: SingleRecordActionKeys.DESTROY, key: SingleRecordActionKeys.DESTROY,
label: 'Permanently destroy record', label: 'Permanently destroy record',
shortLabel: 'Destroy', shortLabel: 'Destroy',
position: 4, position: 7,
Icon: IconTrashX, Icon: IconTrashX,
accent: 'danger', accent: 'danger',
isPinned: true, isPinned: true,
@ -109,7 +153,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
key: SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD, key: SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
label: 'Navigate to previous record', label: 'Navigate to previous record',
shortLabel: '', shortLabel: '',
position: 5, position: 8,
isPinned: true, isPinned: true,
Icon: IconChevronUp, Icon: IconChevronUp,
availableOn: [ActionViewType.SHOW_PAGE], availableOn: [ActionViewType.SHOW_PAGE],
@ -121,7 +165,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
key: SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD, key: SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
label: 'Navigate to next record', label: 'Navigate to next record',
shortLabel: '', shortLabel: '',
position: 6, position: 9,
isPinned: true, isPinned: true,
Icon: IconChevronDown, Icon: IconChevronDown,
availableOn: [ActionViewType.SHOW_PAGE], availableOn: [ActionViewType.SHOW_PAGE],

View File

@ -1,3 +1,7 @@
import { useDeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction';
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
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 { useDestroySingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction'; import { useDestroySingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction';
@ -14,8 +18,8 @@ import { useSeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/reco
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 { WorkflowSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-actions/types/WorkflowSingleRecordActionsKeys'; import { WorkflowSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-actions/types/WorkflowSingleRecordActionsKeys';
import { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
ActionMenuEntryScope, ActionMenuEntryScope,
@ -24,6 +28,7 @@ import {
import { import {
IconChevronDown, IconChevronDown,
IconChevronUp, IconChevronUp,
IconDatabaseExport,
IconHeart, IconHeart,
IconHeartOff, IconHeartOff,
IconHistory, IconHistory,
@ -35,10 +40,10 @@ import {
IconTrashX, IconTrashX,
} from 'twenty-ui'; } from 'twenty-ui';
export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< export const WORKFLOW_ACTIONS_CONFIG: Record<
string, string,
ActionMenuEntry & { ActionMenuEntry & {
actionHook: SingleRecordActionHook; actionHook: ActionHook;
} }
> = { > = {
activateWorkflowDraftSingleRecord: { activateWorkflowDraftSingleRecord: {
@ -229,13 +234,26 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
], ],
actionHook: useDeleteSingleRecordAction, actionHook: useDeleteSingleRecordAction,
}, },
deleteMultipleRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.DELETE,
label: 'Delete records',
shortLabel: 'Delete',
position: 14,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useDeleteMultipleRecordsAction,
},
destroySingleRecord: { destroySingleRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: SingleRecordActionKeys.DESTROY, key: SingleRecordActionKeys.DESTROY,
label: 'Permanently destroy record', label: 'Permanently destroy record',
shortLabel: 'Destroy', shortLabel: 'Destroy',
position: 14, position: 15,
Icon: IconTrashX, Icon: IconTrashX,
accent: 'danger', accent: 'danger',
isPinned: false, isPinned: false,
@ -245,4 +263,30 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
], ],
actionHook: useDestroySingleRecordAction, actionHook: useDestroySingleRecordAction,
}, },
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.EXPORT,
label: 'Export records',
shortLabel: 'Export',
position: 16,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
label: 'Export view',
shortLabel: 'Export',
position: 17,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
}; };

View File

@ -1,10 +1,13 @@
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
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 { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction'; 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 { 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 { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
import { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
ActionMenuEntryScope, ActionMenuEntryScope,
@ -13,14 +16,15 @@ import {
import { import {
IconChevronDown, IconChevronDown,
IconChevronUp, IconChevronUp,
IconDatabaseExport,
IconHeart, IconHeart,
IconHeartOff, IconHeartOff,
} from 'twenty-ui'; } from 'twenty-ui';
export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record< export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
string, string,
ActionMenuEntry & { ActionMenuEntry & {
actionHook: SingleRecordActionHook; actionHook: ActionHook;
} }
> = { > = {
addToFavoritesSingleRecord: { addToFavoritesSingleRecord: {
@ -77,4 +81,30 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
availableOn: [ActionViewType.SHOW_PAGE], availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction, actionHook: useNavigateToNextRecordSingleRecordAction,
}, },
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.EXPORT,
label: 'Export records',
shortLabel: 'Export',
position: 4,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
label: 'Export view',
shortLabel: 'Export',
position: 5,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
}; };

View File

@ -1,3 +1,6 @@
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
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 { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction'; 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 { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
@ -7,8 +10,8 @@ import { useSeeRunsWorkflowVersionSingleRecordAction } from '@/action-menu/actio
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 { WorkflowVersionSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/types/WorkflowVersionSingleRecordActionsKeys'; import { WorkflowVersionSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/types/WorkflowVersionSingleRecordActionsKeys';
import { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
ActionMenuEntryScope, ActionMenuEntryScope,
@ -17,6 +20,7 @@ import {
import { import {
IconChevronDown, IconChevronDown,
IconChevronUp, IconChevronUp,
IconDatabaseExport,
IconHeart, IconHeart,
IconHeartOff, IconHeartOff,
IconHistory, IconHistory,
@ -24,10 +28,10 @@ import {
IconPencil, IconPencil,
} from 'twenty-ui'; } from 'twenty-ui';
export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
string, string,
ActionMenuEntry & { ActionMenuEntry & {
actionHook: SingleRecordActionHook; actionHook: ActionHook;
} }
> = { > = {
useAsDraftWorkflowVersionSingleRecord: { useAsDraftWorkflowVersionSingleRecord: {
@ -125,4 +129,30 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
], ],
actionHook: useRemoveFromFavoritesSingleRecordAction, actionHook: useRemoveFromFavoritesSingleRecordAction,
}, },
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.EXPORT,
label: 'Export records',
shortLabel: 'Export',
position: 8,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
label: 'Export view',
shortLabel: 'Export',
position: 9,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
},
}; };

View File

@ -1,24 +0,0 @@
import { useMultipleRecordsActions } from '@/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useEffect } from 'react';
export const MultipleRecordsActionMenuEntrySetterEffect = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { registerMultipleRecordsActions, unregisterMultipleRecordsActions } =
useMultipleRecordsActions({
objectMetadataItem,
});
useEffect(() => {
registerMultipleRecordsActions();
return () => {
unregisterMultipleRecordsActions();
};
}, [registerMultipleRecordsActions, unregisterMultipleRecordsActions]);
return null;
};

View File

@ -1,132 +1,87 @@
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { renderHook, waitFor } from '@testing-library/react';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { expect } from '@storybook/test';
import { renderHook } from '@testing-library/react';
import { act } from 'react'; import { act } from 'react';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getPeopleMock } from '~/testing/mock-data/people';
import { useDeleteMultipleRecordsAction } from '../useDeleteMultipleRecordsAction'; import { useDeleteMultipleRecordsAction } from '../useDeleteMultipleRecordsAction';
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const peopleMock = getPeopleMock();
const deleteManyRecordsMock = jest.fn();
const resetTableRowSelectionMock = jest.fn();
jest.mock('@/object-record/hooks/useDeleteManyRecords', () => ({ jest.mock('@/object-record/hooks/useDeleteManyRecords', () => ({
useDeleteManyRecords: () => ({ useDeleteManyRecords: () => ({
deleteManyRecords: jest.fn(), deleteManyRecords: deleteManyRecordsMock,
}), }),
})); }));
jest.mock('@/favorites/hooks/useDeleteFavorite', () => ({
useDeleteFavorite: () => ({ jest.mock('@/object-record/hooks/useLazyFetchAllRecords', () => ({
deleteFavorite: jest.fn(), useLazyFetchAllRecords: () => {
}), return {
})); fetchAllRecords: () => [peopleMock[0], peopleMock[1]],
jest.mock('@/favorites/hooks/useFavorites', () => ({ };
useFavorites: () => ({ },
sortedFavorites: [],
}),
})); }));
jest.mock('@/object-record/record-table/hooks/useRecordTable', () => ({ jest.mock('@/object-record/record-table/hooks/useRecordTable', () => ({
useRecordTable: () => ({ useRecordTable: () => ({
resetTableRowSelection: jest.fn(), resetTableRowSelection: resetTableRowSelectionMock,
}), }),
})); }));
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find( const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
(item) => item.nameSingular === 'company',
)!;
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [], apolloMocks: [],
onInitializeRecoilSnapshot: ({ set }) => { componentInstanceId: '1',
set( contextStoreCurrentObjectMetadataNameSingular:
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ personMockObjectMetadataItem.nameSingular,
instanceId: '1', contextStoreTargetedRecordsRule: {
}), mode: 'selection',
3, selectedRecordIds: [peopleMock[0].id, peopleMock[1].id],
); },
contextStoreNumberOfSelectedRecords: 2,
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(peopleMock[0].id), peopleMock[0]);
snapshot.set(recordStoreFamilyState(peopleMock[1].id), peopleMock[1]);
}, },
}); });
describe('useDeleteMultipleRecordsAction', () => { describe('useDeleteMultipleRecordsAction', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => ( it('should call deleteManyRecords on click', async () => {
<JestMetadataAndApolloMocksWrapper>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
{children}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</JestMetadataAndApolloMocksWrapper>
);
it('should register delete action', () => {
const { result } = renderHook( const { result } = renderHook(
() => { () =>
const actionMenuEntries = useRecoilComponentValueV2( useDeleteMultipleRecordsAction({
actionMenuEntriesComponentState, objectMetadataItem: personMockObjectMetadataItem,
); }),
{
return { wrapper,
actionMenuEntries,
useDeleteMultipleRecordsAction: useDeleteMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
}, },
{ wrapper },
); );
act(() => { expect(result.current.ConfirmationModal?.props?.isOpen).toBe(false);
result.current.useDeleteMultipleRecordsAction.registerDeleteMultipleRecordsAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get('delete-multiple-records'),
).toBeDefined();
expect(
result.current.actionMenuEntries.get('delete-multiple-records')?.position,
).toBe(1);
});
it('should unregister delete action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeleteMultipleRecordsAction: useDeleteMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => { act(() => {
result.current.useDeleteMultipleRecordsAction.registerDeleteMultipleRecordsAction( result.current.onClick();
{ position: 1 },
);
}); });
expect(result.current.actionMenuEntries.size).toBe(1); expect(result.current.ConfirmationModal?.props?.isOpen).toBe(true);
act(() => { act(() => {
result.current.useDeleteMultipleRecordsAction.unregisterDeleteMultipleRecordsAction(); result.current.ConfirmationModal?.props?.onConfirmClick();
}); });
expect(result.current.actionMenuEntries.size).toBe(0); await waitFor(() => {
expect(resetTableRowSelectionMock).toHaveBeenCalled();
expect(deleteManyRecordsMock).toHaveBeenCalledWith([
peopleMock[0].id,
peopleMock[1].id,
]);
});
}); });
}); });

View File

@ -1,105 +1,59 @@
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { renderHook, waitFor } from '@testing-library/react';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { expect } from '@storybook/test';
import { renderHook } from '@testing-library/react';
import { act } from 'react'; import { act } from 'react';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getPeopleMock } from '~/testing/mock-data/people';
import { useExportMultipleRecordsAction } from '../useExportMultipleRecordsAction'; import { useExportMultipleRecordsAction } from '../useExportMultipleRecordsAction';
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find( const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company', (item) => item.nameSingular === 'person',
)!; )!;
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({ const peopleMock = getPeopleMock();
const downloadMock = jest.fn();
jest.mock('@/object-record/record-index/export/hooks/useExportRecords', () => ({
useExportRecords: () => ({
download: downloadMock,
}),
}));
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [], apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [peopleMock[0].id, peopleMock[1].id],
},
contextStoreNumberOfSelectedRecords: 2,
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(peopleMock[0].id), peopleMock[0]);
snapshot.set(recordStoreFamilyState(peopleMock[1].id), peopleMock[1]);
},
}); });
describe('useExportMultipleRecordsAction', () => { describe('useExportMultipleRecordsAction', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => ( it('should call exportManyRecords on click', async () => {
<JestMetadataAndApolloMocksWrapper>
<JestObjectMetadataItemSetter>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
{children}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</JestMetadataAndApolloMocksWrapper>
);
it('should register export multiple records action', () => {
const { result } = renderHook( const { result } = renderHook(
() => { () =>
const actionMenuEntries = useRecoilComponentValueV2( useExportMultipleRecordsAction({
actionMenuEntriesComponentState, objectMetadataItem: personMockObjectMetadataItem,
); }),
{
return { wrapper,
actionMenuEntries,
useExportMultipleRecordsAction: useExportMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
}, },
{ wrapper },
); );
act(() => { act(() => {
result.current.useExportMultipleRecordsAction.registerExportMultipleRecordsAction( result.current.onClick();
{ position: 1 },
);
}); });
expect(result.current.actionMenuEntries.size).toBe(1); await waitFor(() => {
expect( expect(downloadMock).toHaveBeenCalled();
result.current.actionMenuEntries.get('export-multiple-records'),
).toBeDefined();
expect(
result.current.actionMenuEntries.get('export-multiple-records')?.position,
).toBe(1);
});
it('should unregister export multiple records action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useExportMultipleRecordsAction: useExportMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useExportMultipleRecordsAction.registerExportMultipleRecordsAction(
{ position: 1 },
);
}); });
expect(result.current.actionMenuEntries.size).toBe(1);
act(() => {
result.current.useExportMultipleRecordsAction.unregisterExportMultipleRecordsAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
}); });
}); });

View File

@ -1,15 +1,9 @@
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys'; import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
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';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
@ -18,140 +12,101 @@ import { FilterOperand } from '@/object-record/object-filter-dropdown/types/Filt
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useContext, useState } from 'react'; import { useCallback, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useDeleteMultipleRecordsAction = ({ export const useDeleteMultipleRecordsAction: ActionHookWithObjectMetadataItem =
objectMetadataItem, ({ objectMetadataItem }) => {
}: { const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
objectMetadataItem: ObjectMetadataItem; useState(false);
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = const { resetTableRowSelection } = useRecordTable({
useState(false); recordTableId: objectMetadataItem.namePlural,
});
const { resetTableRowSelection } = useRecordTable({ const { deleteManyRecords } = useDeleteManyRecords({
recordTableId: objectMetadataItem.namePlural, objectNameSingular: objectMetadataItem.nameSingular,
}); });
const { deleteManyRecords } = useDeleteManyRecords({ const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
objectNameSingular: objectMetadataItem.nameSingular, contextStoreNumberOfSelectedRecordsComponentState,
}); );
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState, contextStoreTargetedRecordsRuleComponentState,
); );
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( const contextStoreFilters = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState, contextStoreFiltersComponentState,
); );
const contextStoreFilters = useRecoilComponentValueV2( const { filterValueDependencies } = useFilterValueDependencies();
contextStoreFiltersComponentState,
);
const { filterValueDependencies } = useFilterValueDependencies(); const graphqlFilter = computeContextStoreFilters(
contextStoreTargetedRecordsRule,
contextStoreFilters,
objectMetadataItem,
filterValueDependencies,
);
const graphqlFilter = computeContextStoreFilters( const deletedAtFieldMetadata = objectMetadataItem.fields.find(
contextStoreTargetedRecordsRule, (field) => field.name === 'deletedAt',
contextStoreFilters, );
objectMetadataItem,
filterValueDependencies,
);
const deletedAtFieldMetadata = objectMetadataItem.fields.find( const isDeletedFilterActive = contextStoreFilters.some(
(field) => field.name === 'deletedAt', (filter) =>
); filter.fieldMetadataId === deletedAtFieldMetadata?.id &&
filter.operand === FilterOperand.IsNotEmpty,
);
const isDeletedFilterActive = contextStoreFilters.some( const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
(filter) => objectNameSingular: objectMetadataItem.nameSingular,
filter.fieldMetadataId === deletedAtFieldMetadata?.id && filter: graphqlFilter,
filter.operand === FilterOperand.IsNotEmpty, limit: DEFAULT_QUERY_PAGE_SIZE,
); recordGqlFields: { id: true },
});
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({ const handleDeleteClick = useCallback(async () => {
objectNameSingular: objectMetadataItem.nameSingular, const recordsToDelete = await fetchAllRecordIds();
filter: graphqlFilter, const recordIdsToDelete = recordsToDelete.map((record) => record.id);
limit: DEFAULT_QUERY_PAGE_SIZE,
recordGqlFields: { id: true },
});
const { closeRightDrawer } = useRightDrawer(); resetTableRowSelection();
const handleDeleteClick = useCallback(async () => { await deleteManyRecords(recordIdsToDelete);
const recordsToDelete = await fetchAllRecordIds(); }, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]);
const recordIdsToDelete = recordsToDelete.map((record) => record.id);
resetTableRowSelection(); const isRemoteObject = objectMetadataItem.isRemote;
await deleteManyRecords(recordIdsToDelete); const shouldBeRegistered =
}, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]); !isRemoteObject &&
!isDeletedFilterActive &&
isDefined(contextStoreNumberOfSelectedRecords) &&
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;
const isRemoteObject = objectMetadataItem.isRemote; const onClick = () => {
if (!shouldBeRegistered) {
return;
}
const canDelete = setIsDeleteRecordsModalOpen(true);
!isRemoteObject && };
!isDeletedFilterActive &&
isDefined(contextStoreNumberOfSelectedRecords) &&
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;
const { isInRightDrawer, onActionStartedCallback, onActionExecutedCallback } = const confirmationModal = (
useContext(ActionMenuContext); <ConfirmationModal
isOpen={isDeleteRecordsModalOpen}
setIsOpen={setIsDeleteRecordsModalOpen}
title={'Delete Records'}
subtitle={`Are you sure you want to delete these records? They can be recovered from the Options menu.`}
onConfirmClick={handleDeleteClick}
deleteButtonText={'Delete Records'}
/>
);
const registerDeleteMultipleRecordsAction = ({ return {
position, shouldBeRegistered,
}: { onClick,
position: number; ConfirmationModal: confirmationModal,
}) => { };
if (canDelete) {
addActionMenuEntry({
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.DELETE,
label: 'Delete records',
shortLabel: 'Delete',
position,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
onClick: () => {
setIsDeleteRecordsModalOpen(true);
},
ConfirmationModal: (
<ConfirmationModal
isOpen={isDeleteRecordsModalOpen}
setIsOpen={setIsDeleteRecordsModalOpen}
title={'Delete Records'}
subtitle={`Are you sure you want to delete these records? They can be recovered from the Options menu.`}
onConfirmClick={async () => {
onActionStartedCallback?.({
key: 'delete-multiple-records',
});
await handleDeleteClick();
onActionExecutedCallback?.({
key: 'delete-multiple-records',
});
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={'Delete Records'}
/>
),
});
}
}; };
const unregisterDeleteMultipleRecordsAction = () => {
removeActionMenuEntry('delete-multiple-records');
};
return {
registerDeleteMultipleRecordsAction,
unregisterDeleteMultipleRecordsAction,
};
};

View File

@ -1,68 +1,25 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { IconDatabaseExport } from 'twenty-ui';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys'; import { useExportRecords } from '@/object-record/record-index/export/hooks/useExportRecords';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import {
displayedExportProgress,
useExportRecords,
} from '@/object-record/record-index/export/hooks/useExportRecords';
import { useContext } from 'react';
export const useExportMultipleRecordsAction = ({ export const useExportMultipleRecordsAction = ({
objectMetadataItem, objectMetadataItem,
}: { }: {
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { download } = useExportRecords({
const { progress, download } = useExportRecords({
delayMs: 100, delayMs: 100,
objectMetadataItem, objectMetadataItem,
recordIndexId: objectMetadataItem.namePlural, recordIndexId: objectMetadataItem.namePlural,
filename: `${objectMetadataItem.nameSingular}.csv`, filename: `${objectMetadataItem.nameSingular}.csv`,
}); });
const { onActionStartedCallback, onActionExecutedCallback } = const onClick = async () => {
useContext(ActionMenuContext); await download();
const registerExportMultipleRecordsAction = ({
position,
}: {
position: number;
}) => {
addActionMenuEntry({
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: MultipleRecordsActionKeys.EXPORT,
position,
label: displayedExportProgress(progress),
shortLabel: 'Export',
Icon: IconDatabaseExport,
accent: 'default',
onClick: async () => {
await onActionStartedCallback?.({
key: MultipleRecordsActionKeys.EXPORT,
});
await download();
await onActionExecutedCallback?.({
key: MultipleRecordsActionKeys.EXPORT,
});
},
});
};
const unregisterExportMultipleRecordsAction = () => {
removeActionMenuEntry('export-multiple-records');
}; };
return { return {
registerExportMultipleRecordsAction, shouldBeRegistered: true,
unregisterExportMultipleRecordsAction, onClick,
}; };
}; };

View File

@ -1,38 +0,0 @@
import { useDeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction';
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const useMultipleRecordsActions = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const {
registerDeleteMultipleRecordsAction,
unregisterDeleteMultipleRecordsAction,
} = useDeleteMultipleRecordsAction({
objectMetadataItem,
});
const {
registerExportMultipleRecordsAction,
unregisterExportMultipleRecordsAction,
} = useExportMultipleRecordsAction({
objectMetadataItem,
});
const registerMultipleRecordsActions = () => {
registerDeleteMultipleRecordsAction({ position: 1 });
registerExportMultipleRecordsAction({ position: 2 });
};
const unregisterMultipleRecordsActions = () => {
unregisterDeleteMultipleRecordsAction();
unregisterExportMultipleRecordsAction();
};
return {
registerMultipleRecordsActions,
unregisterMultipleRecordsActions,
};
};

View File

@ -1,26 +0,0 @@
import { useNoSelectionRecordActions } from '@/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useEffect } from 'react';
export const NoSelectionActionMenuEntrySetterEffect = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const {
registerNoSelectionRecordActions,
unregisterNoSelectionRecordActions,
} = useNoSelectionRecordActions({
objectMetadataItem,
});
useEffect(() => {
registerNoSelectionRecordActions();
return () => {
unregisterNoSelectionRecordActions();
};
}, [registerNoSelectionRecordActions, unregisterNoSelectionRecordActions]);
return null;
};

View File

@ -1,108 +0,0 @@
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { expect } from '@storybook/test';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { useExportViewNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/hooks/useExportViewNoSelectionRecordAction';
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useExportViewNoSelectionRecordAction', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<JestMetadataAndApolloMocksWrapper>
<JestObjectMetadataItemSetter>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
{children}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</JestMetadataAndApolloMocksWrapper>
);
it('should register export view action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useExportViewNoSelectionRecordAction:
useExportViewNoSelectionRecordAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useExportViewNoSelectionRecordAction.registerExportViewNoSelectionRecordsAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get('export-view-no-selection'),
).toBeDefined();
expect(
result.current.actionMenuEntries.get('export-view-no-selection')
?.position,
).toBe(1);
});
it('should unregister export view action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useExportViewNoSelectionRecordAction:
useExportViewNoSelectionRecordAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useExportViewNoSelectionRecordAction.registerExportViewNoSelectionRecordsAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
act(() => {
result.current.useExportViewNoSelectionRecordAction.unregisterExportViewNoSelectionRecordsAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

@ -1,54 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { IconDatabaseExport } from 'twenty-ui';
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import {
displayedExportProgress,
useExportRecords,
} from '@/object-record/record-index/export/hooks/useExportRecords';
export const useExportViewNoSelectionRecordAction = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { progress, download } = useExportRecords({
delayMs: 100,
objectMetadataItem,
recordIndexId: objectMetadataItem.namePlural,
filename: `${objectMetadataItem.nameSingular}.csv`,
});
const registerExportViewNoSelectionRecordsAction = ({
position,
}: {
position: number;
}) => {
addActionMenuEntry({
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.Global,
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
position,
label: displayedExportProgress(progress),
Icon: IconDatabaseExport,
accent: 'default',
onClick: () => download(),
});
};
const unregisterExportViewNoSelectionRecordsAction = () => {
removeActionMenuEntry('export-view-no-selection');
};
return {
registerExportViewNoSelectionRecordsAction,
unregisterExportViewNoSelectionRecordsAction,
};
};

View File

@ -1,28 +0,0 @@
import { useExportViewNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/hooks/useExportViewNoSelectionRecordAction';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const useNoSelectionRecordActions = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const {
registerExportViewNoSelectionRecordsAction,
unregisterExportViewNoSelectionRecordsAction,
} = useExportViewNoSelectionRecordAction({
objectMetadataItem,
});
const registerNoSelectionRecordActions = () => {
registerExportViewNoSelectionRecordsAction({ position: 1 });
};
const unregisterNoSelectionRecordActions = () => {
unregisterExportViewNoSelectionRecordsAction();
};
return {
registerNoSelectionRecordActions,
unregisterNoSelectionRecordActions,
};
};

View File

@ -1,34 +0,0 @@
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { useActionMenuEntriesWithCallbacks } from '@/action-menu/hooks/useActionMenuEntriesWithCallbacks';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useEffect } from 'react';
export const SingleRecordActionMenuEntrySetterEffect = ({
objectMetadataItem,
viewType,
}: {
objectMetadataItem: ObjectMetadataItem;
viewType: ActionViewType;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { actionMenuEntries } = useActionMenuEntriesWithCallbacks(
objectMetadataItem,
viewType,
);
useEffect(() => {
for (const action of actionMenuEntries) {
addActionMenuEntry(action);
}
return () => {
for (const action of actionMenuEntries) {
removeActionMenuEntry(action.key);
}
};
}, [actionMenuEntries, addActionMenuEntry, removeActionMenuEntry]);
return null;
};

View File

@ -85,7 +85,6 @@ describe('useAddToFavoritesSingleRecordAction', () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
useAddToFavoritesSingleRecordAction({ useAddToFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem, objectMetadataItem: personMockObjectMetadataItem,
}), }),
{ {
@ -100,7 +99,6 @@ describe('useAddToFavoritesSingleRecordAction', () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
useAddToFavoritesSingleRecordAction({ useAddToFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem, objectMetadataItem: personMockObjectMetadataItem,
}), }),
{ {
@ -115,7 +113,6 @@ describe('useAddToFavoritesSingleRecordAction', () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
useAddToFavoritesSingleRecordAction({ useAddToFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem, objectMetadataItem: personMockObjectMetadataItem,
}), }),
{ {

View File

@ -39,7 +39,6 @@ describe('useDeleteSingleRecordAction', () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
useDeleteSingleRecordAction({ useDeleteSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem, objectMetadataItem: personMockObjectMetadataItem,
}), }),
{ {

View File

@ -85,7 +85,6 @@ describe('useRemoveFromFavoritesSingleRecordAction', () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
useRemoveFromFavoritesSingleRecordAction({ useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem, objectMetadataItem: personMockObjectMetadataItem,
}), }),
{ {
@ -100,7 +99,6 @@ describe('useRemoveFromFavoritesSingleRecordAction', () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
useRemoveFromFavoritesSingleRecordAction({ useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem, objectMetadataItem: personMockObjectMetadataItem,
}), }),
{ {
@ -115,7 +113,6 @@ describe('useRemoveFromFavoritesSingleRecordAction', () => {
const { result } = renderHook( const { result } = renderHook(
() => () =>
useRemoveFromFavoritesSingleRecordAction({ useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem, objectMetadataItem: personMockObjectMetadataItem,
}), }),
{ {

View File

@ -1,4 +1,5 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite'; import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@ -6,8 +7,10 @@ import { isNull } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useAddToFavoritesSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = export const useAddToFavoritesSingleRecordAction: ActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => { ({ objectMetadataItem }) => {
const recordId = useSelectedRecordIdOrThrow();
const { sortedFavorites: favorites } = useFavorites(); const { sortedFavorites: favorites } = useFavorites();
const { createFavorite } = useCreateFavorite(); const { createFavorite } = useCreateFavorite();

View File

@ -1,4 +1,5 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
@ -12,80 +13,83 @@ import { useCallback, useContext, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
({ recordId, objectMetadataItem }) => { objectMetadataItem,
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = }) => {
useState(false); const recordId = useSelectedRecordIdOrThrow();
const { resetTableRowSelection } = useRecordTable({ const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
recordTableId: objectMetadataItem.namePlural, useState(false);
});
const { deleteOneRecord } = useDeleteOneRecord({ const { resetTableRowSelection } = useRecordTable({
objectNameSingular: objectMetadataItem.nameSingular, recordTableId: objectMetadataItem.namePlural,
}); });
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId)); const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { sortedFavorites: favorites } = useFavorites(); const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const { deleteFavorite } = useDeleteFavorite();
const { closeRightDrawer } = useRightDrawer(); const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const handleDeleteClick = useCallback(async () => { const { closeRightDrawer } = useRightDrawer();
resetTableRowSelection();
const foundFavorite = favorites?.find( const handleDeleteClick = useCallback(async () => {
(favorite) => favorite.recordId === recordId, resetTableRowSelection();
);
if (isDefined(foundFavorite)) { const foundFavorite = favorites?.find(
deleteFavorite(foundFavorite.id); (favorite) => favorite.recordId === recordId,
} );
await deleteOneRecord(recordId); if (isDefined(foundFavorite)) {
}, [ deleteFavorite(foundFavorite.id);
deleteFavorite, }
deleteOneRecord,
favorites,
resetTableRowSelection,
recordId,
]);
const isRemoteObject = objectMetadataItem.isRemote; await deleteOneRecord(recordId);
}, [
deleteFavorite,
deleteOneRecord,
favorites,
resetTableRowSelection,
recordId,
]);
const { isInRightDrawer } = useContext(ActionMenuContext); const isRemoteObject = objectMetadataItem.isRemote;
const shouldBeRegistered = const { isInRightDrawer } = useContext(ActionMenuContext);
!isRemoteObject && isNull(selectedRecord?.deletedAt);
const onClick = () => { const shouldBeRegistered =
if (!shouldBeRegistered) { !isRemoteObject && isNull(selectedRecord?.deletedAt);
return;
}
setIsDeleteRecordsModalOpen(true); const onClick = () => {
}; if (!shouldBeRegistered) {
return;
}
return { setIsDeleteRecordsModalOpen(true);
shouldBeRegistered,
onClick,
ConfirmationModal: (
<ConfirmationModal
isOpen={isDeleteRecordsModalOpen}
setIsOpen={setIsDeleteRecordsModalOpen}
title={'Delete Record'}
subtitle={
'Are you sure you want to delete this record? It can be recovered from the Options menu.'
}
onConfirmClick={() => {
handleDeleteClick();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={'Delete Record'}
/>
),
};
}; };
return {
shouldBeRegistered,
onClick,
ConfirmationModal: (
<ConfirmationModal
isOpen={isDeleteRecordsModalOpen}
setIsOpen={setIsDeleteRecordsModalOpen}
title={'Delete Record'}
subtitle={
'Are you sure you want to delete this record? It can be recovered from the Options menu.'
}
onConfirmClick={() => {
handleDeleteClick();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={'Delete Record'}
/>
),
};
};

View File

@ -1,4 +1,5 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord'; import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@ -9,63 +10,66 @@ import { useCallback, useContext, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useDestroySingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
({ recordId, objectMetadataItem }) => { objectMetadataItem,
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] = }) => {
useState(false); const recordId = useSelectedRecordIdOrThrow();
const { resetTableRowSelection } = useRecordTable({ const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
recordTableId: objectMetadataItem.namePlural, useState(false);
});
const { destroyOneRecord } = useDestroyOneRecord({ const { resetTableRowSelection } = useRecordTable({
objectNameSingular: objectMetadataItem.nameSingular, recordTableId: objectMetadataItem.namePlural,
}); });
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId)); const { destroyOneRecord } = useDestroyOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { closeRightDrawer } = useRightDrawer(); const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const handleDeleteClick = useCallback(async () => { const { closeRightDrawer } = useRightDrawer();
resetTableRowSelection();
await destroyOneRecord(recordId); const handleDeleteClick = useCallback(async () => {
}, [resetTableRowSelection, destroyOneRecord, recordId]); resetTableRowSelection();
const isRemoteObject = objectMetadataItem.isRemote; await destroyOneRecord(recordId);
}, [resetTableRowSelection, destroyOneRecord, recordId]);
const { isInRightDrawer } = useContext(ActionMenuContext); const isRemoteObject = objectMetadataItem.isRemote;
const shouldBeRegistered = const { isInRightDrawer } = useContext(ActionMenuContext);
!isRemoteObject && isDefined(selectedRecord?.deletedAt);
const onClick = () => { const shouldBeRegistered =
if (!shouldBeRegistered) { !isRemoteObject && isDefined(selectedRecord?.deletedAt);
return;
}
setIsDestroyRecordsModalOpen(true); const onClick = () => {
}; if (!shouldBeRegistered) {
return;
}
return { setIsDestroyRecordsModalOpen(true);
shouldBeRegistered,
onClick,
ConfirmationModal: (
<ConfirmationModal
isOpen={isDestroyRecordsModalOpen}
setIsOpen={setIsDestroyRecordsModalOpen}
title={'Permanently Destroy Record'}
subtitle={
'Are you sure you want to destroy this record? It cannot be recovered anymore.'
}
onConfirmClick={async () => {
await handleDeleteClick();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={'Permanently Destroy Record'}
/>
),
};
}; };
return {
shouldBeRegistered,
onClick,
ConfirmationModal: (
<ConfirmationModal
isOpen={isDestroyRecordsModalOpen}
setIsOpen={setIsDestroyRecordsModalOpen}
title={'Permanently Destroy Record'}
subtitle={
'Are you sure you want to destroy this record? It cannot be recovered anymore.'
}
onConfirmClick={async () => {
await handleDeleteClick();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={'Permanently Destroy Record'}
/>
),
};
};

View File

@ -1,49 +1,51 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { BlockNoteEditor } from '@blocknote/core'; import { BlockNoteEditor } from '@blocknote/core';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useExportNoteAction: SingleRecordActionHookWithObjectMetadataItem = export const useExportNoteAction: ActionHookWithObjectMetadataItem = ({
({ recordId, objectMetadataItem }) => { objectMetadataItem,
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId)); }) => {
const recordId = useSelectedRecordIdOrThrow();
const filename = `${(selectedRecord?.title || 'Untitled Note').replace(/[<>:"/\\|?*]/g, '-')}`; const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const isNoteOrTask = const filename = `${(selectedRecord?.title || 'Untitled Note').replace(/[<>:"/\\|?*]/g, '-')}`;
objectMetadataItem?.nameSingular === CoreObjectNameSingular.Note ||
objectMetadataItem?.nameSingular === CoreObjectNameSingular.Task;
const shouldBeRegistered = const isNoteOrTask =
isDefined(objectMetadataItem) && objectMetadataItem?.nameSingular === CoreObjectNameSingular.Note ||
isDefined(selectedRecord) && objectMetadataItem?.nameSingular === CoreObjectNameSingular.Task;
isNoteOrTask;
const onClick = async () => { const shouldBeRegistered =
if (!shouldBeRegistered || !selectedRecord?.body) { isDefined(objectMetadataItem) && isDefined(selectedRecord) && isNoteOrTask;
return;
}
const editor = await BlockNoteEditor.create({ const onClick = async () => {
initialContent: JSON.parse(selectedRecord.body), if (!shouldBeRegistered || !selectedRecord?.body) {
}); return;
}
const { exportBlockNoteEditorToPdf } = await import( const editor = await BlockNoteEditor.create({
'@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf' initialContent: JSON.parse(selectedRecord.body),
); });
await exportBlockNoteEditorToPdf(editor, filename); const { exportBlockNoteEditorToPdf } = await import(
'@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf'
);
// TODO later: implement DOCX export await exportBlockNoteEditorToPdf(editor, filename);
// const { exportBlockNoteEditorToDocx } = await import(
// '@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToDocx'
// );
// await exportBlockNoteEditorToDocx(editor, filename);
};
return { // TODO later: implement DOCX export
shouldBeRegistered, // const { exportBlockNoteEditorToDocx } = await import(
onClick, // '@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToDocx'
}; // );
// await exportBlockNoteEditorToDocx(editor, filename);
}; };
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -1,10 +1,13 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination'; import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { useContext } from 'react'; import { useContext } from 'react';
export const useNavigateToNextRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = export const useNavigateToNextRecordSingleRecordAction: ActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => { ({ objectMetadataItem }) => {
const recordId = useSelectedRecordIdOrThrow();
const { isInRightDrawer } = useContext(ActionMenuContext); const { isInRightDrawer } = useContext(ActionMenuContext);
const { navigateToNextRecord } = useRecordShowPagePagination( const { navigateToNextRecord } = useRecordShowPagePagination(

View File

@ -1,11 +1,15 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination'; import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { useContext } from 'react'; import { useContext } from 'react';
export const useNavigateToPreviousRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = export const useNavigateToPreviousRecordSingleRecordAction: ActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => { ({ objectMetadataItem }) => {
const recordId = useSelectedRecordIdOrThrow();
const { isInRightDrawer } = useContext(ActionMenuContext); const { isInRightDrawer } = useContext(ActionMenuContext);
const { navigateToPreviousRecord } = useRecordShowPagePagination( const { navigateToPreviousRecord } = useRecordShowPagePagination(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
recordId, recordId,

View File

@ -1,10 +1,13 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useRemoveFromFavoritesSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = export const useRemoveFromFavoritesSingleRecordAction: ActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => { ({ objectMetadataItem }) => {
const recordId = useSelectedRecordIdOrThrow();
const { sortedFavorites: favorites } = useFavorites(); const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite(); const { deleteFavorite } = useDeleteFavorite();

View File

@ -0,0 +1,18 @@
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const useSelectedRecordIdOrThrow = () => {
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
if (
contextStoreTargetedRecordsRule.mode === 'exclusion' ||
(contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0)
) {
throw new Error('Selected record ID is required');
}
return contextStoreTargetedRecordsRule.selectedRecordIds[0];
};

View File

@ -1,27 +0,0 @@
import { DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1 } from '@/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV1';
import { DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2 } from '@/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2';
import { WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/single-record/workflow-actions/constants/WorkflowSingleRecordActionsConfig';
import { WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/constants/WorkflowRunsSingleRecordActionsConfig';
import { WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/constants/WorkflowVersionsSingleRecordActionsConfig';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getActionConfig = (
objectMetadataItem: ObjectMetadataItem,
isPageHeaderV2Enabled: boolean,
) => {
if (objectMetadataItem.nameSingular === CoreObjectNameSingular.Workflow) {
return WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG;
}
if (
objectMetadataItem.nameSingular === CoreObjectNameSingular.WorkflowVersion
) {
return WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG;
}
if (objectMetadataItem.nameSingular === CoreObjectNameSingular.WorkflowRun) {
return WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG;
}
return isPageHeaderV2Enabled
? DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2
: DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1;
};

View File

@ -55,10 +55,7 @@ const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
describe('useActivateDraftWorkflowSingleRecordAction', () => { describe('useActivateDraftWorkflowSingleRecordAction', () => {
it('should be registered', () => { it('should be registered', () => {
const { result } = renderHook( const { result } = renderHook(
() => () => useActivateDraftWorkflowSingleRecordAction(),
useActivateDraftWorkflowSingleRecordAction({
recordId: workflowMock.id,
}),
{ {
wrapper, wrapper,
}, },
@ -69,10 +66,7 @@ describe('useActivateDraftWorkflowSingleRecordAction', () => {
it('should call activateWorkflowVersion on click', () => { it('should call activateWorkflowVersion on click', () => {
const { result } = renderHook( const { result } = renderHook(
() => () => useActivateDraftWorkflowSingleRecordAction(),
useActivateDraftWorkflowSingleRecordAction({
recordId: workflowMock.id,
}),
{ {
wrapper, wrapper,
}, },

View File

@ -56,10 +56,7 @@ const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
describe('useActivateLastPublishedVersionWorkflowSingleRecordAction', () => { describe('useActivateLastPublishedVersionWorkflowSingleRecordAction', () => {
it('should be registered', () => { it('should be registered', () => {
const { result } = renderHook( const { result } = renderHook(
() => () => useActivateLastPublishedVersionWorkflowSingleRecordAction(),
useActivateLastPublishedVersionWorkflowSingleRecordAction({
recordId: workflowMock.id,
}),
{ {
wrapper, wrapper,
}, },
@ -70,10 +67,7 @@ describe('useActivateLastPublishedVersionWorkflowSingleRecordAction', () => {
it('should call activateWorkflowVersion on click', () => { it('should call activateWorkflowVersion on click', () => {
const { result } = renderHook( const { result } = renderHook(
() => () => useActivateLastPublishedVersionWorkflowSingleRecordAction(),
useActivateLastPublishedVersionWorkflowSingleRecordAction({
recordId: workflowMock.id,
}),
{ {
wrapper, wrapper,
}, },

View File

@ -104,10 +104,7 @@ describe('useDeactivateWorkflowSingleRecordAction', () => {
() => deactivatedWorkflowMock, () => deactivatedWorkflowMock,
); );
const { result } = renderHook( const { result } = renderHook(
() => () => useDeactivateWorkflowSingleRecordAction(),
useDeactivateWorkflowSingleRecordAction({
recordId: deactivatedWorkflowMock.id,
}),
{ {
wrapper: deactivatedWorkflowWrapper, wrapper: deactivatedWorkflowWrapper,
}, },
@ -121,10 +118,7 @@ describe('useDeactivateWorkflowSingleRecordAction', () => {
() => activeWorkflowMock, () => activeWorkflowMock,
); );
const { result } = renderHook( const { result } = renderHook(
() => () => useDeactivateWorkflowSingleRecordAction(),
useDeactivateWorkflowSingleRecordAction({
recordId: activeWorkflowMock.id,
}),
{ {
wrapper: activeWorkflowWrapper, wrapper: activeWorkflowWrapper,
}, },
@ -138,10 +132,7 @@ describe('useDeactivateWorkflowSingleRecordAction', () => {
() => activeWorkflowMock, () => activeWorkflowMock,
); );
const { result } = renderHook( const { result } = renderHook(
() => () => useDeactivateWorkflowSingleRecordAction(),
useDeactivateWorkflowSingleRecordAction({
recordId: activeWorkflowMock.id,
}),
{ {
wrapper: activeWorkflowWrapper, wrapper: activeWorkflowWrapper,
}, },

View File

@ -178,10 +178,7 @@ describe('useDiscardDraftWorkflowSingleRecordAction', () => {
() => noDraftWorkflowMock, () => noDraftWorkflowMock,
); );
const { result } = renderHook( const { result } = renderHook(
() => () => useDiscardDraftWorkflowSingleRecordAction(),
useDiscardDraftWorkflowSingleRecordAction({
recordId: noDraftWorkflowMock.id,
}),
{ {
wrapper: noDraftWorkflowWrapper, wrapper: noDraftWorkflowWrapper,
}, },
@ -196,10 +193,7 @@ describe('useDiscardDraftWorkflowSingleRecordAction', () => {
); );
const { result } = renderHook( const { result } = renderHook(
() => () => useDiscardDraftWorkflowSingleRecordAction(),
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMockWithOneVersion.id,
}),
{ {
wrapper: draftWorkflowWithOneVersionWrapper, wrapper: draftWorkflowWithOneVersionWrapper,
}, },
@ -213,10 +207,7 @@ describe('useDiscardDraftWorkflowSingleRecordAction', () => {
() => draftWorkflowMock, () => draftWorkflowMock,
); );
const { result } = renderHook( const { result } = renderHook(
() => () => useDiscardDraftWorkflowSingleRecordAction(),
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMock.id,
}),
{ {
wrapper: draftWorkflowWrapper, wrapper: draftWorkflowWrapper,
}, },
@ -230,10 +221,7 @@ describe('useDiscardDraftWorkflowSingleRecordAction', () => {
() => draftWorkflowMock, () => draftWorkflowMock,
); );
const { result } = renderHook( const { result } = renderHook(
() => () => useDiscardDraftWorkflowSingleRecordAction(),
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMock.id,
}),
{ {
wrapper: draftWorkflowWrapper, wrapper: draftWorkflowWrapper,
}, },

View File

@ -1,10 +1,13 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion'; import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useActivateDraftWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useActivateDraftWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const { activateWorkflowVersion } = useActivateWorkflowVersion(); const { activateWorkflowVersion } = useActivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId); const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);

View File

@ -1,10 +1,13 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion'; import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useActivateLastPublishedVersionWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useActivateLastPublishedVersionWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const { activateWorkflowVersion } = useActivateWorkflowVersion(); const { activateWorkflowVersion } = useActivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId); const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);

View File

@ -1,10 +1,13 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion'; import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useDeactivateWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useDeactivateWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion(); const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId); const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);

View File

@ -1,10 +1,13 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion'; import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useDiscardDraftWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useDiscardDraftWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion(); const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId); const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);

View File

@ -1,12 +1,15 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion'; import { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useSeeActiveVersionWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useSeeActiveVersionWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const workflow = useWorkflowWithCurrentVersion(recordId); const workflow = useWorkflowWithCurrentVersion(recordId);
const isDraft = workflow?.statuses?.includes('DRAFT') || false; const isDraft = workflow?.statuses?.includes('DRAFT') || false;

View File

@ -1,4 +1,5 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
@ -6,8 +7,10 @@ import qs from 'qs';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useSeeRunsWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useSeeRunsWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId); const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -1,4 +1,5 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
@ -6,8 +7,10 @@ import qs from 'qs';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useSeeVersionsWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useSeeVersionsWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId); const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -1,10 +1,13 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useTestWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useTestWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId); const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const { runWorkflowVersion } = useRunWorkflowVersion(); const { runWorkflowVersion } = useRunWorkflowVersion();

View File

@ -1,4 +1,5 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
@ -8,8 +9,10 @@ import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useSeeRunsWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useSeeRunsWorkflowVersionSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId)); const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion( const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(

View File

@ -1,21 +1,23 @@
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
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 { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useSeeVersionsWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useSeeVersionsWorkflowVersionSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId)); const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
if (!isDefined(workflowVersion)) { if (!isDefined(workflowVersion)) {
throw new Error('Workflow version not found'); throw new Error('Workflow version not found');
} }
// TODO: Add recordIds to the hook
const { shouldBeRegistered, onClick } = const { shouldBeRegistered, onClick } =
useSeeVersionsWorkflowSingleRecordAction({ useSeeVersionsWorkflowSingleRecordAction();
recordId: workflowVersion.workflow.id,
});
return { return {
shouldBeRegistered, shouldBeRegistered,

View File

@ -1,4 +1,5 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL'; import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL';
import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal'; import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
@ -10,8 +11,10 @@ import { useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useUseAsDraftWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem = export const useUseAsDraftWorkflowVersionSingleRecordAction: ActionHookWithoutObjectMetadataItem =
({ recordId }) => { () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useWorkflowVersion(recordId); const workflowVersion = useWorkflowVersion(recordId);
const workflow = useWorkflowWithCurrentVersion( const workflow = useWorkflowWithCurrentVersion(

View File

@ -0,0 +1,14 @@
import { ActionHookResult } from '@/action-menu/actions/types/ActionHookResult';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export type ActionHook =
| ActionHookWithoutObjectMetadataItem
| ActionHookWithObjectMetadataItem;
export type ActionHookWithoutObjectMetadataItem = () => ActionHookResult;
export type ActionHookWithObjectMetadataItem = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => ActionHookResult;

View File

@ -1,20 +0,0 @@
import { ActionHookResult } from '@/action-menu/actions/types/ActionHookResult';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export type SingleRecordActionHook =
| SingleRecordActionHookWithoutObjectMetadataItem
| SingleRecordActionHookWithObjectMetadataItem;
export type SingleRecordActionHookWithoutObjectMetadataItem = ({
recordId,
}: {
recordId: string;
}) => ActionHookResult;
export type SingleRecordActionHookWithObjectMetadataItem = ({
recordId,
objectMetadataItem,
}: {
recordId: string;
objectMetadataItem: ObjectMetadataItem;
}) => ActionHookResult;

View File

@ -0,0 +1,25 @@
import { DEFAULT_ACTIONS_CONFIG_V1 } from '@/action-menu/actions/record-actions/constants/DefaultActionsConfigV1';
import { DEFAULT_ACTIONS_CONFIG_V2 } from '@/action-menu/actions/record-actions/constants/DefaultActionsConfigV2';
import { WORKFLOW_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowActionsConfig';
import { WORKFLOW_RUNS_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowRunsActionsConfig';
import { WORKFLOW_VERSIONS_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowVersionsActionsConfig';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getActionConfig = (
objectMetadataItem: ObjectMetadataItem,
isPageHeaderV2Enabled: boolean,
) => {
switch (objectMetadataItem.nameSingular) {
case CoreObjectNameSingular.Workflow:
return WORKFLOW_ACTIONS_CONFIG;
case CoreObjectNameSingular.WorkflowVersion:
return WORKFLOW_VERSIONS_ACTIONS_CONFIG;
case CoreObjectNameSingular.WorkflowRun:
return WORKFLOW_RUNS_ACTIONS_CONFIG;
default:
return isPageHeaderV2Enabled
? DEFAULT_ACTIONS_CONFIG_V2
: DEFAULT_ACTIONS_CONFIG_V1;
}
};

View File

@ -0,0 +1,32 @@
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
export const getActionViewType = (
contextStoreCurrentViewType: ContextStoreViewType | null,
contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule,
) => {
if (contextStoreCurrentViewType === null) {
return null;
}
if (contextStoreCurrentViewType === ContextStoreViewType.ShowPage) {
return ActionViewType.SHOW_PAGE;
}
if (
contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0
) {
return ActionViewType.INDEX_PAGE_NO_SELECTION;
}
if (
contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1
) {
return ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION;
}
return ActionViewType.INDEX_PAGE_BULK_SELECTION;
};

View File

@ -1,70 +0,0 @@
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
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 { useContext } from 'react';
import { isDefined } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
export const useActionMenuEntriesWithCallbacks = (
objectMetadataItem: ObjectMetadataItem,
viewType: ActionViewType,
) => {
const isPageHeaderV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPageHeaderV2Enabled,
);
const actionConfig = getActionConfig(
objectMetadataItem,
isPageHeaderV2Enabled,
);
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 { onActionStartedCallback, onActionExecutedCallback } =
useContext(ActionMenuContext);
const actionMenuEntries = Object.values(actionConfig ?? {})
.filter((action) => action.availableOn?.includes(viewType))
.map((action) => {
const { shouldBeRegistered, onClick, ConfirmationModal } =
action.actionHook({
recordId: selectedRecordId,
objectMetadataItem,
});
if (!shouldBeRegistered) {
return undefined;
}
const wrappedAction = wrapActionInCallbacks({
action: {
...action,
onClick,
ConfirmationModal,
},
onActionStartedCallback,
onActionExecutedCallback,
});
return wrappedAction;
})
.filter(isDefined);
return { actionMenuEntries };
};

View File

@ -1,6 +1,7 @@
import { ReactNode, useEffect, useState } from 'react'; import { ReactNode, useEffect, useState } from 'react';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { import {
ContextStoreTargetedRecordsRule, ContextStoreTargetedRecordsRule,
contextStoreTargetedRecordsRuleComponentState, contextStoreTargetedRecordsRuleComponentState,
@ -13,10 +14,12 @@ export const JestContextStoreSetter = ({
mode: 'selection', mode: 'selection',
selectedRecordIds: [], selectedRecordIds: [],
}, },
contextStoreNumberOfSelectedRecords = 0,
contextStoreCurrentObjectMetadataNameSingular = '', contextStoreCurrentObjectMetadataNameSingular = '',
children, children,
}: { }: {
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
contextStoreNumberOfSelectedRecords?: number;
contextStoreCurrentObjectMetadataNameSingular?: string; contextStoreCurrentObjectMetadataNameSingular?: string;
children: ReactNode; children: ReactNode;
}) => { }) => {
@ -28,6 +31,10 @@ export const JestContextStoreSetter = ({
contextStoreCurrentObjectMetadataIdComponentState, contextStoreCurrentObjectMetadataIdComponentState,
); );
const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: contextStoreCurrentObjectMetadataNameSingular, objectNameSingular: contextStoreCurrentObjectMetadataNameSingular,
}); });
@ -38,12 +45,15 @@ export const JestContextStoreSetter = ({
useEffect(() => { useEffect(() => {
setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule); setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule);
setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId); setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId);
setContextStoreNumberOfSelectedRecords(contextStoreNumberOfSelectedRecords);
setIsLoaded(true); setIsLoaded(true);
}, [ }, [
setContextStoreTargetedRecordsRule, setContextStoreTargetedRecordsRule,
setContextStoreCurrentObjectMetadataId, setContextStoreCurrentObjectMetadataId,
contextStoreTargetedRecordsRule, contextStoreTargetedRecordsRule,
contextStoreCurrentObjectMetadataId, contextStoreCurrentObjectMetadataId,
setContextStoreNumberOfSelectedRecords,
contextStoreNumberOfSelectedRecords,
]); ]);
return isLoaded ? <>{children}</> : null; return isLoaded ? <>{children}</> : null;

View File

@ -13,6 +13,7 @@ export type GetJestMetadataAndApolloMocksAndActionMenuWrapperProps = {
| undefined; | undefined;
onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void; onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void;
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
contextStoreNumberOfSelectedRecords?: number;
contextStoreCurrentObjectMetadataNameSingular?: string; contextStoreCurrentObjectMetadataNameSingular?: string;
componentInstanceId: string; componentInstanceId: string;
}; };
@ -21,6 +22,7 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
apolloMocks, apolloMocks,
onInitializeRecoilSnapshot, onInitializeRecoilSnapshot,
contextStoreTargetedRecordsRule, contextStoreTargetedRecordsRule,
contextStoreNumberOfSelectedRecords,
contextStoreCurrentObjectMetadataNameSingular, contextStoreCurrentObjectMetadataNameSingular,
componentInstanceId, componentInstanceId,
}: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps) => { }: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps) => {
@ -43,6 +45,9 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
> >
<JestContextStoreSetter <JestContextStoreSetter
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule} contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
contextStoreNumberOfSelectedRecords={
contextStoreNumberOfSelectedRecords
}
contextStoreCurrentObjectMetadataNameSingular={ contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular contextStoreCurrentObjectMetadataNameSingular
} }