From d1531aa1b65de177253f034c81891a09ace232df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:37:29 +0100 Subject: [PATCH] 8191 command k workflow trigger for selected record (#8315) Closes #8191 https://github.com/user-attachments/assets/694da229-cc91-4df2-97a0-49cd5dabcf12 --- .../twenty-front/src/generated/graphql.tsx | 42 ++++++- .../components/DeleteRecordsActionEffect.tsx | 1 + .../components/ExportRecordsActionEffect.tsx | 25 ++++- .../ManageFavoritesActionEffect.tsx | 1 + .../RecordActionMenuEntriesSetter.tsx | 27 +++-- .../WorkflowRunRecordActionEffect.tsx | 104 ++++++++++++++++++ .../components/RecordIndexActionMenuBar.tsx | 2 +- .../RecordIndexActionMenuBar.stories.tsx | 30 ++--- .../RecordIndexActionMenuBarEntry.stories.tsx | 3 + .../RecordIndexActionMenuDropdown.stories.tsx | 65 +++++------ .../RecordShowActionMenuBar.stories.tsx | 66 +++++------ .../__tests__/useExportRecordData.test.ts | 4 +- .../action-menu/hooks/useExportRecordData.ts | 7 +- .../action-menu/types/ActionMenuEntry.ts | 1 + .../command-menu/components/CommandMenu.tsx | 101 +++++++++++------ .../command-menu/hooks/useCommandMenu.ts | 18 ++- .../src/modules/command-menu/types/Command.ts | 9 +- .../hooks/useCreateOneDatabaseConnection.ts | 4 +- .../hooks/useDeleteOneDatabaseConnection.ts | 4 +- .../databases/hooks/useSyncRemoteTable.ts | 4 +- .../hooks/useSyncRemoteTableSchemaChanges.ts | 4 +- .../databases/hooks/useUnsyncRemoteTable.ts | 4 +- .../hooks/useUpdateOneDatabaseConnection.ts | 4 +- .../hooks/useApolloMetadataClient.ts | 4 + .../hooks/useCreateOneFieldMetadataItem.ts | 4 +- .../hooks/useCreateOneObjectMetadataItem.ts | 4 +- .../hooks/useCreateOneRelationMetadataItem.ts | 4 +- .../hooks/useDeleteOneFieldMetadataItem.ts | 4 +- .../hooks/useDeleteOneObjectMetadataItem.ts | 4 +- .../hooks/useDeleteOneRelationMetadataItem.ts | 4 +- .../RecordIndexOptionsDropdownContent.tsx | 10 +- .../hooks/useCreateOneServerlessFunction.ts | 12 +- .../hooks/useDeleteOneServerlessFunction.ts | 10 +- .../hooks/useExecuteOneServerlessFunction.ts | 6 +- .../hooks/usePublishOneServerlessFunction.ts | 10 +- .../hooks/useUpdateOneServerlessFunction.ts | 10 +- .../graphql/mutations/runWorkflowVersion.ts | 9 ++ .../hooks/useActivateWorkflowVersion.ts | 4 +- .../useAllActiveWorkflowVersionsForObject.ts | 52 +++++++++ .../hooks/useComputeStepOutputSchema.ts | 6 +- .../hooks/useDeactivateWorkflowVersion.ts | 4 +- .../workflow/hooks/useRunWorkflowVersion.ts | 28 +++++ .../display/icon/components/TablerIcons.ts | 9 +- .../icon/providers/internal/AllIcons.ts | 24 ++-- 44 files changed, 543 insertions(+), 209 deletions(-) create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx create mode 100644 packages/twenty-front/src/modules/workflow/graphql/mutations/runWorkflowVersion.ts create mode 100644 packages/twenty-front/src/modules/workflow/hooks/useAllActiveWorkflowVersionsForObject.ts create mode 100644 packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 8635146f8..18005dbce 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import * as Apollo from '@apollo/client'; import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -1849,6 +1849,13 @@ export type DeactivateWorkflowVersionMutationVariables = Exact<{ export type DeactivateWorkflowVersionMutation = { __typename?: 'Mutation', deactivateWorkflowVersion: boolean }; +export type RunWorkflowVersionMutationVariables = Exact<{ + input: RunWorkflowVersionInput; +}>; + + +export type RunWorkflowVersionMutation = { __typename?: 'Mutation', runWorkflowVersion: { __typename?: 'WorkflowRun', workflowRunId: any } }; + export type DeleteWorkspaceInvitationMutationVariables = Exact<{ appTokenId: Scalars['String']; }>; @@ -3525,6 +3532,39 @@ export function useDeactivateWorkflowVersionMutation(baseOptions?: Apollo.Mutati export type DeactivateWorkflowVersionMutationHookResult = ReturnType; export type DeactivateWorkflowVersionMutationResult = Apollo.MutationResult; export type DeactivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions; +export const RunWorkflowVersionDocument = gql` + mutation RunWorkflowVersion($input: RunWorkflowVersionInput!) { + runWorkflowVersion(input: $input) { + workflowRunId + } +} + `; +export type RunWorkflowVersionMutationFn = Apollo.MutationFunction; + +/** + * __useRunWorkflowVersionMutation__ + * + * To run a mutation, you first call `useRunWorkflowVersionMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRunWorkflowVersionMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [runWorkflowVersionMutation, { data, loading, error }] = useRunWorkflowVersionMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useRunWorkflowVersionMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RunWorkflowVersionDocument, options); + } +export type RunWorkflowVersionMutationHookResult = ReturnType; +export type RunWorkflowVersionMutationResult = Apollo.MutationResult; +export type RunWorkflowVersionMutationOptions = Apollo.BaseMutationOptions; export const DeleteWorkspaceInvitationDocument = gql` mutation DeleteWorkspaceInvitation($appTokenId: String!) { deleteWorkspaceInvitation(appTokenId: $appTokenId) diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx index d53e118ae..b4c7e585c 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx @@ -97,6 +97,7 @@ export const DeleteRecordsActionEffect = ({ useEffect(() => { if (canDelete) { addActionMenuEntry({ + type: 'standard', key: 'delete', label: 'Delete', position, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx index bd5ce07cf..870eb85a8 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx @@ -3,10 +3,12 @@ import { displayedExportProgress, useExportRecordData, } from '@/action-menu/hooks/useExportRecordData'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { IconDatabaseExport } from 'twenty-ui'; import { useEffect } from 'react'; -import { IconFileExport } from 'twenty-ui'; export const ExportRecordsActionEffect = ({ position, @@ -16,6 +18,9 @@ export const ExportRecordsActionEffect = ({ objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, + ); const { progress, download } = useExportRecordData({ delayMs: 100, @@ -26,10 +31,14 @@ export const ExportRecordsActionEffect = ({ useEffect(() => { addActionMenuEntry({ + type: 'standard', key: 'export', position, - label: displayedExportProgress(progress), - Icon: IconFileExport, + label: displayedExportProgress( + contextStoreNumberOfSelectedRecords > 0 ? 'selection' : 'all', + progress, + ), + Icon: IconDatabaseExport, accent: 'default', onClick: () => download(), }); @@ -37,6 +46,14 @@ export const ExportRecordsActionEffect = ({ return () => { removeActionMenuEntry('export'); }; - }, [download, progress, addActionMenuEntry, removeActionMenuEntry, position]); + }, [ + contextStoreNumberOfSelectedRecords, + download, + progress, + addActionMenuEntry, + removeActionMenuEntry, + position, + ]); + return null; }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx index face48034..25d03d071 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx @@ -44,6 +44,7 @@ export const ManageFavoritesActionEffect = ({ } addActionMenuEntry({ + type: 'standard', key: 'manage-favorites', label: isFavorite ? 'Remove from favorites' : 'Add to favorites', position, 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 fc4b47209..e82002fdd 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,21 +1,20 @@ import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect'; +import { WorkflowRunRecordActionEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +const globalRecordActionEffects = [ExportRecordsActionEffect]; + const singleRecordActionEffects = [ ManageFavoritesActionEffect, - ExportRecordsActionEffect, DeleteRecordsActionEffect, ]; -const multipleRecordActionEffects = [ - ExportRecordsActionEffect, - DeleteRecordsActionEffect, -]; +const multipleRecordActionEffects = [DeleteRecordsActionEffect]; export const RecordActionMenuEntriesSetter = () => { const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( @@ -36,10 +35,6 @@ export const RecordActionMenuEntriesSetter = () => { ); } - if (!contextStoreNumberOfSelectedRecords) { - return null; - } - const actions = contextStoreNumberOfSelectedRecords === 1 ? singleRecordActionEffects @@ -47,13 +42,25 @@ export const RecordActionMenuEntriesSetter = () => { return ( <> - {actions.map((ActionEffect, index) => ( + {globalRecordActionEffects.map((ActionEffect, index) => ( ))} + {actions.map((ActionEffect, index) => ( + + ))} + {contextStoreNumberOfSelectedRecords === 1 && ( + + )} ); }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx new file mode 100644 index 000000000..abca99701 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx @@ -0,0 +1,104 @@ +import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; +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 { useAllActiveWorkflowVersionsForObject } from '@/workflow/hooks/useAllActiveWorkflowVersionsForObject'; +import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; + +import { useTheme } from '@emotion/react'; +import { useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; +import { IconSettingsAutomation, isDefined } from 'twenty-ui'; +import { capitalize } from '~/utils/string/capitalize'; + +export const WorkflowRunRecordActionEffect = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); + + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, + ); + + const selectedRecordId = + contextStoreTargetedRecordsRule.mode === 'selection' + ? contextStoreTargetedRecordsRule.selectedRecordIds[0] + : undefined; + + const selectedRecord = useRecoilValue( + recordStoreFamilyState(selectedRecordId ?? ''), + ); + + const { records: activeWorkflowVersions } = + useAllActiveWorkflowVersionsForObject({ + objectNameSingular: objectMetadataItem.nameSingular, + triggerType: 'MANUAL', + }); + + const { runWorkflowVersion } = useRunWorkflowVersion(); + + const { enqueueSnackBar } = useSnackBar(); + + const theme = useTheme(); + + useEffect(() => { + if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) { + return; + } + + for (const [ + index, + activeWorkflowVersion, + ] of activeWorkflowVersions.entries()) { + addActionMenuEntry({ + type: 'workflow-run', + key: `workflow-run-${activeWorkflowVersion.workflow.name}`, + label: capitalize(activeWorkflowVersion.workflow.name), + position: index, + Icon: IconSettingsAutomation, + onClick: async () => { + if (!isDefined(selectedRecord)) { + return; + } + + await runWorkflowVersion(activeWorkflowVersion.id, selectedRecord); + + enqueueSnackBar('', { + variant: SnackBarVariant.Success, + title: `${capitalize(activeWorkflowVersion.workflow.name)} starting...`, + icon: ( + + ), + }); + }, + }); + } + + return () => { + for (const activeWorkflowVersion of activeWorkflowVersions) { + removeActionMenuEntry( + `workflow-run-${activeWorkflowVersion.workflow.name}`, + ); + } + }; + }, [ + activeWorkflowVersions, + addActionMenuEntry, + enqueueSnackBar, + objectMetadataItem, + removeActionMenuEntry, + runWorkflowVersion, + selectedRecord, + theme.snackBar.success.color, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx index 9a22c29f9..861d6bfa8 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx @@ -33,7 +33,7 @@ export const RecordIndexActionMenuBar = () => { const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned); - if (pinnedEntries.length === 0) { + if (contextStoreNumberOfSelectedRecords === 0) { return null; } diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx index 4cb1ef1a4..8aeae25fc 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx @@ -5,6 +5,7 @@ import { RecoilRoot } from 'recoil'; import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; @@ -34,30 +35,33 @@ const meta: Meta = { selectedRecordIds: ['1', '2', '3'], }, ); + set( contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ instanceId: 'story-action-menu', }), 3, ); + + const map = new Map(); + + map.set('delete', { + isPinned: true, + type: 'standard', + key: 'delete', + label: 'Delete', + position: 0, + Icon: IconTrash, + onClick: deleteMock, + }); + set( actionMenuEntriesComponentState.atomFamily({ instanceId: 'story-action-menu', }), - new Map([ - [ - 'delete', - { - isPinned: true, - key: 'delete', - label: 'Delete', - position: 0, - Icon: IconTrash, - onClick: deleteMock, - }, - ], - ]), + map, ); + set( isBottomBarOpenedComponentState.atomFamily({ instanceId: 'action-bar-story-action-menu', diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx index 96267fed3..275058bc2 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx @@ -21,6 +21,7 @@ const markAsDoneMock = jest.fn(); export const Default: Story = { args: { entry: { + type: 'standard', key: 'delete', label: 'Delete', position: 0, @@ -33,6 +34,7 @@ export const Default: Story = { export const WithDangerAccent: Story = { args: { entry: { + type: 'standard', key: 'delete', label: 'Delete', position: 0, @@ -46,6 +48,7 @@ export const WithDangerAccent: Story = { export const WithInteraction: Story = { args: { entry: { + type: 'standard', key: 'markAsDone', label: 'Mark as done', position: 0, diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx index bb9260755..e9ba359df 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx @@ -7,6 +7,7 @@ import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIn import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { IconCheckbox, IconHeart, IconTrash } from 'twenty-ui'; @@ -29,43 +30,43 @@ const meta: Meta = { ), { x: 10, y: 10 }, ); + + const map = new Map(); + set( actionMenuEntriesComponentState.atomFamily({ instanceId: 'story-action-menu', }), - new Map([ - [ - 'delete', - { - key: 'delete', - label: 'Delete', - position: 0, - Icon: IconTrash, - onClick: deleteMock, - }, - ], - [ - 'markAsDone', - { - key: 'markAsDone', - label: 'Mark as done', - position: 1, - Icon: IconCheckbox, - onClick: markAsDoneMock, - }, - ], - [ - 'addToFavorites', - { - key: 'addToFavorites', - label: 'Add to favorites', - position: 2, - Icon: IconHeart, - onClick: addToFavoritesMock, - }, - ], - ]), + map, ); + + map.set('delete', { + type: 'standard', + key: 'delete', + label: 'Delete', + position: 0, + Icon: IconTrash, + onClick: deleteMock, + }); + + map.set('markAsDone', { + type: 'standard', + key: 'markAsDone', + label: 'Mark as done', + position: 1, + Icon: IconCheckbox, + onClick: markAsDoneMock, + }); + + map.set('addToFavorites', { + type: 'standard', + key: 'addToFavorites', + label: 'Add to favorites', + position: 2, + Icon: IconHeart, + onClick: addToFavoritesMock, + }); + set( extractComponentState( isDropdownOpenComponentState, diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx index 2be67a567..f2391b558 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx @@ -5,6 +5,7 @@ import { RecoilRoot } from 'recoil'; import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; @@ -42,44 +43,43 @@ const meta: Meta = { }), 1, ); + + const map = new Map(); + set( actionMenuEntriesComponentState.atomFamily({ instanceId: 'story-action-menu', }), - new Map([ - [ - 'addToFavorites', - { - key: 'addToFavorites', - label: 'Add to favorites', - position: 0, - Icon: IconHeart, - onClick: addToFavoritesMock, - }, - ], - [ - 'export', - { - key: 'export', - label: 'Export', - position: 1, - Icon: IconFileExport, - onClick: exportMock, - }, - ], - [ - 'delete', - { - key: 'delete', - label: 'Delete', - position: 2, - Icon: IconTrash, - onClick: deleteMock, - accent: 'danger' as MenuItemAccent, - }, - ], - ]), + map, ); + + map.set('addToFavorites', { + type: 'standard', + key: 'addToFavorites', + label: 'Add to favorites', + position: 0, + Icon: IconHeart, + onClick: addToFavoritesMock, + }); + + map.set('export', { + type: 'standard', + key: 'export', + label: 'Export', + position: 1, + Icon: IconFileExport, + onClick: exportMock, + }); + + map.set('delete', { + type: 'standard', + key: 'delete', + label: 'Delete', + position: 2, + Icon: IconTrash, + onClick: deleteMock, + accent: 'danger' as MenuItemAccent, + }); }} > { describe('displayedExportProgress', () => { it.each([ - [undefined, undefined, 'percentage', 'Export'], + [undefined, undefined, 'percentage', 'Export View as CSV'], [20, 50, 'percentage', 'Export (40%)'], [0, 100, 'number', 'Export (0)'], [10, 10, 'percentage', 'Export (100%)'], @@ -96,7 +96,7 @@ describe('displayedExportProgress', () => { 'displays the export progress', (exportedRecordCount, totalRecordCount, displayType, expected) => { expect( - displayedExportProgress({ + displayedExportProgress('all', { exportedRecordCount, totalRecordCount, displayType: displayType as 'percentage' | 'number', diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts b/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts index 8fa6d6f59..7ae40eb4d 100644 --- a/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts @@ -108,9 +108,12 @@ const percentage = (part: number, whole: number): number => { return Math.round((part / whole) * 100); }; -export const displayedExportProgress = (progress?: ExportProgress): string => { +export const displayedExportProgress = ( + mode: 'all' | 'selection' = 'all', + progress?: ExportProgress, +): string => { if (isUndefinedOrNull(progress?.exportedRecordCount)) { - return 'Export'; + return mode === 'all' ? 'Export View as CSV' : 'Export Selection as CSV'; } if ( diff --git a/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts index c6f559be8..7cef4b2fe 100644 --- a/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts +++ b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts @@ -4,6 +4,7 @@ import { IconComponent } from 'twenty-ui'; import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; export type ActionMenuEntry = { + type: 'standard' | 'workflow-run'; key: string; label: string; position: number; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 4dd360c93..a7b56427e 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -288,12 +288,20 @@ export const CommandMenu = () => { : true) && cmd.type === CommandType.Create, ); - const matchingActionCommands = commandMenuCommands.filter( + const matchingStandardActionCommands = commandMenuCommands.filter( (cmd) => (deferredCommandMenuSearch.length > 0 ? checkInShortcuts(cmd, deferredCommandMenuSearch) || checkInLabels(cmd, deferredCommandMenuSearch) - : true) && cmd.type === CommandType.Action, + : true) && cmd.type === CommandType.StandardAction, + ); + + const matchingWorkflowRunCommands = commandMenuCommands.filter( + (cmd) => + (deferredCommandMenuSearch.length > 0 + ? checkInShortcuts(cmd, deferredCommandMenuSearch) || + checkInLabels(cmd, deferredCommandMenuSearch) + : true) && cmd.type === CommandType.WorkflowRun, ); useListenClickOutside({ @@ -321,7 +329,7 @@ export const CommandMenu = () => { const selectableItemIds = copilotCommands .map((cmd) => cmd.id) - .concat(matchingActionCommands.map((cmd) => cmd.id)) + .concat(matchingStandardActionCommands.map((cmd) => cmd.id)) .concat(matchingCreateCommand.map((cmd) => cmd.id)) .concat(matchingNavigateCommand.map((cmd) => cmd.id)) .concat(people?.map((person) => person.id)) @@ -330,7 +338,8 @@ export const CommandMenu = () => { .concat(notes?.map((note) => note.id)); const isNoResults = - !matchingActionCommands.length && + !matchingStandardActionCommands.length && + !matchingWorkflowRunCommands.length && !matchingCreateCommand.length && !matchingNavigateCommand.length && !people?.length && @@ -410,38 +419,44 @@ export const CommandMenu = () => { )} {mainContextStoreComponentInstanceId && ( - - {matchingActionCommands?.map((actionCommand) => ( - - - - ))} - + <> + + {matchingStandardActionCommands?.map( + (standardActionCommand) => ( + + + + ), + )} + + + + {matchingWorkflowRunCommands?.map( + (workflowRunCommand) => ( + + + + ), + )} + + )} - - {matchingCreateCommand.map((cmd) => ( - - - - ))} - {matchingNavigateCommand.map((cmd) => ( @@ -458,6 +473,22 @@ export const CommandMenu = () => { ))} + + {matchingCreateCommand.map((cmd) => ( + + + + ))} + {people?.map((person) => ( diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index c1bf10c69..715333599 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -47,15 +47,29 @@ export const useCommandMenu = () => { const actionCommands = actionMenuEntries .getValue() + ?.filter((actionMenuEntry) => actionMenuEntry.type === 'standard') ?.map((actionMenuEntry) => ({ id: actionMenuEntry.key, label: actionMenuEntry.label, Icon: actionMenuEntry.Icon, onCommandClick: actionMenuEntry.onClick, - type: CommandType.Action, + type: CommandType.StandardAction, })); - setCommands([...commands, ...actionCommands]); + const workflowRunCommands = actionMenuEntries + .getValue() + ?.filter( + (actionMenuEntry) => actionMenuEntry.type === 'workflow-run', + ) + ?.map((actionMenuEntry) => ({ + id: actionMenuEntry.key, + label: actionMenuEntry.label, + Icon: actionMenuEntry.Icon, + onCommandClick: actionMenuEntry.onClick, + type: CommandType.WorkflowRun, + })); + + setCommands([...commands, ...actionCommands, ...workflowRunCommands]); } setIsCommandMenuOpened(true); diff --git a/packages/twenty-front/src/modules/command-menu/types/Command.ts b/packages/twenty-front/src/modules/command-menu/types/Command.ts index b43b23f9c..181ec4391 100644 --- a/packages/twenty-front/src/modules/command-menu/types/Command.ts +++ b/packages/twenty-front/src/modules/command-menu/types/Command.ts @@ -3,14 +3,19 @@ import { IconComponent } from 'twenty-ui'; export enum CommandType { Navigate = 'Navigate', Create = 'Create', - Action = 'Action', + StandardAction = 'StandardAction', + WorkflowRun = 'WorkflowRun', } export type Command = { id: string; to?: string; label: string; - type: CommandType.Navigate | CommandType.Create | CommandType.Action; + type: + | CommandType.Navigate + | CommandType.Create + | CommandType.StandardAction + | CommandType.WorkflowRun; Icon?: IconComponent; firstHotKey?: string; secondHotKey?: string; diff --git a/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts b/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts index 8155f348d..e74c3f6ed 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { CREATE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/createOneDatabaseConnection'; import { GET_MANY_DATABASE_CONNECTIONS } from '@/databases/graphql/queries/findManyDatabaseConnections'; @@ -17,7 +17,7 @@ export const useCreateOneDatabaseConnection = () => { CreateServerMutation, CreateServerMutationVariables >(CREATE_ONE_DATABASE_CONNECTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const createOneDatabaseConnection = async ( diff --git a/packages/twenty-front/src/modules/databases/hooks/useDeleteOneDatabaseConnection.ts b/packages/twenty-front/src/modules/databases/hooks/useDeleteOneDatabaseConnection.ts index eae2f4a42..cc10c9d2e 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useDeleteOneDatabaseConnection.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useDeleteOneDatabaseConnection.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { DELETE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/deleteOneDatabaseConnection'; @@ -17,7 +17,7 @@ export const useDeleteOneDatabaseConnection = () => { DeleteServerMutation, DeleteServerMutationVariables >(DELETE_ONE_DATABASE_CONNECTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const deleteOneDatabaseConnection = async (input: RemoteServerIdInput) => { diff --git a/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTable.ts b/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTable.ts index 24da83024..884339b46 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTable.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTable.ts @@ -1,5 +1,5 @@ +import { useApolloClient, useMutation } from '@apollo/client'; import { useCallback } from 'react'; -import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; import { SYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/syncRemoteTable'; import { modifyRemoteTableFromCache } from '@/databases/utils/modifyRemoteTableFromCache'; @@ -28,7 +28,7 @@ export const useSyncRemoteTable = () => { SyncRemoteTableMutation, SyncRemoteTableMutationVariables >(SYNC_REMOTE_TABLE, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const syncRemoteTable = useCallback( diff --git a/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTableSchemaChanges.ts b/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTableSchemaChanges.ts index a352eb0b7..0ce4bf68c 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTableSchemaChanges.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTableSchemaChanges.ts @@ -1,5 +1,5 @@ +import { useApolloClient, useMutation } from '@apollo/client'; import { useCallback } from 'react'; -import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; import { SYNC_REMOTE_TABLE_SCHEMA_CHANGES } from '@/databases/graphql/mutations/syncRemoteTableSchemaChanges'; import { modifyRemoteTableFromCache } from '@/databases/utils/modifyRemoteTableFromCache'; @@ -29,7 +29,7 @@ export const useSyncRemoteTableSchemaChanges = () => { SyncRemoteTableSchemaChangesMutation, SyncRemoteTableSchemaChangesMutationVariables >(SYNC_REMOTE_TABLE_SCHEMA_CHANGES, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const syncRemoteTableSchemaChanges = useCallback( diff --git a/packages/twenty-front/src/modules/databases/hooks/useUnsyncRemoteTable.ts b/packages/twenty-front/src/modules/databases/hooks/useUnsyncRemoteTable.ts index 7e08b5fde..4a331e389 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useUnsyncRemoteTable.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useUnsyncRemoteTable.ts @@ -1,5 +1,5 @@ +import { useMutation } from '@apollo/client'; import { useCallback } from 'react'; -import { ApolloClient, useMutation } from '@apollo/client'; import { UNSYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/unsyncRemoteTable'; import { modifyRemoteTableFromCache } from '@/databases/utils/modifyRemoteTableFromCache'; @@ -21,7 +21,7 @@ export const useUnsyncRemoteTable = () => { UnsyncRemoteTableMutation, UnsyncRemoteTableMutationVariables >(UNSYNC_REMOTE_TABLE, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const unsyncRemoteTable = useCallback( diff --git a/packages/twenty-front/src/modules/databases/hooks/useUpdateOneDatabaseConnection.ts b/packages/twenty-front/src/modules/databases/hooks/useUpdateOneDatabaseConnection.ts index 5cffbf2a3..4fcc930a2 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useUpdateOneDatabaseConnection.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useUpdateOneDatabaseConnection.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { UPDATE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/updateOneDatabaseConnection'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; @@ -15,7 +15,7 @@ export const useUpdateOneDatabaseConnection = () => { UpdateServerMutation, UpdateServerMutationVariables >(UPDATE_ONE_DATABASE_CONNECTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const updateOneDatabaseConnection = async ( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useApolloMetadataClient.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useApolloMetadataClient.ts index 4dcb83de1..2a75b39dd 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useApolloMetadataClient.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useApolloMetadataClient.ts @@ -10,5 +10,9 @@ export const useApolloMetadataClient = () => { return apolloClient; } + if (!apolloMetadataClient) { + throw new Error('ApolloMetadataClient not found'); + } + return apolloMetadataClient; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts index d272404c7..e7ed996c5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { @@ -19,7 +19,7 @@ export const useCreateOneFieldMetadataItem = () => { CreateOneFieldMetadataItemMutation, CreateOneFieldMetadataItemMutationVariables >(CREATE_ONE_FIELD_METADATA_ITEM, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const createOneFieldMetadataItem = async (input: CreateFieldInput) => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts index 6cbb5e02a..98f34c91f 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; +import { useApolloClient, useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -26,7 +26,7 @@ export const useCreateOneObjectMetadataItem = () => { CreateOneObjectMetadataItemMutation, CreateOneObjectMetadataItemMutationVariables >(CREATE_ONE_OBJECT_METADATA_ITEM, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const createOneObjectMetadataItem = async (input: CreateObjectInput) => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneRelationMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneRelationMetadataItem.ts index 0c41eb5cb..f8c0b1aec 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneRelationMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneRelationMetadataItem.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { @@ -22,7 +22,7 @@ export const useCreateOneRelationMetadataItem = () => { CreateOneRelationMetadataMutation, CreateOneRelationMetadataMutationVariables >(CREATE_ONE_RELATION_METADATA_ITEM, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const createOneRelationMetadataItem = async ( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts index 23c1b28c5..117051e4d 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { @@ -18,7 +18,7 @@ export const useDeleteOneFieldMetadataItem = () => { DeleteOneFieldMetadataItemMutation, DeleteOneFieldMetadataItemMutationVariables >(DELETE_ONE_FIELD_METADATA_ITEM, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const deleteOneFieldMetadataItem = async ( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts index 6c4a10b47..f4101ca66 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { @@ -18,7 +18,7 @@ export const useDeleteOneObjectMetadataItem = () => { DeleteOneObjectMetadataItemMutation, DeleteOneObjectMetadataItemMutationVariables >(DELETE_ONE_OBJECT_METADATA_ITEM, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const deleteOneObjectMetadataItem = async ( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneRelationMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneRelationMetadataItem.ts index db78dd8eb..c380fdbab 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneRelationMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneRelationMetadataItem.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { DELETE_ONE_RELATION_METADATA_ITEM } from '@/object-metadata/graphql/mutations'; @@ -18,7 +18,7 @@ export const useDeleteOneRelationMetadataItem = () => { DeleteOneRelationMetadataItemMutation, DeleteOneRelationMetadataItemMutationVariables >(DELETE_ONE_RELATION_METADATA_ITEM, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const deleteOneRelationMetadataItem = async ( diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index 4fa2394d6..67cfa73d5 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -21,6 +21,7 @@ import { displayedExportProgress, useExportRecordData, } from '@/action-menu/hooks/useExportRecordData'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; @@ -41,6 +42,7 @@ import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemTog import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; @@ -182,6 +184,12 @@ export const RecordIndexOptionsDropdownContent = ({ viewGroupFieldMetadataItem && (visibleRecordGroups.length > 0 || hiddenRecordGroups.length > 0); + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, + ); + + const mode = contextStoreNumberOfSelectedRecords > 0 ? 'selection' : 'all'; + useEffect(() => { if (currentMenu === 'hiddenViewGroups' && hiddenRecordGroups.length === 0) { setCurrentMenu('viewGroups'); @@ -214,7 +222,7 @@ export const RecordIndexOptionsDropdownContent = ({ { diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts index 427f98466..5627feea7 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts @@ -1,14 +1,14 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; +import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction'; +import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; +import { getOperationName } from '@apollo/client/utilities'; import { - CreateServerlessFunctionInput, CreateOneServerlessFunctionItemMutation, CreateOneServerlessFunctionItemMutationVariables, + CreateServerlessFunctionInput, } from '~/generated-metadata/graphql'; -import { getOperationName } from '@apollo/client/utilities'; -import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; -import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction'; export const useCreateOneServerlessFunction = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -16,7 +16,7 @@ export const useCreateOneServerlessFunction = () => { CreateOneServerlessFunctionItemMutation, CreateOneServerlessFunctionItemMutationVariables >(CREATE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const createOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts index 4654487b5..1c3afaf03 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts @@ -1,13 +1,13 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; -import { ApolloClient, useMutation } from '@apollo/client'; -import { getOperationName } from '@apollo/client/utilities'; import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction'; +import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; +import { useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; import { - ServerlessFunctionIdInput, DeleteOneServerlessFunctionMutation, DeleteOneServerlessFunctionMutationVariables, + ServerlessFunctionIdInput, } from '~/generated-metadata/graphql'; -import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; export const useDeleteOneServerlessFunction = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -15,7 +15,7 @@ export const useDeleteOneServerlessFunction = () => { DeleteOneServerlessFunctionMutation, DeleteOneServerlessFunctionMutationVariables >(DELETE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const deleteOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useExecuteOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useExecuteOneServerlessFunction.ts index 8825a68d9..4b69a953e 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useExecuteOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useExecuteOneServerlessFunction.ts @@ -1,10 +1,10 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; -import { ApolloClient, useMutation } from '@apollo/client'; import { EXECUTE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/executeOneServerlessFunction'; +import { useMutation } from '@apollo/client'; import { - ExecuteServerlessFunctionInput, ExecuteOneServerlessFunctionMutation, ExecuteOneServerlessFunctionMutationVariables, + ExecuteServerlessFunctionInput, } from '~/generated-metadata/graphql'; export const useExecuteOneServerlessFunction = () => { @@ -13,7 +13,7 @@ export const useExecuteOneServerlessFunction = () => { ExecuteOneServerlessFunctionMutation, ExecuteOneServerlessFunctionMutationVariables >(EXECUTE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const executeOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePublishOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePublishOneServerlessFunction.ts index 02b8a3e1f..364eefd75 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePublishOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePublishOneServerlessFunction.ts @@ -1,13 +1,13 @@ -import { ApolloClient, useMutation } from '@apollo/client'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { PUBLISH_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/publishOneServerlessFunction'; +import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; +import { useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; import { - PublishServerlessFunctionInput, PublishOneServerlessFunctionMutation, PublishOneServerlessFunctionMutationVariables, + PublishServerlessFunctionInput, } from '~/generated-metadata/graphql'; -import { getOperationName } from '@apollo/client/utilities'; -import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; export const usePublishOneServerlessFunction = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -15,7 +15,7 @@ export const usePublishOneServerlessFunction = () => { PublishOneServerlessFunctionMutation, PublishOneServerlessFunctionMutationVariables >(PUBLISH_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const publishOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts index d75529ebc..61c0a52c4 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts @@ -1,13 +1,13 @@ -import { ApolloClient, useMutation } from '@apollo/client'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction'; +import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; +import { useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; import { - UpdateServerlessFunctionInput, UpdateOneServerlessFunctionMutation, UpdateOneServerlessFunctionMutationVariables, + UpdateServerlessFunctionInput, } from '~/generated-metadata/graphql'; -import { getOperationName } from '@apollo/client/utilities'; -import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; export const useUpdateOneServerlessFunction = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -15,7 +15,7 @@ export const useUpdateOneServerlessFunction = () => { UpdateOneServerlessFunctionMutation, UpdateOneServerlessFunctionMutationVariables >(UPDATE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const updateOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/runWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/runWorkflowVersion.ts new file mode 100644 index 000000000..961efec89 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/runWorkflowVersion.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const RUN_WORKFLOW_VERSION = gql` + mutation RunWorkflowVersion($input: RunWorkflowVersionInput!) { + runWorkflowVersion(input: $input) { + workflowRunId + } + } +`; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts index 4f6395f32..5797c9707 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts @@ -1,5 +1,5 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; -import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; +import { useApolloClient, useMutation } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -18,7 +18,7 @@ export const useActivateWorkflowVersion = () => { ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables >(ACTIVATE_WORKFLOW_VERSION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const { objectMetadataItem: objectMetadataItemWorkflowVersion } = diff --git a/packages/twenty-front/src/modules/workflow/hooks/useAllActiveWorkflowVersionsForObject.ts b/packages/twenty-front/src/modules/workflow/hooks/useAllActiveWorkflowVersionsForObject.ts new file mode 100644 index 000000000..2ab48de46 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useAllActiveWorkflowVersionsForObject.ts @@ -0,0 +1,52 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { + Workflow, + WorkflowTriggerType, + WorkflowVersion, +} from '@/workflow/types/Workflow'; + +export const useAllActiveWorkflowVersionsForObject = ({ + objectNameSingular, + triggerType, +}: { + objectNameSingular: string; + triggerType: WorkflowTriggerType; +}) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { records } = useFindManyRecords< + WorkflowVersion & { workflow: Workflow } + >({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + filter: { + and: [ + { + status: { + eq: 'ACTIVE', + }, + }, + { + trigger: { + like: `%"type": "${triggerType}"%`, + }, + }, + { + trigger: { + like: `%"objectType": "${objectNameSingular}"%`, + }, + }, + ], + }, + recordGqlFields: { + ...generateDepthOneRecordGqlFields({ objectMetadataItem }), + workflow: true, + }, + }); + + return { records }; +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useComputeStepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/hooks/useComputeStepOutputSchema.ts index 3b84d3907..d79f9646d 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useComputeStepOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useComputeStepOutputSchema.ts @@ -1,11 +1,11 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; -import { ApolloClient, useMutation } from '@apollo/client'; +import { COMPUTE_STEP_OUTPUT_SCHEMA } from '@/workflow/graphql/mutations/computeStepOutputSchema'; +import { useMutation } from '@apollo/client'; import { ComputeStepOutputSchemaInput, ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables, } from '~/generated/graphql'; -import { COMPUTE_STEP_OUTPUT_SCHEMA } from '@/workflow/graphql/mutations/computeStepOutputSchema'; export const useComputeStepOutputSchema = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -13,7 +13,7 @@ export const useComputeStepOutputSchema = () => { ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables >(COMPUTE_STEP_OUTPUT_SCHEMA, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const computeStepOutputSchema = async ( diff --git a/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts index 2059e1339..83257c86f 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts @@ -1,5 +1,5 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; -import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; +import { useApolloClient, useMutation } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -17,7 +17,7 @@ export const useDeactivateWorkflowVersion = () => { ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables >(DEACTIVATE_WORKFLOW_VERSION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const { objectMetadataItem: objectMetadataItemWorkflowVersion } = diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.ts new file mode 100644 index 000000000..fe1a9ed78 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.ts @@ -0,0 +1,28 @@ +import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; +import { RUN_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/runWorkflowVersion'; +import { useMutation } from '@apollo/client'; +import { + RunWorkflowVersionMutation, + RunWorkflowVersionMutationVariables, +} from '~/generated/graphql'; + +export const useRunWorkflowVersion = () => { + const apolloMetadataClient = useApolloMetadataClient(); + const [mutate] = useMutation< + RunWorkflowVersionMutation, + RunWorkflowVersionMutationVariables + >(RUN_WORKFLOW_VERSION, { + client: apolloMetadataClient, + }); + + const runWorkflowVersion = async ( + workflowVersionId: string, + payload: Record, + ) => { + await mutate({ + variables: { input: { workflowVersionId, payload } }, + }); + }; + + return { runWorkflowVersion }; +}; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 8f75e4ae1..ffe53e314 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -108,6 +108,7 @@ export { IconCurrencyYuan, IconCurrencyZloty, IconDatabase, + IconDatabaseExport, IconDeviceFloppy, IconDoorEnter, IconDotsVertical, @@ -125,8 +126,8 @@ export { IconFileUpload, IconFileZip, IconFilter, - IconFilterOff, IconFilterCog, + IconFilterOff, IconFocusCentered, IconForbid, IconFunction, @@ -136,6 +137,7 @@ export { IconH2, IconH3, IconHandClick, + IconHandMove, IconHeadphones, IconHeart, IconHeartOff, @@ -181,13 +183,12 @@ export { IconPlayerPlay, IconPlayerStop, IconPlaylistAdd, - IconHandMove, - IconSquare, IconPlaystationSquare, IconPlug, IconPlus, IconPower, IconPresentation, + IconPrinter, IconProgressCheck, IconPuzzle, IconQuestionMark, @@ -209,6 +210,7 @@ export { IconSortDescending, IconSparkles, IconSql, + IconSquare, IconSquareKey, IconSquareRoundedCheck, IconTable, @@ -221,7 +223,6 @@ export { IconTimelineEvent, IconTool, IconTrash, - IconPrinter, IconUnlink, IconUpload, IconUser, 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 8e547dbdd..f85a10d41 100644 --- a/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts +++ b/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts @@ -1,13 +1,13 @@ /* eslint-disable no-restricted-imports */ import { + Icon123, + Icon24Hours, Icon2fa, + Icon360, + Icon360View, Icon3dCubeSphere, Icon3dCubeSphereOff, Icon3dRotate, - Icon24Hours, - Icon123, - Icon360, - Icon360View, IconAB, IconAB2, IconAbacus, @@ -1240,6 +1240,9 @@ import { IconClockExclamation, IconClockHeart, IconClockHour1, + IconClockHour10, + IconClockHour11, + IconClockHour12, IconClockHour2, IconClockHour3, IconClockHour4, @@ -1248,9 +1251,6 @@ import { IconClockHour7, IconClockHour8, IconClockHour9, - IconClockHour10, - IconClockHour11, - IconClockHour12, IconClockMinus, IconClockOff, IconClockPause, @@ -2917,10 +2917,10 @@ import { IconMovieOff, IconMug, IconMugOff, - IconMultiplier1x, - IconMultiplier2x, IconMultiplier05x, IconMultiplier15x, + IconMultiplier1x, + IconMultiplier2x, IconMushroom, IconMushroomOff, IconMusic, @@ -3315,20 +3315,20 @@ import { IconReservedLine, IconResize, IconRestore, - IconRewindBackward5, IconRewindBackward10, IconRewindBackward15, IconRewindBackward20, IconRewindBackward30, IconRewindBackward40, + IconRewindBackward5, IconRewindBackward50, IconRewindBackward60, - IconRewindForward5, IconRewindForward10, IconRewindForward15, IconRewindForward20, IconRewindForward30, IconRewindForward40, + IconRewindForward5, IconRewindForward50, IconRewindForward60, IconRibbonHealth, @@ -3880,11 +3880,11 @@ import { IconTiltShift, IconTiltShiftOff, IconTimeDuration0, - IconTimeDuration5, IconTimeDuration10, IconTimeDuration15, IconTimeDuration30, IconTimeDuration45, + IconTimeDuration5, IconTimeDuration60, IconTimeDuration90, IconTimeDurationOff,