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 { 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 { 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@ -32,30 +32,35 @@ const ActionEffects = ({
objectId: objectMetadataItemId,
});
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
return (
<>
{contextStoreNumberOfSelectedRecords === 0 && (
<NoSelectionActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
{contextStoreNumberOfSelectedRecords === 1 && (
<SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
{contextStoreNumberOfSelectedRecords === 1 && isWorkflowEnabled && (
<WorkflowRunRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
{contextStoreNumberOfSelectedRecords > 1 && (
{contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0 && (
<NoSelectionActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
{contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
<>
<SingleRecordActionMenuEntrySetter
objectMetadataItem={objectMetadataItem}
/>
{isWorkflowEnabled && (
<WorkflowRunRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
</>
)}
{(contextStoreTargetedRecordsRule.mode === 'exclusion' ||
contextStoreTargetedRecordsRule.selectedRecordIds.length > 1) && (
<MultipleRecordsActionMenuEntrySetterEffect
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';
export const useDeleteMultipleRecordsAction = ({
position,
objectMetadataItem,
}: {
position: number;
objectMetadataItem: ObjectMetadataItem;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -106,7 +104,11 @@ export const useDeleteMultipleRecordsAction = ({
const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
const registerDeleteMultipleRecordsAction = () => {
const registerDeleteMultipleRecordsAction = ({
position,
}: {
position: number;
}) => {
if (canDelete) {
addActionMenuEntry({
type: ActionMenuEntryType.Standard,

View File

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

View File

@ -1,5 +1,5 @@
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';
export const useMultipleRecordsActions = ({
@ -11,26 +11,24 @@ export const useMultipleRecordsActions = ({
registerDeleteMultipleRecordsAction,
unregisterDeleteMultipleRecordsAction,
} = useDeleteMultipleRecordsAction({
position: 0,
objectMetadataItem,
});
const {
registerExportViewNoSelectionRecordsAction,
unregisterExportViewNoSelectionRecordsAction,
} = useExportViewNoSelectionRecordAction({
position: 1,
registerExportMultipleRecordsAction,
unregisterExportMultipleRecordsAction,
} = useExportMultipleRecordsAction({
objectMetadataItem,
});
const registerMultipleRecordsActions = () => {
registerDeleteMultipleRecordsAction();
registerExportViewNoSelectionRecordsAction();
registerDeleteMultipleRecordsAction({ position: 1 });
registerExportMultipleRecordsAction({ position: 2 });
};
const unregisterMultipleRecordsActions = () => {
unregisterDeleteMultipleRecordsAction();
unregisterExportViewNoSelectionRecordsAction();
unregisterExportMultipleRecordsAction();
};
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';
export const useExportViewNoSelectionRecordAction = ({
position,
objectMetadataItem,
}: {
position: number;
objectMetadataItem: ObjectMetadataItem;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -27,7 +25,11 @@ export const useExportViewNoSelectionRecordAction = ({
filename: `${objectMetadataItem.nameSingular}.csv`,
});
const registerExportViewNoSelectionRecordsAction = () => {
const registerExportViewNoSelectionRecordsAction = ({
position,
}: {
position: number;
}) => {
addActionMenuEntry({
type: ActionMenuEntryType.Standard,
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';
export const useNoSelectionRecordActions = ({
@ -10,12 +10,11 @@ export const useNoSelectionRecordActions = ({
registerExportViewNoSelectionRecordsAction,
unregisterExportViewNoSelectionRecordsAction,
} = useExportViewNoSelectionRecordAction({
position: 0,
objectMetadataItem,
});
const registerNoSelectionRecordActions = () => {
registerExportViewNoSelectionRecordsAction();
registerExportViewNoSelectionRecordsAction({ position: 1 });
};
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,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
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 { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useContext, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui';
export const useDeleteSingleRecordAction = ({
position,
recordId,
objectMetadataItem,
}: {
position: number;
recordId: string;
objectMetadataItem: ObjectMetadataItem;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -39,39 +37,26 @@ export const useDeleteSingleRecordAction = ({
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const { closeRightDrawer } = useRightDrawer();
const recordIdToDelete =
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds?.[0]
: undefined;
const handleDeleteClick = useCallback(async () => {
if (!isDefined(recordIdToDelete)) {
return;
}
resetTableRowSelection();
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordIdToDelete,
(favorite) => favorite.recordId === recordId,
);
if (isDefined(foundFavorite)) {
deleteFavorite(foundFavorite.id);
}
await deleteOneRecord(recordIdToDelete);
await deleteOneRecord(recordId);
}, [
deleteFavorite,
deleteOneRecord,
favorites,
recordIdToDelete,
resetTableRowSelection,
recordId,
]);
const isRemoteObject = objectMetadataItem.isRemote;
@ -79,8 +64,12 @@ export const useDeleteSingleRecordAction = ({
const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
const registerDeleteSingleRecordAction = () => {
if (isRemoteObject || !isDefined(recordIdToDelete)) {
const registerDeleteSingleRecordAction = ({
position,
}: {
position: number;
}) => {
if (isRemoteObject) {
return;
}

View File

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

View File

@ -1,17 +1,33 @@
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
import { useManageFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-ui';
export const useSingleRecordActions = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const selectedRecordId =
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
: undefined;
if (!isDefined(selectedRecordId)) {
throw new Error('Selected record ID is required');
}
const {
registerManageFavoritesSingleRecordAction,
unregisterManageFavoritesSingleRecordAction,
} = useManageFavoritesSingleRecordAction({
position: 0,
recordId: selectedRecordId,
objectMetadataItem,
});
@ -19,13 +35,13 @@ export const useSingleRecordActions = ({
registerDeleteSingleRecordAction,
unregisterDeleteSingleRecordAction,
} = useDeleteSingleRecordAction({
position: 1,
recordId: selectedRecordId,
objectMetadataItem,
});
const registerSingleRecordActions = () => {
registerManageFavoritesSingleRecordAction();
registerDeleteSingleRecordAction();
registerManageFavoritesSingleRecordAction({ position: 1 });
registerDeleteSingleRecordAction({ position: 2 });
};
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
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 { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil';
import { IconSettingsAutomation, isDefined } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize';
@ -33,8 +30,12 @@ export const useWorkflowRunRecordActions = ({
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
: undefined;
if (!isDefined(selectedRecordId)) {
throw new Error('Selected record ID is required');
}
const selectedRecord = useRecoilValue(
recordStoreFamilyState(selectedRecordId ?? ''),
recordStoreFamilyState(selectedRecordId),
);
const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({
@ -44,10 +45,6 @@ export const useWorkflowRunRecordActions = ({
const { runWorkflowVersion } = useRunWorkflowVersion();
const { enqueueSnackBar } = useSnackBar();
const theme = useTheme();
const registerWorkflowRunRecordActions = () => {
if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) {
return;
@ -57,6 +54,9 @@ export const useWorkflowRunRecordActions = ({
index,
activeWorkflowVersion,
] of activeWorkflowVersions.entries()) {
if (!isDefined(activeWorkflowVersion.workflow)) {
continue;
}
const name = capitalize(activeWorkflowVersion.workflow.name);
addActionMenuEntry({
type: ActionMenuEntryType.WorkflowRun,
@ -72,19 +72,9 @@ export const useWorkflowRunRecordActions = ({
await runWorkflowVersion({
workflowVersionId: activeWorkflowVersion.id,
workflowName: name,
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,
ActionMenuEntryType,
} 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 { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import { IconSettingsAutomation } from 'twenty-ui';
import { IconSettingsAutomation, isDefined } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize';
export const useWorkflowRunActions = () => {
@ -24,10 +21,6 @@ export const useWorkflowRunActions = () => {
const { runWorkflowVersion } = useRunWorkflowVersion();
const { enqueueSnackBar } = useSnackBar();
const theme = useTheme();
const addWorkflowRunActions = () => {
if (!isWorkflowEnabled) {
return;
@ -37,7 +30,12 @@ export const useWorkflowRunActions = () => {
index,
activeWorkflowVersion,
] of activeWorkflowVersions.entries()) {
if (!isDefined(activeWorkflowVersion.workflow)) {
continue;
}
const name = capitalize(activeWorkflowVersion.workflow.name);
addActionMenuEntry({
type: ActionMenuEntryType.WorkflowRun,
key: `workflow-run-${activeWorkflowVersion.id}`,
@ -48,17 +46,7 @@ export const useWorkflowRunActions = () => {
onClick: async () => {
await runWorkflowVersion({
workflowVersionId: activeWorkflowVersion.id,
});
enqueueSnackBar('', {
variant: SnackBarVariant.Success,
title: `${name} starting...`,
icon: (
<IconSettingsAutomation
size={16}
color={theme.snackBar.success.color}
/>
),
workflowName: name,
});
},
});

View File

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

View File

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

View File

@ -131,6 +131,7 @@ export const RecordShowPageWorkflowVersionHeader = ({
{isDefined(workflowVersion) && isDefined(draftWorkflowVersion) ? (
<OverrideWorkflowDraftConfirmationModal
draftWorkflowVersionId={draftWorkflowVersion.id}
workflowId={workflowVersion.workflowId}
workflowVersionUpdateInput={{
steps: workflowVersion.steps,
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 { 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 { useMutation } from '@apollo/client';
import { useTheme } from '@emotion/react';
import { IconSettingsAutomation } from 'twenty-ui';
import {
RunWorkflowVersionMutation,
RunWorkflowVersionMutationVariables,
} from '~/generated/graphql';
import { capitalize } from '~/utils/string/capitalize';
export const useRunWorkflowVersion = () => {
const apolloMetadataClient = useApolloMetadataClient();
@ -15,16 +20,33 @@ export const useRunWorkflowVersion = () => {
client: apolloMetadataClient,
});
const { enqueueSnackBar } = useSnackBar();
const theme = useTheme();
const runWorkflowVersion = async ({
workflowVersionId,
workflowName,
payload,
}: {
workflowVersionId: string;
workflowName: string;
payload?: Record<string, any>;
}) => {
await mutate({
variables: { input: { workflowVersionId, payload } },
});
enqueueSnackBar('', {
variant: SnackBarVariant.Success,
title: `${capitalize(workflowName)} starting...`,
icon: (
<IconSettingsAutomation
size={16}
color={theme.snackBar.success.color}
/>
),
});
};
return { runWorkflowVersion };

View File

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

View File

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

View File

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