Implement contextual actions for the workflows (#8814)

Implemented the following actions for the workflows:
- Test Workflow
- Discard Draft
- Activate Draft
- Activate Workflow Last Published Version
- Deactivate Workflow
- See Workflow Executions
- See Workflow Active Version
- See Workflow Versions History

And the following actions for the workflow versions:
- Use As Draft
- See Workflow Versions History
- See Workflow Executions

Video example:


https://github.com/user-attachments/assets/016956a6-6f2e-4de5-9605-d6e14526cbb3

A few of these actions are links to the relations of the workflow object
(link to a filtered table). To generalize this behavior, I will create
an hook named `useSeeRelationsActionSingleRecordAction` to automatically
generate links to a showPage if the relation is a Many To One or to a
filtered table if the relation is a One to Many for all the record
types.
I will also create actions which will allow to create a relation.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Raphaël Bosi
2024-12-03 12:09:51 +01:00
committed by GitHub
parent 7b2d9894f3
commit 32194a88b3
45 changed files with 2334 additions and 254 deletions

View File

@ -1,9 +1,9 @@
import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect'; import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect';
import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect'; import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect';
import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect'; import { SingleRecordActionMenuEntrySetter } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetter';
import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter'; import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@ -32,30 +32,35 @@ const ActionEffects = ({
objectId: objectMetadataItemId, objectId: objectMetadataItemId,
}); });
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState, contextStoreTargetedRecordsRuleComponentState,
); );
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
return ( return (
<> <>
{contextStoreNumberOfSelectedRecords === 0 && ( {contextStoreTargetedRecordsRule.mode === 'selection' &&
<NoSelectionActionMenuEntrySetterEffect contextStoreTargetedRecordsRule.selectedRecordIds.length === 0 && (
objectMetadataItem={objectMetadataItem} <NoSelectionActionMenuEntrySetterEffect
/> objectMetadataItem={objectMetadataItem}
)} />
{contextStoreNumberOfSelectedRecords === 1 && ( )}
<SingleRecordActionMenuEntrySetterEffect {contextStoreTargetedRecordsRule.mode === 'selection' &&
objectMetadataItem={objectMetadataItem} contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
/> <>
)} <SingleRecordActionMenuEntrySetter
{contextStoreNumberOfSelectedRecords === 1 && isWorkflowEnabled && ( objectMetadataItem={objectMetadataItem}
<WorkflowRunRecordActionMenuEntrySetterEffect />
objectMetadataItem={objectMetadataItem} {isWorkflowEnabled && (
/> <WorkflowRunRecordActionMenuEntrySetterEffect
)} objectMetadataItem={objectMetadataItem}
{contextStoreNumberOfSelectedRecords > 1 && ( />
)}
</>
)}
{(contextStoreTargetedRecordsRule.mode === 'exclusion' ||
contextStoreTargetedRecordsRule.selectedRecordIds.length > 1) && (
<MultipleRecordsActionMenuEntrySetterEffect <MultipleRecordsActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem} objectMetadataItem={objectMetadataItem}
/> />

View File

@ -0,0 +1,132 @@
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { expect } from '@storybook/test';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { useDeleteMultipleRecordsAction } from '../useDeleteMultipleRecordsAction';
jest.mock('@/object-record/hooks/useDeleteManyRecords', () => ({
useDeleteManyRecords: () => ({
deleteManyRecords: 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(),
}),
}));
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
onInitializeRecoilSnapshot: ({ set }) => {
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: '1',
}),
3,
);
},
});
describe('useDeleteMultipleRecordsAction', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<JestMetadataAndApolloMocksWrapper>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
{children}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</JestMetadataAndApolloMocksWrapper>
);
it('should register delete action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeleteMultipleRecordsAction: useDeleteMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useDeleteMultipleRecordsAction.registerDeleteMultipleRecordsAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get('delete-multiple-records'),
).toBeDefined();
expect(
result.current.actionMenuEntries.get('delete-multiple-records')?.position,
).toBe(1);
});
it('should unregister delete action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeleteMultipleRecordsAction: useDeleteMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useDeleteMultipleRecordsAction.registerDeleteMultipleRecordsAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
act(() => {
result.current.useDeleteMultipleRecordsAction.unregisterDeleteMultipleRecordsAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

@ -0,0 +1,105 @@
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 { useExportMultipleRecordsAction } from '../useExportMultipleRecordsAction';
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useExportMultipleRecordsAction', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<JestMetadataAndApolloMocksWrapper>
<JestObjectMetadataItemSetter>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: '1',
}}
>
{children}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</JestMetadataAndApolloMocksWrapper>
);
it('should register export multiple records action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useExportMultipleRecordsAction: useExportMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useExportMultipleRecordsAction.registerExportMultipleRecordsAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
expect(
result.current.actionMenuEntries.get('export-multiple-records'),
).toBeDefined();
expect(
result.current.actionMenuEntries.get('export-multiple-records')?.position,
).toBe(1);
});
it('should unregister export multiple records action', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useExportMultipleRecordsAction: useExportMultipleRecordsAction({
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ wrapper },
);
act(() => {
result.current.useExportMultipleRecordsAction.registerExportMultipleRecordsAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
act(() => {
result.current.useExportMultipleRecordsAction.unregisterExportMultipleRecordsAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

@ -22,10 +22,8 @@ import { useCallback, useContext, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui'; import { IconTrash, isDefined } from 'twenty-ui';
export const useDeleteMultipleRecordsAction = ({ export const useDeleteMultipleRecordsAction = ({
position,
objectMetadataItem, objectMetadataItem,
}: { }: {
position: number;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -106,7 +104,11 @@ export const useDeleteMultipleRecordsAction = ({
const { isInRightDrawer, onActionExecutedCallback } = const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext); useContext(ActionMenuContext);
const registerDeleteMultipleRecordsAction = () => { const registerDeleteMultipleRecordsAction = ({
position,
}: {
position: number;
}) => {
if (canDelete) { if (canDelete) {
addActionMenuEntry({ addActionMenuEntry({
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,

View File

@ -12,10 +12,8 @@ import {
} from '@/object-record/record-index/export/hooks/useExportRecords'; } from '@/object-record/record-index/export/hooks/useExportRecords';
export const useExportMultipleRecordsAction = ({ export const useExportMultipleRecordsAction = ({
position,
objectMetadataItem, objectMetadataItem,
}: { }: {
position: number;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -27,7 +25,11 @@ export const useExportMultipleRecordsAction = ({
filename: `${objectMetadataItem.nameSingular}.csv`, filename: `${objectMetadataItem.nameSingular}.csv`,
}); });
const registerExportMultipleRecordsAction = () => { const registerExportMultipleRecordsAction = ({
position,
}: {
position: number;
}) => {
addActionMenuEntry({ addActionMenuEntry({
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,

View File

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

View File

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

View File

@ -12,10 +12,8 @@ import {
} from '@/object-record/record-index/export/hooks/useExportRecords'; } from '@/object-record/record-index/export/hooks/useExportRecords';
export const useExportViewNoSelectionRecordAction = ({ export const useExportViewNoSelectionRecordAction = ({
position,
objectMetadataItem, objectMetadataItem,
}: { }: {
position: number;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -27,7 +25,11 @@ export const useExportViewNoSelectionRecordAction = ({
filename: `${objectMetadataItem.nameSingular}.csv`, filename: `${objectMetadataItem.nameSingular}.csv`,
}); });
const registerExportViewNoSelectionRecordsAction = () => { const registerExportViewNoSelectionRecordsAction = ({
position,
}: {
position: number;
}) => {
addActionMenuEntry({ addActionMenuEntry({
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.Global, scope: ActionMenuEntryScope.Global,

View File

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

View File

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

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

View File

@ -0,0 +1,121 @@
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 { RecoilRoot } from 'recoil';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { useDeleteSingleRecordAction } from '../useDeleteSingleRecordAction';
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(),
}),
}));
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
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', () => {
const { result } = renderHook(
() => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
);
return {
actionMenuEntries,
useDeleteSingleRecordAction: useDeleteSingleRecordAction({
recordId: 'record1',
objectMetadataItem: companyMockObjectMetadataItem,
}),
};
},
{ 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 },
);
act(() => {
result.current.useDeleteSingleRecordAction.registerDeleteSingleRecordAction(
{ position: 1 },
);
});
expect(result.current.actionMenuEntries.size).toBe(1);
act(() => {
result.current.useDeleteSingleRecordAction.unregisterDeleteSingleRecordAction();
});
expect(result.current.actionMenuEntries.size).toBe(0);
});
});

View File

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

@ -4,7 +4,6 @@ import {
ActionMenuEntryScope, ActionMenuEntryScope,
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } from '@/action-menu/types/ActionMenuEntry';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@ -12,15 +11,14 @@ import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useContext, useState } from 'react'; import { useCallback, useContext, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui'; import { IconTrash, isDefined } from 'twenty-ui';
export const useDeleteSingleRecordAction = ({ export const useDeleteSingleRecordAction = ({
position, recordId,
objectMetadataItem, objectMetadataItem,
}: { }: {
position: number; recordId: string;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -39,39 +37,26 @@ export const useDeleteSingleRecordAction = ({
const { sortedFavorites: favorites } = useFavorites(); const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite(); const { deleteFavorite } = useDeleteFavorite();
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const { closeRightDrawer } = useRightDrawer(); const { closeRightDrawer } = useRightDrawer();
const recordIdToDelete =
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds?.[0]
: undefined;
const handleDeleteClick = useCallback(async () => { const handleDeleteClick = useCallback(async () => {
if (!isDefined(recordIdToDelete)) {
return;
}
resetTableRowSelection(); resetTableRowSelection();
const foundFavorite = favorites?.find( const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordIdToDelete, (favorite) => favorite.recordId === recordId,
); );
if (isDefined(foundFavorite)) { if (isDefined(foundFavorite)) {
deleteFavorite(foundFavorite.id); deleteFavorite(foundFavorite.id);
} }
await deleteOneRecord(recordIdToDelete); await deleteOneRecord(recordId);
}, [ }, [
deleteFavorite, deleteFavorite,
deleteOneRecord, deleteOneRecord,
favorites, favorites,
recordIdToDelete,
resetTableRowSelection, resetTableRowSelection,
recordId,
]); ]);
const isRemoteObject = objectMetadataItem.isRemote; const isRemoteObject = objectMetadataItem.isRemote;
@ -79,8 +64,12 @@ export const useDeleteSingleRecordAction = ({
const { isInRightDrawer, onActionExecutedCallback } = const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext); useContext(ActionMenuContext);
const registerDeleteSingleRecordAction = () => { const registerDeleteSingleRecordAction = ({
if (isRemoteObject || !isDefined(recordIdToDelete)) { position,
}: {
position: number;
}) => {
if (isRemoteObject) {
return; return;
} }

View File

@ -3,51 +3,42 @@ import {
ActionMenuEntryScope, ActionMenuEntryScope,
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } from '@/action-menu/types/ActionMenuEntry';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite'; import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui'; import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui';
export const useManageFavoritesSingleRecordAction = ({ export const useManageFavoritesSingleRecordAction = ({
position, recordId,
objectMetadataItem, objectMetadataItem,
}: { }: {
position: number; recordId: string;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const { sortedFavorites: favorites } = useFavorites(); const { sortedFavorites: favorites } = useFavorites();
const { createFavorite } = useCreateFavorite(); const { createFavorite } = useCreateFavorite();
const { deleteFavorite } = useDeleteFavorite(); const { deleteFavorite } = useDeleteFavorite();
const selectedRecordId = const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
: undefined;
const selectedRecord = useRecoilValue(
recordStoreFamilyState(selectedRecordId ?? ''),
);
const foundFavorite = favorites?.find( const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === selectedRecordId, (favorite) => favorite.recordId === recordId,
); );
const isFavorite = !!selectedRecordId && !!foundFavorite; const isFavorite = !!foundFavorite;
const registerManageFavoritesSingleRecordAction = () => { const registerManageFavoritesSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) { if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) {
return; return;
} }

View File

@ -1,17 +1,33 @@
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction'; import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
import { useManageFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction'; 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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-ui';
export const useSingleRecordActions = ({ export const useSingleRecordActions = ({
objectMetadataItem, objectMetadataItem,
}: { }: {
objectMetadataItem: 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 { const {
registerManageFavoritesSingleRecordAction, registerManageFavoritesSingleRecordAction,
unregisterManageFavoritesSingleRecordAction, unregisterManageFavoritesSingleRecordAction,
} = useManageFavoritesSingleRecordAction({ } = useManageFavoritesSingleRecordAction({
position: 0, recordId: selectedRecordId,
objectMetadataItem, objectMetadataItem,
}); });
@ -19,13 +35,13 @@ export const useSingleRecordActions = ({
registerDeleteSingleRecordAction, registerDeleteSingleRecordAction,
unregisterDeleteSingleRecordAction, unregisterDeleteSingleRecordAction,
} = useDeleteSingleRecordAction({ } = useDeleteSingleRecordAction({
position: 1, recordId: selectedRecordId,
objectMetadataItem, objectMetadataItem,
}); });
const registerSingleRecordActions = () => { const registerSingleRecordActions = () => {
registerManageFavoritesSingleRecordAction(); registerManageFavoritesSingleRecordAction({ position: 1 });
registerDeleteSingleRecordAction(); registerDeleteSingleRecordAction({ position: 2 });
}; };
const unregisterSingleRecordActions = () => { const unregisterSingleRecordActions = () => {

View File

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

@ -0,0 +1,124 @@
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,113 @@
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,128 @@
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,64 @@
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

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

@ -0,0 +1,63 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { IconTrash, isDefined } from 'twenty-ui';
export const useDiscardDraftWorkflowSingleRecordAction = ({
workflowId,
}: {
workflowId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const registerDiscardDraftWorkflowSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (
!isDefined(workflowWithCurrentVersion) ||
workflowWithCurrentVersion.versions.length < 2
) {
return;
}
const isDraft =
workflowWithCurrentVersion.currentVersion.status === 'DRAFT';
if (!isDraft) {
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,
});
},
});
};
const unregisterDiscardDraftWorkflowSingleRecordAction = () => {
removeActionMenuEntry('discard-workflow-draft-single-record');
};
return {
registerDiscardDraftWorkflowSingleRecordAction,
unregisterDiscardDraftWorkflowSingleRecordAction,
};
};

View File

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

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

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

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

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

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

@ -0,0 +1,92 @@
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 { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconPencil, isDefined } from 'twenty-ui';
export const useUseAsDraftWorkflowVersionSingleRecordAction = ({
workflowVersionId,
}: {
workflowVersionId: string;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const workflowVersion = useRecoilValue(
recordStoreFamilyState(workflowVersionId),
);
const workflow = useWorkflowWithCurrentVersion(
workflowVersion?.workflow?.id ?? '',
);
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
const setOpenOverrideWorkflowDraftConfirmationModal = useSetRecoilState(
openOverrideWorkflowDraftConfirmationModalState,
);
const registerUseAsDraftWorkflowVersionSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (
!isDefined(workflowVersion) ||
!isDefined(workflow) ||
!isDefined(workflow.statuses) ||
workflowVersion.status === 'DRAFT'
) {
return;
}
const hasAlreadyDraftVersion = workflow.statuses.includes('DRAFT');
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 unregisterUseAsDraftWorkflowVersionSingleRecordAction = () => {
removeActionMenuEntry('use-workflow-version-as-draft-single-record');
};
return {
registerUseAsDraftWorkflowVersionSingleRecordAction,
unregisterUseAsDraftWorkflowVersionSingleRecordAction,
};
};

View File

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

@ -6,13 +6,10 @@ import {
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions'; import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconSettingsAutomation, isDefined } from 'twenty-ui'; import { IconSettingsAutomation, isDefined } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
@ -33,8 +30,12 @@ export const useWorkflowRunRecordActions = ({
? contextStoreTargetedRecordsRule.selectedRecordIds[0] ? contextStoreTargetedRecordsRule.selectedRecordIds[0]
: undefined; : undefined;
if (!isDefined(selectedRecordId)) {
throw new Error('Selected record ID is required');
}
const selectedRecord = useRecoilValue( const selectedRecord = useRecoilValue(
recordStoreFamilyState(selectedRecordId ?? ''), recordStoreFamilyState(selectedRecordId),
); );
const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({ const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({
@ -44,10 +45,6 @@ export const useWorkflowRunRecordActions = ({
const { runWorkflowVersion } = useRunWorkflowVersion(); const { runWorkflowVersion } = useRunWorkflowVersion();
const { enqueueSnackBar } = useSnackBar();
const theme = useTheme();
const registerWorkflowRunRecordActions = () => { const registerWorkflowRunRecordActions = () => {
if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) { if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) {
return; return;
@ -57,6 +54,9 @@ export const useWorkflowRunRecordActions = ({
index, index,
activeWorkflowVersion, activeWorkflowVersion,
] of activeWorkflowVersions.entries()) { ] of activeWorkflowVersions.entries()) {
if (!isDefined(activeWorkflowVersion.workflow)) {
continue;
}
const name = capitalize(activeWorkflowVersion.workflow.name); const name = capitalize(activeWorkflowVersion.workflow.name);
addActionMenuEntry({ addActionMenuEntry({
type: ActionMenuEntryType.WorkflowRun, type: ActionMenuEntryType.WorkflowRun,
@ -72,19 +72,9 @@ export const useWorkflowRunRecordActions = ({
await runWorkflowVersion({ await runWorkflowVersion({
workflowVersionId: activeWorkflowVersion.id, workflowVersionId: activeWorkflowVersion.id,
workflowName: name,
payload: selectedRecord, payload: selectedRecord,
}); });
enqueueSnackBar('', {
variant: SnackBarVariant.Success,
title: `${name} starting...`,
icon: (
<IconSettingsAutomation
size={16}
color={theme.snackBar.success.color}
/>
),
});
}, },
}); });
} }

View File

@ -3,14 +3,11 @@ import {
ActionMenuEntryScope, ActionMenuEntryScope,
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } from '@/action-menu/types/ActionMenuEntry';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions'; import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react'; import { IconSettingsAutomation, isDefined } from 'twenty-ui';
import { IconSettingsAutomation } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
export const useWorkflowRunActions = () => { export const useWorkflowRunActions = () => {
@ -24,10 +21,6 @@ export const useWorkflowRunActions = () => {
const { runWorkflowVersion } = useRunWorkflowVersion(); const { runWorkflowVersion } = useRunWorkflowVersion();
const { enqueueSnackBar } = useSnackBar();
const theme = useTheme();
const addWorkflowRunActions = () => { const addWorkflowRunActions = () => {
if (!isWorkflowEnabled) { if (!isWorkflowEnabled) {
return; return;
@ -37,7 +30,12 @@ export const useWorkflowRunActions = () => {
index, index,
activeWorkflowVersion, activeWorkflowVersion,
] of activeWorkflowVersions.entries()) { ] of activeWorkflowVersions.entries()) {
if (!isDefined(activeWorkflowVersion.workflow)) {
continue;
}
const name = capitalize(activeWorkflowVersion.workflow.name); const name = capitalize(activeWorkflowVersion.workflow.name);
addActionMenuEntry({ addActionMenuEntry({
type: ActionMenuEntryType.WorkflowRun, type: ActionMenuEntryType.WorkflowRun,
key: `workflow-run-${activeWorkflowVersion.id}`, key: `workflow-run-${activeWorkflowVersion.id}`,
@ -48,17 +46,7 @@ export const useWorkflowRunActions = () => {
onClick: async () => { onClick: async () => {
await runWorkflowVersion({ await runWorkflowVersion({
workflowVersionId: activeWorkflowVersion.id, workflowVersionId: activeWorkflowVersion.id,
}); workflowName: name,
enqueueSnackBar('', {
variant: SnackBarVariant.Success,
title: `${name} starting...`,
icon: (
<IconSettingsAutomation
size={16}
color={theme.snackBar.success.color}
/>
),
}); });
}, },
}); });

View File

@ -7,14 +7,17 @@ import {
} from '@/ui/layout/modal/components/ConfirmationModal'; } from '@/ui/layout/modal/components/ConfirmationModal';
import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState'; import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState';
import { WorkflowVersion } from '@/workflow/types/Workflow'; import { WorkflowVersion } from '@/workflow/types/Workflow';
import { useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
export const OverrideWorkflowDraftConfirmationModal = ({ export const OverrideWorkflowDraftConfirmationModal = ({
draftWorkflowVersionId, draftWorkflowVersionId,
workflowVersionUpdateInput, workflowVersionUpdateInput,
workflowId,
}: { }: {
draftWorkflowVersionId: string; draftWorkflowVersionId: string;
workflowVersionUpdateInput: Pick<WorkflowVersion, 'trigger' | 'steps'>; workflowVersionUpdateInput: Pick<WorkflowVersion, 'trigger' | 'steps'>;
workflowId: string;
}) => { }) => {
const [ const [
openOverrideWorkflowDraftConfirmationModal, openOverrideWorkflowDraftConfirmationModal,
@ -26,11 +29,15 @@ export const OverrideWorkflowDraftConfirmationModal = ({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion, objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
}); });
const navigate = useNavigate();
const handleOverrideDraft = async () => { const handleOverrideDraft = async () => {
await updateOneWorkflowVersion({ await updateOneWorkflowVersion({
idToUpdate: draftWorkflowVersionId, idToUpdate: draftWorkflowVersionId,
updateOneRecordInput: workflowVersionUpdateInput, updateOneRecordInput: workflowVersionUpdateInput,
}); });
navigate(buildShowPageURL(CoreObjectNameSingular.Workflow, workflowId));
}; };
return ( return (

View File

@ -71,6 +71,7 @@ export const RecordShowPageWorkflowHeader = ({
await runWorkflowVersion({ await runWorkflowVersion({
workflowVersionId: workflowWithCurrentVersion.currentVersion.id, workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
workflowName: workflowWithCurrentVersion.name,
}); });
enqueueSnackBar('', { enqueueSnackBar('', {

View File

@ -131,6 +131,7 @@ export const RecordShowPageWorkflowVersionHeader = ({
{isDefined(workflowVersion) && isDefined(draftWorkflowVersion) ? ( {isDefined(workflowVersion) && isDefined(draftWorkflowVersion) ? (
<OverrideWorkflowDraftConfirmationModal <OverrideWorkflowDraftConfirmationModal
draftWorkflowVersionId={draftWorkflowVersion.id} draftWorkflowVersionId={draftWorkflowVersion.id}
workflowId={workflowVersion.workflowId}
workflowVersionUpdateInput={{ workflowVersionUpdateInput={{
steps: workflowVersion.steps, steps: workflowVersion.steps,
trigger: workflowVersion.trigger, trigger: workflowVersion.trigger,

View File

@ -0,0 +1,43 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow';
export const useActiveWorkflowVersion = (workflowId: string) => {
const { records: workflowVersions } = useFindManyRecords<
WorkflowVersion & {
workflow: Omit<Workflow, 'versions'> & {
versions: Array<{ __typename: string }>;
};
}
>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
filter: {
workflowId: {
eq: workflowId,
},
status: {
eq: 'ACTIVE',
},
},
recordGqlFields: {
id: true,
name: true,
createdAt: true,
updatedAt: true,
workflowId: true,
trigger: true,
steps: true,
status: true,
workflow: {
id: true,
name: true,
statuses: true,
versions: {
totalCount: true,
},
},
},
});
return workflowVersions?.[0];
};

View File

@ -1,10 +1,15 @@
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { RUN_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/runWorkflowVersion'; import { RUN_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/runWorkflowVersion';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import { useTheme } from '@emotion/react';
import { IconSettingsAutomation } from 'twenty-ui';
import { import {
RunWorkflowVersionMutation, RunWorkflowVersionMutation,
RunWorkflowVersionMutationVariables, RunWorkflowVersionMutationVariables,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { capitalize } from '~/utils/string/capitalize';
export const useRunWorkflowVersion = () => { export const useRunWorkflowVersion = () => {
const apolloMetadataClient = useApolloMetadataClient(); const apolloMetadataClient = useApolloMetadataClient();
@ -15,16 +20,33 @@ export const useRunWorkflowVersion = () => {
client: apolloMetadataClient, client: apolloMetadataClient,
}); });
const { enqueueSnackBar } = useSnackBar();
const theme = useTheme();
const runWorkflowVersion = async ({ const runWorkflowVersion = async ({
workflowVersionId, workflowVersionId,
workflowName,
payload, payload,
}: { }: {
workflowVersionId: string; workflowVersionId: string;
workflowName: string;
payload?: Record<string, any>; payload?: Record<string, any>;
}) => { }) => {
await mutate({ await mutate({
variables: { input: { workflowVersionId, payload } }, variables: { input: { workflowVersionId, payload } },
}); });
enqueueSnackBar('', {
variant: SnackBarVariant.Success,
title: `${capitalize(workflowName)} starting...`,
icon: (
<IconSettingsAutomation
size={16}
color={theme.snackBar.success.color}
/>
),
});
}; };
return { runWorkflowVersion }; return { runWorkflowVersion };

View File

@ -17,6 +17,7 @@ export const useWorkflowWithCurrentVersion = (
id: true, id: true,
name: true, name: true,
statuses: true, statuses: true,
lastPublishedVersionId: true,
versions: true, versions: true,
}, },
skip: !isDefined(workflowId), skip: !isDefined(workflowId),

View File

@ -150,6 +150,8 @@ export {
IconHeartOff, IconHeartOff,
IconHelpCircle, IconHelpCircle,
IconHierarchy2, IconHierarchy2,
IconHistory,
IconHistoryToggle,
IconHome, IconHome,
IconInbox, IconInbox,
IconInfoCircle, IconInfoCircle,
@ -192,6 +194,7 @@ export {
IconPhoto, IconPhoto,
IconPhotoUp, IconPhotoUp,
IconPilcrow, IconPilcrow,
IconPlayerPause,
IconPlayerPlay, IconPlayerPlay,
IconPlayerStop, IconPlayerStop,
IconPlaylistAdd, IconPlaylistAdd,

View File

@ -10,20 +10,21 @@ import {
Icon3dRotate, Icon3dRotate,
IconAB, IconAB,
IconAB2, IconAB2,
IconABOff,
IconAbacus, IconAbacus,
IconAbacusOff, IconAbacusOff,
IconAbc, IconAbc,
IconABOff,
IconAccessible,
IconAccessibleOff,
IconAccessPoint, IconAccessPoint,
IconAccessPointOff, IconAccessPointOff,
IconAccessible,
IconAccessibleOff,
IconActivity, IconActivity,
IconActivityHeartbeat, IconActivityHeartbeat,
IconAd, IconAd,
IconAd2, IconAd2,
IconAdCircle, IconAdCircle,
IconAdCircleOff, IconAdCircleOff,
IconAdOff,
IconAddressBookOff, IconAddressBookOff,
IconAdjustments, IconAdjustments,
IconAdjustmentsAlt, IconAdjustmentsAlt,
@ -48,7 +49,6 @@ import {
IconAdjustmentsStar, IconAdjustmentsStar,
IconAdjustmentsUp, IconAdjustmentsUp,
IconAdjustmentsX, IconAdjustmentsX,
IconAdOff,
IconAerialLift, IconAerialLift,
IconAffiliate, IconAffiliate,
IconAirBalloon, IconAirBalloon,
@ -118,10 +118,10 @@ import {
IconApiApp, IconApiApp,
IconApiAppOff, IconApiAppOff,
IconApiOff, IconApiOff,
IconAppWindow,
IconApple, IconApple,
IconApps, IconApps,
IconAppsOff, IconAppsOff,
IconAppWindow,
IconArchive, IconArchive,
IconArchiveOff, IconArchiveOff,
IconArmchair, IconArmchair,
@ -233,6 +233,23 @@ import {
IconArrowRotaryStraight, IconArrowRotaryStraight,
IconArrowRoundaboutLeft, IconArrowRoundaboutLeft,
IconArrowRoundaboutRight, IconArrowRoundaboutRight,
IconArrowSharpTurnLeft,
IconArrowSharpTurnRight,
IconArrowUp,
IconArrowUpBar,
IconArrowUpCircle,
IconArrowUpLeft,
IconArrowUpLeftCircle,
IconArrowUpRhombus,
IconArrowUpRight,
IconArrowUpRightCircle,
IconArrowUpSquare,
IconArrowUpTail,
IconArrowWaveLeftDown,
IconArrowWaveLeftUp,
IconArrowWaveRightDown,
IconArrowWaveRightUp,
IconArrowZigZag,
IconArrowsCross, IconArrowsCross,
IconArrowsDiagonal, IconArrowsDiagonal,
IconArrowsDiagonal2, IconArrowsDiagonal2,
@ -247,8 +264,6 @@ import {
IconArrowsDownUp, IconArrowsDownUp,
IconArrowsExchange, IconArrowsExchange,
IconArrowsExchange2, IconArrowsExchange2,
IconArrowSharpTurnLeft,
IconArrowSharpTurnRight,
IconArrowsHorizontal, IconArrowsHorizontal,
IconArrowsJoin, IconArrowsJoin,
IconArrowsJoin2, IconArrowsJoin2,
@ -276,21 +291,6 @@ import {
IconArrowsUpLeft, IconArrowsUpLeft,
IconArrowsUpRight, IconArrowsUpRight,
IconArrowsVertical, IconArrowsVertical,
IconArrowUp,
IconArrowUpBar,
IconArrowUpCircle,
IconArrowUpLeft,
IconArrowUpLeftCircle,
IconArrowUpRhombus,
IconArrowUpRight,
IconArrowUpRightCircle,
IconArrowUpSquare,
IconArrowUpTail,
IconArrowWaveLeftDown,
IconArrowWaveLeftUp,
IconArrowWaveRightDown,
IconArrowWaveRightUp,
IconArrowZigZag,
IconArtboard, IconArtboard,
IconArtboardOff, IconArtboardOff,
IconArticle, IconArticle,
@ -331,13 +331,13 @@ import {
IconBadgeCc, IconBadgeCc,
IconBadgeHd, IconBadgeHd,
IconBadgeOff, IconBadgeOff,
IconBadges,
IconBadgeSd, IconBadgeSd,
IconBadgesOff,
IconBadgeTm, IconBadgeTm,
IconBadgeVo, IconBadgeVo,
IconBadgeVr, IconBadgeVr,
IconBadgeWc, IconBadgeWc,
IconBadges,
IconBadgesOff,
IconBaguette, IconBaguette,
IconBallAmericanFootball, IconBallAmericanFootball,
IconBallAmericanFootballOff, IconBallAmericanFootballOff,
@ -346,12 +346,12 @@ import {
IconBallBowling, IconBallBowling,
IconBallFootball, IconBallFootball,
IconBallFootballOff, IconBallFootballOff,
IconBallTennis,
IconBallVolleyball,
IconBalloon, IconBalloon,
IconBalloonOff, IconBalloonOff,
IconBallpen, IconBallpen,
IconBallpenOff, IconBallpenOff,
IconBallTennis,
IconBallVolleyball,
IconBan, IconBan,
IconBandage, IconBandage,
IconBandageOff, IconBandageOff,
@ -468,6 +468,8 @@ import {
IconBook, IconBook,
IconBook2, IconBook2,
IconBookDownload, IconBookDownload,
IconBookOff,
IconBookUpload,
IconBookmark, IconBookmark,
IconBookmarkEdit, IconBookmarkEdit,
IconBookmarkMinus, IconBookmarkMinus,
@ -476,10 +478,8 @@ import {
IconBookmarkQuestion, IconBookmarkQuestion,
IconBookmarks, IconBookmarks,
IconBookmarksOff, IconBookmarksOff,
IconBookOff,
IconBooks, IconBooks,
IconBooksOff, IconBooksOff,
IconBookUpload,
IconBorderAll, IconBorderAll,
IconBorderBottom, IconBorderBottom,
IconBorderCorners, IconBorderCorners,
@ -582,6 +582,7 @@ import {
IconBrandBulma, IconBrandBulma,
IconBrandBumble, IconBrandBumble,
IconBrandBunpo, IconBrandBunpo,
IconBrandCSharp,
IconBrandCake, IconBrandCake,
IconBrandCakephp, IconBrandCakephp,
IconBrandCampaignmonitor, IconBrandCampaignmonitor,
@ -603,7 +604,6 @@ import {
IconBrandCpp, IconBrandCpp,
IconBrandCraft, IconBrandCraft,
IconBrandCrunchbase, IconBrandCrunchbase,
IconBrandCSharp,
IconBrandCss3, IconBrandCss3,
IconBrandCtemplar, IconBrandCtemplar,
IconBrandCucumber, IconBrandCucumber,
@ -739,8 +739,8 @@ import {
IconBrandOkRu, IconBrandOkRu,
IconBrandOnedrive, IconBrandOnedrive,
IconBrandOnlyfans, IconBrandOnlyfans,
IconBrandOpenai,
IconBrandOpenSource, IconBrandOpenSource,
IconBrandOpenai,
IconBrandOpenvpn, IconBrandOpenvpn,
IconBrandOpera, IconBrandOpera,
IconBrandPagekit, IconBrandPagekit,
@ -934,9 +934,9 @@ import {
IconBulbOff, IconBulbOff,
IconBulldozer, IconBulldozer,
IconBus, IconBus,
IconBusinessplan,
IconBusOff, IconBusOff,
IconBusStop, IconBusStop,
IconBusinessplan,
IconButterfly, IconButterfly,
IconCactus, IconCactus,
IconCactusOff, IconCactusOff,
@ -1006,9 +1006,11 @@ import {
IconCapture, IconCapture,
IconCaptureOff, IconCaptureOff,
IconCar, IconCar,
IconCaravan,
IconCarCrane, IconCarCrane,
IconCarCrash, IconCarCrash,
IconCarOff,
IconCarTurbine,
IconCaravan,
IconCardboards, IconCardboards,
IconCardboardsOff, IconCardboardsOff,
IconCards, IconCards,
@ -1016,12 +1018,10 @@ import {
IconCaretLeft, IconCaretLeft,
IconCaretRight, IconCaretRight,
IconCaretUp, IconCaretUp,
IconCarOff,
IconCarouselHorizontal, IconCarouselHorizontal,
IconCarouselVertical, IconCarouselVertical,
IconCarrot, IconCarrot,
IconCarrotOff, IconCarrotOff,
IconCarTurbine,
IconCash, IconCash,
IconCashBanknote, IconCashBanknote,
IconCashBanknoteOff, IconCashBanknoteOff,
@ -1032,6 +1032,7 @@ import {
IconCategory, IconCategory,
IconCategory2, IconCategory2,
IconCe, IconCe,
IconCeOff,
IconCell, IconCell,
IconCellSignal1, IconCellSignal1,
IconCellSignal2, IconCellSignal2,
@ -1039,7 +1040,6 @@ import {
IconCellSignal4, IconCellSignal4,
IconCellSignal5, IconCellSignal5,
IconCellSignalOff, IconCellSignalOff,
IconCeOff,
IconCertificate, IconCertificate,
IconCertificate2, IconCertificate2,
IconCertificate2Off, IconCertificate2Off,
@ -1105,6 +1105,9 @@ import {
IconChevronLeftPipe, IconChevronLeftPipe,
IconChevronRight, IconChevronRight,
IconChevronRightPipe, IconChevronRightPipe,
IconChevronUp,
IconChevronUpLeft,
IconChevronUpRight,
IconChevronsDown, IconChevronsDown,
IconChevronsDownLeft, IconChevronsDownLeft,
IconChevronsDownRight, IconChevronsDownRight,
@ -1113,9 +1116,6 @@ import {
IconChevronsUp, IconChevronsUp,
IconChevronsUpLeft, IconChevronsUpLeft,
IconChevronsUpRight, IconChevronsUpRight,
IconChevronUp,
IconChevronUpLeft,
IconChevronUpRight,
IconChisel, IconChisel,
IconChristmasTree, IconChristmasTree,
IconChristmasTreeOff, IconChristmasTreeOff,
@ -1136,11 +1136,11 @@ import {
IconCircleChevronDown, IconCircleChevronDown,
IconCircleChevronLeft, IconCircleChevronLeft,
IconCircleChevronRight, IconCircleChevronRight,
IconCircleChevronUp,
IconCircleChevronsDown, IconCircleChevronsDown,
IconCircleChevronsLeft, IconCircleChevronsLeft,
IconCircleChevronsRight, IconCircleChevronsRight,
IconCircleChevronsUp, IconCircleChevronsUp,
IconCircleChevronUp,
IconCircleDashed, IconCircleDashed,
IconCircleDot, IconCircleDot,
IconCircleDotted, IconCircleDotted,
@ -1189,11 +1189,11 @@ import {
IconCirclePlus, IconCirclePlus,
IconCircleRectangle, IconCircleRectangle,
IconCircleRectangleOff, IconCircleRectangleOff,
IconCircles,
IconCircleSquare, IconCircleSquare,
IconCirclesRelation,
IconCircleTriangle, IconCircleTriangle,
IconCircleX, IconCircleX,
IconCircles,
IconCirclesRelation,
IconCircuitAmmeter, IconCircuitAmmeter,
IconCircuitBattery, IconCircuitBattery,
IconCircuitBulb, IconCircuitBulb,
@ -1320,9 +1320,9 @@ import {
IconCoinOff, IconCoinOff,
IconCoinPound, IconCoinPound,
IconCoinRupee, IconCoinRupee,
IconCoins,
IconCoinYen, IconCoinYen,
IconCoinYuan, IconCoinYuan,
IconCoins,
IconColorFilter, IconColorFilter,
IconColorPicker, IconColorPicker,
IconColorPickerOff, IconColorPickerOff,
@ -1361,9 +1361,9 @@ import {
IconCookieMan, IconCookieMan,
IconCookieOff, IconCookieOff,
IconCopy, IconCopy,
IconCopyOff,
IconCopyleft, IconCopyleft,
IconCopyleftOff, IconCopyleftOff,
IconCopyOff,
IconCopyright, IconCopyright,
IconCopyrightOff, IconCopyrightOff,
IconCornerDownLeft, IconCornerDownLeft,
@ -1399,8 +1399,8 @@ import {
IconCricket, IconCricket,
IconCrop, IconCrop,
IconCross, IconCross,
IconCrosshair,
IconCrossOff, IconCrossOff,
IconCrosshair,
IconCrown, IconCrown,
IconCrownOff, IconCrownOff,
IconCrutches, IconCrutches,
@ -1648,37 +1648,13 @@ import {
IconDeviceNintendoOff, IconDeviceNintendoOff,
IconDeviceProjector, IconDeviceProjector,
IconDeviceRemote, IconDeviceRemote,
IconDevices,
IconDevices2,
IconDevicesBolt,
IconDevicesCancel,
IconDevicesCheck,
IconDevicesCode,
IconDevicesCog,
IconDeviceSdCard, IconDeviceSdCard,
IconDevicesDollar,
IconDevicesDown,
IconDevicesExclamation,
IconDevicesHeart,
IconDeviceSim, IconDeviceSim,
IconDeviceSim1, IconDeviceSim1,
IconDeviceSim2, IconDeviceSim2,
IconDeviceSim3, IconDeviceSim3,
IconDevicesMinus,
IconDevicesOff,
IconDevicesPause,
IconDevicesPc,
IconDevicesPcOff,
IconDeviceSpeaker, IconDeviceSpeaker,
IconDeviceSpeakerOff, IconDeviceSpeakerOff,
IconDevicesPin,
IconDevicesPlus,
IconDevicesQuestion,
IconDevicesSearch,
IconDevicesShare,
IconDevicesStar,
IconDevicesUp,
IconDevicesX,
IconDeviceTablet, IconDeviceTablet,
IconDeviceTabletBolt, IconDeviceTabletBolt,
IconDeviceTabletCancel, IconDeviceTabletCancel,
@ -1727,6 +1703,30 @@ import {
IconDeviceWatchStats2, IconDeviceWatchStats2,
IconDeviceWatchUp, IconDeviceWatchUp,
IconDeviceWatchX, IconDeviceWatchX,
IconDevices,
IconDevices2,
IconDevicesBolt,
IconDevicesCancel,
IconDevicesCheck,
IconDevicesCode,
IconDevicesCog,
IconDevicesDollar,
IconDevicesDown,
IconDevicesExclamation,
IconDevicesHeart,
IconDevicesMinus,
IconDevicesOff,
IconDevicesPause,
IconDevicesPc,
IconDevicesPcOff,
IconDevicesPin,
IconDevicesPlus,
IconDevicesQuestion,
IconDevicesSearch,
IconDevicesShare,
IconDevicesStar,
IconDevicesUp,
IconDevicesX,
IconDiabolo, IconDiabolo,
IconDiaboloOff, IconDiaboloOff,
IconDiaboloPlus, IconDiaboloPlus,
@ -1745,9 +1745,9 @@ import {
IconDimensions, IconDimensions,
IconDirection, IconDirection,
IconDirectionHorizontal, IconDirectionHorizontal,
IconDirections,
IconDirectionSign, IconDirectionSign,
IconDirectionSignOff, IconDirectionSignOff,
IconDirections,
IconDirectionsOff, IconDirectionsOff,
IconDisabled, IconDisabled,
IconDisabled2, IconDisabled2,
@ -1801,13 +1801,14 @@ import {
IconDropletPin, IconDropletPin,
IconDropletPlus, IconDropletPlus,
IconDropletQuestion, IconDropletQuestion,
IconDroplets,
IconDropletSearch, IconDropletSearch,
IconDropletShare, IconDropletShare,
IconDropletStar, IconDropletStar,
IconDropletUp, IconDropletUp,
IconDropletX, IconDropletX,
IconDroplets,
IconDualScreen, IconDualScreen,
IconEPassport,
IconEar, IconEar,
IconEarOff, IconEarOff,
IconEaseIn, IconEaseIn,
@ -1833,7 +1834,6 @@ import {
IconEmphasis, IconEmphasis,
IconEngine, IconEngine,
IconEngineOff, IconEngineOff,
IconEPassport,
IconEqual, IconEqual,
IconEqualDouble, IconEqualDouble,
IconEqualNot, IconEqualNot,
@ -1872,9 +1872,6 @@ import {
IconEyeDown, IconEyeDown,
IconEyeEdit, IconEyeEdit,
IconEyeExclamation, IconEyeExclamation,
IconEyeglass,
IconEyeglass2,
IconEyeglassOff,
IconEyeHeart, IconEyeHeart,
IconEyeMinus, IconEyeMinus,
IconEyeOff, IconEyeOff,
@ -1888,6 +1885,9 @@ import {
IconEyeTable, IconEyeTable,
IconEyeUp, IconEyeUp,
IconEyeX, IconEyeX,
IconEyeglass,
IconEyeglass2,
IconEyeglassOff,
IconFaceId, IconFaceId,
IconFaceIdError, IconFaceIdError,
IconFaceMask, IconFaceMask,
@ -1942,13 +1942,11 @@ import {
IconFilePower, IconFilePower,
IconFileReport, IconFileReport,
IconFileRss, IconFileRss,
IconFiles,
IconFileScissors, IconFileScissors,
IconFileSearch, IconFileSearch,
IconFileSettings, IconFileSettings,
IconFileShredder, IconFileShredder,
IconFileSignal, IconFileSignal,
IconFilesOff,
IconFileSpreadsheet, IconFileSpreadsheet,
IconFileStack, IconFileStack,
IconFileStar, IconFileStar,
@ -1985,6 +1983,8 @@ import {
IconFileVector, IconFileVector,
IconFileX, IconFileX,
IconFileZip, IconFileZip,
IconFiles,
IconFilesOff,
IconFilter, IconFilter,
IconFilterBolt, IconFilterBolt,
IconFilterCancel, IconFilterCancel,
@ -2003,12 +2003,12 @@ import {
IconFilterPin, IconFilterPin,
IconFilterPlus, IconFilterPlus,
IconFilterQuestion, IconFilterQuestion,
IconFilters,
IconFilterSearch, IconFilterSearch,
IconFilterShare, IconFilterShare,
IconFilterStar, IconFilterStar,
IconFilterUp, IconFilterUp,
IconFilterX, IconFilterX,
IconFilters,
IconFingerprint, IconFingerprint,
IconFingerprintOff, IconFingerprintOff,
IconFireExtinguisher, IconFireExtinguisher,
@ -2070,6 +2070,7 @@ import {
IconFocusCentered, IconFocusCentered,
IconFold, IconFold,
IconFoldDown, IconFoldDown,
IconFoldUp,
IconFolder, IconFolder,
IconFolderBolt, IconFolderBolt,
IconFolderCancel, IconFolderCancel,
@ -2087,15 +2088,14 @@ import {
IconFolderPin, IconFolderPin,
IconFolderPlus, IconFolderPlus,
IconFolderQuestion, IconFolderQuestion,
IconFolders,
IconFolderSearch, IconFolderSearch,
IconFolderShare, IconFolderShare,
IconFoldersOff,
IconFolderStar, IconFolderStar,
IconFolderSymlink, IconFolderSymlink,
IconFolderUp, IconFolderUp,
IconFolderX, IconFolderX,
IconFoldUp, IconFolders,
IconFoldersOff,
IconForbid, IconForbid,
IconForbid2, IconForbid2,
IconForklift, IconForklift,
@ -2224,7 +2224,6 @@ import {
IconHeadsetOff, IconHeadsetOff,
IconHealthRecognition, IconHealthRecognition,
IconHeart, IconHeart,
IconHeartbeat,
IconHeartBolt, IconHeartBolt,
IconHeartBroken, IconHeartBroken,
IconHeartCancel, IconHeartCancel,
@ -2243,13 +2242,14 @@ import {
IconHeartPlus, IconHeartPlus,
IconHeartQuestion, IconHeartQuestion,
IconHeartRateMonitor, IconHeartRateMonitor,
IconHearts,
IconHeartSearch, IconHeartSearch,
IconHeartShare, IconHeartShare,
IconHeartsOff,
IconHeartStar, IconHeartStar,
IconHeartUp, IconHeartUp,
IconHeartX, IconHeartX,
IconHeartbeat,
IconHearts,
IconHeartsOff,
IconHelicopter, IconHelicopter,
IconHelicopterLanding, IconHelicopterLanding,
IconHelmet, IconHelmet,
@ -2268,12 +2268,6 @@ import {
IconHemispherePlus, IconHemispherePlus,
IconHexagon, IconHexagon,
IconHexagon3d, IconHexagon3d,
IconHexagonalPrism,
IconHexagonalPrismOff,
IconHexagonalPrismPlus,
IconHexagonalPyramid,
IconHexagonalPyramidOff,
IconHexagonalPyramidPlus,
IconHexagonLetterA, IconHexagonLetterA,
IconHexagonLetterB, IconHexagonLetterB,
IconHexagonLetterC, IconHexagonLetterC,
@ -2311,6 +2305,12 @@ import {
IconHexagonNumber8, IconHexagonNumber8,
IconHexagonNumber9, IconHexagonNumber9,
IconHexagonOff, IconHexagonOff,
IconHexagonalPrism,
IconHexagonalPrismOff,
IconHexagonalPrismPlus,
IconHexagonalPyramid,
IconHexagonalPyramidOff,
IconHexagonalPyramidPlus,
IconHexagons, IconHexagons,
IconHexagonsOff, IconHexagonsOff,
IconHierarchy, IconHierarchy,
@ -2424,6 +2424,7 @@ import {
IconKayak, IconKayak,
IconKering, IconKering,
IconKey, IconKey,
IconKeyOff,
IconKeyboard, IconKeyboard,
IconKeyboardHide, IconKeyboardHide,
IconKeyboardOff, IconKeyboardOff,
@ -2433,7 +2434,6 @@ import {
IconKeyframeAlignHorizontal, IconKeyframeAlignHorizontal,
IconKeyframeAlignVertical, IconKeyframeAlignVertical,
IconKeyframes, IconKeyframes,
IconKeyOff,
IconLadder, IconLadder,
IconLadderOff, IconLadderOff,
IconLadle, IconLadle,
@ -2627,8 +2627,6 @@ import {
IconMail, IconMail,
IconMailAi, IconMailAi,
IconMailBolt, IconMailBolt,
IconMailbox,
IconMailboxOff,
IconMailCancel, IconMailCancel,
IconMailCheck, IconMailCheck,
IconMailCode, IconMailCode,
@ -2651,6 +2649,8 @@ import {
IconMailStar, IconMailStar,
IconMailUp, IconMailUp,
IconMailX, IconMailX,
IconMailbox,
IconMailboxOff,
IconMan, IconMan,
IconManualGearbox, IconManualGearbox,
IconMap, IconMap,
@ -2684,12 +2684,12 @@ import {
IconMapPinPin, IconMapPinPin,
IconMapPinPlus, IconMapPinPlus,
IconMapPinQuestion, IconMapPinQuestion,
IconMapPins,
IconMapPinSearch, IconMapPinSearch,
IconMapPinShare, IconMapPinShare,
IconMapPinStar, IconMapPinStar,
IconMapPinUp, IconMapPinUp,
IconMapPinX, IconMapPinX,
IconMapPins,
IconMapPlus, IconMapPlus,
IconMapQuestion, IconMapQuestion,
IconMapSearch, IconMapSearch,
@ -2720,8 +2720,8 @@ import {
IconMathFunctionY, IconMathFunctionY,
IconMathGreater, IconMathGreater,
IconMathIntegral, IconMathIntegral,
IconMathIntegrals,
IconMathIntegralX, IconMathIntegralX,
IconMathIntegrals,
IconMathLower, IconMathLower,
IconMathMax, IconMathMax,
IconMathMin, IconMathMin,
@ -2820,13 +2820,13 @@ import {
IconMessagePlus, IconMessagePlus,
IconMessageQuestion, IconMessageQuestion,
IconMessageReport, IconMessageReport,
IconMessages,
IconMessageSearch, IconMessageSearch,
IconMessageShare, IconMessageShare,
IconMessagesOff,
IconMessageStar, IconMessageStar,
IconMessageUp, IconMessageUp,
IconMessageX, IconMessageX,
IconMessages,
IconMessagesOff,
IconMeteor, IconMeteor,
IconMeteorOff, IconMeteorOff,
IconMichelinBibGourmand, IconMichelinBibGourmand,
@ -2972,8 +2972,8 @@ import {
IconNeedleThread, IconNeedleThread,
IconNetwork, IconNetwork,
IconNetworkOff, IconNetworkOff,
IconNews,
IconNewSection, IconNewSection,
IconNews,
IconNewsOff, IconNewsOff,
IconNfc, IconNfc,
IconNfcOff, IconNfcOff,
@ -2982,9 +2982,9 @@ import {
IconNoDerivatives, IconNoDerivatives,
IconNorthStar, IconNorthStar,
IconNote, IconNote,
IconNoteOff,
IconNotebook, IconNotebook,
IconNotebookOff, IconNotebookOff,
IconNoteOff,
IconNotes, IconNotes,
IconNotesOff, IconNotesOff,
IconNotification, IconNotification,
@ -3143,8 +3143,8 @@ import {
IconPlaneDeparture, IconPlaneDeparture,
IconPlaneInflight, IconPlaneInflight,
IconPlaneOff, IconPlaneOff,
IconPlanet,
IconPlaneTilt, IconPlaneTilt,
IconPlanet,
IconPlanetOff, IconPlanetOff,
IconPlant, IconPlant,
IconPlant2, IconPlant2,
@ -3153,6 +3153,9 @@ import {
IconPlayBasketball, IconPlayBasketball,
IconPlayCard, IconPlayCard,
IconPlayCardOff, IconPlayCardOff,
IconPlayFootball,
IconPlayHandball,
IconPlayVolleyball,
IconPlayerEject, IconPlayerEject,
IconPlayerPause, IconPlayerPause,
IconPlayerPlay, IconPlayerPlay,
@ -3162,8 +3165,6 @@ import {
IconPlayerStop, IconPlayerStop,
IconPlayerTrackNext, IconPlayerTrackNext,
IconPlayerTrackPrev, IconPlayerTrackPrev,
IconPlayFootball,
IconPlayHandball,
IconPlaylist, IconPlaylist,
IconPlaylistAdd, IconPlaylistAdd,
IconPlaylistOff, IconPlaylistOff,
@ -3172,7 +3173,6 @@ import {
IconPlaystationSquare, IconPlaystationSquare,
IconPlaystationTriangle, IconPlaystationTriangle,
IconPlaystationX, IconPlaystationX,
IconPlayVolleyball,
IconPlug, IconPlug,
IconPlugConnected, IconPlugConnected,
IconPlugConnectedX, IconPlugConnectedX,
@ -3185,6 +3185,7 @@ import {
IconPodium, IconPodium,
IconPodiumOff, IconPodiumOff,
IconPoint, IconPoint,
IconPointOff,
IconPointer, IconPointer,
IconPointerBolt, IconPointerBolt,
IconPointerCancel, IconPointerCancel,
@ -3206,7 +3207,6 @@ import {
IconPointerStar, IconPointerStar,
IconPointerUp, IconPointerUp,
IconPointerX, IconPointerX,
IconPointOff,
IconPokeball, IconPokeball,
IconPokeballOff, IconPokeballOff,
IconPokerChip, IconPokerChip,
@ -3256,9 +3256,9 @@ import {
IconRadar2, IconRadar2,
IconRadarOff, IconRadarOff,
IconRadio, IconRadio,
IconRadioOff,
IconRadioactive, IconRadioactive,
IconRadioactiveOff, IconRadioactiveOff,
IconRadioOff,
IconRadiusBottomLeft, IconRadiusBottomLeft,
IconRadiusBottomRight, IconRadiusBottomRight,
IconRadiusTopLeft, IconRadiusTopLeft,
@ -3342,9 +3342,9 @@ import {
IconRobotOff, IconRobotOff,
IconRocket, IconRocket,
IconRocketOff, IconRocketOff,
IconRollerSkating,
IconRollercoaster, IconRollercoaster,
IconRollercoasterOff, IconRollercoasterOff,
IconRollerSkating,
IconRosette, IconRosette,
IconRosetteNumber0, IconRosetteNumber0,
IconRosetteNumber1, IconRosetteNumber1,
@ -3381,6 +3381,10 @@ import {
IconRulerMeasure, IconRulerMeasure,
IconRulerOff, IconRulerOff,
IconRun, IconRun,
IconSTurnDown,
IconSTurnLeft,
IconSTurnRight,
IconSTurnUp,
IconSailboat, IconSailboat,
IconSailboat2, IconSailboat2,
IconSailboatOff, IconSailboatOff,
@ -3471,6 +3475,7 @@ import {
IconShare2, IconShare2,
IconShare3, IconShare3,
IconShareOff, IconShareOff,
IconShiJumping,
IconShield, IconShield,
IconShieldBolt, IconShieldBolt,
IconShieldCancel, IconShieldCancel,
@ -3496,7 +3501,6 @@ import {
IconShieldStar, IconShieldStar,
IconShieldUp, IconShieldUp,
IconShieldX, IconShieldX,
IconShiJumping,
IconShip, IconShip,
IconShipOff, IconShipOff,
IconShirt, IconShirt,
@ -3538,6 +3542,8 @@ import {
IconShoppingCartX, IconShoppingCartX,
IconShovel, IconShovel,
IconShredder, IconShredder,
IconSignLeft,
IconSignRight,
IconSignal2g, IconSignal2g,
IconSignal3g, IconSignal3g,
IconSignal4g, IconSignal4g,
@ -3551,13 +3557,11 @@ import {
IconSignalLte, IconSignalLte,
IconSignature, IconSignature,
IconSignatureOff, IconSignatureOff,
IconSignLeft,
IconSignRight,
IconSitemap, IconSitemap,
IconSitemapOff, IconSitemapOff,
IconSkateboard, IconSkateboard,
IconSkateboarding,
IconSkateboardOff, IconSkateboardOff,
IconSkateboarding,
IconSkull, IconSkull,
IconSlash, IconSlash,
IconSlashes, IconSlashes,
@ -3581,11 +3585,11 @@ import {
IconSolarPanel2, IconSolarPanel2,
IconSort09, IconSort09,
IconSort90, IconSort90,
IconSortAZ,
IconSortAscending, IconSortAscending,
IconSortAscending2, IconSortAscending2,
IconSortAscendingLetters, IconSortAscendingLetters,
IconSortAscendingNumbers, IconSortAscendingNumbers,
IconSortAZ,
IconSortDescending, IconSortDescending,
IconSortDescending2, IconSortDescending2,
IconSortDescendingLetters, IconSortDescendingLetters,
@ -3624,11 +3628,11 @@ import {
IconSquareChevronDown, IconSquareChevronDown,
IconSquareChevronLeft, IconSquareChevronLeft,
IconSquareChevronRight, IconSquareChevronRight,
IconSquareChevronUp,
IconSquareChevronsDown, IconSquareChevronsDown,
IconSquareChevronsLeft, IconSquareChevronsLeft,
IconSquareChevronsRight, IconSquareChevronsRight,
IconSquareChevronsUp, IconSquareChevronsUp,
IconSquareChevronUp,
IconSquareDot, IconSquareDot,
IconSquareF0, IconSquareF0,
IconSquareF1, IconSquareF1,
@ -3698,11 +3702,11 @@ import {
IconSquareRoundedChevronDown, IconSquareRoundedChevronDown,
IconSquareRoundedChevronLeft, IconSquareRoundedChevronLeft,
IconSquareRoundedChevronRight, IconSquareRoundedChevronRight,
IconSquareRoundedChevronUp,
IconSquareRoundedChevronsDown, IconSquareRoundedChevronsDown,
IconSquareRoundedChevronsLeft, IconSquareRoundedChevronsLeft,
IconSquareRoundedChevronsRight, IconSquareRoundedChevronsRight,
IconSquareRoundedChevronsUp, IconSquareRoundedChevronsUp,
IconSquareRoundedChevronUp,
IconSquareRoundedLetterA, IconSquareRoundedLetterA,
IconSquareRoundedLetterB, IconSquareRoundedLetterB,
IconSquareRoundedLetterC, IconSquareRoundedLetterC,
@ -3742,10 +3746,10 @@ import {
IconSquareRoundedNumber9, IconSquareRoundedNumber9,
IconSquareRoundedPlus, IconSquareRoundedPlus,
IconSquareRoundedX, IconSquareRoundedX,
IconSquaresDiagonal,
IconSquareToggle, IconSquareToggle,
IconSquareToggleHorizontal, IconSquareToggleHorizontal,
IconSquareX, IconSquareX,
IconSquaresDiagonal,
IconStack, IconStack,
IconStack2, IconStack2,
IconStack3, IconStack3,
@ -3774,25 +3778,21 @@ import {
IconStretching, IconStretching,
IconStretching2, IconStretching2,
IconStrikethrough, IconStrikethrough,
IconSTurnDown,
IconSTurnLeft,
IconSTurnRight,
IconSTurnUp,
IconSubmarine, IconSubmarine,
IconSubscript, IconSubscript,
IconSubtask, IconSubtask,
IconSum, IconSum,
IconSumOff, IconSumOff,
IconSun, IconSun,
IconSunglasses,
IconSunHigh, IconSunHigh,
IconSunLow, IconSunLow,
IconSunMoon, IconSunMoon,
IconSunOff, IconSunOff,
IconSunWind,
IconSunglasses,
IconSunrise, IconSunrise,
IconSunset, IconSunset,
IconSunset2, IconSunset2,
IconSunWind,
IconSuperscript, IconSuperscript,
IconSvg, IconSvg,
IconSwimming, IconSwimming,
@ -3863,18 +3863,18 @@ import {
IconTextResize, IconTextResize,
IconTextSize, IconTextSize,
IconTextSpellcheck, IconTextSpellcheck,
IconTexture,
IconTextWrap, IconTextWrap,
IconTextWrapDisabled, IconTextWrapDisabled,
IconTexture,
IconTheater, IconTheater,
IconThermometer, IconThermometer,
IconThumbDown, IconThumbDown,
IconThumbDownOff, IconThumbDownOff,
IconThumbUp, IconThumbUp,
IconThumbUpOff, IconThumbUpOff,
IconTicTac,
IconTicket, IconTicket,
IconTicketOff, IconTicketOff,
IconTicTac,
IconTie, IconTie,
IconTilde, IconTilde,
IconTiltShift, IconTiltShift,
@ -3960,8 +3960,8 @@ import {
IconTriangle, IconTriangle,
IconTriangleInverted, IconTriangleInverted,
IconTriangleOff, IconTriangleOff,
IconTriangles,
IconTriangleSquareCircle, IconTriangleSquareCircle,
IconTriangles,
IconTrident, IconTrident,
IconTrolley, IconTrolley,
IconTrophy, IconTrophy,
@ -4002,16 +4002,16 @@ import {
IconUserPin, IconUserPin,
IconUserPlus, IconUserPlus,
IconUserQuestion, IconUserQuestion,
IconUsers,
IconUserSearch, IconUserSearch,
IconUsersGroup,
IconUserShare, IconUserShare,
IconUserShield, IconUserShield,
IconUsersMinus,
IconUsersPlus,
IconUserStar, IconUserStar,
IconUserUp, IconUserUp,
IconUserX, IconUserX,
IconUsers,
IconUsersGroup,
IconUsersMinus,
IconUsersPlus,
IconUvIndex, IconUvIndex,
IconUxCircle, IconUxCircle,
IconVaccine, IconVaccine,
@ -4060,9 +4060,9 @@ import {
IconVolumeOff, IconVolumeOff,
IconWalk, IconWalk,
IconWall, IconWall,
IconWallOff,
IconWallet, IconWallet,
IconWalletOff, IconWalletOff,
IconWallOff,
IconWallpaper, IconWallpaper,
IconWallpaperOff, IconWallpaperOff,
IconWand, IconWand,
@ -4073,8 +4073,6 @@ import {
IconWashDry2, IconWashDry2,
IconWashDry3, IconWashDry3,
IconWashDryA, IconWashDryA,
IconWashDryclean,
IconWashDrycleanOff,
IconWashDryDip, IconWashDryDip,
IconWashDryF, IconWashDryF,
IconWashDryFlat, IconWashDryFlat,
@ -4083,6 +4081,8 @@ import {
IconWashDryP, IconWashDryP,
IconWashDryShade, IconWashDryShade,
IconWashDryW, IconWashDryW,
IconWashDryclean,
IconWashDrycleanOff,
IconWashEco, IconWashEco,
IconWashGentle, IconWashGentle,
IconWashHand, IconWashHand,
@ -4113,9 +4113,9 @@ import {
IconWifi2, IconWifi2,
IconWifiOff, IconWifiOff,
IconWind, IconWind,
IconWindOff,
IconWindmill, IconWindmill,
IconWindmillOff, IconWindmillOff,
IconWindOff,
IconWindow, IconWindow,
IconWindowMaximize, IconWindowMaximize,
IconWindowMinimize, IconWindowMinimize,