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

@ -1,7 +1,6 @@
import { gql } from '@apollo/client';
import { AvatarType } from 'twenty-ui';
import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
export const mockId = '8f3b2121-f194-4ba4-9fbf-2d5a37126806';
@ -48,36 +47,38 @@ export const initialFavorites = [
},
];
export const sortedFavorites = [
{
id: '1',
recordId: '2',
position: 0,
avatarType: 'squared',
avatarUrl: undefined,
labelIdentifier: 'ABC Corp',
link: '/object/company/2',
},
{
id: '2',
recordId: '4',
position: 1,
avatarType: 'squared',
avatarUrl: undefined,
labelIdentifier: 'Company Test',
link: '/object/company/4',
},
{
id: '3',
position: 2,
key: '8f3b2121-f194-4ba4-9fbf-2d5a37126806',
labelIdentifier: 'favoriteLabel',
avatarUrl: 'example.com',
avatarType: 'squared',
link: 'example.com',
recordId: '1',
},
];
export const sortedFavorites = [
{
"avatarType": "rounded",
"avatarUrl": "",
"id": "1",
"labelIdentifier": " ",
"link": "/object/person/1",
"position": 0,
"recordId": "1",
"workspaceMemberId": undefined,
},
{
"avatarType": "rounded",
"avatarUrl": "",
"id": "2",
"labelIdentifier": " ",
"link": "/object/person/3",
"position": 1,
"recordId": "3",
"workspaceMemberId": undefined,
},
{
"avatarType": "squared",
"avatarUrl": "example.com",
"id": "3",
"key": "8f3b2121-f194-4ba4-9fbf-2d5a37126806",
"labelIdentifier": "favoriteLabel",
"link": "example.com",
"position": 2,
"recordId": "1",
},
]
export const mocks = [
{
@ -86,132 +87,155 @@ export const mocks = [
mutation CreateOneFavorite($input: FavoriteCreateInput!) {
createFavorite(data: $input) {
__typename
noteId
taskId
myCustomObjectId
workspaceMemberId
workspaceMember {
person {
__typename
userId
updatedAt
dateFormat
id
locale
avatarUrl
timeZone
name {
firstName
lastName
}
userEmail
createdAt
timeFormat
colorScheme
}
companyId
myCustomObject {
__typename
createdBy {
source
workspaceMemberId
name
}
position
updatedAt
name
myCustomField
id
createdAt
}
updatedAt
id
opportunity {
__typename
companyId
closeDate
stage
createdBy {
source
workspaceMemberId
name
}
id
updatedAt
name
createdAt
pointOfContactId
amount {
amountMicros
currencyCode
}
position
}
noteId
note {
__typename
createdBy {
source
workspaceMemberId
name
}
position
body
updatedAt
createdAt
title
id
}
personId
task {
__typename
status
assigneeId
updatedAt
body
createdAt
dueAt
position
id
title
createdBy {
source
workspaceMemberId
name
}
}
opportunityId
position
createdAt
company {
__typename
id
visaSponsorship
createdBy {
source
workspaceMemberId
name
}
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
position
annualRecurringRevenue {
amountMicros
currencyCode
}
employees
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
workPolicy
deletedAt
createdAt
updatedAt
jobTitle
intro
workPrefereance
performanceRating
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
city
companyId
phones {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
createdBy {
source
workspaceMemberId
name
}
id
position
emails {
primaryEmail
additionalEmails
}
avatarUrl
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
}
task {
__typename
updatedAt
createdAt
deletedAt
dueAt
id
status
body
createdBy {
source
workspaceMemberId
name
}
assigneeId
position
title
}
rocketId
viewId
updatedAt
workflowId
personId
workspaceMemberId
note {
__typename
deletedAt
id
position
updatedAt
createdBy {
source
workspaceMemberId
name
}
body
title
createdAt
}
createdAt
view {
__typename
id
type
icon
key
isCompact
kanbanFieldMetadataId
objectMetadataId
position
createdAt
deletedAt
updatedAt
name
}
opportunityId
position
deletedAt
id
companyId
workflow {
__typename
deletedAt
lastPublishedVersionId
createdAt
id
statuses
name
position
updatedAt
}
workspaceMember {
__typename
name {
firstName
lastName
}
avatarUrl
userId
createdAt
timeZone
id
timeFormat
updatedAt
locale
userEmail
deletedAt
colorScheme
dateFormat
}
company {
__typename
updatedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
visaSponsorship
address {
addressStreet1
addressStreet2
@ -222,21 +246,76 @@ export const mocks = [
addressLat
addressLng
}
position
employees
deletedAt
accountOwnerId
annualRecurringRevenue {
amountMicros
currencyCode
}
id
name
updatedAt
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
myCustomField
createdAt
accountOwnerId
createdBy {
source
workspaceMemberId
name
}
workPolicy
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
tagline
idealCustomerProfile
}
person {
${PERSON_FRAGMENT}
rocket {
__typename
createdBy {
source
workspaceMemberId
name
}
updatedAt
name
position
createdAt
id
deletedAt
}
opportunity {
__typename
createdBy {
source
workspaceMemberId
name
}
amount {
amountMicros
currencyCode
}
stage
position
closeDate
id
name
pointOfContactId
companyId
updatedAt
deletedAt
createdAt
}
}
}
@ -286,132 +365,155 @@ export const mocks = [
) {
updateFavorite(id: $idToUpdate, data: $input) {
__typename
noteId
taskId
myCustomObjectId
workspaceMemberId
workspaceMember {
person {
__typename
userId
updatedAt
dateFormat
id
locale
avatarUrl
timeZone
name {
firstName
lastName
}
userEmail
createdAt
timeFormat
colorScheme
}
companyId
myCustomObject {
__typename
createdBy {
source
workspaceMemberId
name
}
position
updatedAt
name
myCustomField
id
createdAt
}
updatedAt
id
opportunity {
__typename
companyId
closeDate
stage
createdBy {
source
workspaceMemberId
name
}
id
updatedAt
name
createdAt
pointOfContactId
amount {
amountMicros
currencyCode
}
position
}
noteId
note {
__typename
createdBy {
source
workspaceMemberId
name
}
position
body
updatedAt
createdAt
title
id
}
personId
task {
__typename
status
assigneeId
updatedAt
body
createdAt
dueAt
position
id
title
createdBy {
source
workspaceMemberId
name
}
}
opportunityId
position
createdAt
company {
__typename
id
visaSponsorship
createdBy {
source
workspaceMemberId
name
}
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
position
annualRecurringRevenue {
amountMicros
currencyCode
}
employees
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
workPolicy
deletedAt
createdAt
updatedAt
jobTitle
intro
workPrefereance
performanceRating
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
city
companyId
phones {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
createdBy {
source
workspaceMemberId
name
}
id
position
emails {
primaryEmail
additionalEmails
}
avatarUrl
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
}
task {
__typename
updatedAt
createdAt
deletedAt
dueAt
id
status
body
createdBy {
source
workspaceMemberId
name
}
assigneeId
position
title
}
rocketId
viewId
updatedAt
workflowId
personId
workspaceMemberId
note {
__typename
deletedAt
id
position
updatedAt
createdBy {
source
workspaceMemberId
name
}
body
title
createdAt
}
createdAt
view {
__typename
id
type
icon
key
isCompact
kanbanFieldMetadataId
objectMetadataId
position
createdAt
deletedAt
updatedAt
name
}
opportunityId
position
deletedAt
id
companyId
workflow {
__typename
deletedAt
lastPublishedVersionId
createdAt
id
statuses
name
position
updatedAt
}
workspaceMember {
__typename
name {
firstName
lastName
}
avatarUrl
userId
createdAt
timeZone
id
timeFormat
updatedAt
locale
userEmail
deletedAt
colorScheme
dateFormat
}
company {
__typename
updatedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
visaSponsorship
address {
addressStreet1
addressStreet2
@ -422,21 +524,76 @@ export const mocks = [
addressLat
addressLng
}
position
employees
deletedAt
accountOwnerId
annualRecurringRevenue {
amountMicros
currencyCode
}
id
name
updatedAt
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
myCustomField
createdAt
accountOwnerId
createdBy {
source
workspaceMemberId
name
}
workPolicy
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
tagline
idealCustomerProfile
}
person {
${PERSON_FRAGMENT}
rocket {
__typename
createdBy {
source
workspaceMemberId
name
}
updatedAt
name
position
createdAt
id
deletedAt
}
opportunity {
__typename
createdBy {
source
workspaceMemberId
name
}
amount {
amountMicros
currencyCode
}
stage
position
closeDate
id
name
pointOfContactId
companyId
updatedAt
deletedAt
createdAt
}
}
}

View File

@ -8,9 +8,9 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import {
favoriteId,
favoriteTargetObjectRecord,
@ -29,8 +29,6 @@ jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
useFindManyRecords: () => ({ records: initialFavorites }),
}));
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
@ -51,7 +49,7 @@ describe('useFavorites', () => {
setCurrentWorkspaceMember(mockWorkspaceMember);
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFavorites();
},
@ -72,7 +70,7 @@ describe('useFavorites', () => {
setCurrentWorkspaceMember(mockWorkspaceMember);
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFavorites();
},
@ -100,7 +98,7 @@ describe('useFavorites', () => {
setCurrentWorkspaceMember(mockWorkspaceMember);
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFavorites();
},
@ -125,7 +123,7 @@ describe('useFavorites', () => {
setCurrentWorkspaceMember(mockWorkspaceMember);
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFavorites();
},