diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx index 88f849fc8c7ed..1d1bfdf5beaf2 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx @@ -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 && ( - - )} - {contextStoreNumberOfSelectedRecords === 1 && ( - - )} - {contextStoreNumberOfSelectedRecords === 1 && isWorkflowEnabled && ( - - )} - {contextStoreNumberOfSelectedRecords > 1 && ( + {contextStoreTargetedRecordsRule.mode === 'selection' && + contextStoreTargetedRecordsRule.selectedRecordIds.length === 0 && ( + + )} + {contextStoreTargetedRecordsRule.mode === 'selection' && + contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && ( + <> + + {isWorkflowEnabled && ( + + )} + + )} + {(contextStoreTargetedRecordsRule.mode === 'exclusion' || + contextStoreTargetedRecordsRule.selectedRecordIds.length > 1) && ( diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/__tests__/useDeleteMultipleRecordsAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/__tests__/useDeleteMultipleRecordsAction.test.tsx new file mode 100644 index 0000000000000..447a08447877d --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/__tests__/useDeleteMultipleRecordsAction.test.tsx @@ -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 }) => ( + + + + {children} + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/__tests__/useExportMultipleRecordsAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/__tests__/useExportMultipleRecordsAction.test.tsx new file mode 100644 index 0000000000000..7d9dbff6ef776 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/__tests__/useExportMultipleRecordsAction.test.tsx @@ -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 }) => ( + + + + + {children} + + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx index 29be66860a838..5e517ace13461 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx index eacdf1e5e643e..b8cebf5fc2471 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions.tsx index 78e459e23d388..2b135aaa0e332 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions.tsx @@ -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 { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/__tests__/useExportViewNoSelectionRecordAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/__tests__/useExportViewNoSelectionRecordAction.test.tsx new file mode 100644 index 0000000000000..7b00c186536a4 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/__tests__/useExportViewNoSelectionRecordAction.test.tsx @@ -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 }) => ( + + + + + {children} + + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportViewNoSelectionRecordAction.tsx similarity index 92% rename from packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction.tsx rename to packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportViewNoSelectionRecordAction.tsx index d3a257144430d..3bb7cebbbe7f6 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportViewNoSelectionRecordAction.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions.tsx index a647e449b7401..af08e721b4046 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions.tsx @@ -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 = () => { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetter.tsx new file mode 100644 index 0000000000000..acd2c99a80100 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetter.tsx @@ -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 ( + <> + + {objectMetadataItem.nameSingular === CoreObjectNameSingular.Workflow && ( + + )} + {objectMetadataItem.nameSingular === + CoreObjectNameSingular.WorkflowVersion && ( + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/NumberOfStandardSingleRecordActionsOnAllObjects.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/NumberOfStandardSingleRecordActionsOnAllObjects.ts new file mode 100644 index 0000000000000..9bc77b975169f --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/NumberOfStandardSingleRecordActionsOnAllObjects.ts @@ -0,0 +1 @@ +export const NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS = 2; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/__tests__/useDeleteSingleRecordAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/__tests__/useDeleteSingleRecordAction.test.tsx new file mode 100644 index 0000000000000..b05aa3900e0b6 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/__tests__/useDeleteSingleRecordAction.test.tsx @@ -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 }) => ( + + + + {children} + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/__tests__/useManageFavoritesSingleRecordAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/__tests__/useManageFavoritesSingleRecordAction.test.tsx new file mode 100644 index 0000000000000..86e279ecf49d4 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/__tests__/useManageFavoritesSingleRecordAction.test.tsx @@ -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 }) => ( + + + + + {children} + + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx index 0148468eb10fe..90888ccc4a5a8 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx @@ -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; } diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.ts similarity index 70% rename from packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.tsx rename to packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.ts index 3687318ed74a5..25a56d6fecef6 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.ts @@ -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; } diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.ts similarity index 56% rename from packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.tsx rename to packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.ts index 4b6a3763c67fc..fb723c46d349d 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.ts @@ -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 = () => { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/components/WorkflowSingleRecordActionMenuEntrySetterEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/components/WorkflowSingleRecordActionMenuEntrySetterEffect.tsx new file mode 100644 index 0000000000000..4725f00574d31 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/components/WorkflowSingleRecordActionMenuEntrySetterEffect.tsx @@ -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; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowDraftWorkflowSingleRecordAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowDraftWorkflowSingleRecordAction.test.tsx new file mode 100644 index 0000000000000..a468db65a544a --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowDraftWorkflowSingleRecordAction.test.tsx @@ -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 }) => ( + + + + + {children} + + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.test.tsx new file mode 100644 index 0000000000000..e81a4e419dc08 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.test.tsx @@ -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 }) => ( + + + + + {children} + + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDeactivateWorkflowWorkflowSingleRecordAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDeactivateWorkflowWorkflowSingleRecordAction.test.tsx new file mode 100644 index 0000000000000..3b56488b46617 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDeactivateWorkflowWorkflowSingleRecordAction.test.tsx @@ -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 }) => ( + + + + + {children} + + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDiscardDraftWorkflowSingleRecordAction.test.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDiscardDraftWorkflowSingleRecordAction.test.tsx new file mode 100644 index 0000000000000..428d21704f651 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDiscardDraftWorkflowSingleRecordAction.test.tsx @@ -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 }) => ( + + + + + {children} + + + + + ); + + 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); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowDraftWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowDraftWorkflowSingleRecordAction.ts new file mode 100644 index 0000000000000..abaf0ba08ff93 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowDraftWorkflowSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.ts new file mode 100644 index 0000000000000..ee6d6463cd7e9 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.ts @@ -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, + }; + }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowWorkflowSingleRecordAction.ts new file mode 100644 index 0000000000000..ab132227f3fa4 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowWorkflowSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDiscardDraftWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDiscardDraftWorkflowSingleRecordAction.ts new file mode 100644 index 0000000000000..22a7cb519fb52 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDiscardDraftWorkflowSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowActiveVersionWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowActiveVersionWorkflowSingleRecordAction.ts new file mode 100644 index 0000000000000..e40794f4c5af3 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowActiveVersionWorkflowSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowRunsWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowRunsWorkflowSingleRecordAction.ts new file mode 100644 index 0000000000000..2a4cd8de1dfda --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowRunsWorkflowSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction.ts new file mode 100644 index 0000000000000..2204c1af1eb33 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowWorkflowSingleRecordAction.ts new file mode 100644 index 0000000000000..d99f35784c352 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowWorkflowSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useWorkflowSingleRecordActions.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useWorkflowSingleRecordActions.ts new file mode 100644 index 0000000000000..a35ccc486881e --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useWorkflowSingleRecordActions.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/components/WorkflowVersionsSingleRecordActionMenuEntrySetterEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/components/WorkflowVersionsSingleRecordActionMenuEntrySetterEffect.tsx new file mode 100644 index 0000000000000..edc710b83ff15 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/components/WorkflowVersionsSingleRecordActionMenuEntrySetterEffect.tsx @@ -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; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction.ts new file mode 100644 index 0000000000000..f0452d4f3282e --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction.ts new file mode 100644 index 0000000000000..ea607ff96a32a --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction.tsx new file mode 100644 index 0000000000000..3fc9906da2e0e --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction.tsx @@ -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: ( + + ), + }); + }; + + const unregisterUseAsDraftWorkflowVersionSingleRecordAction = () => { + removeActionMenuEntry('use-workflow-version-as-draft-single-record'); + }; + + return { + registerUseAsDraftWorkflowVersionSingleRecordAction, + unregisterUseAsDraftWorkflowVersionSingleRecordAction, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useWorkflowVersionsSingleRecordActions.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useWorkflowVersionsSingleRecordActions.ts new file mode 100644 index 0000000000000..258c06777ffe5 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useWorkflowVersionsSingleRecordActions.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/hooks/useWorkflowRunRecordActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/hooks/useWorkflowRunRecordActions.tsx index dbb7f7cc8aea6..8bd8a3d0fa068 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/hooks/useWorkflowRunRecordActions.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/hooks/useWorkflowRunRecordActions.tsx @@ -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: ( - - ), - }); }, }); } diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions.tsx index 24a99704607b2..33a2c270cfd2a 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions.tsx @@ -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: ( - - ), + workflowName: name, }); }, }); diff --git a/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx b/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx index d66b4629a732c..2cc66103abf5d 100644 --- a/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx +++ b/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx @@ -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; + 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 ( diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx index 7afd648c77c57..d6b9f263a3697 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx @@ -71,6 +71,7 @@ export const RecordShowPageWorkflowHeader = ({ await runWorkflowVersion({ workflowVersionId: workflowWithCurrentVersion.currentVersion.id, + workflowName: workflowWithCurrentVersion.name, }); enqueueSnackBar('', { diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx index 08c388b49eda6..3fb44ace77d7e 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx @@ -131,6 +131,7 @@ export const RecordShowPageWorkflowVersionHeader = ({ {isDefined(workflowVersion) && isDefined(draftWorkflowVersion) ? ( { + const { records: workflowVersions } = useFindManyRecords< + WorkflowVersion & { + workflow: Omit & { + 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]; +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.tsx similarity index 56% rename from packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.ts rename to packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.tsx index e330f0c91f993..a93cc7ac0e646 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.tsx @@ -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; }) => { await mutate({ variables: { input: { workflowVersionId, payload } }, }); + + enqueueSnackBar('', { + variant: SnackBarVariant.Success, + title: `${capitalize(workflowName)} starting...`, + icon: ( + + ), + }); }; return { runWorkflowVersion }; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowWithCurrentVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowWithCurrentVersion.ts index 18b17713d2620..b67e8f874073c 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowWithCurrentVersion.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowWithCurrentVersion.ts @@ -17,6 +17,7 @@ export const useWorkflowWithCurrentVersion = ( id: true, name: true, statuses: true, + lastPublishedVersionId: true, versions: true, }, skip: !isDefined(workflowId), diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 7b2cfe6fa3438..25fa6ff2977f7 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -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, diff --git a/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts b/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts index f85a10d416ae8..d8262e70c060a 100644 --- a/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts +++ b/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts @@ -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,