diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowSingleRecordAction.test.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowSingleRecordAction.test.ts index ba8c40ad9..150d62dbd 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowSingleRecordAction.test.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useActivateWorkflowSingleRecordAction.test.ts @@ -1,15 +1,25 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { renderHook } from '@testing-library/react'; import { act } from 'react'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +import { mockCurrentWorkspace } from '~/testing/mock-data/users'; import { useActivateWorkflowSingleRecordAction } from '../useActivateWorkflowSingleRecordAction'; const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find( (item) => item.nameSingular === 'workflow', )!; +const mockedWorkflowEnabledFeatureFlag = { + id: '1', + key: FeatureFlagKey.IsWorkflowEnabled, + value: true, + workspaceId: '1', +}; + const baseWorkflowMock = { __typename: 'Workflow', id: 'workflowId', @@ -116,6 +126,10 @@ const createWrapper = (workflow: { }, onInitializeRecoilSnapshot: (snapshot) => { snapshot.set(recordStoreFamilyState(workflow.id), workflow); + snapshot.set(currentWorkspaceState, { + ...mockCurrentWorkspace, + featureFlags: [mockedWorkflowEnabledFeatureFlag], + }); }, }); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDeactivateWorkflowSingleRecordAction.test.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDeactivateWorkflowSingleRecordAction.test.ts index 4faa84f12..a8323cefc 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDeactivateWorkflowSingleRecordAction.test.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDeactivateWorkflowSingleRecordAction.test.ts @@ -1,14 +1,24 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { renderHook } from '@testing-library/react'; import { act } from 'react'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +import { mockCurrentWorkspace } from '~/testing/mock-data/users'; import { useDeactivateWorkflowSingleRecordAction } from '../useDeactivateWorkflowSingleRecordAction'; const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find( (item) => item.nameSingular === 'workflow', )!; +const mockedWorkflowEnabledFeatureFlag = { + id: '1', + key: FeatureFlagKey.IsWorkflowEnabled, + value: true, + workspaceId: '1', +}; + const activeWorkflowMock = { __typename: 'Workflow', id: 'workflowId', @@ -72,6 +82,10 @@ const activeWorkflowWrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper( recordStoreFamilyState(activeWorkflowMock.id), activeWorkflowMock, ); + snapshot.set(currentWorkspaceState, { + ...mockCurrentWorkspace, + featureFlags: [mockedWorkflowEnabledFeatureFlag], + }); }, }, ); @@ -91,6 +105,10 @@ const deactivatedWorkflowWrapper = recordStoreFamilyState(deactivatedWorkflowMock.id), deactivatedWorkflowMock, ); + snapshot.set(currentWorkspaceState, { + ...mockCurrentWorkspace, + featureFlags: [mockedWorkflowEnabledFeatureFlag], + }); }, }); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDiscardDraftWorkflowSingleRecordAction.test.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDiscardDraftWorkflowSingleRecordAction.test.ts index 4157e5f8a..5c686c3f4 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDiscardDraftWorkflowSingleRecordAction.test.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/__tests__/useDiscardDraftWorkflowSingleRecordAction.test.ts @@ -1,15 +1,25 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { renderHook } from '@testing-library/react'; import { act } from 'react'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +import { mockCurrentWorkspace } from '~/testing/mock-data/users'; import { useDiscardDraftWorkflowSingleRecordAction } from '../useDiscardDraftWorkflowSingleRecordAction'; const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find( (item) => item.nameSingular === 'workflow', )!; +const mockedWorkflowEnabledFeatureFlag = { + id: '1', + key: FeatureFlagKey.IsWorkflowEnabled, + value: true, + workspaceId: '1', +}; + const noDraftWorkflowMock = { __typename: 'Workflow', id: 'workflowId', @@ -130,6 +140,10 @@ const noDraftWorkflowWrapper = recordStoreFamilyState(noDraftWorkflowMock.id), noDraftWorkflowMock, ); + snapshot.set(currentWorkspaceState, { + ...mockCurrentWorkspace, + featureFlags: [mockedWorkflowEnabledFeatureFlag], + }); }, }); @@ -147,6 +161,10 @@ const draftWorkflowWrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ recordStoreFamilyState(draftWorkflowMock.id), draftWorkflowMock, ); + snapshot.set(currentWorkspaceState, { + ...mockCurrentWorkspace, + featureFlags: [mockedWorkflowEnabledFeatureFlag], + }); }, }); @@ -165,6 +183,10 @@ const draftWorkflowWithOneVersionWrapper = recordStoreFamilyState(draftWorkflowMockWithOneVersion.id), draftWorkflowMockWithOneVersion, ); + snapshot.set(currentWorkspaceState, { + ...mockCurrentWorkspace, + featureFlags: [mockedWorkflowEnabledFeatureFlag], + }); }, }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts index c6dd05e4a..eb947c622 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts @@ -1,12 +1,29 @@ import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; export const useFilteredObjectMetadataItems = () => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const isWorkflowEnabled = useIsFeatureEnabled( + FeatureFlagKey.IsWorkflowEnabled, + ); + + const isWorkflowToBeFiltered = (nameSingular: string) => { + return ( + !isWorkflowEnabled && + (nameSingular === CoreObjectNameSingular.Workflow || + isWorkflowSubObjectMetadata(nameSingular)) + ); + }; + const activeObjectMetadataItems = objectMetadataItems.filter( - ({ isActive, isSystem }) => isActive && !isSystem, + ({ isActive, isSystem, nameSingular }) => + isActive && !isSystem && !isWorkflowToBeFiltered(nameSingular), ); const alphaSortedActiveObjectMetadataItems = activeObjectMetadataItems.sort( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index 5247be17c..a3a5df952 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -5,6 +5,10 @@ import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objec import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { isDefined } from 'twenty-shared'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier'; export const useObjectMetadataItem = ({ @@ -17,8 +21,23 @@ export const useObjectMetadataItem = ({ }), ); + const isWorkflowEnabled = useIsFeatureEnabled( + FeatureFlagKey.IsWorkflowEnabled, + ); + + const isWorkflowToBeFiltered = + !isWorkflowEnabled && + (objectNameSingular === CoreObjectNameSingular.Workflow || + isWorkflowSubObjectMetadata(objectNameSingular)); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + if (isWorkflowToBeFiltered) { + throw new Error( + 'Workflow is not enabled. If you want to use it, please enable it in the lab.', + ); + } + if (!isDefined(objectMetadataItem)) { throw new ObjectMetadataItemNotFoundError( objectNameSingular, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItem.ts index e959e627a..f1e9eec68 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useRefreshObjectMetadataItem.ts @@ -2,16 +2,10 @@ import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queri import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata'; import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useRecoilCallback } from 'recoil'; -import { - FeatureFlagKey, - ObjectMetadataItemsQuery, -} from '~/generated-metadata/graphql'; +import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type FetchPolicy = 'network-only' | 'cache-first'; @@ -20,9 +14,6 @@ export const useRefreshObjectMetadataItems = ( fetchPolicy: FetchPolicy = 'cache-first', ) => { const client = useApolloMetadataClient(); - const isWorkflowEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsWorkflowEnabled, - ); const refreshObjectMetadataItems = async () => { const result = await client.query({ @@ -36,15 +27,7 @@ export const useRefreshObjectMetadataItems = ( pagedObjectMetadataItems: result.data, }); - const filteredObjectMetadataItems = objectMetadataItems.filter((object) => { - return ( - isWorkflowEnabled || - (object.nameSingular !== CoreObjectNameSingular.Workflow && - !isWorkflowSubObjectMetadata(object.nameSingular)) - ); - }); - - replaceObjectMetadataItemIfDifferent(filteredObjectMetadataItems); + replaceObjectMetadataItemIfDifferent(objectMetadataItems); }; const replaceObjectMetadataItemIfDifferent = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/workflow/components/__stories__/RecordShowPageWorkflowHeader.stories.tsx b/packages/twenty-front/src/modules/workflow/components/__stories__/RecordShowPageWorkflowHeader.stories.tsx deleted file mode 100644 index 40cc12512..000000000 --- a/packages/twenty-front/src/modules/workflow/components/__stories__/RecordShowPageWorkflowHeader.stories.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; -import { ComponentDecorator } from 'twenty-ui'; - -import { RecordShowPageWorkflowHeader } from '@/workflow/components/RecordShowPageWorkflowHeader'; -import { expect, within } from '@storybook/test'; -import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; -import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; -import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; - -const meta: Meta = { - title: 'Modules/Workflow/RecordShowPageWorkflowHeader', - component: RecordShowPageWorkflowHeader, - decorators: [ - I18nFrontDecorator, - ComponentDecorator, - ObjectMetadataItemsDecorator, - SnackBarDecorator, - ], - parameters: { - container: { width: 728 }, - }, -}; - -export default meta; -type Story = StoryObj; - -const blankInitialVersionWorkflowId = '78fd5184-08f4-47b7-bb60-adb541608f65'; - -export const BlankInitialVersion: Story = { - args: { - workflowId: blankInitialVersionWorkflowId, - }, - parameters: { - msw: { - handlers: [ - graphql.query('FindOneWorkflow', () => { - return HttpResponse.json({ - data: { - workflow: { - __typename: 'Workflow', - id: blankInitialVersionWorkflowId, - name: '1231 qqerrt', - statuses: null, - lastPublishedVersionId: '', - deletedAt: null, - updatedAt: '2024-09-19T10:10:04.505Z', - position: 0, - createdAt: '2024-09-19T10:10:04.505Z', - favorites: { - __typename: 'FavoriteConnection', - edges: [], - }, - eventListeners: { - __typename: 'WorkflowEventListenerConnection', - edges: [], - }, - runs: { - __typename: 'WorkflowRunConnection', - edges: [], - }, - versions: { - __typename: 'WorkflowVersionConnection', - edges: [ - { - __typename: 'WorkflowVersionEdge', - node: { - __typename: 'WorkflowVersion', - updatedAt: '2024-09-19T10:13:12.075Z', - steps: null, - createdAt: '2024-09-19T10:10:04.725Z', - status: 'DRAFT', - name: 'v1', - id: 'f618843a-26be-4a54-a60f-f4ce88a594f0', - trigger: null, - deletedAt: null, - workflowId: blankInitialVersionWorkflowId, - }, - }, - ], - }, - }, - }, - }); - }), - ...graphqlMocks.handlers, - ], - }, - }, - play: async () => { - const canvas = within(document.body); - - expect(await canvas.findByText('Test')).toBeVisible(); - expect(await canvas.findByText('Activate')).toBeVisible(); - expect(canvas.queryByText('Discard Draft')).not.toBeInTheDocument(); - }, -}; - -const activeVersionWorkflowId = 'ca177fb1-7780-4911-8b1f-ef0a245fbd61'; - -export const ActiveVersion: Story = { - args: { - workflowId: activeVersionWorkflowId, - }, - parameters: { - msw: { - handlers: [ - graphql.query('FindOneWorkflow', () => { - return HttpResponse.json({ - data: { - workflow: { - __typename: 'Workflow', - id: blankInitialVersionWorkflowId, - name: '1231 qqerrt', - statuses: null, - lastPublishedVersionId: '', - deletedAt: null, - updatedAt: '2024-09-19T10:10:04.505Z', - position: 0, - createdAt: '2024-09-19T10:10:04.505Z', - favorites: { - __typename: 'FavoriteConnection', - edges: [], - }, - eventListeners: { - __typename: 'WorkflowEventListenerConnection', - edges: [], - }, - runs: { - __typename: 'WorkflowRunConnection', - edges: [], - }, - versions: { - __typename: 'WorkflowVersionConnection', - edges: [ - { - __typename: 'WorkflowVersionEdge', - node: { - __typename: 'WorkflowVersion', - updatedAt: '2024-09-19T10:13:12.075Z', - steps: null, - createdAt: '2024-09-19T10:10:04.725Z', - status: 'ACTIVE', - name: 'v1', - id: 'f618843a-26be-4a54-a60f-f4ce88a594f0', - trigger: null, - deletedAt: null, - workflowId: blankInitialVersionWorkflowId, - }, - }, - ], - }, - }, - }, - }); - }), - ...graphqlMocks.handlers, - ], - }, - }, - play: async () => { - const canvas = within(document.body); - - expect(await canvas.findByText('Test')).toBeVisible(); - expect(await canvas.findByText('Deactivate')).toBeVisible(); - }, -}; - -const draftVersionWithPreviousActiveVersionWorkflowId = - '89c00f14-4ebd-4675-a098-cdf59eee372b'; - -export const DraftVersionWithPreviousActiveVersion: Story = { - args: { - workflowId: draftVersionWithPreviousActiveVersionWorkflowId, - }, - parameters: { - msw: { - handlers: [ - graphql.query('FindOneWorkflow', () => { - return HttpResponse.json({ - data: { - workflow: { - __typename: 'Workflow', - id: draftVersionWithPreviousActiveVersionWorkflowId, - name: '1231 qqerrt', - statuses: null, - lastPublishedVersionId: '', - deletedAt: null, - updatedAt: '2024-09-19T10:10:04.505Z', - position: 0, - createdAt: '2024-09-19T10:10:04.505Z', - favorites: { - __typename: 'FavoriteConnection', - edges: [], - }, - eventListeners: { - __typename: 'WorkflowEventListenerConnection', - edges: [], - }, - runs: { - __typename: 'WorkflowRunConnection', - edges: [], - }, - versions: { - __typename: 'WorkflowVersionConnection', - edges: [ - { - __typename: 'WorkflowVersionEdge', - node: { - __typename: 'WorkflowVersion', - updatedAt: '2024-09-19T10:13:12.075Z', - steps: null, - createdAt: '2024-09-19T10:10:04.725Z', - status: 'ACTIVE', - name: 'v1', - id: 'f618843a-26be-4a54-a60f-f4ce88a594f0', - trigger: null, - deletedAt: null, - workflowId: - draftVersionWithPreviousActiveVersionWorkflowId, - }, - }, - { - __typename: 'WorkflowVersionEdge', - node: { - __typename: 'WorkflowVersion', - updatedAt: '2024-09-19T10:13:12.075Z', - steps: null, - createdAt: '2024-09-19T10:10:05.725Z', - status: 'DRAFT', - name: 'v2', - id: 'f618843a-26be-4a54-a60f-f4ce88a594f1', - trigger: null, - deletedAt: null, - workflowId: - draftVersionWithPreviousActiveVersionWorkflowId, - }, - }, - ], - }, - }, - }, - }); - }), - ...graphqlMocks.handlers, - ], - }, - }, - play: async () => { - const canvas = within(document.body); - - expect(await canvas.findByText('Test')).toBeVisible(); - expect(await canvas.findByText('Discard Draft')).toBeVisible(); - }, -}; diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 187504385..8761be8ec 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -62,6 +62,12 @@ export const mockCurrentWorkspace: Workspace = { value: true, workspaceId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6w', }, + { + id: '1492de61-5018-4368-8923-4f1eeaf988c6', + key: FeatureFlagKey.IsWorkflowEnabled, + value: true, + workspaceId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6w', + }, ], createdAt: '2023-04-26T10:23:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00', diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts index c62484429..2f312b6ac 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts @@ -7,7 +7,10 @@ type FeatureFlagMetadata = { }; export type PublicFeatureFlag = { - key: Extract; + key: Extract< + FeatureFlagKey, + FeatureFlagKey.IsLocalizationEnabled | FeatureFlagKey.IsWorkflowEnabled + >; metadata: FeatureFlagMetadata; }; @@ -21,4 +24,12 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [ imagePath: 'https://twenty.com/images/releases/labs/translation.png', }, }, + { + key: FeatureFlagKey.IsWorkflowEnabled, + metadata: { + label: 'Workflows', + description: 'Create custom workflows to automate your work.', + imagePath: 'https://twenty.com/images/lab/is-workflow-enabled.png', + }, + }, ];