Activate/Deactivate workflow and Discard Draft (#7022)

## Setup

This PR can be tested only if some feature flags have specific values:

- `IsWorkflowEnabled` equals `true`
- `IsQueryRunnerTwentyORMEnabled` equals `false`

These feature flags weren't committed to don't break other branches.

## What this PR brings

- Display buttons to activate and deactivate a workflow version and a
button to discard the current draft version. I also scaffolded a "Test"
button, which doesn't do anything for now.
- Wired the activate, deactivate and discard draft buttons to the
backend.
- Made it possible to "edit" active and deactivated versions by
automatically creating a new draft version when the user tries to edit
the version.
- Hide the "Discard Draft", button if the current version is not a draft
or is the first version ever created.
- On the backend, don't consider discarded drafts when checking if a new
draft version can be created.
- On the backend, disallow deleting the first created workflow version.
Otherwise, we will end up with a blank canvas in the front end, and it
will be impossible to recover from it.
- On the backend, disallow running deactivation steps if the workflow
version is not currently active. Previously, we were throwing, which is
unnecessary as it's a valid case.

## Spotted bugs that we must dive into

### Duplicate workflow versions in Apollo cache


https://github.com/user-attachments/assets/7cfffd06-11e0-417a-8da0-f9a5f43b84e2

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Baptiste Devessier
2024-09-25 18:09:31 +02:00
committed by GitHub
parent 75b493ba6c
commit 729c990546
76 changed files with 19152 additions and 27309 deletions

View File

@ -416,6 +416,133 @@ export const graphqlMocks = {
},
});
}),
graphql.query('FindManyWorkflows', () => {
return HttpResponse.json({
data: {
workflows: {
__typename: 'WorkflowConnection',
totalCount: 1,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
endCursor:
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
},
edges: [
{
__typename: 'WorkflowEdge',
cursor:
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
node: {
__typename: 'Workflow',
id: '200c1508-f102-4bb9-af32-eda55239ae61',
},
},
],
},
},
});
}),
graphql.query('FindOneWorkflow', () => {
return HttpResponse.json({
data: {
workflow: {
__typename: 'Workflow',
id: '200c1508-f102-4bb9-af32-eda55239ae61',
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: {
type: 'DATABASE_EVENT',
settings: {
eventName: 'note.created',
},
},
deletedAt: null,
workflowId: '200c1508-f102-4bb9-af32-eda55239ae61',
},
},
],
},
},
},
});
}),
graphql.query('FindManyWorkflowVersions', () => {
return HttpResponse.json({
data: {
workflowVersions: {
__typename: 'WorkflowVersionConnection',
totalCount: 1,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
endCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
},
edges: [
{
__typename: 'WorkflowVersionEdge',
cursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
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: {
type: 'DATABASE_EVENT',
settings: {
eventName: 'note.created',
},
},
deletedAt: null,
workflowId: '200c1508-f102-4bb9-af32-eda55239ae61',
},
},
],
},
},
});
}),
http.get('https://chat-assets.frontapp.com/v1/chat.bundle.js', () => {
return HttpResponse.text(
`

View File

@ -2,7 +2,7 @@ import { ReactNode, useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
export const JestObjectMetadataItemSetter = ({
children,
@ -12,7 +12,7 @@ export const JestObjectMetadataItemSetter = ({
const setObjectMetadataItems = useSetRecoilState(objectMetadataItemsState);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setObjectMetadataItems(getObjectMetadataItemsMock());
setObjectMetadataItems(generatedMockObjectMetadataItems);
setIsLoaded(true);
}, [setObjectMetadataItems]);

View File

@ -5,7 +5,7 @@ import {
ObjectEdge,
ObjectMetadataItemsQuery,
} from '~/generated-metadata/graphql';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/standard-metadata-query-result';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
// TODO: replace with new mock
const customObjectMetadataItemEdge: ObjectEdge = {

View File

@ -1,5 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/standard-metadata-query-result';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
mockedStandardObjectMetadataQueryResult.objects.edges.map((edge) => ({