Refactored all single record actions (#9045)

## Context

Refactored all single record actions so they can be defined by a config
file.
This refactoring is made with the idea that later the actions will be
stored in the database, so we needed a way to serialize them.
For each object we can define a config file, if an object has no config
file, it falls back to the default config.
I introduced action hooks, which return:
- `shouldBeRegistered`: `boolean` Whether the action should be
registered.
- `onClick`: `() => void` The code that will be executed when we click
on an action
- `ConfirmationModal`?: `React.ReactNode` (optional) The confirmation
modal which will be displayed on click

This PR also closes #8973 

## Next steps

- Refactor multiple records actions
- Refactor no selection actions
- Add tests
This commit is contained in:
Raphaël Bosi 2024-12-16 16:30:18 +01:00 committed by GitHub
parent 5d51a826ea
commit 8ce6f6daea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 1868 additions and 1893 deletions

View File

@ -1,6 +1,6 @@
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 { SingleRecordActionMenuEntrySetter } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetter';
import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect';
import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
@ -59,7 +59,7 @@ const ActionEffects = ({
{contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
<>
<SingleRecordActionMenuEntrySetter
<SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
{isWorkflowEnabled && (

View File

@ -1,26 +0,0 @@
import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect';
import { WorkflowSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/workflow-actions/components/WorkflowSingleRecordActionMenuEntrySetterEffect';
import { WorkflowVersionsSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/components/WorkflowVersionsSingleRecordActionMenuEntrySetterEffect';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const SingleRecordActionMenuEntrySetter = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
return (
<>
<SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
{objectMetadataItem.nameSingular === CoreObjectNameSingular.Workflow && (
<WorkflowSingleRecordActionMenuEntrySetterEffect />
)}
{objectMetadataItem.nameSingular ===
CoreObjectNameSingular.WorkflowVersion && (
<WorkflowVersionsSingleRecordActionMenuEntrySetterEffect />
)}
</>
);
};

View File

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

View File

@ -0,0 +1,47 @@
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 { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { IconHeart, IconHeartOff, IconTrash } from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
}
> = {
addToFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'add-to-favorites-single-record',
label: 'Add to favorites',
position: 0,
Icon: IconHeart,
actionHook: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'remove-from-favorites-single-record',
label: 'Remove from favorites',
position: 1,
Icon: IconHeartOff,
actionHook: useRemoveFromFavoritesSingleRecordAction,
},
deleteSingleRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'delete-single-record',
label: 'Delete',
position: 2,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
actionHook: useDeleteSingleRecordAction,
},
};

View File

@ -0,0 +1,49 @@
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 { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { IconHeart, IconHeartOff, IconTrash } from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
}
> = {
addToFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'add-to-favorites-single-record',
label: 'Add to favorites',
position: 0,
isPinned: true,
Icon: IconHeart,
actionHook: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'remove-from-favorites-single-record',
label: 'Remove from favorites',
isPinned: true,
position: 1,
Icon: IconHeartOff,
actionHook: useRemoveFromFavoritesSingleRecordAction,
},
deleteSingleRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'delete-single-record',
label: 'Delete',
position: 2,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
actionHook: useDeleteSingleRecordAction,
},
};

View File

@ -1 +0,0 @@
export const NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS = 2;

View File

@ -0,0 +1,135 @@
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import {
GetJestMetadataAndApolloMocksAndActionMenuWrapperProps,
getJestMetadataAndApolloMocksAndActionMenuWrapper,
} from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getPeopleMock } from '~/testing/mock-data/people';
import { useAddToFavoritesSingleRecordAction } from '../useAddToFavoritesSingleRecordAction';
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const peopleMock = getPeopleMock();
const favoritesMock = [
{
id: '1',
recordId: peopleMock[0].id,
position: 0,
avatarType: 'rounded',
avatarUrl: '',
labelIdentifier: ' ',
link: `/object/${personMockObjectMetadataItem.nameSingular}/${peopleMock[0].id}`,
objectNameSingular: personMockObjectMetadataItem.nameSingular,
workspaceMemberId: '1',
favoriteFolderId: undefined,
},
];
jest.mock('@/favorites/hooks/useFavorites', () => ({
useFavorites: () => ({
favorites: favoritesMock,
sortedFavorites: favoritesMock,
}),
}));
const createFavoriteMock = jest.fn();
jest.mock('@/favorites/hooks/useCreateFavorite', () => ({
useCreateFavorite: () => ({
createFavorite: createFavoriteMock,
}),
}));
const wrapperConfigWithSelectedRecordAsFavorite: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps =
{
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [peopleMock[0].id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(peopleMock[0].id), peopleMock[0]);
snapshot.set(recordStoreFamilyState(peopleMock[1].id), peopleMock[1]);
},
};
const wrapperConfigWithSelectedRecordNotAsFavorite: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps =
{
...wrapperConfigWithSelectedRecordAsFavorite,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [peopleMock[1].id],
},
};
const wrapperWithSelectedRecordAsFavorite =
getJestMetadataAndApolloMocksAndActionMenuWrapper(
wrapperConfigWithSelectedRecordAsFavorite,
);
const wrapperWithSelectedRecordNotAsFavorite =
getJestMetadataAndApolloMocksAndActionMenuWrapper(
wrapperConfigWithSelectedRecordNotAsFavorite,
);
describe('useAddToFavoritesSingleRecordAction', () => {
it('should be registered when the record is not a favorite', () => {
const { result } = renderHook(
() =>
useAddToFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper: wrapperWithSelectedRecordNotAsFavorite,
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should not be registered when the record is a favorite', () => {
const { result } = renderHook(
() =>
useAddToFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper: wrapperWithSelectedRecordAsFavorite,
},
);
expect(result.current.shouldBeRegistered).toBe(false);
});
it('should call createFavorite on click', () => {
const { result } = renderHook(
() =>
useAddToFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper: wrapperWithSelectedRecordNotAsFavorite,
},
);
act(() => {
result.current.onClick();
});
expect(createFavoriteMock).toHaveBeenCalledWith(
peopleMock[1],
personMockObjectMetadataItem.nameSingular,
);
});
});

View File

@ -1,121 +1,64 @@
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot } from 'recoil';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getPeopleMock } from '~/testing/mock-data/people';
import { useDeleteSingleRecordAction } from '../useDeleteSingleRecordAction';
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const peopleMock = getPeopleMock();
const deleteOneRecordMock = jest.fn();
jest.mock('@/object-record/hooks/useDeleteOneRecord', () => ({
useDeleteOneRecord: () => ({
deleteOneRecord: jest.fn(),
}),
}));
jest.mock('@/favorites/hooks/useDeleteFavorite', () => ({
useDeleteFavorite: () => ({
deleteFavorite: jest.fn(),
}),
}));
jest.mock('@/favorites/hooks/useFavorites', () => ({
useFavorites: () => ({
sortedFavorites: [],
}),
}));
jest.mock('@/object-record/record-table/hooks/useRecordTable', () => ({
useRecordTable: () => ({
resetTableRowSelection: jest.fn(),
deleteOneRecord: deleteOneRecordMock,
}),
}));
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [peopleMock[0].id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(peopleMock[0].id), peopleMock[0]);
},
});
describe('useDeleteSingleRecordAction', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
{children}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecoilRoot>
);
it('should register delete action', () => {
it('should call deleteOneRecord on click', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeleteSingleRecordAction: useDeleteSingleRecordAction({
recordId: 'record1',
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
() =>
useDeleteSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper,
},
{ wrapper },
);
act(() => {
result.current.useDeleteSingleRecordAction.registerDeleteSingleRecordAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get('delete-single-record'),
).toBeDefined();
expect(
result.current.actionMenuEntries.get('delete-single-record')?.position,
).toBe(1);
});
it('should unregister delete action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeleteSingleRecordAction: useDeleteSingleRecordAction({
recordId: 'record1',
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
expect(result.current.ConfirmationModal?.props?.isOpen).toBe(false);
act(() => {
result.current.useDeleteSingleRecordAction.registerDeleteSingleRecordAction(
{ position: 1 },
);
result.current.onClick();
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(result.current.ConfirmationModal?.props?.isOpen).toBe(true);
act(() => {
result.current.useDeleteSingleRecordAction.unregisterDeleteSingleRecordAction();
result.current.ConfirmationModal?.props?.onConfirmClick();
});
expect(result.current.actionMenuEntries.size).toBe(0);
expect(deleteOneRecordMock).toHaveBeenCalledWith(peopleMock[0].id);
});
});

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 { useManageFavoritesSingleRecordAction } from '../useManageFavoritesSingleRecordAction';
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useManageFavoritesSingleRecordAction', () => {
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 manage favorites action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useManageFavoritesSingleRecordAction:
useManageFavoritesSingleRecordAction({
recordId: 'record1',
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useManageFavoritesSingleRecordAction.registerManageFavoritesSingleRecordAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get('manage-favorites-single-record'),
).toBeDefined();
expect(
result.current.actionMenuEntries.get('manage-favorites-single-record')
?.position,
).toBe(1);
});
it('should unregister manage favorites action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useManageFavoritesSingleRecordAction:
useManageFavoritesSingleRecordAction({
recordId: 'record1',
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useManageFavoritesSingleRecordAction.registerManageFavoritesSingleRecordAction(
{ position: 1 },
);
});
act(() => {
result.current.useManageFavoritesSingleRecordAction.unregisterManageFavoritesSingleRecordAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

@ -0,0 +1,132 @@
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import {
GetJestMetadataAndApolloMocksAndActionMenuWrapperProps,
getJestMetadataAndApolloMocksAndActionMenuWrapper,
} from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getPeopleMock } from '~/testing/mock-data/people';
import { useRemoveFromFavoritesSingleRecordAction } from '../useRemoveFromFavoritesSingleRecordAction';
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const peopleMock = getPeopleMock();
const favoritesMock = [
{
id: '1',
recordId: peopleMock[0].id,
position: 0,
avatarType: 'rounded',
avatarUrl: '',
labelIdentifier: ' ',
link: `/object/${personMockObjectMetadataItem.nameSingular}/${peopleMock[0].id}`,
objectNameSingular: personMockObjectMetadataItem.nameSingular,
workspaceMemberId: '1',
favoriteFolderId: undefined,
},
];
jest.mock('@/favorites/hooks/useFavorites', () => ({
useFavorites: () => ({
favorites: favoritesMock,
sortedFavorites: favoritesMock,
}),
}));
const deleteFavoriteMock = jest.fn();
jest.mock('@/favorites/hooks/useDeleteFavorite', () => ({
useDeleteFavorite: () => ({
deleteFavorite: deleteFavoriteMock,
}),
}));
const wrapperConfigWithSelectedRecordAsFavorite: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps =
{
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [peopleMock[0].id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(peopleMock[0].id), peopleMock[0]);
snapshot.set(recordStoreFamilyState(peopleMock[1].id), peopleMock[1]);
},
};
const wrapperConfigWithSelectedRecordNotAsFavorite: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps =
{
...wrapperConfigWithSelectedRecordAsFavorite,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [peopleMock[1].id],
},
};
const wrapperWithSelectedRecordAsFavorite =
getJestMetadataAndApolloMocksAndActionMenuWrapper(
wrapperConfigWithSelectedRecordAsFavorite,
);
const wrapperWithSelectedRecordNotAsFavorite =
getJestMetadataAndApolloMocksAndActionMenuWrapper(
wrapperConfigWithSelectedRecordNotAsFavorite,
);
describe('useRemoveFromFavoritesSingleRecordAction', () => {
it('should be registered when the record is a favorite', () => {
const { result } = renderHook(
() =>
useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper: wrapperWithSelectedRecordAsFavorite,
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should not be registered when the record is not a favorite', () => {
const { result } = renderHook(
() =>
useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[1].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper: wrapperWithSelectedRecordNotAsFavorite,
},
);
expect(result.current.shouldBeRegistered).toBe(false);
});
it('should call deleteFavorite on click', () => {
const { result } = renderHook(
() =>
useRemoveFromFavoritesSingleRecordAction({
recordId: peopleMock[0].id,
objectMetadataItem: personMockObjectMetadataItem,
}),
{
wrapper: wrapperWithSelectedRecordAsFavorite,
},
);
act(() => {
result.current.onClick();
});
expect(deleteFavoriteMock).toHaveBeenCalledWith(favoritesMock[0].id);
});
});

View File

@ -0,0 +1,40 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useAddToFavoritesSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const { sortedFavorites: favorites } = useFavorites();
const { createFavorite } = useCreateFavorite();
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordId,
);
const isFavorite = !!foundFavorite;
const shouldBeRegistered =
isDefined(objectMetadataItem) &&
isDefined(selectedRecord) &&
!objectMetadataItem.isRemote &&
!isFavorite;
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -1,90 +1,70 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
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 { useCallback, useContext, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui';
import { isDefined } from 'twenty-ui';
export const useDeleteSingleRecordAction = ({
recordId,
objectMetadataItem,
}: {
recordId: string;
objectMetadataItem: ObjectMetadataItem;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem.namePlural,
});
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const { closeRightDrawer } = useRightDrawer();
const { closeRightDrawer } = useRightDrawer();
const handleDeleteClick = useCallback(async () => {
resetTableRowSelection();
const handleDeleteClick = useCallback(async () => {
resetTableRowSelection();
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordId,
);
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordId,
);
if (isDefined(foundFavorite)) {
deleteFavorite(foundFavorite.id);
}
if (isDefined(foundFavorite)) {
deleteFavorite(foundFavorite.id);
}
await deleteOneRecord(recordId);
}, [
deleteFavorite,
deleteOneRecord,
favorites,
resetTableRowSelection,
recordId,
]);
await deleteOneRecord(recordId);
}, [
deleteFavorite,
deleteOneRecord,
favorites,
resetTableRowSelection,
recordId,
]);
const isRemoteObject = objectMetadataItem.isRemote;
const isRemoteObject = objectMetadataItem.isRemote;
const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
const shouldBeRegistered = !isRemoteObject;
const registerDeleteSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (isRemoteObject) {
return;
}
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
addActionMenuEntry({
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'delete-single-record',
label: 'Delete',
position,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
onClick: () => {
setIsDeleteRecordsModalOpen(true);
},
setIsDeleteRecordsModalOpen(true);
};
return {
shouldBeRegistered,
onClick,
ConfirmationModal: (
<ConfirmationModal
isOpen={isDeleteRecordsModalOpen}
@ -103,15 +83,5 @@ export const useDeleteSingleRecordAction = ({
deleteButtonText={'Delete Record'}
/>
),
});
};
};
const unregisterDeleteSingleRecordAction = () => {
removeActionMenuEntry('delete-single-record');
};
return {
registerDeleteSingleRecordAction,
unregisterDeleteSingleRecordAction,
};
};

View File

@ -1,77 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui';
export const useManageFavoritesSingleRecordAction = ({
recordId,
objectMetadataItem,
}: {
recordId: string;
objectMetadataItem: ObjectMetadataItem;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { sortedFavorites: favorites } = useFavorites();
const { createFavorite } = useCreateFavorite();
const { deleteFavorite } = useDeleteFavorite();
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordId,
);
const isFavorite = !!foundFavorite;
const isPageHeaderV2Enabled = useIsFeatureEnabled(
'IS_PAGE_HEADER_V2_ENABLED',
);
const registerManageFavoritesSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) {
return;
}
addActionMenuEntry({
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'manage-favorites-single-record',
isPinned: isPageHeaderV2Enabled,
label: isFavorite ? 'Remove from favorites' : 'Add to favorites',
position,
Icon: isFavorite ? IconHeartOff : IconHeart,
onClick: () => {
if (isFavorite && isDefined(foundFavorite?.id)) {
deleteFavorite(foundFavorite.id);
} else if (isDefined(selectedRecord)) {
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
}
},
});
};
const unregisterManageFavoritesSingleRecordAction = () => {
removeActionMenuEntry('manage-favorites-single-record');
};
return {
registerManageFavoritesSingleRecordAction,
unregisterManageFavoritesSingleRecordAction,
};
};

View File

@ -0,0 +1,35 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { isDefined } from 'twenty-ui';
export const useRemoveFromFavoritesSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordId,
);
const isFavorite = !!foundFavorite;
const shouldBeRegistered =
isDefined(objectMetadataItem) &&
!objectMetadataItem.isRemote &&
isFavorite;
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
deleteFavorite(foundFavorite.id);
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -1,56 +0,0 @@
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
import { useManageFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction';
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 { isDefined } from 'twenty-ui';
export const useSingleRecordActions = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
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 {
registerManageFavoritesSingleRecordAction,
unregisterManageFavoritesSingleRecordAction,
} = useManageFavoritesSingleRecordAction({
recordId: selectedRecordId,
objectMetadataItem,
});
const {
registerDeleteSingleRecordAction,
unregisterDeleteSingleRecordAction,
} = useDeleteSingleRecordAction({
recordId: selectedRecordId,
objectMetadataItem,
});
const registerSingleRecordActions = () => {
registerManageFavoritesSingleRecordAction({ position: 1 });
registerDeleteSingleRecordAction({ position: 2 });
};
const unregisterSingleRecordActions = () => {
unregisterManageFavoritesSingleRecordAction();
unregisterDeleteSingleRecordAction();
};
return {
registerSingleRecordActions,
unregisterSingleRecordActions,
};
};

View File

@ -0,0 +1,23 @@
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_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;
}
return isPageHeaderV2Enabled
? DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2
: DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1;
};

View File

@ -1,21 +0,0 @@
import { NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS } from '@/action-menu/actions/record-actions/single-record/constants/NumberOfStandardSingleRecordActionsOnAllObjects';
import { useEffect } from 'react';
import { useWorkflowSingleRecordActions } from '../hooks/useWorkflowSingleRecordActions';
export const WorkflowSingleRecordActionMenuEntrySetterEffect = () => {
const { registerSingleRecordActions, unregisterSingleRecordActions } =
useWorkflowSingleRecordActions();
useEffect(() => {
registerSingleRecordActions({
startPosition:
NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS + 1,
});
return () => {
unregisterSingleRecordActions();
};
}, [registerSingleRecordActions, unregisterSingleRecordActions]);
return null;
};

View File

@ -0,0 +1,110 @@
import { useActivateDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateDraftWorkflowSingleRecordAction';
import { useActivateLastPublishedVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateLastPublishedVersionWorkflowSingleRecordAction';
import { useDeactivateWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowSingleRecordAction';
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';
import { useSeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeRunsWorkflowSingleRecordAction';
import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction';
import { useTestWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import {
IconHistory,
IconHistoryToggle,
IconPlayerPause,
IconPlayerPlay,
IconPower,
IconTrash,
} from 'twenty-ui';
export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
}
> = {
activateWorkflowDraftSingleRecord: {
key: 'activate-workflow-draft-single-record',
label: 'Activate Draft',
isPinned: true,
position: 1,
Icon: IconPower,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
actionHook: useActivateDraftWorkflowSingleRecordAction,
},
activateWorkflowLastPublishedVersionSingleRecord: {
key: 'activate-workflow-last-published-version-single-record',
label: 'Activate last published version',
isPinned: true,
position: 2,
Icon: IconPower,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
},
deactivateWorkflowSingleRecord: {
key: 'deactivate-workflow-single-record',
label: 'Deactivate Workflow',
isPinned: true,
position: 3,
Icon: IconPlayerPause,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
actionHook: useDeactivateWorkflowSingleRecordAction,
},
discardWorkflowDraftSingleRecord: {
key: 'discard-workflow-draft-single-record',
label: 'Discard Draft',
isPinned: true,
position: 4,
Icon: IconTrash,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
actionHook: useDiscardDraftWorkflowSingleRecordAction,
},
seeWorkflowActiveVersionSingleRecord: {
key: 'see-workflow-active-version-single-record',
label: 'See active version',
isPinned: false,
position: 5,
Icon: IconHistory,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
},
seeWorkflowRunsSingleRecord: {
key: 'see-workflow-runs-single-record',
label: 'See runs',
isPinned: false,
position: 6,
Icon: IconHistoryToggle,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
actionHook: useSeeRunsWorkflowSingleRecordAction,
},
seeWorkflowVersionsHistorySingleRecord: {
key: 'see-workflow-versions-history-single-record',
label: 'See versions history',
isPinned: false,
position: 7,
Icon: IconHistory,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
actionHook: useSeeVersionsWorkflowSingleRecordAction,
},
testWorkflowSingleRecord: {
key: 'test-workflow-single-record',
label: 'Test Workflow',
isPinned: true,
position: 8,
Icon: IconPlayerPlay,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
actionHook: useTestWorkflowSingleRecordAction,
},
};

View File

@ -0,0 +1,90 @@
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({
recordId: workflowMock.id,
}),
{
wrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should call activateWorkflowVersion on click', () => {
const { result } = renderHook(
() =>
useActivateDraftWorkflowSingleRecordAction({
recordId: workflowMock.id,
}),
{
wrapper,
},
);
act(() => {
result.current.onClick();
});
expect(activateWorkflowVersionMock).toHaveBeenCalledWith({
workflowId: workflowMock.id,
workflowVersionId: workflowMock.currentVersion.id,
});
});
});

View File

@ -0,0 +1,91 @@
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({
recordId: workflowMock.id,
}),
{
wrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should call activateWorkflowVersion on click', () => {
const { result } = renderHook(
() =>
useActivateLastPublishedVersionWorkflowSingleRecordAction({
recordId: workflowMock.id,
}),
{
wrapper,
},
);
act(() => {
result.current.onClick();
});
expect(activateWorkflowVersionMock).toHaveBeenCalledWith({
workflowId: workflowMock.id,
workflowVersionId: workflowMock.currentVersion.id,
});
});
});

View File

@ -1,123 +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 { useActivateWorkflowDraftWorkflowSingleRecordAction } from '../useActivateWorkflowDraftWorkflowSingleRecordAction';
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: () => ({
id: 'workflowId',
currentVersion: {
id: 'currentVersionId',
trigger: 'trigger',
status: 'DRAFT',
steps: [
{
id: 'stepId1',
},
{
id: 'stepId2',
},
],
},
}),
}));
describe('useActivateWorkflowDraftWorkflowSingleRecordAction', () => {
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 activate workflow draft workflow action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useActivateWorkflowDraftWorkflowSingleRecordAction:
useActivateWorkflowDraftWorkflowSingleRecordAction({
workflowId: 'workflowId',
}),
};
},
{ wrapper },
);
act(() => {
result.current.useActivateWorkflowDraftWorkflowSingleRecordAction.registerActivateWorkflowDraftWorkflowSingleRecordAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get(
'activate-workflow-draft-single-record',
),
).toBeDefined();
expect(
result.current.actionMenuEntries.get(
'activate-workflow-draft-single-record',
)?.position,
).toBe(1);
});
it('should unregister activate workflow draft workflow action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useActivateWorkflowDraftWorkflowSingleRecordAction:
useActivateWorkflowDraftWorkflowSingleRecordAction({
workflowId: 'workflow1',
}),
};
},
{ wrapper },
);
act(() => {
result.current.useActivateWorkflowDraftWorkflowSingleRecordAction.registerActivateWorkflowDraftWorkflowSingleRecordAction(
{ position: 1 },
);
});
act(() => {
result.current.useActivateWorkflowDraftWorkflowSingleRecordAction.unregisterActivateWorkflowDraftWorkflowSingleRecordAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

@ -1,124 +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 { useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction } from '../useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction';
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: () => ({
id: 'workflowId',
currentVersion: {
id: 'currentVersionId',
trigger: 'trigger',
status: 'DEACTIVATED',
steps: [
{
id: 'stepId1',
},
{
id: 'stepId2',
},
],
},
lastPublishedVersionId: 'lastPublishedVersionId',
}),
}));
describe('useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction', () => {
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 activate workflow last published version workflow action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction:
useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction({
workflowId: 'workflowId',
}),
};
},
{ wrapper },
);
act(() => {
result.current.useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get(
'activate-workflow-last-published-version-single-record',
),
).toBeDefined();
expect(
result.current.actionMenuEntries.get(
'activate-workflow-last-published-version-single-record',
)?.position,
).toBe(1);
});
it('should unregister activate workflow last published version workflow action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction:
useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction({
workflowId: 'workflow1',
}),
};
},
{ wrapper },
);
act(() => {
result.current.useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction(
{ position: 1 },
);
});
act(() => {
result.current.useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

@ -0,0 +1,158 @@
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 { useDeactivateWorkflowSingleRecordAction } from '../useDeactivateWorkflowSingleRecordAction';
const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'workflow',
)!;
const activeWorkflowMock = {
__typename: 'Workflow',
id: 'workflowId',
lastPublishedVersionId: 'lastPublishedVersionId',
currentVersion: {
__typename: 'WorkflowVersion',
id: 'currentVersionId',
trigger: 'trigger',
status: 'ACTIVE',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
};
const deactivatedWorkflowMock = {
__typename: 'Workflow',
id: 'workflowId',
lastPublishedVersionId: 'lastPublishedVersionId',
currentVersion: {
__typename: 'WorkflowVersion',
id: 'currentVersionId',
trigger: 'trigger',
status: 'DEACTIVATED',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
};
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: jest.fn(),
}));
const deactivateWorkflowVersionMock = jest.fn();
jest.mock('@/workflow/hooks/useDeactivateWorkflowVersion', () => ({
useDeactivateWorkflowVersion: () => ({
deactivateWorkflowVersion: deactivateWorkflowVersionMock,
}),
}));
const activeWorkflowWrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper(
{
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [activeWorkflowMock.id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
recordStoreFamilyState(activeWorkflowMock.id),
activeWorkflowMock,
);
},
},
);
const deactivatedWorkflowWrapper =
getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [deactivatedWorkflowMock.id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
recordStoreFamilyState(deactivatedWorkflowMock.id),
deactivatedWorkflowMock,
);
},
});
describe('useDeactivateWorkflowSingleRecordAction', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should not be registered when the workflow is deactivated', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockImplementation(
() => deactivatedWorkflowMock,
);
const { result } = renderHook(
() =>
useDeactivateWorkflowSingleRecordAction({
recordId: deactivatedWorkflowMock.id,
}),
{
wrapper: deactivatedWorkflowWrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(false);
});
it('should be registered when the workflow is active', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockImplementation(
() => activeWorkflowMock,
);
const { result } = renderHook(
() =>
useDeactivateWorkflowSingleRecordAction({
recordId: activeWorkflowMock.id,
}),
{
wrapper: activeWorkflowWrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should call deactivateWorkflowVersion on click', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockImplementation(
() => activeWorkflowMock,
);
const { result } = renderHook(
() =>
useDeactivateWorkflowSingleRecordAction({
recordId: activeWorkflowMock.id,
}),
{
wrapper: activeWorkflowWrapper,
},
);
act(() => {
result.current.onClick();
});
expect(deactivateWorkflowVersionMock).toHaveBeenCalledWith(
activeWorkflowMock.currentVersion.id,
);
});
});

View File

@ -1,113 +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 { useDeactivateWorkflowWorkflowSingleRecordAction } from '../useDeactivateWorkflowWorkflowSingleRecordAction';
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: () => ({
id: 'workflowId',
currentVersion: {
id: 'currentVersionId',
trigger: 'trigger',
status: 'ACTIVE',
},
lastPublishedVersionId: 'lastPublishedVersionId',
}),
}));
describe('useDeactivateWorkflowWorkflowSingleRecordAction', () => {
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 activate workflow last published version workflow action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeactivateWorkflowWorkflowSingleRecordAction:
useDeactivateWorkflowWorkflowSingleRecordAction({
workflowId: 'workflowId',
}),
};
},
{ wrapper },
);
act(() => {
result.current.useDeactivateWorkflowWorkflowSingleRecordAction.registerDeactivateWorkflowWorkflowSingleRecordAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get('deactivate-workflow-single-record'),
).toBeDefined();
expect(
result.current.actionMenuEntries.get('deactivate-workflow-single-record')
?.position,
).toBe(1);
});
it('should unregister deactivate workflow workflow action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeactivateWorkflowWorkflowSingleRecordAction:
useDeactivateWorkflowWorkflowSingleRecordAction({
workflowId: 'workflow1',
}),
};
},
{ wrapper },
);
act(() => {
result.current.useDeactivateWorkflowWorkflowSingleRecordAction.registerDeactivateWorkflowWorkflowSingleRecordAction(
{ position: 1 },
);
});
act(() => {
result.current.useDeactivateWorkflowWorkflowSingleRecordAction.unregisterDeactivateWorkflowWorkflowSingleRecordAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

@ -0,0 +1,250 @@
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 { useDiscardDraftWorkflowSingleRecordAction } from '../useDiscardDraftWorkflowSingleRecordAction';
const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'workflow',
)!;
const noDraftWorkflowMock = {
__typename: 'Workflow',
id: 'workflowId',
lastPublishedVersionId: 'lastPublishedVersionId',
currentVersion: {
__typename: 'WorkflowVersion',
id: 'currentVersionId',
trigger: 'trigger',
status: 'ACTIVE',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
versions: [
{
__typename: 'WorkflowVersion',
id: 'currentVersionId',
trigger: 'trigger',
status: 'ACTIVE',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
{
__typename: 'WorkflowVersion',
id: 'versionId2',
trigger: 'trigger',
status: 'ACTIVE',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId2',
},
],
},
],
};
const draftWorkflowMock = {
__typename: 'Workflow',
id: 'workflowId',
lastPublishedVersionId: 'lastPublishedVersionId',
currentVersion: {
__typename: 'WorkflowVersion',
id: 'currentVersionId',
trigger: 'trigger',
status: 'DRAFT',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
versions: [
{
__typename: 'WorkflowVersion',
id: 'currentVersionId',
trigger: 'trigger',
status: 'DRAFT',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId1',
},
],
},
{
__typename: 'WorkflowVersion',
id: 'versionId2',
trigger: 'trigger',
status: 'ACTIVE',
steps: [
{
__typename: 'WorkflowStep',
id: 'stepId2',
},
],
},
],
};
const draftWorkflowMockWithOneVersion = {
...draftWorkflowMock,
versions: [draftWorkflowMock.currentVersion],
};
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: jest.fn(),
}));
const deleteOneWorkflowVersionMock = jest.fn();
jest.mock('@/workflow/hooks/useDeleteOneWorkflowVersion', () => ({
useDeleteOneWorkflowVersion: () => ({
deleteOneWorkflowVersion: deleteOneWorkflowVersionMock,
}),
}));
const noDraftWorkflowWrapper =
getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [noDraftWorkflowMock.id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
recordStoreFamilyState(noDraftWorkflowMock.id),
noDraftWorkflowMock,
);
},
});
const draftWorkflowWrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [draftWorkflowMock.id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
recordStoreFamilyState(draftWorkflowMock.id),
draftWorkflowMock,
);
},
});
const draftWorkflowWithOneVersionWrapper =
getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: '1',
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [draftWorkflowMockWithOneVersion.id],
},
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
recordStoreFamilyState(draftWorkflowMockWithOneVersion.id),
draftWorkflowMockWithOneVersion,
);
},
});
describe('useDiscardDraftWorkflowSingleRecordAction', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should not be registered when there is no draft', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockImplementation(
() => noDraftWorkflowMock,
);
const { result } = renderHook(
() =>
useDiscardDraftWorkflowSingleRecordAction({
recordId: noDraftWorkflowMock.id,
}),
{
wrapper: noDraftWorkflowWrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(false);
});
it('should not be registered when there is only one version', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockImplementation(
() => draftWorkflowMockWithOneVersion,
);
const { result } = renderHook(
() =>
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMockWithOneVersion.id,
}),
{
wrapper: draftWorkflowWithOneVersionWrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(false);
});
it('should be registered when the workflow is draft', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockImplementation(
() => draftWorkflowMock,
);
const { result } = renderHook(
() =>
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMock.id,
}),
{
wrapper: draftWorkflowWrapper,
},
);
expect(result.current.shouldBeRegistered).toBe(true);
});
it('should call deactivateWorkflowVersion on click', () => {
(useWorkflowWithCurrentVersion as jest.Mock).mockImplementation(
() => draftWorkflowMock,
);
const { result } = renderHook(
() =>
useDiscardDraftWorkflowSingleRecordAction({
recordId: draftWorkflowMock.id,
}),
{
wrapper: draftWorkflowWrapper,
},
);
act(() => {
result.current.onClick();
});
expect(deleteOneWorkflowVersionMock).toHaveBeenCalledWith({
workflowVersionId: draftWorkflowMock.currentVersion.id,
});
});
});

View File

@ -1,128 +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 { useDiscardDraftWorkflowSingleRecordAction } from '../useDiscardDraftWorkflowSingleRecordAction';
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: () => ({
id: 'workflowId',
currentVersion: {
id: 'currentVersionId',
trigger: 'trigger',
status: 'DRAFT',
},
lastPublishedVersionId: 'lastPublishedVersionId',
versions: [
{
id: 'currentVersionId',
trigger: 'trigger',
status: 'DRAFT',
},
{
id: 'lastPublishedVersionId',
trigger: 'trigger',
status: 'ACTIVE',
},
],
}),
}));
describe('useDiscardDraftWorkflowSingleRecordAction', () => {
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 discard workflow draft workflow action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDiscardDraftWorkflowSingleRecordAction:
useDiscardDraftWorkflowSingleRecordAction({
workflowId: 'workflowId',
}),
};
},
{ wrapper },
);
act(() => {
result.current.useDiscardDraftWorkflowSingleRecordAction.registerDiscardDraftWorkflowSingleRecordAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get(
'discard-workflow-draft-single-record',
),
).toBeDefined();
expect(
result.current.actionMenuEntries.get(
'discard-workflow-draft-single-record',
)?.position,
).toBe(1);
});
it('should unregister deactivate workflow workflow action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDiscardDraftWorkflowSingleRecordAction:
useDiscardDraftWorkflowSingleRecordAction({
workflowId: 'workflow1',
}),
};
},
{ wrapper },
);
act(() => {
result.current.useDiscardDraftWorkflowSingleRecordAction.registerDiscardDraftWorkflowSingleRecordAction(
{ position: 1 },
);
});
act(() => {
result.current.useDiscardDraftWorkflowSingleRecordAction.unregisterDiscardDraftWorkflowSingleRecordAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

@ -0,0 +1,32 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useActivateDraftWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const { activateWorkflowVersion } = useActivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const shouldBeRegistered =
isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) &&
isDefined(workflowWithCurrentVersion.currentVersion?.steps) &&
workflowWithCurrentVersion.currentVersion.status === 'DRAFT';
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
activateWorkflowVersion({
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
workflowId: workflowWithCurrentVersion.id,
});
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -0,0 +1,35 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useActivateLastPublishedVersionWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const { activateWorkflowVersion } = useActivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const shouldBeRegistered =
isDefined(workflowWithCurrentVersion) &&
isDefined(workflowWithCurrentVersion.currentVersion.trigger) &&
isDefined(workflowWithCurrentVersion.lastPublishedVersionId) &&
workflowWithCurrentVersion.currentVersion.status !== '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

@ -1,64 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { IconPower, isDefined } from 'twenty-ui';
export const useActivateWorkflowDraftWorkflowSingleRecordAction = ({
workflowId,
}: {
workflowId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { activateWorkflowVersion } = useActivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const registerActivateWorkflowDraftWorkflowSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (
!isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) ||
!isDefined(workflowWithCurrentVersion.currentVersion?.steps)
) {
return;
}
const isDraft =
workflowWithCurrentVersion.currentVersion.status === 'DRAFT';
if (!isDraft) {
return;
}
addActionMenuEntry({
key: 'activate-workflow-draft-single-record',
label: 'Activate Draft',
position,
Icon: IconPower,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
onClick: () => {
activateWorkflowVersion({
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
workflowId: workflowWithCurrentVersion.id,
});
},
});
};
const unregisterActivateWorkflowDraftWorkflowSingleRecordAction = () => {
removeActionMenuEntry('activate-workflow-draft-single-record');
};
return {
registerActivateWorkflowDraftWorkflowSingleRecordAction,
unregisterActivateWorkflowDraftWorkflowSingleRecordAction,
};
};

View File

@ -1,61 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { IconPower, isDefined } from 'twenty-ui';
export const useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction =
({ workflowId }: { workflowId: string }) => {
const { addActionMenuEntry, removeActionMenuEntry } =
useActionMenuEntries();
const { activateWorkflowVersion } = useActivateWorkflowVersion();
const workflowWithCurrentVersion =
useWorkflowWithCurrentVersion(workflowId);
const registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction =
({ position }: { position: number }) => {
if (
!isDefined(workflowWithCurrentVersion) ||
!isDefined(workflowWithCurrentVersion.currentVersion.trigger) ||
!isDefined(workflowWithCurrentVersion.lastPublishedVersionId) ||
workflowWithCurrentVersion.currentVersion.status === 'ACTIVE' ||
!isDefined(workflowWithCurrentVersion.currentVersion?.steps) ||
workflowWithCurrentVersion.currentVersion?.steps.length === 0
) {
return;
}
addActionMenuEntry({
key: 'activate-workflow-last-published-version-single-record',
label: 'Activate last published version',
position,
Icon: IconPower,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
onClick: () => {
activateWorkflowVersion({
workflowVersionId:
workflowWithCurrentVersion.lastPublishedVersionId,
workflowId: workflowWithCurrentVersion.id,
});
},
});
};
const unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction =
() => {
removeActionMenuEntry(
'activate-workflow-last-published-version-single-record',
);
};
return {
registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction,
unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction,
};
};

View File

@ -0,0 +1,28 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useDeactivateWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const shouldBeRegistered =
isDefined(workflowWithCurrentVersion) &&
workflowWithCurrentVersion.currentVersion.status === 'ACTIVE';
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
deactivateWorkflowVersion(workflowWithCurrentVersion.currentVersion.id);
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -1,55 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { IconPlayerPause, isDefined } from 'twenty-ui';
export const useDeactivateWorkflowWorkflowSingleRecordAction = ({
workflowId,
}: {
workflowId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const isWorkflowActive =
isDefined(workflowWithCurrentVersion) &&
workflowWithCurrentVersion.currentVersion.status === 'ACTIVE';
const registerDeactivateWorkflowWorkflowSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (!isDefined(workflowWithCurrentVersion) || !isWorkflowActive) {
return;
}
addActionMenuEntry({
key: 'deactivate-workflow-single-record',
label: 'Deactivate Workflow',
position,
Icon: IconPlayerPause,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
onClick: () => {
deactivateWorkflowVersion(workflowWithCurrentVersion.currentVersion.id);
},
});
};
const unregisterDeactivateWorkflowWorkflowSingleRecordAction = () => {
removeActionMenuEntry('deactivate-workflow-single-record');
};
return {
registerDeactivateWorkflowWorkflowSingleRecordAction,
unregisterDeactivateWorkflowWorkflowSingleRecordAction,
};
};

View File

@ -1,63 +1,31 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { IconTrash, isDefined } from 'twenty-ui';
import { isDefined } from 'twenty-ui';
export const useDiscardDraftWorkflowSingleRecordAction = ({
workflowId,
}: {
workflowId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
export const useDiscardDraftWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const registerDiscardDraftWorkflowSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (
!isDefined(workflowWithCurrentVersion) ||
workflowWithCurrentVersion.versions.length < 2
) {
return;
}
const isDraft =
const shouldBeRegistered =
isDefined(workflowWithCurrentVersion) &&
workflowWithCurrentVersion.versions.length > 1 &&
workflowWithCurrentVersion.currentVersion.status === 'DRAFT';
if (!isDraft) {
return;
}
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
addActionMenuEntry({
key: 'discard-workflow-draft-single-record',
label: 'Discard Draft',
position,
Icon: IconTrash,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
onClick: () => {
deleteOneWorkflowVersion({
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
});
},
});
};
deleteOneWorkflowVersion({
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
});
};
const unregisterDiscardDraftWorkflowSingleRecordAction = () => {
removeActionMenuEntry('discard-workflow-draft-single-record');
return {
shouldBeRegistered,
onClick,
};
};
return {
registerDiscardDraftWorkflowSingleRecordAction,
unregisterDiscardDraftWorkflowSingleRecordAction,
};
};

View File

@ -0,0 +1,34 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
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 }) => {
const workflow = useWorkflowWithCurrentVersion(recordId);
const isDraft = workflow?.statuses?.includes('DRAFT') || false;
const workflowActiveVersion = useActiveWorkflowVersion(recordId);
const navigate = useNavigate();
const shouldBeRegistered = isDefined(workflowActiveVersion) && isDraft;
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
navigate(
`/object/${CoreObjectNameSingular.WorkflowVersion}/${workflowActiveVersion.id}`,
);
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -0,0 +1,41 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import qs from 'qs';
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui';
export const useSeeRunsWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const navigate = useNavigate();
const shouldBeRegistered = isDefined(workflowWithCurrentVersion);
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
const filterQueryParams: FilterQueryParams = {
filter: {
workflow: {
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
},
},
};
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowRun}?${qs.stringify(
filterQueryParams,
)}`;
navigate(filterLinkHref);
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -0,0 +1,41 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import qs from 'qs';
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-ui';
export const useSeeVersionsWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const navigate = useNavigate();
const shouldBeRegistered = isDefined(workflowWithCurrentVersion);
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
const filterQueryParams: FilterQueryParams = {
filter: {
workflow: {
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
},
},
};
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowVersion}?${qs.stringify(
filterQueryParams,
)}`;
navigate(filterLinkHref);
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -1,60 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { IconHistory, isDefined } from 'twenty-ui';
export const useSeeWorkflowActiveVersionWorkflowSingleRecordAction = ({
workflowId,
}: {
workflowId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const workflow = useRecoilValue(recordStoreFamilyState(workflowId));
const isDraft = workflow?.statuses?.includes('DRAFT');
const workflowActiveVersion = useActiveWorkflowVersion(workflowId);
const navigate = useNavigate();
const registerSeeWorkflowActiveVersionWorkflowSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (!isDefined(workflowActiveVersion) || !isDraft) {
return;
}
addActionMenuEntry({
key: 'see-workflow-active-version-single-record',
label: 'See active version',
position,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistory,
onClick: () => {
navigate(
`/object/${CoreObjectNameSingular.WorkflowVersion}/${workflowActiveVersion.id}`,
);
},
});
};
const unregisterSeeWorkflowActiveVersionWorkflowSingleRecordAction = () => {
removeActionMenuEntry('see-workflow-active-version-single-record');
};
return {
registerSeeWorkflowActiveVersionWorkflowSingleRecordAction,
unregisterSeeWorkflowActiveVersionWorkflowSingleRecordAction,
};
};

View File

@ -1,66 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import qs from 'qs';
import { useNavigate } from 'react-router-dom';
import { IconHistoryToggle, isDefined } from 'twenty-ui';
export const useSeeWorkflowRunsWorkflowSingleRecordAction = ({
workflowId,
}: {
workflowId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const navigate = useNavigate();
const registerSeeWorkflowRunsWorkflowSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (!isDefined(workflowWithCurrentVersion)) {
return;
}
const filterQueryParams: FilterQueryParams = {
filter: {
workflow: {
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
},
},
};
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowRun}?${qs.stringify(
filterQueryParams,
)}`;
addActionMenuEntry({
key: 'see-workflow-runs-single-record',
label: 'See runs',
position,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistoryToggle,
onClick: () => {
navigate(filterLinkHref);
},
});
};
const unregisterSeeWorkflowRunsWorkflowSingleRecordAction = () => {
removeActionMenuEntry('see-workflow-runs-single-record');
};
return {
registerSeeWorkflowRunsWorkflowSingleRecordAction,
unregisterSeeWorkflowRunsWorkflowSingleRecordAction,
};
};

View File

@ -1,66 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import qs from 'qs';
import { useNavigate } from 'react-router-dom';
import { IconHistory, isDefined } from 'twenty-ui';
export const useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction = ({
workflowId,
}: {
workflowId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const navigate = useNavigate();
const registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (!isDefined(workflowWithCurrentVersion)) {
return;
}
const filterQueryParams: FilterQueryParams = {
filter: {
workflow: {
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
},
},
};
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowVersion}?${qs.stringify(
filterQueryParams,
)}`;
addActionMenuEntry({
key: 'see-workflow-versions-history-single-record',
label: 'See versions history',
position,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistory,
onClick: () => {
navigate(filterLinkHref);
},
});
};
const unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction = () => {
removeActionMenuEntry('see-workflow-versions-history-single-record');
};
return {
registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction,
unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction,
};
};

View File

@ -0,0 +1,34 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-ui';
export const useTestWorkflowSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(recordId);
const { runWorkflowVersion } = useRunWorkflowVersion();
const shouldBeRegistered =
isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) &&
workflowWithCurrentVersion.currentVersion.trigger.type === 'MANUAL' &&
!isDefined(
workflowWithCurrentVersion.currentVersion.trigger.settings.objectType,
);
const onClick = () => {
if (!shouldBeRegistered) {
return;
}
runWorkflowVersion({
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
workflowName: workflowWithCurrentVersion.name,
});
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -1,60 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { IconPlayerPlay, isDefined } from 'twenty-ui';
export const useTestWorkflowWorkflowSingleRecordAction = ({
workflowId,
}: {
workflowId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const { runWorkflowVersion } = useRunWorkflowVersion();
const registerTestWorkflowWorkflowSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (
!isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) ||
workflowWithCurrentVersion.currentVersion.trigger.type !== 'MANUAL' ||
isDefined(
workflowWithCurrentVersion.currentVersion.trigger.settings.objectType,
)
) {
return;
}
addActionMenuEntry({
key: 'test-workflow-single-record',
label: 'Test workflow',
position,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconPlayerPlay,
onClick: () => {
runWorkflowVersion({
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
workflowName: workflowWithCurrentVersion.name,
});
},
});
};
const unregisterTestWorkflowWorkflowSingleRecordAction = () => {
removeActionMenuEntry('test-workflow-single-record');
};
return {
registerTestWorkflowWorkflowSingleRecordAction,
unregisterTestWorkflowWorkflowSingleRecordAction,
};
};

View File

@ -1,127 +0,0 @@
import { useActivateWorkflowDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowDraftWorkflowSingleRecordAction';
import { useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction';
import { useDeactivateWorkflowWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowWorkflowSingleRecordAction';
import { useDiscardDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDiscardDraftWorkflowSingleRecordAction';
import { useSeeWorkflowActiveVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowActiveVersionWorkflowSingleRecordAction';
import { useSeeWorkflowRunsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowRunsWorkflowSingleRecordAction';
import { useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction';
import { useTestWorkflowWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowWorkflowSingleRecordAction';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-ui';
export const useWorkflowSingleRecordActions = () => {
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 {
registerTestWorkflowWorkflowSingleRecordAction,
unregisterTestWorkflowWorkflowSingleRecordAction,
} = useTestWorkflowWorkflowSingleRecordAction({
workflowId: selectedRecordId,
});
const {
registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction,
unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction,
} = useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction({
workflowId: selectedRecordId,
});
const {
registerDeactivateWorkflowWorkflowSingleRecordAction,
unregisterDeactivateWorkflowWorkflowSingleRecordAction,
} = useDeactivateWorkflowWorkflowSingleRecordAction({
workflowId: selectedRecordId,
});
const {
registerSeeWorkflowRunsWorkflowSingleRecordAction,
unregisterSeeWorkflowRunsWorkflowSingleRecordAction,
} = useSeeWorkflowRunsWorkflowSingleRecordAction({
workflowId: selectedRecordId,
});
const {
registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction,
unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction,
} = useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction({
workflowId: selectedRecordId,
});
const {
registerSeeWorkflowActiveVersionWorkflowSingleRecordAction,
unregisterSeeWorkflowActiveVersionWorkflowSingleRecordAction,
} = useSeeWorkflowActiveVersionWorkflowSingleRecordAction({
workflowId: selectedRecordId,
});
const {
registerActivateWorkflowDraftWorkflowSingleRecordAction,
unregisterActivateWorkflowDraftWorkflowSingleRecordAction,
} = useActivateWorkflowDraftWorkflowSingleRecordAction({
workflowId: selectedRecordId,
});
const {
registerDiscardDraftWorkflowSingleRecordAction,
unregisterDiscardDraftWorkflowSingleRecordAction,
} = useDiscardDraftWorkflowSingleRecordAction({
workflowId: selectedRecordId,
});
const registerSingleRecordActions = ({
startPosition,
}: {
startPosition: number;
}) => {
registerTestWorkflowWorkflowSingleRecordAction({ position: startPosition });
registerDiscardDraftWorkflowSingleRecordAction({
position: startPosition + 1,
});
registerActivateWorkflowDraftWorkflowSingleRecordAction({
position: startPosition + 2,
});
registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction({
position: startPosition + 3,
});
registerDeactivateWorkflowWorkflowSingleRecordAction({
position: startPosition + 4,
});
registerSeeWorkflowRunsWorkflowSingleRecordAction({
position: startPosition + 5,
});
registerSeeWorkflowActiveVersionWorkflowSingleRecordAction({
position: startPosition + 6,
});
registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction({
position: startPosition + 7,
});
};
const unregisterSingleRecordActions = () => {
unregisterTestWorkflowWorkflowSingleRecordAction();
unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction();
unregisterDiscardDraftWorkflowSingleRecordAction();
unregisterActivateWorkflowDraftWorkflowSingleRecordAction();
unregisterDeactivateWorkflowWorkflowSingleRecordAction();
unregisterSeeWorkflowRunsWorkflowSingleRecordAction();
unregisterSeeWorkflowActiveVersionWorkflowSingleRecordAction();
unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction();
};
return {
registerSingleRecordActions,
unregisterSingleRecordActions,
};
};

View File

@ -1,21 +0,0 @@
import { NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS } from '@/action-menu/actions/record-actions/single-record/constants/NumberOfStandardSingleRecordActionsOnAllObjects';
import { useWorkflowVersionsSingleRecordActions } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useWorkflowVersionsSingleRecordActions';
import { useEffect } from 'react';
export const WorkflowVersionsSingleRecordActionMenuEntrySetterEffect = () => {
const { registerSingleRecordActions, unregisterSingleRecordActions } =
useWorkflowVersionsSingleRecordActions();
useEffect(() => {
registerSingleRecordActions({
startPosition:
NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS + 1,
});
return () => {
unregisterSingleRecordActions();
};
}, [registerSingleRecordActions, unregisterSingleRecordActions]);
return null;
};

View File

@ -0,0 +1,46 @@
import { useSeeExecutionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeExecutionsWorkflowVersionSingleRecordAction';
import { useSeeVersionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeVersionsWorkflowVersionSingleRecordAction';
import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { IconHistory, IconHistoryToggle, IconPencil } from 'twenty-ui';
export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
actionHook: SingleRecordActionHook;
}
> = {
useAsDraftWorkflowVersionSingleRecord: {
key: 'use-as-draft-workflow-version-single-record',
label: 'Use as draft',
position: 1,
isPinned: true,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconPencil,
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
},
seeWorkflowExecutionsSingleRecord: {
key: 'see-workflow-executions-single-record',
label: 'See executions',
position: 2,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistoryToggle,
actionHook: useSeeExecutionsWorkflowVersionSingleRecordAction,
},
seeWorkflowVersionsHistorySingleRecord: {
key: 'see-workflow-versions-history-single-record',
label: 'See versions history',
position: 3,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistory,
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction,
},
};

View File

@ -0,0 +1,48 @@
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import qs from 'qs';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useSeeExecutionsWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
workflowVersion?.workflow.id,
);
const navigate = useNavigate();
const shouldBeRegistered = isDefined(workflowWithCurrentVersion);
const onClick = () => {
if (!shouldBeRegistered) return;
const filterQueryParams: FilterQueryParams = {
filter: {
workflow: {
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
},
workflowVersion: {
[ViewFilterOperand.Is]: [recordId],
},
},
};
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowRun}?${qs.stringify(
filterQueryParams,
)}`;
navigate(filterLinkHref);
};
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -0,0 +1,24 @@
import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction';
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useSeeVersionsWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
if (!isDefined(workflowVersion)) {
throw new Error('Workflow version not found');
}
const { shouldBeRegistered, onClick } =
useSeeVersionsWorkflowSingleRecordAction({
recordId: workflowVersion.workflow.id,
});
return {
shouldBeRegistered,
onClick,
};
};

View File

@ -1,78 +0,0 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import qs from 'qs';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { IconHistoryToggle, isDefined } from 'twenty-ui';
export const useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction = ({
workflowVersionId,
}: {
workflowVersionId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const workflowVersion = useRecoilValue(
recordStoreFamilyState(workflowVersionId),
);
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
workflowVersion?.workflow.id,
);
const navigate = useNavigate();
const registerSeeWorkflowExecutionsWorkflowVersionSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (!isDefined(workflowWithCurrentVersion)) {
return;
}
const filterQueryParams: FilterQueryParams = {
filter: {
workflow: {
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
},
workflowVersion: {
[ViewFilterOperand.Is]: [workflowVersionId],
},
},
};
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowRun}?${qs.stringify(
filterQueryParams,
)}`;
addActionMenuEntry({
key: 'see-workflow-executions-single-record',
label: 'See executions',
position,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistoryToggle,
onClick: () => {
navigate(filterLinkHref);
},
});
};
const unregisterSeeWorkflowExecutionsWorkflowVersionSingleRecordAction =
() => {
removeActionMenuEntry('see-workflow-executions-single-record');
};
return {
registerSeeWorkflowExecutionsWorkflowVersionSingleRecordAction,
unregisterSeeWorkflowExecutionsWorkflowVersionSingleRecordAction,
};
};

View File

@ -1,27 +0,0 @@
import { useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilValue } from 'recoil';
export const useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction = ({
workflowVersionId,
}: {
workflowVersionId: string;
}) => {
const workflowVersion = useRecoilValue(
recordStoreFamilyState(workflowVersionId),
);
const {
registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction:
registerSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction:
unregisterSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
} = useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction({
workflowId: workflowVersion?.workflow.id,
});
return {
registerSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
unregisterSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
};
};

View File

@ -1,92 +1,66 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconPencil, isDefined } from 'twenty-ui';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useUseAsDraftWorkflowVersionSingleRecordAction = ({
workflowVersionId,
}: {
workflowVersionId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
export const useUseAsDraftWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
({ recordId }) => {
const workflowVersion = useWorkflowVersion(recordId);
const workflowVersion = useRecoilValue(
recordStoreFamilyState(workflowVersionId),
);
const workflow = useWorkflowWithCurrentVersion(
workflowVersion?.workflow?.id ?? '',
);
const workflow = useWorkflowWithCurrentVersion(
workflowVersion?.workflow?.id ?? '',
);
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
const setOpenOverrideWorkflowDraftConfirmationModal = useSetRecoilState(
openOverrideWorkflowDraftConfirmationModalState,
);
const setOpenOverrideWorkflowDraftConfirmationModal = useSetRecoilState(
openOverrideWorkflowDraftConfirmationModalState,
);
const workflowStatuses = workflow?.statuses;
const registerUseAsDraftWorkflowVersionSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (
!isDefined(workflowVersion) ||
!isDefined(workflow) ||
!isDefined(workflow.statuses) ||
workflowVersion.status === 'DRAFT'
) {
return;
}
const shouldBeRegistered =
isDefined(workflowVersion) &&
isDefined(workflow) &&
isDefined(workflowStatuses) &&
workflowVersion.status !== 'DRAFT';
const hasAlreadyDraftVersion = workflow.statuses.includes('DRAFT');
const onClick = async () => {
if (!shouldBeRegistered) return;
addActionMenuEntry({
key: 'use-workflow-version-as-draft-single-record',
label: 'Use as draft',
position,
Icon: IconPencil,
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
onClick: async () => {
if (hasAlreadyDraftVersion) {
setOpenOverrideWorkflowDraftConfirmationModal(true);
} else {
await createNewWorkflowVersion({
workflowId: workflowVersion.workflow.id,
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: workflowVersion.trigger,
steps: workflowVersion.steps,
});
}
},
ConfirmationModal: (
<OverrideWorkflowDraftConfirmationModal
draftWorkflowVersionId={workflow?.currentVersion?.id ?? ''}
workflowId={workflow?.id ?? ''}
workflowVersionUpdateInput={{
steps: workflowVersion.steps,
trigger: workflowVersion.trigger,
}}
/>
),
});
const hasAlreadyDraftVersion = workflowStatuses.includes('DRAFT');
if (hasAlreadyDraftVersion) {
setOpenOverrideWorkflowDraftConfirmationModal(true);
} else {
await createNewWorkflowVersion({
workflowId: workflowVersion.workflow.id,
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: workflowVersion.trigger,
steps: workflowVersion.steps,
});
}
};
const ConfirmationModal = shouldBeRegistered ? (
<OverrideWorkflowDraftConfirmationModal
draftWorkflowVersionId={workflow?.currentVersion?.id ?? ''}
workflowId={workflow?.id ?? ''}
workflowVersionUpdateInput={{
steps: workflowVersion.steps,
trigger: workflowVersion.trigger,
}}
/>
) : undefined;
return {
shouldBeRegistered,
onClick,
ConfirmationModal,
};
};
const unregisterUseAsDraftWorkflowVersionSingleRecordAction = () => {
removeActionMenuEntry('use-workflow-version-as-draft-single-record');
};
return {
registerUseAsDraftWorkflowVersionSingleRecordAction,
unregisterUseAsDraftWorkflowVersionSingleRecordAction,
};
};

View File

@ -1,70 +0,0 @@
import { useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction';
import { useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction';
import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-ui';
export const useWorkflowVersionsSingleRecordActions = () => {
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 {
registerSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
unregisterSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
} = useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction({
workflowVersionId: selectedRecordId,
});
const {
registerUseAsDraftWorkflowVersionSingleRecordAction,
unregisterUseAsDraftWorkflowVersionSingleRecordAction,
} = useUseAsDraftWorkflowVersionSingleRecordAction({
workflowVersionId: selectedRecordId,
});
const {
registerSeeWorkflowExecutionsWorkflowVersionSingleRecordAction,
unregisterSeeWorkflowExecutionsWorkflowVersionSingleRecordAction,
} = useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction({
workflowVersionId: selectedRecordId,
});
const registerSingleRecordActions = ({
startPosition,
}: {
startPosition: number;
}) => {
registerUseAsDraftWorkflowVersionSingleRecordAction({
position: startPosition,
});
registerSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction({
position: startPosition + 1,
});
registerSeeWorkflowExecutionsWorkflowVersionSingleRecordAction({
position: startPosition + 2,
});
};
const unregisterSingleRecordActions = () => {
unregisterUseAsDraftWorkflowVersionSingleRecordAction();
unregisterSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction();
unregisterSeeWorkflowExecutionsWorkflowVersionSingleRecordAction();
};
return {
registerSingleRecordActions,
unregisterSingleRecordActions,
};
};

View File

@ -0,0 +1,5 @@
export type ActionHookResult = {
shouldBeRegistered: boolean;
onClick: () => void;
ConfirmationModal?: React.ReactElement;
};

View File

@ -0,0 +1,20 @@
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

@ -1,4 +1,4 @@
import { MouseEvent, ReactNode } from 'react';
import { MouseEvent, ReactElement } from 'react';
import { IconComponent, MenuItemAccent } from 'twenty-ui';
export enum ActionMenuEntryType {
@ -21,5 +21,5 @@ export type ActionMenuEntry = {
isPinned?: boolean;
accent?: MenuItemAccent;
onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactNode;
ConfirmationModal?: ReactElement;
};

View File

@ -121,12 +121,50 @@ export const useCommandMenu = () => {
);
const closeCommandMenu = useRecoilCallback(
({ snapshot }) =>
({ snapshot, set }) =>
() => {
const isCommandMenuOpened = snapshot
.getLoadable(isCommandMenuOpenedState)
.getValue();
set(
contextStoreCurrentObjectMetadataIdComponentState.atomFamily({
instanceId: 'command-menu',
}),
null,
);
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'command-menu',
}),
{
mode: 'selection',
selectedRecordIds: [],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: 'command-menu',
}),
0,
);
set(
contextStoreFiltersComponentState.atomFamily({
instanceId: 'command-menu',
}),
[],
);
set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: 'command-menu',
}),
null,
);
if (isCommandMenuOpened) {
setIsCommandMenuOpened(false);
resetSelectedItem();

View File

@ -14,7 +14,7 @@ import { ViewType } from '@/views/types/ViewType';
import { MockedResponse } from '@apollo/client/testing';
import { expect } from '@storybook/test';
import gql from 'graphql-tag';
import { getJestMetadataAndApolloMocksAndContextStoreWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const defaultResponseData = {
@ -130,17 +130,15 @@ const mocks: MockedResponse[] = [
},
];
const WrapperWithResponse = getJestMetadataAndApolloMocksAndContextStoreWrapper(
{
apolloMocks: mocks,
componentInstanceId: 'recordIndexId',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreCurrentObjectMetadataNameSingular: 'person',
const WrapperWithResponse = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: mocks,
componentInstanceId: 'recordIndexId',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
);
contextStoreCurrentObjectMetadataNameSingular: 'person',
});
const graphqlEmptyResponse = [
{
@ -157,7 +155,7 @@ const graphqlEmptyResponse = [
];
const WrapperWithEmptyResponse =
getJestMetadataAndApolloMocksAndContextStoreWrapper({
getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: graphqlEmptyResponse,
componentInstanceId: 'recordIndexId',
contextStoreTargetedRecordsRule: {

View File

@ -14,6 +14,7 @@ import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { RecordShowPageWorkflowHeader } from '@/workflow/components/RecordShowPageWorkflowHeader';
import { RecordShowPageWorkflowVersionHeader } from '@/workflow/components/RecordShowPageWorkflowVersionHeader';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
export const RecordShowPage = () => {
@ -38,6 +39,10 @@ export const RecordShowPage = () => {
parameters.objectRecordId ?? '',
);
const isPageHeaderV2Enabled = useIsFeatureEnabled(
'IS_PAGE_HEADER_V2_ENABLED',
);
return (
<RecordFieldValueSelectorContextProvider>
<ContextStoreComponentInstanceContext.Provider
@ -57,25 +62,30 @@ export const RecordShowPage = () => {
headerIcon={headerIcon}
>
<>
{objectNameSingular === CoreObjectNameSingular.Workflow ? (
<RecordShowPageWorkflowHeader workflowId={objectRecordId} />
) : objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion ? (
<RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId}
/>
) : (
<>
<RecordShowActionMenu
{...{
isFavorite,
record,
handleFavoriteButtonClick,
objectMetadataItem,
objectNameSingular,
}}
{!isPageHeaderV2Enabled &&
objectNameSingular === CoreObjectNameSingular.Workflow && (
<RecordShowPageWorkflowHeader workflowId={objectRecordId} />
)}
{!isPageHeaderV2Enabled &&
objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion && (
<RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId}
/>
</>
)}
{(isPageHeaderV2Enabled ||
(objectNameSingular !== CoreObjectNameSingular.Workflow &&
objectNameSingular !==
CoreObjectNameSingular.WorkflowVersion)) && (
<RecordShowActionMenu
{...{
isFavorite,
record,
handleFavoriteButtonClick,
objectMetadataItem,
objectNameSingular,
}}
/>
)}
</>
</RecordShowPageHeader>

View File

@ -1,3 +1,4 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { MockedResponse } from '@apollo/client/testing';
@ -6,13 +7,7 @@ import { MutableSnapshot } from 'recoil';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({
apolloMocks,
onInitializeRecoilSnapshot,
contextStoreTargetedRecordsRule,
contextStoreCurrentObjectMetadataNameSingular,
componentInstanceId,
}: {
export type GetJestMetadataAndApolloMocksAndActionMenuWrapperProps = {
apolloMocks:
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
| undefined;
@ -20,11 +15,20 @@ export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
contextStoreCurrentObjectMetadataNameSingular?: string;
componentInstanceId: string;
}) => {
};
export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
apolloMocks,
onInitializeRecoilSnapshot,
contextStoreTargetedRecordsRule,
contextStoreCurrentObjectMetadataNameSingular,
componentInstanceId,
}: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps) => {
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks,
onInitializeRecoilSnapshot,
});
return ({ children }: { children: ReactNode }) => (
<Wrapper>
<ContextStoreComponentInstanceContext.Provider
@ -32,14 +36,20 @@ export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({
instanceId: componentInstanceId,
}}
>
<JestContextStoreSetter
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
>
{children}
</JestContextStoreSetter>
<JestContextStoreSetter
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
>
{children}
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</Wrapper>
);