Refactor activate workflow actions (#9434)

Closes #9407
This commit is contained in:
Raphaël Bosi
2025-01-07 14:17:16 +01:00
committed by GitHub
parent b71246bc5d
commit 04f648f7f3
7 changed files with 256 additions and 250 deletions

View File

@ -9,8 +9,7 @@ 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 { useActivateDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateDraftWorkflowSingleRecordAction';
import { useActivateLastPublishedVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateLastPublishedVersionWorkflowSingleRecordAction';
import { useActivateWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowSingleRecordAction';
import { useDeactivateWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowSingleRecordAction';
import { useDiscardDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDiscardDraftWorkflowSingleRecordAction';
import { useSeeActiveVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeActiveVersionWorkflowSingleRecordAction';
@ -46,12 +45,12 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
actionHook: ActionHook;
}
> = {
activateWorkflowDraftSingleRecord: {
key: WorkflowSingleRecordActionKeys.ACTIVATE_DRAFT,
label: 'Activate Draft',
shortLabel: 'Activate Draft',
activateWorkflowSingleRecord: {
key: WorkflowSingleRecordActionKeys.ACTIVATE,
label: 'Activate Workflow',
shortLabel: 'Activate',
isPinned: true,
position: 1,
position: 0,
Icon: IconPower,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
@ -59,29 +58,14 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateDraftWorkflowSingleRecordAction,
},
activateWorkflowLastPublishedVersionSingleRecord: {
key: WorkflowSingleRecordActionKeys.ACTIVATE_LAST_PUBLISHED,
label: 'Activate last published version',
shortLabel: 'Activate last version',
isPinned: true,
position: 2,
Icon: IconPower,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
actionHook: useActivateWorkflowSingleRecordAction,
},
deactivateWorkflowSingleRecord: {
key: WorkflowSingleRecordActionKeys.DEACTIVATE,
label: 'Deactivate Workflow',
shortLabel: 'Deactivate',
isPinned: true,
position: 3,
position: 1,
Icon: IconPlayerPause,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
@ -96,7 +80,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
label: 'Discard Draft',
shortLabel: 'Discard Draft',
isPinned: true,
position: 4,
position: 2,
Icon: IconTrash,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
@ -111,7 +95,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
label: 'See active version',
shortLabel: 'See active version',
isPinned: false,
position: 5,
position: 3,
Icon: IconHistory,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
@ -126,7 +110,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
label: 'See runs',
shortLabel: 'See runs',
isPinned: false,
position: 6,
position: 4,
Icon: IconHistoryToggle,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
@ -141,7 +125,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
label: 'See versions history',
shortLabel: 'See versions',
isPinned: false,
position: 7,
position: 5,
Icon: IconHistory,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
@ -156,7 +140,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
label: 'Test Workflow',
shortLabel: 'Test',
isPinned: true,
position: 8,
position: 6,
Icon: IconPlayerPlay,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
@ -172,7 +156,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
key: SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
label: 'Navigate to previous workflow',
shortLabel: '',
position: 9,
position: 7,
Icon: IconChevronUp,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
@ -183,7 +167,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
key: SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
label: 'Navigate to next workflow',
shortLabel: '',
position: 10,
position: 8,
Icon: IconChevronDown,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
@ -194,7 +178,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
key: SingleRecordActionKeys.ADD_TO_FAVORITES,
label: 'Add to favorites',
shortLabel: 'Add to favorites',
position: 11,
position: 9,
isPinned: false,
Icon: IconHeart,
availableOn: [
@ -210,7 +194,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
label: 'Remove from favorites',
shortLabel: 'Remove from favorites',
isPinned: false,
position: 12,
position: 10,
Icon: IconHeartOff,
availableOn: [
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
@ -224,7 +208,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
key: SingleRecordActionKeys.DELETE,
label: 'Delete record',
shortLabel: 'Delete',
position: 13,
position: 11,
Icon: IconTrash,
accent: 'danger',
isPinned: false,
@ -240,7 +224,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
key: MultipleRecordsActionKeys.DELETE,
label: 'Delete records',
shortLabel: 'Delete',
position: 14,
position: 12,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
@ -253,7 +237,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
key: SingleRecordActionKeys.DESTROY,
label: 'Permanently destroy record',
shortLabel: 'Destroy',
position: 15,
position: 13,
Icon: IconTrashX,
accent: 'danger',
isPinned: false,
@ -269,7 +253,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
key: MultipleRecordsActionKeys.EXPORT,
label: 'Export records',
shortLabel: 'Export',
position: 16,
position: 14,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,
@ -282,7 +266,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
label: 'Export view',
shortLabel: 'Export',
position: 17,
position: 15,
Icon: IconDatabaseExport,
accent: 'default',
isPinned: false,

View File

@ -1,84 +0,0 @@
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { useActivateDraftWorkflowSingleRecordAction } from '../useActivateDraftWorkflowSingleRecordAction';
const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'workflow',
)!;
const workflowMock = {
__typename: 'Workflow',
id: 'workflowId',
currentVersion: {
__typename: 'WorkflowVersion',
id: 'currentVersionId',
trigger: 'trigger',
status: 'DRAFT',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
};
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: () => workflowMock,
}));
const activateWorkflowVersionMock = jest.fn();
jest.mock('@/workflow/hooks/useActivateWorkflowVersion', () => ({
useActivateWorkflowVersion: () => ({
activateWorkflowVersion: activateWorkflowVersionMock,
}),
}));
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [workflowMock.id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(workflowMock.id), workflowMock);
},
});
describe('useActivateDraftWorkflowSingleRecordAction', () => {
it('should be registered', () => {
const { result } = renderHook(
() => useActivateDraftWorkflowSingleRecordAction(),
{
wrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should call activateWorkflowVersion on click', () => {
const { result } = renderHook(
() => useActivateDraftWorkflowSingleRecordAction(),
{
wrapper,
},
);
act(() => {
result.current.onClick();
});
expect(activateWorkflowVersionMock).toHaveBeenCalledWith({
workflowId: workflowMock.id,
workflowVersionId: workflowMock.currentVersion.id,
});
});
});

View File

@ -1,85 +0,0 @@
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { useActivateLastPublishedVersionWorkflowSingleRecordAction } from '../useActivateLastPublishedVersionWorkflowSingleRecordAction';
const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'workflow',
)!;
const workflowMock = {
__typename: 'Workflow',
id: 'workflowId',
lastPublishedVersionId: 'lastPublishedVersionId',
currentVersion: {
__typename: 'WorkflowVersion',
id: 'lastPublishedVersionId',
trigger: 'trigger',
status: 'DEACTIVATED',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
};
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: () => workflowMock,
}));
const activateWorkflowVersionMock = jest.fn();
jest.mock('@/workflow/hooks/useActivateWorkflowVersion', () => ({
useActivateWorkflowVersion: () => ({
activateWorkflowVersion: activateWorkflowVersionMock,
}),
}));
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [workflowMock.id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(workflowMock.id), workflowMock);
},
});
describe('useActivateLastPublishedVersionWorkflowSingleRecordAction', () => {
it('should be registered', () => {
const { result } = renderHook(
() => useActivateLastPublishedVersionWorkflowSingleRecordAction(),
{
wrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should call activateWorkflowVersion on click', () => {
const { result } = renderHook(
() => useActivateLastPublishedVersionWorkflowSingleRecordAction(),
{
wrapper,
},
);
act(() => {
result.current.onClick();
});
expect(activateWorkflowVersionMock).toHaveBeenCalledWith({
workflowId: workflowMock.id,
workflowVersionId: workflowMock.currentVersion.id,
});
});
});

View File

@ -0,0 +1,227 @@
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { useActivateWorkflowSingleRecordAction } from '../useActivateWorkflowSingleRecordAction';
const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'workflow',
)!;
const baseWorkflowMock = {
__typename: 'Workflow',
id: 'workflowId',
currentVersion: {
__typename: 'WorkflowVersion',
id: 'currentVersionId',
trigger: 'trigger',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
};
const draftWorkflowMock = {
...baseWorkflowMock,
currentVersion: {
...baseWorkflowMock.currentVersion,
status: 'DRAFT',
},
versions: [
{
__typename: 'WorkflowVersion',
id: 'currentVersionId',
status: 'DRAFT',
},
],
};
const activeWorkflowMock = {
...baseWorkflowMock,
currentVersion: {
...baseWorkflowMock.currentVersion,
status: 'ACTIVE',
},
versions: [
{
__typename: 'WorkflowVersion',
id: 'currentVersionId',
status: 'ACTIVE',
},
],
};
const noStepsWorkflowMock = {
...baseWorkflowMock,
currentVersion: {
...baseWorkflowMock.currentVersion,
steps: [],
},
versions: [
{
__typename: 'WorkflowVersion',
id: 'currentVersionId',
status: 'ACTIVE',
},
],
};
const noTriggerWorkflowMock = {
...baseWorkflowMock,
currentVersion: {
...baseWorkflowMock.currentVersion,
trigger: undefined,
},
versions: [
{
__typename: 'WorkflowVersion',
id: 'currentVersionId',
status: 'ACTIVE',
},
],
};
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: jest.fn(),
}));
const activateWorkflowVersionMock = jest.fn();
jest.mock('@/workflow/hooks/useActivateWorkflowVersion', () => ({
useActivateWorkflowVersion: () => ({
activateWorkflowVersion: activateWorkflowVersionMock,
}),
}));
const createWrapper = (workflow: {
__typename: string;
id: string;
currentVersion: {
id: string;
};
}) =>
getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [workflow.id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(workflow.id), workflow);
},
});
describe('useActivateWorkflowSingleRecordAction', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should be registered when workflow has trigger and steps and is not active', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockReturnValue(
draftWorkflowMock,
);
const { result } = renderHook(
() => useActivateWorkflowSingleRecordAction(),
{
wrapper: createWrapper(draftWorkflowMock),
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should not be registered when workflow is already active', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockReturnValue(
activeWorkflowMock,
);
const { result } = renderHook(
() => useActivateWorkflowSingleRecordAction(),
{
wrapper: createWrapper(activeWorkflowMock),
},
);
expect(result.current.shouldBeRegistered).toBe(false);
});
it('should not be registered when workflow has no steps', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockReturnValue(
noStepsWorkflowMock,
);
const { result } = renderHook(
() => useActivateWorkflowSingleRecordAction(),
{
wrapper: createWrapper(noStepsWorkflowMock),
},
);
expect(result.current.shouldBeRegistered).toBe(false);
});
it('should not be registered when workflow has no trigger', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockReturnValue(
noTriggerWorkflowMock,
);
const { result } = renderHook(
() => useActivateWorkflowSingleRecordAction(),
{
wrapper: createWrapper(noTriggerWorkflowMock),
},
);
expect(result.current.shouldBeRegistered).toBe(false);
});
it('should call activateWorkflowVersion on click', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockReturnValue(
draftWorkflowMock,
);
const { result } = renderHook(
() => useActivateWorkflowSingleRecordAction(),
{
wrapper: createWrapper(draftWorkflowMock),
},
);
act(() => {
result.current.onClick();
});
expect(activateWorkflowVersionMock).toHaveBeenCalledWith({
workflowId: draftWorkflowMock.id,
workflowVersionId: draftWorkflowMock.currentVersion.id,
});
});
it('should not call activateWorkflowVersion when not registered', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockReturnValue(
activeWorkflowMock,
);
const { result } = renderHook(
() => useActivateWorkflowSingleRecordAction(),
{
wrapper: createWrapper(activeWorkflowMock),
},
);
act(() => {
result.current.onClick();
});
expect(activateWorkflowVersionMock).not.toHaveBeenCalled();
});
});

View File

@ -1,39 +0,0 @@
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: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
const { activateWorkflowVersion } = useActivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const shouldBeRegistered =
isDefined(workflowWithCurrentVersion) &&
isDefined(workflowWithCurrentVersion.currentVersion.trigger) &&
isDefined(workflowWithCurrentVersion.lastPublishedVersionId) &&
workflowWithCurrentVersion.lastPublishedVersionId !== '' &&
!workflowWithCurrentVersion.statuses?.includes('ACTIVE') &&
isDefined(workflowWithCurrentVersion.currentVersion?.steps) &&
workflowWithCurrentVersion.currentVersion?.steps.length !== 0;
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
activateWorkflowVersion({
workflowVersionId: workflowWithCurrentVersion.lastPublishedVersionId,
workflowId: workflowWithCurrentVersion.id,
});
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -4,7 +4,7 @@ import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflow
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useActivateDraftWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
export const useActivateWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataItem =
() => {
const recordId = useSelectedRecordIdOrThrow();
@ -15,7 +15,11 @@ export const useActivateDraftWorkflowSingleRecordAction: ActionHookWithoutObject
const shouldBeRegistered =
isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) &&
isDefined(workflowWithCurrentVersion.currentVersion?.steps) &&
workflowWithCurrentVersion.currentVersion.status === 'DRAFT';
workflowWithCurrentVersion.currentVersion.steps.length > 0 &&
(workflowWithCurrentVersion.currentVersion.status === 'DRAFT' ||
!workflowWithCurrentVersion.versions?.some(
(version) => version.status === 'ACTIVE',
));
const onClick = () => {
if (!shouldBeRegistered) {

View File

@ -1,6 +1,5 @@
export enum WorkflowSingleRecordActionKeys {
ACTIVATE_DRAFT = 'activate-draft-workflow-single-record',
ACTIVATE_LAST_PUBLISHED = 'activate-last-published-workflow-single-record',
ACTIVATE = 'activate-workflow-single-record',
DEACTIVATE = 'deactivate-workflow-single-record',
DISCARD_DRAFT = 'discard-draft-workflow-single-record',
SEE_ACTIVE_VERSION = 'see-active-version-workflow-single-record',