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 { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect';
import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect';
import { RegisterRecordActionEffect } from '@/action-menu/actions/record-actions/components/RegisterRecordActionEffect';
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 { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@ -19,33 +16,13 @@ export const RecordActionMenuEntriesSetter = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
(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(
contextStoreTargetedRecordsRuleComponentState,
);
@ -58,43 +35,52 @@ const ActionEffects = ({
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 (
<>
{contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0 && (
<NoSelectionActionMenuEntrySetterEffect
{actionsToRegister.map((action) => (
<RegisterRecordActionEffect
key={action.key}
action={action}
objectMetadataItem={objectMetadataItem}
/>
))}
{isWorkflowEnabled &&
!(
contextStoreTargetedRecordsRule?.mode === 'selection' &&
contextStoreTargetedRecordsRule?.selectedRecordIds.length === 0
) && (
<WorkflowRunRecordActionMenuEntrySetterEffect
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 { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
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 { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { IconHeart, IconHeartOff, IconTrash } from 'twenty-ui';
import {
IconDatabaseExport,
IconHeart,
IconHeartOff,
IconTrash,
} from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
export const DEFAULT_ACTIONS_CONFIG_V1: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
actionHook: ActionHook;
}
> = {
addToFavoritesSingleRecord: {
@ -58,4 +67,43 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
],
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 { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
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 { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
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 { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
@ -16,6 +20,7 @@ import {
import {
IconChevronDown,
IconChevronUp,
IconDatabaseExport,
IconFileExport,
IconHeart,
IconHeartOff,
@ -23,10 +28,10 @@ import {
IconTrashX,
} from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
export const DEFAULT_ACTIONS_CONFIG_V2: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
actionHook: ActionHook;
}
> = {
exportNoteToPdf: {
@ -87,13 +92,52 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
],
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: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: SingleRecordActionKeys.DESTROY,
label: 'Permanently destroy record',
shortLabel: 'Destroy',
position: 4,
position: 7,
Icon: IconTrashX,
accent: 'danger',
isPinned: true,
@ -109,7 +153,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
key: SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
label: 'Navigate to previous record',
shortLabel: '',
position: 5,
position: 8,
isPinned: true,
Icon: IconChevronUp,
availableOn: [ActionViewType.SHOW_PAGE],
@ -121,7 +165,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
key: SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
label: 'Navigate to next record',
shortLabel: '',
position: 6,
position: 9,
isPinned: true,
Icon: IconChevronDown,
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 { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
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 { 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 { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
@ -24,6 +28,7 @@ import {
import {
IconChevronDown,
IconChevronUp,
IconDatabaseExport,
IconHeart,
IconHeartOff,
IconHistory,
@ -35,10 +40,10 @@ import {
IconTrashX,
} from 'twenty-ui';
export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
export const WORKFLOW_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
actionHook: ActionHook;
}
> = {
activateWorkflowDraftSingleRecord: {
@ -229,13 +234,26 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
],
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: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: SingleRecordActionKeys.DESTROY,
label: 'Permanently destroy record',
shortLabel: 'Destroy',
position: 14,
position: 15,
Icon: IconTrashX,
accent: 'danger',
isPinned: false,
@ -245,4 +263,30 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
],
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 { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
import { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
@ -13,14 +16,15 @@ import {
import {
IconChevronDown,
IconChevronUp,
IconDatabaseExport,
IconHeart,
IconHeartOff,
} from 'twenty-ui';
export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
actionHook: ActionHook;
}
> = {
addToFavoritesSingleRecord: {
@ -77,4 +81,30 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
availableOn: [ActionViewType.SHOW_PAGE],
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 { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
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 { 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 { ActionHook } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
@ -17,6 +20,7 @@ import {
import {
IconChevronDown,
IconChevronUp,
IconDatabaseExport,
IconHeart,
IconHeartOff,
IconHistory,
@ -24,10 +28,10 @@ import {
IconPencil,
} from 'twenty-ui';
export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
actionHook: ActionHook;
}
> = {
useAsDraftWorkflowVersionSingleRecord: {
@ -125,4 +129,30 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
],
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 { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { renderHook, waitFor } from '@testing-library/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 { getPeopleMock } from '~/testing/mock-data/people';
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', () => ({
useDeleteManyRecords: () => ({
deleteManyRecords: jest.fn(),
deleteManyRecords: deleteManyRecordsMock,
}),
}));
jest.mock('@/favorites/hooks/useDeleteFavorite', () => ({
useDeleteFavorite: () => ({
deleteFavorite: jest.fn(),
}),
}));
jest.mock('@/favorites/hooks/useFavorites', () => ({
useFavorites: () => ({
sortedFavorites: [],
}),
jest.mock('@/object-record/hooks/useLazyFetchAllRecords', () => ({
useLazyFetchAllRecords: () => {
return {
fetchAllRecords: () => [peopleMock[0], peopleMock[1]],
};
},
}));
jest.mock('@/object-record/record-table/hooks/useRecordTable', () => ({
useRecordTable: () => ({
resetTableRowSelection: jest.fn(),
resetTableRowSelection: resetTableRowSelectionMock,
}),
}));
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
onInitializeRecoilSnapshot: ({ set }) => {
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: '1',
}),
3,
);
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('useDeleteMultipleRecordsAction', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<JestMetadataAndApolloMocksWrapper>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
{children}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</JestMetadataAndApolloMocksWrapper>
);
it('should register delete action', () => {
it('should call deleteManyRecords on click', async () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeleteMultipleRecordsAction: useDeleteMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
() =>
useDeleteMultipleRecordsAction({
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper,
},
{ wrapper },
);
act(() => {
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 },
);
expect(result.current.ConfirmationModal?.props?.isOpen).toBe(false);
act(() => {
result.current.useDeleteMultipleRecordsAction.registerDeleteMultipleRecordsAction(
{ position: 1 },
);
result.current.onClick();
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(result.current.ConfirmationModal?.props?.isOpen).toBe(true);
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 { 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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { renderHook, waitFor } from '@testing-library/react';
import { act } from 'react';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getPeopleMock } from '~/testing/mock-data/people';
import { useExportMultipleRecordsAction } from '../useExportMultipleRecordsAction';
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(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: [],
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', () => {
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 multiple records action', () => {
it('should call exportManyRecords on click', async () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useExportMultipleRecordsAction: useExportMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
() =>
useExportMultipleRecordsAction({
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper,
},
{ wrapper },
);
act(() => {
result.current.useExportMultipleRecordsAction.registerExportMultipleRecordsAction(
{ position: 1 },
);
result.current.onClick();
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
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 },
);
await waitFor(() => {
expect(downloadMock).toHaveBeenCalled();
});
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 { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
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 { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
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 { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
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 { useCallback, useContext, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui';
import { useCallback, useState } from 'react';
import { isDefined } from 'twenty-ui';
export const useDeleteMultipleRecordsAction = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
export const useDeleteMultipleRecordsAction: ActionHookWithObjectMetadataItem =
({ objectMetadataItem }) => {
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
const { deleteManyRecords } = useDeleteManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { deleteManyRecords } = useDeleteManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
});
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const contextStoreFilters = useRecoilComponentValueV2(
contextStoreFiltersComponentState,
);
const contextStoreFilters = useRecoilComponentValueV2(
contextStoreFiltersComponentState,
);
const { filterValueDependencies } = useFilterValueDependencies();
const { filterValueDependencies } = useFilterValueDependencies();
const graphqlFilter = computeContextStoreFilters(
contextStoreTargetedRecordsRule,
contextStoreFilters,
objectMetadataItem,
filterValueDependencies,
);
const graphqlFilter = computeContextStoreFilters(
contextStoreTargetedRecordsRule,
contextStoreFilters,
objectMetadataItem,
filterValueDependencies,
);
const deletedAtFieldMetadata = objectMetadataItem.fields.find(
(field) => field.name === 'deletedAt',
);
const deletedAtFieldMetadata = objectMetadataItem.fields.find(
(field) => field.name === 'deletedAt',
);
const isDeletedFilterActive = contextStoreFilters.some(
(filter) =>
filter.fieldMetadataId === deletedAtFieldMetadata?.id &&
filter.operand === FilterOperand.IsNotEmpty,
);
const isDeletedFilterActive = contextStoreFilters.some(
(filter) =>
filter.fieldMetadataId === deletedAtFieldMetadata?.id &&
filter.operand === FilterOperand.IsNotEmpty,
);
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
objectNameSingular: objectMetadataItem.nameSingular,
filter: graphqlFilter,
limit: DEFAULT_QUERY_PAGE_SIZE,
recordGqlFields: { id: true },
});
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
objectNameSingular: objectMetadataItem.nameSingular,
filter: graphqlFilter,
limit: DEFAULT_QUERY_PAGE_SIZE,
recordGqlFields: { id: true },
});
const handleDeleteClick = useCallback(async () => {
const recordsToDelete = await fetchAllRecordIds();
const recordIdsToDelete = recordsToDelete.map((record) => record.id);
const { closeRightDrawer } = useRightDrawer();
resetTableRowSelection();
const handleDeleteClick = useCallback(async () => {
const recordsToDelete = await fetchAllRecordIds();
const recordIdsToDelete = recordsToDelete.map((record) => record.id);
await deleteManyRecords(recordIdsToDelete);
}, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]);
resetTableRowSelection();
const isRemoteObject = objectMetadataItem.isRemote;
await deleteManyRecords(recordIdsToDelete);
}, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]);
const shouldBeRegistered =
!isRemoteObject &&
!isDeletedFilterActive &&
isDefined(contextStoreNumberOfSelectedRecords) &&
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;
const isRemoteObject = objectMetadataItem.isRemote;
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
const canDelete =
!isRemoteObject &&
!isDeletedFilterActive &&
isDefined(contextStoreNumberOfSelectedRecords) &&
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;
setIsDeleteRecordsModalOpen(true);
};
const { isInRightDrawer, onActionStartedCallback, onActionExecutedCallback } =
useContext(ActionMenuContext);
const 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={handleDeleteClick}
deleteButtonText={'Delete Records'}
/>
);
const registerDeleteMultipleRecordsAction = ({
position,
}: {
position: number;
}) => {
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'}
/>
),
});
}
return {
shouldBeRegistered,
onClick,
ConfirmationModal: confirmationModal,
};
};
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 { IconDatabaseExport } from 'twenty-ui';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
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';
import { useExportRecords } from '@/object-record/record-index/export/hooks/useExportRecords';
export const useExportMultipleRecordsAction = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { progress, download } = useExportRecords({
const { download } = useExportRecords({
delayMs: 100,
objectMetadataItem,
recordIndexId: objectMetadataItem.namePlural,
filename: `${objectMetadataItem.nameSingular}.csv`,
});
const { onActionStartedCallback, onActionExecutedCallback } =
useContext(ActionMenuContext);
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');
const onClick = async () => {
await download();
};
return {
registerExportMultipleRecordsAction,
unregisterExportMultipleRecordsAction,
shouldBeRegistered: true,
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(
() =>
useAddToFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
@ -100,7 +99,6 @@ describe('useAddToFavoritesSingleRecordAction', () => {
const { result } = renderHook(
() =>
useAddToFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
@ -115,7 +113,6 @@ describe('useAddToFavoritesSingleRecordAction', () => {
const { result } = renderHook(
() =>
useAddToFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{

View File

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

View File

@ -85,7 +85,6 @@ describe('useRemoveFromFavoritesSingleRecordAction', () => {
const { result } = renderHook(
() =>
useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
@ -100,7 +99,6 @@ describe('useRemoveFromFavoritesSingleRecordAction', () => {
const { result } = renderHook(
() =>
useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
@ -115,7 +113,6 @@ describe('useRemoveFromFavoritesSingleRecordAction', () => {
const { result } = renderHook(
() =>
useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
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 { useFavorites } from '@/favorites/hooks/useFavorites';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@ -6,8 +7,10 @@ import { isNull } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useAddToFavoritesSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
export const useAddToFavoritesSingleRecordAction: ActionHookWithObjectMetadataItem =
({ objectMetadataItem }) => {
const recordId = useSelectedRecordIdOrThrow();
const { sortedFavorites: favorites } = useFavorites();
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 { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
@ -12,80 +13,83 @@ import { useCallback, useContext, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
objectMetadataItem,
}) => {
const recordId = useSelectedRecordIdOrThrow();
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const { closeRightDrawer } = useRightDrawer();
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const handleDeleteClick = useCallback(async () => {
resetTableRowSelection();
const { closeRightDrawer } = useRightDrawer();
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordId,
);
const handleDeleteClick = useCallback(async () => {
resetTableRowSelection();
if (isDefined(foundFavorite)) {
deleteFavorite(foundFavorite.id);
}
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordId,
);
await deleteOneRecord(recordId);
}, [
deleteFavorite,
deleteOneRecord,
favorites,
resetTableRowSelection,
recordId,
]);
if (isDefined(foundFavorite)) {
deleteFavorite(foundFavorite.id);
}
const isRemoteObject = objectMetadataItem.isRemote;
await deleteOneRecord(recordId);
}, [
deleteFavorite,
deleteOneRecord,
favorites,
resetTableRowSelection,
recordId,
]);
const { isInRightDrawer } = useContext(ActionMenuContext);
const isRemoteObject = objectMetadataItem.isRemote;
const shouldBeRegistered =
!isRemoteObject && isNull(selectedRecord?.deletedAt);
const { isInRightDrawer } = useContext(ActionMenuContext);
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
const shouldBeRegistered =
!isRemoteObject && isNull(selectedRecord?.deletedAt);
setIsDeleteRecordsModalOpen(true);
};
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
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'}
/>
),
};
setIsDeleteRecordsModalOpen(true);
};
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 { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@ -9,63 +10,66 @@ import { useCallback, useContext, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useDestroySingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
useState(false);
export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
objectMetadataItem,
}) => {
const recordId = useSelectedRecordIdOrThrow();
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
useState(false);
const { destroyOneRecord } = useDestroyOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { resetTableRowSelection } = useRecordTable({
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 () => {
resetTableRowSelection();
const { closeRightDrawer } = useRightDrawer();
await destroyOneRecord(recordId);
}, [resetTableRowSelection, destroyOneRecord, recordId]);
const handleDeleteClick = useCallback(async () => {
resetTableRowSelection();
const isRemoteObject = objectMetadataItem.isRemote;
await destroyOneRecord(recordId);
}, [resetTableRowSelection, destroyOneRecord, recordId]);
const { isInRightDrawer } = useContext(ActionMenuContext);
const isRemoteObject = objectMetadataItem.isRemote;
const shouldBeRegistered =
!isRemoteObject && isDefined(selectedRecord?.deletedAt);
const { isInRightDrawer } = useContext(ActionMenuContext);
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
const shouldBeRegistered =
!isRemoteObject && isDefined(selectedRecord?.deletedAt);
setIsDestroyRecordsModalOpen(true);
};
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
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'}
/>
),
};
setIsDestroyRecordsModalOpen(true);
};
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { BlockNoteEditor } from '@blocknote/core';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useExportNoteAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
export const useExportNoteAction: ActionHookWithObjectMetadataItem = ({
objectMetadataItem,
}) => {
const recordId = useSelectedRecordIdOrThrow();
const filename = `${(selectedRecord?.title || 'Untitled Note').replace(/[<>:"/\\|?*]/g, '-')}`;
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const isNoteOrTask =
objectMetadataItem?.nameSingular === CoreObjectNameSingular.Note ||
objectMetadataItem?.nameSingular === CoreObjectNameSingular.Task;
const filename = `${(selectedRecord?.title || 'Untitled Note').replace(/[<>:"/\\|?*]/g, '-')}`;
const shouldBeRegistered =
isDefined(objectMetadataItem) &&
isDefined(selectedRecord) &&
isNoteOrTask;
const isNoteOrTask =
objectMetadataItem?.nameSingular === CoreObjectNameSingular.Note ||
objectMetadataItem?.nameSingular === CoreObjectNameSingular.Task;
const onClick = async () => {
if (!shouldBeRegistered || !selectedRecord?.body) {
return;
}
const shouldBeRegistered =
isDefined(objectMetadataItem) && isDefined(selectedRecord) && isNoteOrTask;
const editor = await BlockNoteEditor.create({
initialContent: JSON.parse(selectedRecord.body),
});
const onClick = async () => {
if (!shouldBeRegistered || !selectedRecord?.body) {
return;
}
const { exportBlockNoteEditorToPdf } = await import(
'@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf'
);
const editor = await BlockNoteEditor.create({
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
// const { exportBlockNoteEditorToDocx } = await import(
// '@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToDocx'
// );
// await exportBlockNoteEditorToDocx(editor, filename);
};
await exportBlockNoteEditorToPdf(editor, filename);
return {
shouldBeRegistered,
onClick,
};
// TODO later: implement DOCX export
// const { exportBlockNoteEditorToDocx } = await import(
// '@/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 { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { useContext } from 'react';
export const useNavigateToNextRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
export const useNavigateToNextRecordSingleRecordAction: ActionHookWithObjectMetadataItem =
({ objectMetadataItem }) => {
const recordId = useSelectedRecordIdOrThrow();
const { isInRightDrawer } = useContext(ActionMenuContext);
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 { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { useContext } from 'react';
export const useNavigateToPreviousRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
export const useNavigateToPreviousRecordSingleRecordAction: ActionHookWithObjectMetadataItem =
({ objectMetadataItem }) => {
const recordId = useSelectedRecordIdOrThrow();
const { isInRightDrawer } = useContext(ActionMenuContext);
const { navigateToPreviousRecord } = useRecordShowPagePagination(
objectMetadataItem.nameSingular,
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 { useFavorites } from '@/favorites/hooks/useFavorites';
import { isDefined } from 'twenty-ui';
export const useRemoveFromFavoritesSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
export const useRemoveFromFavoritesSingleRecordAction: ActionHookWithObjectMetadataItem =
({ objectMetadataItem }) => {
const recordId = useSelectedRecordIdOrThrow();
const { sortedFavorites: favorites } = useFavorites();
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', () => {
it('should be registered', () => {
const { result } = renderHook(
() =>
useActivateDraftWorkflowSingleRecordAction({
recordId: workflowMock.id,
}),
() => useActivateDraftWorkflowSingleRecordAction(),
{
wrapper,
},
@ -69,10 +66,7 @@ describe('useActivateDraftWorkflowSingleRecordAction', () => {
it('should call activateWorkflowVersion on click', () => {
const { result } = renderHook(
() =>
useActivateDraftWorkflowSingleRecordAction({
recordId: workflowMock.id,
}),
() => useActivateDraftWorkflowSingleRecordAction(),
{
wrapper,
},

View File

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

View File

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

View File

@ -178,10 +178,7 @@ describe('useDiscardDraftWorkflowSingleRecordAction', () => {
() => noDraftWorkflowMock,
);
const { result } = renderHook(
() =>
useDiscardDraftWorkflowSingleRecordAction({
recordId: noDraftWorkflowMock.id,
}),
() => useDiscardDraftWorkflowSingleRecordAction(),
{
wrapper: noDraftWorkflowWrapper,
},
@ -196,10 +193,7 @@ describe('useDiscardDraftWorkflowSingleRecordAction', () => {
);
const { result } = renderHook(
() =>
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMockWithOneVersion.id,
}),
() => useDiscardDraftWorkflowSingleRecordAction(),
{
wrapper: draftWorkflowWithOneVersionWrapper,
},
@ -213,10 +207,7 @@ describe('useDiscardDraftWorkflowSingleRecordAction', () => {
() => draftWorkflowMock,
);
const { result } = renderHook(
() =>
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMock.id,
}),
() => useDiscardDraftWorkflowSingleRecordAction(),
{
wrapper: draftWorkflowWrapper,
},
@ -230,10 +221,7 @@ describe('useDiscardDraftWorkflowSingleRecordAction', () => {
() => draftWorkflowMock,
);
const { result } = renderHook(
() =>
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMock.id,
}),
() => useDiscardDraftWorkflowSingleRecordAction(),
{
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 { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useActivateDraftWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useActivateDraftWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const { activateWorkflowVersion } = useActivateWorkflowVersion();
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 { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useActivateLastPublishedVersionWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useActivateLastPublishedVersionWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const { activateWorkflowVersion } = useActivateWorkflowVersion();
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 { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useDeactivateWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useDeactivateWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
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 { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useDiscardDraftWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useDiscardDraftWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
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 { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui';
export const useSeeActiveVersionWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useSeeActiveVersionWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const workflow = useWorkflowWithCurrentVersion(recordId);
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 { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
@ -6,8 +7,10 @@ import qs from 'qs';
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui';
export const useSeeRunsWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useSeeRunsWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
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 { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
@ -6,8 +7,10 @@ import qs from 'qs';
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui';
export const useSeeVersionsWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useSeeVersionsWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
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 { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useTestWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useTestWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
@ -8,8 +9,10 @@ import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useSeeRunsWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useSeeRunsWorkflowVersionSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
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 { 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 { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useSeeVersionsWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useSeeVersionsWorkflowVersionSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
if (!isDefined(workflowVersion)) {
throw new Error('Workflow version not found');
}
// TODO: Add recordIds to the hook
const { shouldBeRegistered, onClick } =
useSeeVersionsWorkflowSingleRecordAction({
recordId: workflowVersion.workflow.id,
});
useSeeVersionsWorkflowSingleRecordAction();
return {
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 { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL';
import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
@ -10,8 +11,10 @@ import { useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useUseAsDraftWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
export const useUseAsDraftWorkflowVersionSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useWorkflowVersion(recordId);
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 { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import {
ContextStoreTargetedRecordsRule,
contextStoreTargetedRecordsRuleComponentState,
@ -13,10 +14,12 @@ export const JestContextStoreSetter = ({
mode: 'selection',
selectedRecordIds: [],
},
contextStoreNumberOfSelectedRecords = 0,
contextStoreCurrentObjectMetadataNameSingular = '',
children,
}: {
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
contextStoreNumberOfSelectedRecords?: number;
contextStoreCurrentObjectMetadataNameSingular?: string;
children: ReactNode;
}) => {
@ -28,6 +31,10 @@ export const JestContextStoreSetter = ({
contextStoreCurrentObjectMetadataIdComponentState,
);
const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: contextStoreCurrentObjectMetadataNameSingular,
});
@ -38,12 +45,15 @@ export const JestContextStoreSetter = ({
useEffect(() => {
setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule);
setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId);
setContextStoreNumberOfSelectedRecords(contextStoreNumberOfSelectedRecords);
setIsLoaded(true);
}, [
setContextStoreTargetedRecordsRule,
setContextStoreCurrentObjectMetadataId,
contextStoreTargetedRecordsRule,
contextStoreCurrentObjectMetadataId,
setContextStoreNumberOfSelectedRecords,
contextStoreNumberOfSelectedRecords,
]);
return isLoaded ? <>{children}</> : null;

View File

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