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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 19152 additions and 27309 deletions

View File

@ -6,7 +6,6 @@ module.exports = {
'mockServiceWorker.js',
'**/generated*/*',
'**/generated/standard-metadata-query-result.ts',
'**/getObjectMetadataItemsMock.ts',
'tsup.config.ts',
'build',
'coverage',

View File

@ -2,13 +2,14 @@
/* tslint:disable */
/**
* Mock Service Worker (2.0.11).
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = 'c5f7f8e188b673ea4e677df7ea3c5a39'
const PACKAGE_VERSION = '2.3.5'
const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
@ -48,7 +49,10 @@ self.addEventListener('message', async function (event) {
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
@ -202,13 +206,6 @@ async function getResponse(event, client, requestId) {
return passthrough()
}
// Bypass requests with the explicit bypass header.
// Such requests can be issued by "ctx.fetch()".
const mswIntention = request.headers.get('x-msw-intention')
if (['bypass', 'passthrough'].includes(mswIntention)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const requestBuffer = await request.arrayBuffer()
const clientMessage = await sendToClient(
@ -240,7 +237,7 @@ async function getResponse(event, client, requestId) {
return respondWithMock(clientMessage.data)
}
case 'MOCK_NOT_FOUND': {
case 'PASSTHROUGH': {
return passthrough()
}
}

View File

@ -1544,6 +1544,20 @@ export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
}>;
export type ActivateWorkflowVersionMutation = { __typename?: 'Mutation', activateWorkflowVersion: boolean };
export type DeactivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
}>;
export type DeactivateWorkflowVersionMutation = { __typename?: 'Mutation', deactivateWorkflowVersion: boolean };
export type DeleteWorkspaceInvitationMutationVariables = Exact<{
appTokenId: Scalars['String'];
}>;
@ -2886,6 +2900,68 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>;
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
export const ActivateWorkflowVersionDocument = gql`
mutation ActivateWorkflowVersion($workflowVersionId: String!) {
activateWorkflowVersion(workflowVersionId: $workflowVersionId)
}
`;
export type ActivateWorkflowVersionMutationFn = Apollo.MutationFunction<ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables>;
/**
* __useActivateWorkflowVersionMutation__
*
* To run a mutation, you first call `useActivateWorkflowVersionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useActivateWorkflowVersionMutation` 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 [activateWorkflowVersionMutation, { data, loading, error }] = useActivateWorkflowVersionMutation({
* variables: {
* workflowVersionId: // value for 'workflowVersionId'
* },
* });
*/
export function useActivateWorkflowVersionMutation(baseOptions?: Apollo.MutationHookOptions<ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables>(ActivateWorkflowVersionDocument, options);
}
export type ActivateWorkflowVersionMutationHookResult = ReturnType<typeof useActivateWorkflowVersionMutation>;
export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult<ActivateWorkflowVersionMutation>;
export type ActivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables>;
export const DeactivateWorkflowVersionDocument = gql`
mutation DeactivateWorkflowVersion($workflowVersionId: String!) {
deactivateWorkflowVersion(workflowVersionId: $workflowVersionId)
}
`;
export type DeactivateWorkflowVersionMutationFn = Apollo.MutationFunction<DeactivateWorkflowVersionMutation, DeactivateWorkflowVersionMutationVariables>;
/**
* __useDeactivateWorkflowVersionMutation__
*
* To run a mutation, you first call `useDeactivateWorkflowVersionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeactivateWorkflowVersionMutation` 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 [deactivateWorkflowVersionMutation, { data, loading, error }] = useDeactivateWorkflowVersionMutation({
* variables: {
* workflowVersionId: // value for 'workflowVersionId'
* },
* });
*/
export function useDeactivateWorkflowVersionMutation(baseOptions?: Apollo.MutationHookOptions<DeactivateWorkflowVersionMutation, DeactivateWorkflowVersionMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeactivateWorkflowVersionMutation, DeactivateWorkflowVersionMutationVariables>(DeactivateWorkflowVersionDocument, options);
}
export type DeactivateWorkflowVersionMutationHookResult = ReturnType<typeof useDeactivateWorkflowVersionMutation>;
export type DeactivateWorkflowVersionMutationResult = Apollo.MutationResult<DeactivateWorkflowVersionMutation>;
export type DeactivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<DeactivateWorkflowVersionMutation, DeactivateWorkflowVersionMutationVariables>;
export const DeleteWorkspaceInvitationDocument = gql`
mutation DeleteWorkspaceInvitation($appTokenId: String!) {
deleteWorkspaceInvitation(appTokenId: $appTokenId)

View File

@ -7,11 +7,10 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const cache = new InMemoryCache();
@ -141,7 +140,7 @@ describe('useActivityTargetObjectRecords', () => {
act(() => {
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
result.current.setObjectMetadataItems(mockObjectMetadataItems);
result.current.setObjectMetadataItems(generatedMockObjectMetadataItems);
});
const activityTargetObjectRecords =

View File

@ -26,13 +26,13 @@ const mocks: MockedResponse[] = [
mutation CreateOneTask($input: TaskCreateInput!) {
createTask(data: $input) {
__typename
status
assigneeId
updatedAt
body
createdAt
dueAt
id
status
body
assigneeId
title
}
}

View File

@ -6,10 +6,10 @@ import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import gql from 'graphql-tag';
import pick from 'lodash.pick';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { mockedTasks } from '~/testing/mock-data/tasks';
const mockedDate = '2024-03-15T12:00:00.000Z';
@ -69,7 +69,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
</RecoilRoot>
);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const mockObjectMetadataItems = generatedMockObjectMetadataItems;
describe('useOpenCreateActivityDrawer', () => {
it('works as expected', async () => {

View File

@ -28,20 +28,21 @@ const mocks: MockedResponse[] = [
mutation UpdateOneTask($idToUpdate: ID!, $input: TaskUpdateInput!) {
updateTask(id: $idToUpdate, data: $input) {
__typename
status
assigneeId
updatedAt
body
createdAt
deletedAt
dueAt
position
id
title
status
body
createdBy {
source
workspaceMemberId
name
}
assigneeId
position
title
}
}
`,
@ -53,7 +54,7 @@ const mocks: MockedResponse[] = [
result: jest.fn(() => ({
data: {
updateTask: {
__typename: 'Activity',
__typename: 'Task',
createdAt: '2024-03-15T07:33:14.212Z',
reminderAt: null,
authorId: '123',

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();
},

View File

@ -4,16 +4,17 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import {
COMPANY_OBJECT_METADATA_ID,
getObjectMetadataItemsMock,
} from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { AppPath } from '@/types/AppPath';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { mockedUserData } from '~/testing/mock-data/users';
jest.mock('@/prefetch/hooks/usePrefetchedData');
const setupMockPrefetchedData = (viewId?: string) => {
const companyObjectMetadata = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
);
jest.mocked(usePrefetchedData).mockReturnValue({
isDataPrefetched: true,
records: viewId
@ -21,7 +22,7 @@ const setupMockPrefetchedData = (viewId?: string) => {
{
id: viewId,
__typename: 'object',
objectMetadataId: COMPANY_OBJECT_METADATA_ID,
objectMetadataId: companyObjectMetadata?.id,
},
]
: [],
@ -36,7 +37,7 @@ const renderHooks = (withCurrentUser: boolean) => {
objectMetadataItemsState,
);
setObjectMetadataItems(getObjectMetadataItemsMock());
setObjectMetadataItems(generatedMockObjectMetadataItems);
if (withCurrentUser) {
setCurrentUser(mockedUserData);

View File

@ -6,8 +6,8 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { WorkspaceActivationStatus } from '~/generated/graphql';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -29,7 +29,7 @@ export const ObjectMetadataItemsLoadEffect = () => {
const toSetObjectMetadataItems =
isUndefinedOrNull(currentUser) ||
currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active
? getObjectMetadataItemsMock()
? generatedMockObjectMetadataItems
: newObjectMetadataItems;
if (
!loading &&

View File

@ -3,7 +3,7 @@ import { Nullable } from 'twenty-ui';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
describe('useColumnDefinitionsFromFieldMetadata', () => {
it('should return empty definitions if no object is passed', () => {
@ -22,22 +22,24 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
});
it('should return expected definitions', () => {
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const companyObjectMetadata = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
);
const { result } = renderHook(
(objectMetadataItem?: Nullable<ObjectMetadataItem>) => {
return useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
},
{
initialProps: mockObjectMetadataItems[1],
initialProps: companyObjectMetadata,
},
);
const { columnDefinitions, filterDefinitions, sortDefinitions } =
result.current;
expect(columnDefinitions.length).toBe(5);
expect(filterDefinitions.length).toBe(4);
expect(sortDefinitions.length).toBe(4);
expect(columnDefinitions.length).toBe(21);
expect(filterDefinitions.length).toBe(14);
expect(sortDefinitions.length).toBe(14);
});
});

View File

@ -10,7 +10,7 @@ import {
} from '@/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const mocks = [
{
@ -34,14 +34,12 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
</RecoilRoot>
);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
describe('useFilteredObjectMetadataItems', () => {
it('should findActiveObjectMetadataItemBySlug', async () => {
const { result } = renderHook(
() => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFilteredObjectMetadataItems();
},
@ -61,7 +59,7 @@ describe('useFilteredObjectMetadataItems', () => {
const { result } = renderHook(
() => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFilteredObjectMetadataItems();
},
@ -78,10 +76,14 @@ describe('useFilteredObjectMetadataItems', () => {
});
it('should findObjectMetadataItemById', async () => {
const peopleObjectMetadata = generatedMockObjectMetadataItems.find(
(item) => item.namePlural === 'people',
);
const { result } = renderHook(
() => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFilteredObjectMetadataItems();
},
@ -92,7 +94,7 @@ describe('useFilteredObjectMetadataItems', () => {
act(() => {
const res = result.current.findObjectMetadataItemById(
'ff2881da-89f6-4f15-8f0a-e3f355ea3b94',
peopleObjectMetadata?.id,
);
expect(res).toBeDefined();
expect(res?.namePlural).toBe('people');
@ -103,7 +105,7 @@ describe('useFilteredObjectMetadataItems', () => {
const { result } = renderHook(
() => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFilteredObjectMetadataItems();
},

View File

@ -3,9 +3,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
const mockObjectMetadataItems = getObjectMetadataItemsMock();
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
describe('useGetObjectRecordIdentifierByNameSingular', () => {
it('should work as expected', async () => {
@ -19,7 +17,7 @@ describe('useGetObjectRecordIdentifierByNameSingular', () => {
}) => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useGetObjectRecordIdentifierByNameSingular()(
record,

View File

@ -5,7 +5,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
@ -15,8 +15,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
describe('useGetRelationMetadata', () => {
it('should return correct properties', async () => {
const objectMetadataItems = getObjectMetadataItemsMock();
const objectMetadata = objectMetadataItems.find(
const objectMetadata = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const fieldMetadataItem = objectMetadata.fields.find(
@ -28,7 +27,7 @@ describe('useGetRelationMetadata', () => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
useEffect(() => {
setMetadataItems(objectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
}, [setMetadataItems]);
return useGetRelationMetadata();
@ -45,9 +44,10 @@ describe('useGetRelationMetadata', () => {
relationType,
} = result.current({ fieldMetadataItem }) ?? {};
const expectedRelationObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === 'opportunity',
);
const expectedRelationObjectMetadataItem =
generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'opportunity',
);
const expectedRelationFieldMetadataItem =
expectedRelationObjectMetadataItem?.fields.find(
(field) => field.name === 'pointOfContact',

View File

@ -4,6 +4,7 @@ import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
@ -13,6 +14,9 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
// Split into tests for each new hook
describe('useObjectMetadataItem', () => {
const opportunityObjectMetadata = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'opportunity',
);
it('should return correct properties', async () => {
const { result } = renderHook(
() => useObjectMetadataItem({ objectNameSingular: 'opportunity' }),
@ -23,6 +27,6 @@ describe('useObjectMetadataItem', () => {
const { objectMetadataItem } = result.current;
expect(objectMetadataItem.id).toBe('b95b3f38-9fc2-4d7e-a823-7791cf13d089');
expect(objectMetadataItem.id).toBe(opportunityObjectMetadata?.id);
});
});

View File

@ -4,10 +4,10 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { isDefined } from '~/utils/isDefined';
import { WorkspaceActivationStatus } from '~/generated/graphql';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
export const useObjectMetadataItem = ({
@ -15,9 +15,6 @@ export const useObjectMetadataItem = ({
}: ObjectMetadataItemIdentifier) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
// Todo: deprecate this logic as mocked objectMetadataItems are load in ObjectMetadataItemsLoadEffect anyway
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
objectMetadataItemFamilySelector({
objectName: objectNameSingular,
@ -29,11 +26,11 @@ export const useObjectMetadataItem = ({
if (currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active) {
objectMetadataItem =
mockObjectMetadataItems.find(
generatedMockObjectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
) ?? null;
objectMetadataItems = mockObjectMetadataItems;
objectMetadataItems = generatedMockObjectMetadataItems;
}
if (!isDefined(objectMetadataItem)) {

View File

@ -2,8 +2,8 @@ import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { WorkspaceActivationStatus } from '~/generated/graphql';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { isDefined } from '~/utils/isDefined';
export const useObjectNamePluralFromSingular = ({
@ -12,7 +12,6 @@ export const useObjectNamePluralFromSingular = ({
objectNameSingular: string;
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
objectMetadataItemFamilySelector({
@ -23,7 +22,7 @@ export const useObjectNamePluralFromSingular = ({
if (currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active) {
objectMetadataItem =
mockObjectMetadataItems.find(
generatedMockObjectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
) ?? null;

View File

@ -2,8 +2,8 @@ import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { WorkspaceActivationStatus } from '~/generated/graphql';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { isDefined } from '~/utils/isDefined';
export const useObjectNameSingularFromPlural = ({
@ -13,8 +13,6 @@ export const useObjectNameSingularFromPlural = ({
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
objectMetadataItemFamilySelector({
objectName: objectNamePlural,
@ -24,7 +22,7 @@ export const useObjectNameSingularFromPlural = ({
if (currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active) {
objectMetadataItem =
mockObjectMetadataItems.find(
generatedMockObjectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.namePlural === objectNamePlural,
) ?? null;

View File

@ -1,14 +1,12 @@
import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
const mockObjectMetadataItems = getObjectMetadataItemsMock();
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
describe('getObjectMetadataItemBySingularName', () => {
it('should work as expected', () => {
const firstObjectMetadataItem = mockObjectMetadataItems[0];
const firstObjectMetadataItem = generatedMockObjectMetadataItems[0];
const foundObjectMetadataItem = getObjectMetadataItemByNameSingular({
objectMetadataItems: mockObjectMetadataItems,
objectMetadataItems: generatedMockObjectMetadataItems,
objectNameSingular: firstObjectMetadataItem.nameSingular,
});

View File

@ -1,11 +1,9 @@
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { getOrderByFieldForObjectMetadataItem } from '@/object-metadata/utils/getObjectOrderByField';
const mockObjectMetadataItems = getObjectMetadataItemsMock();
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
describe('getObjectOrderByField', () => {
it('should work as expected', () => {
const objectMetadataItem = mockObjectMetadataItems.find(
const objectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const res = getOrderByFieldForObjectMetadataItem(objectMetadataItem);

View File

@ -1,11 +1,9 @@
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
const mockObjectMetadataItems = getObjectMetadataItemsMock();
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
describe('getObjectSlug', () => {
it('should work as expected', () => {
const objectMetadataItem = mockObjectMetadataItems.find(
const objectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;

View File

@ -1,11 +1,9 @@
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
const mockObjectMetadataItems = getObjectMetadataItemsMock();
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
describe('isObjectMetadataAvailableForRelation', () => {
it('should work as expected', () => {
const objectMetadataItem = mockObjectMetadataItems.find(
const objectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;

View File

@ -1,10 +1,8 @@
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { normalizeGQLField } from '~/utils/normalizeGQLField';
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const personObjectMetadataItem = mockObjectMetadataItems.find(
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
@ -15,7 +13,7 @@ if (!personObjectMetadataItem) {
describe('mapFieldMetadataToGraphQLQuery', () => {
it('should return fieldName if simpleValue', async () => {
const res = mapFieldMetadataToGraphQLQuery({
objectMetadataItems: mockObjectMetadataItems,
objectMetadataItems: generatedMockObjectMetadataItems,
field: personObjectMetadataItem.fields.find(
(field) => field.name === 'id',
)!,
@ -24,7 +22,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
});
it('should return fieldName if composite', async () => {
const res = mapFieldMetadataToGraphQLQuery({
objectMetadataItems: mockObjectMetadataItems,
objectMetadataItems: generatedMockObjectMetadataItems,
field: personObjectMetadataItem.fields.find(
(field) => field.name === 'name',
)!,
@ -40,7 +38,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
it('should return non relation subFields if relation', async () => {
const res = mapFieldMetadataToGraphQLQuery({
objectMetadataItems: mockObjectMetadataItems,
objectMetadataItems: generatedMockObjectMetadataItems,
field: personObjectMetadataItem.fields.find(
(field) => field.name === 'company',
)!,
@ -96,7 +94,7 @@ idealCustomerProfile
it('should return only return relation subFields that are in recordGqlFields', async () => {
const res = mapFieldMetadataToGraphQLQuery({
objectMetadataItems: mockObjectMetadataItems,
objectMetadataItems: generatedMockObjectMetadataItems,
relationrecordFields: {
accountOwner: { id: true, name: true },
people: true,

View File

@ -1,10 +1,8 @@
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { normalizeGQLQuery } from '~/utils/normalizeGQLQuery';
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const personObjectMetadataItem = mockObjectMetadataItems.find(
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
@ -15,7 +13,7 @@ if (!personObjectMetadataItem) {
describe('mapObjectMetadataToGraphQLQuery', () => {
it('should query only specified recordGqlFields', async () => {
const res = mapObjectMetadataToGraphQLQuery({
objectMetadataItems: mockObjectMetadataItems,
objectMetadataItems: generatedMockObjectMetadataItems,
objectMetadataItem: personObjectMetadataItem,
recordGqlFields: {
company: true,
@ -122,7 +120,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
it('should load only specified operation fields nested', async () => {
const res = mapObjectMetadataToGraphQLQuery({
objectMetadataItems: mockObjectMetadataItems,
objectMetadataItems: generatedMockObjectMetadataItems,
objectMetadataItem: personObjectMetadataItem,
recordGqlFields: { company: { id: true }, id: true, name: true },
});

View File

@ -1,45 +1,48 @@
export const PERSON_FRAGMENT = `
__typename
updatedAt
myCustomObjectId
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
name {
firstName
lastName
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
email
position
createdBy {
source
workspaceMemberId
name
}
avatarUrl
deletedAt
createdAt
updatedAt
jobTitle
intro
workPrefereance
performanceRating
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
performanceRating
createdAt
phone {
city
companyId
phones {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
createdBy {
source
workspaceMemberId
name
}
id
city
companyId
intro
workPrefereance
position
emails {
primaryEmail
additionalEmails
}
avatarUrl
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
`

View File

@ -18,7 +18,6 @@ const basePerson = {
},
createdAt: '',
city: '',
email: '',
jobTitle: '',
name: {
firstName: '',

View File

@ -4,7 +4,6 @@ import { ReactNode, useEffect } from 'react';
import { RecoilRoot, useRecoilState } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import {
mockPageSize,
peopleMockWithIdsOnly,
@ -18,6 +17,7 @@ import {
} from '@/object-record/hooks/__mocks__/useFetchAllRecordIds';
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const mocks = [
{
@ -75,7 +75,7 @@ describe('useFetchAllRecordIds', () => {
);
useEffect(() => {
setObjectMetadataItems(getObjectMetadataItemsMock());
setObjectMetadataItems(generatedMockObjectMetadataItems);
}, [setObjectMetadataItems]);
return useFetchAllRecordIds({

View File

@ -5,7 +5,6 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import {
query,
responseData,
@ -13,6 +12,7 @@ import {
} from '@/object-record/hooks/__mocks__/useFindManyRecords';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const mocks = [
{
@ -65,11 +65,9 @@ describe('useFindManyRecords', () => {
locale: 'en',
});
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFindManyRecords({
objectNameSingular: 'person',

View File

@ -1,11 +1,11 @@
import { ReactNode } from 'react';
import { expect } from '@storybook/test';
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
@ -18,7 +18,7 @@ describe('useGenerateFindManyRecordsForMultipleMetadataItemsQuery', () => {
const { result } = renderHook(
() => {
return useGenerateCombinedFindManyRecordsQuery({
operationSignatures: getObjectMetadataItemsMock()
operationSignatures: generatedMockObjectMetadataItems
.slice(0, 2)
.map((item) => ({
objectNameSingular: item.nameSingular,

View File

@ -72,11 +72,11 @@ export const linkFieldDefinition: FieldDefinition<FieldLinkMetadata> = {
},
};
const phoneFieldMetadataItem = mockedPersonObjectMetadataItem.fields?.find(
({ name }) => name === 'phone',
const phonesFieldMetadataItem = mockedPersonObjectMetadataItem.fields?.find(
({ name }) => name === 'phones',
);
export const phoneFieldDefinition = formatFieldMetadataItemAsFieldDefinition({
field: phoneFieldMetadataItem!,
export const phonesFieldDefinition = formatFieldMetadataItemAsFieldDefinition({
field: phonesFieldMetadataItem!,
objectMetadataItem: mockedPersonObjectMetadataItem,
});

View File

@ -1,10 +1,10 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { IconPencil } from 'twenty-ui';
import {
phoneFieldDefinition,
phonesFieldDefinition,
relationFieldDefinition,
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -29,7 +29,7 @@ const getWrapper =
</FieldContext.Provider>
);
const PhoneWrapper = getWrapper(phoneFieldDefinition);
const PhoneWrapper = getWrapper(phonesFieldDefinition);
const RelationWrapper = getWrapper(relationFieldDefinition);
describe('useGetButtonIcon', () => {

View File

@ -1,8 +1,8 @@
import { ReactNode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { phoneFieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { phonesFieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@ -12,7 +12,7 @@ const recordId = 'recordId';
const Wrapper = ({ children }: { children: ReactNode }) => (
<FieldContext.Provider
value={{
fieldDefinition: phoneFieldDefinition,
fieldDefinition: phonesFieldDefinition,
recordId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,

View File

@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import {
phoneFieldDefinition,
phonesFieldDefinition,
ratingFieldDefinition,
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -29,7 +29,7 @@ const getWrapper =
);
const RatingWrapper = getWrapper(ratingFieldDefinition);
const PhoneWrapper = getWrapper(phoneFieldDefinition);
const PhoneWrapper = getWrapper(phonesFieldDefinition);
describe('useIsFieldInputOnly', () => {
it('should return true', () => {

View File

@ -4,7 +4,7 @@ import { RecoilRoot } from 'recoil';
import {
actorFieldDefinition,
phoneFieldDefinition,
phonesFieldDefinition,
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly';
@ -29,7 +29,7 @@ const getWrapper =
);
const ActorWrapper = getWrapper(actorFieldDefinition);
const PhoneWrapper = getWrapper(phoneFieldDefinition);
const PhoneWrapper = getWrapper(phonesFieldDefinition);
describe('useIsFieldReadOnly', () => {
it('should return true', () => {

View File

@ -8,7 +8,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import {
phoneFieldDefinition,
phonesFieldDefinition,
relationFieldDefinition,
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
import {
@ -33,7 +33,16 @@ const mocks: MockedResponse[] = [
{
request: {
query,
variables: { idToUpdate: 'recordId', input: { phone: '+1 123 456' } },
variables: {
idToUpdate: 'recordId',
input: {
phones: {
primaryPhoneNumber: '123 456',
primaryPhoneCountryCode: '+1',
additionalPhones: [],
},
},
},
},
result: jest.fn(() => ({
data: {
@ -98,7 +107,7 @@ const getWrapper =
);
};
const PhoneWrapper = getWrapper(phoneFieldDefinition);
const PhoneWrapper = getWrapper(phonesFieldDefinition);
const RelationWrapper = getWrapper(relationFieldDefinition);
describe('usePersistField', () => {
@ -118,7 +127,11 @@ describe('usePersistField', () => {
);
act(() => {
result.current.persistField('+1 123 456');
result.current.persistField({
primaryPhoneNumber: '123 456',
primaryPhoneCountryCode: '+1',
additionalPhones: [],
});
});
await waitFor(() => {

View File

@ -26,35 +26,13 @@ const mocks: MockedResponse[] = [
) {
updateCompany(id: $idToUpdate, data: $input) {
__typename
id
visaSponsorship
createdBy {
source
workspaceMemberId
name
}
updatedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
position
annualRecurringRevenue {
amountMicros
currencyCode
}
employees
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
workPolicy
visaSponsorship
address {
addressStreet1
addressStreet2
@ -65,16 +43,38 @@ const mocks: MockedResponse[] = [
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
}

View File

@ -12,6 +12,7 @@ import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import gql from 'graphql-tag';
import { BrowserRouter as Router } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const defaultResponseData = {
pageInfo: {
@ -65,7 +66,7 @@ const mockPerson = {
city: 'city',
companyId: '1',
intro: 'intro',
workPrefereance: 'workPrefereance',
workPreference: 'workPrefereance',
};
const mocks: MockedResponse[] = [
{
@ -86,48 +87,51 @@ const mocks: MockedResponse[] = [
edges {
node {
__typename
updatedAt
myCustomObjectId
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
name {
firstName
lastName
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
email
position
createdBy {
source
workspaceMemberId
name
}
avatarUrl
deletedAt
createdAt
updatedAt
jobTitle
intro
workPrefereance
performanceRating
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
performanceRating
createdAt
phone {
city
companyId
phones {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
createdBy {
source
workspaceMemberId
name
}
id
city
companyId
intro
workPrefereance
position
emails {
primaryEmail
additionalEmails
}
avatarUrl
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
}
cursor
}
@ -292,9 +296,17 @@ describe('useTableData', () => {
},
);
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
const updatedAtFieldMetadataItem = personObjectMetadataItem?.fields.find(
(field) => field.name === 'updatedAt',
);
await act(async () => {
result.current.setKanbanFieldName.setKanbanFieldMetadataName(
result.current.kanbanData.hiddenBoardFields[0].metadata.fieldName,
updatedAtFieldMetadataItem?.name,
);
});
@ -309,7 +321,7 @@ describe('useTableData', () => {
{
defaultValue: 'now',
editButtonIcon: undefined,
fieldMetadataId: '102963b7-3e77-4293-a1e6-1ab59a02b663',
fieldMetadataId: updatedAtFieldMetadataItem?.id,
iconName: 'IconCalendarClock',
isFilterable: true,
isLabelIdentifier: false,
@ -329,7 +341,7 @@ describe('useTableData', () => {
relationType: undefined,
targetFieldMetadataName: '',
},
position: 0,
position: 7,
showLabel: undefined,
size: 100,
type: 'DATE_TIME',

View File

@ -5,7 +5,7 @@ import { ComponentDecorator } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import {
RecordFieldValueSelectorContextProvider,
@ -21,10 +21,9 @@ import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorato
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { mockPerformance } from './mock';
const objectMetadataItems = getObjectMetadataItemsMock();
const RelationFieldValueSetterEffect = () => {
const setEntity = useSetRecoilState(
recordStoreFamilyState(mockPerformance.recordId),
@ -48,7 +47,7 @@ const RelationFieldValueSetterEffect = () => {
mockPerformance.relationFieldValue,
);
setObjectMetadataItems(objectMetadataItems);
setObjectMetadataItems(generatedMockObjectMetadataItems);
}, [setEntity, setRelationEntity, setRecordValue, setObjectMetadataItems]);
return null;

View File

@ -5,12 +5,12 @@ import { createState } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const draftValue = 'updated Name';
@ -55,8 +55,6 @@ const updateOneRecordMock = jest.fn();
createOneRecord: createOneRecordMock,
});
const objectMetadataItems = getObjectMetadataItemsMock();
const Wrapper = ({
children,
pendingRecordIdMockedValue,
@ -68,7 +66,7 @@ const Wrapper = ({
}) => (
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(objectMetadataItemsState, objectMetadataItems);
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue);
snapshot.set(draftValueState, draftValueMockedValue);
}}

View File

@ -2,9 +2,9 @@ import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
const scopeId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
@ -13,8 +13,6 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
</RelationPickerScopeInternalContext.Provider>
);
const objectMetadataItemsMock = getObjectMetadataItemsMock();
const opportunityId = 'cb702502-4b1d-488e-9461-df3fb096ebf6';
const personId = 'ab091fd9-1b81-4dfd-bfdb-564ffee032a2';
@ -70,7 +68,7 @@ describe('useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'
},
);
act(() => {
result.current.setObjectMetadata(objectMetadataItemsMock);
result.current.setObjectMetadata(generatedMockObjectMetadataItems);
});
expect(

View File

@ -26,35 +26,13 @@ const companyMocks = [
) {
createCompanies(data: $data, upsert: $upsert) {
__typename
id
visaSponsorship
createdBy {
source
workspaceMemberId
name
}
updatedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
position
annualRecurringRevenue {
amountMicros
currencyCode
}
employees
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
workPolicy
visaSponsorship
address {
addressStreet1
addressStreet2
@ -65,16 +43,38 @@ const companyMocks = [
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
}

View File

@ -1,14 +1,14 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import {
query,
responseData,
@ -75,11 +75,9 @@ describe('useFilteredSearchEntityQuery', () => {
locale: 'en',
});
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
setMetadataItems(generatedMockObjectMetadataItems);
return useFilteredSearchEntityQuery({
orderByField: 'name',

View File

@ -1,4 +1,3 @@
import { COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
@ -45,7 +44,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
},
{
position: 2,
fieldMetadataId: COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID,
fieldMetadataId: 'REPLACE_ME',
label: 'Name',
size: 100,
type: FieldMetadataType.Text,

View File

@ -1,4 +1,3 @@
import { COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
export const SIGN_IN_BACKGROUND_MOCK_FILTER_DEFINITIONS = [
@ -15,7 +14,7 @@ export const SIGN_IN_BACKGROUND_MOCK_FILTER_DEFINITIONS = [
type: 'NUMBER',
},
{
fieldMetadataId: COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID,
fieldMetadataId: 'REPLACE_ME',
label: 'Name',
iconName: 'IconBuildingSkyscraper',
type: 'TEXT',

View File

@ -1,4 +1,3 @@
import { COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
export const SIGN_IN_BACKGROUND_MOCK_SORT_DEFINITIONS = [
@ -13,7 +12,7 @@ export const SIGN_IN_BACKGROUND_MOCK_SORT_DEFINITIONS = [
iconName: 'IconUsers',
},
{
fieldMetadataId: COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID,
fieldMetadataId: 'REPLACE_ME',
label: 'Name',
iconName: 'IconBuildingSkyscraper',
},

View File

@ -1,4 +1,3 @@
import { COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { ViewField } from '@/views/types/ViewField';
export const SIGN_IN_BACKGROUND_MOCK_VIEW_FIELDS = [
@ -60,7 +59,7 @@ export const SIGN_IN_BACKGROUND_MOCK_VIEW_FIELDS = [
{
__typename: 'ViewField',
id: 'cafacdc8-cbfc-4545-8242-94787f144ace',
fieldMetadataId: COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID,
fieldMetadataId: 'REPLACE_ME',
size: 180,
createdAt: '2023-11-23T15:38:03.706Z',
viewId: '20202020-2441-4424-8163-4002c523d415',

View File

@ -0,0 +1,112 @@
// Generate test for getCombinedViewFilters
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getCombinedViewFilters } from '../getCombinedViewFilters';
describe('getCombinedViewFilters', () => {
it('should return expected combined view filters when additional filters are present', () => {
const viewFilters: ViewFilter[] = [
{
__typename: 'ViewFilter',
id: 'id',
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
value: 'testValue',
displayValue: 'Test Display Value',
operand: ViewFilterOperand.Is,
},
];
const toUpsertViewFilters: ViewFilter[] = [
{
__typename: 'ViewFilter',
id: 'id',
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
value: 'testValue',
displayValue: 'Test Display Value',
operand: ViewFilterOperand.Is,
},
];
const toDeleteViewFilterIds: string[] = [];
expect(
getCombinedViewFilters(
viewFilters,
toUpsertViewFilters,
toDeleteViewFilterIds,
),
).toEqual([
{
__typename: 'ViewFilter',
id: 'id',
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
value: 'testValue',
displayValue: 'Test Display Value',
operand: ViewFilterOperand.Is,
},
]);
});
it('should return expected combined view filters when additional filters are not present', () => {
const viewFilters: ViewFilter[] = [
{
__typename: 'ViewFilter',
id: 'id',
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
value: 'testValue',
displayValue: 'Test Display Value',
operand: ViewFilterOperand.Is,
},
];
const toUpsertViewFilters: ViewFilter[] = [];
const toDeleteViewFilterIds: string[] = [];
expect(
getCombinedViewFilters(
viewFilters,
toUpsertViewFilters,
toDeleteViewFilterIds,
),
).toEqual([
{
__typename: 'ViewFilter',
id: 'id',
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
value: 'testValue',
displayValue: 'Test Display Value',
operand: ViewFilterOperand.Is,
},
]);
});
it('should return expected combined view filters when additional filters are present and some filters are to be deleted', () => {
const viewFilters: ViewFilter[] = [
{
__typename: 'ViewFilter',
id: 'id',
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
value: 'testValue',
displayValue: 'Test Display Value',
operand: ViewFilterOperand.Is,
},
];
const toUpsertViewFilters: ViewFilter[] = [
{
__typename: 'ViewFilter',
id: 'id',
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
value: 'testValue',
displayValue: 'Test Display Value',
operand: ViewFilterOperand.Is,
},
];
const toDeleteViewFilterIds: string[] = ['id'];
expect(
getCombinedViewFilters(
viewFilters,
toUpsertViewFilters,
toDeleteViewFilterIds,
),
).toEqual([]);
});
});

View File

@ -0,0 +1,96 @@
import { Button } from '@/ui/input/button/components/Button';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import {
IconPlayerPlay,
IconPlayerStop,
IconPower,
IconTrash,
isDefined,
} from 'twenty-ui';
import { assertWorkflowWithCurrentVersionIsDefined } from '../utils/assertWorkflowWithCurrentVersionIsDefined';
export const RecordShowPageWorkflowHeader = ({
workflowId,
}: {
workflowId: string | undefined;
}) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const isWaitingForWorkflowWithCurrentVersion =
!isDefined(workflowWithCurrentVersion) ||
!isDefined(workflowWithCurrentVersion.currentVersion);
const { activateWorkflowVersion } = useActivateWorkflowVersion();
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
return (
<>
<Button
title="Test"
variant="secondary"
Icon={IconPlayerPlay}
disabled={isWaitingForWorkflowWithCurrentVersion}
onClick={() => {}}
/>
{workflowWithCurrentVersion?.currentVersion?.status === 'DRAFT' &&
workflowWithCurrentVersion.versions?.length > 1 ? (
<Button
title="Discard Draft"
variant="secondary"
Icon={IconTrash}
disabled={isWaitingForWorkflowWithCurrentVersion}
onClick={() => {
assertWorkflowWithCurrentVersionIsDefined(
workflowWithCurrentVersion,
);
return deleteOneWorkflowVersion({
workflowId: workflowWithCurrentVersion.id,
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
});
}}
/>
) : null}
{workflowWithCurrentVersion?.currentVersion?.status === 'DRAFT' ||
workflowWithCurrentVersion?.currentVersion?.status === 'DEACTIVATED' ? (
<Button
title="Activate"
variant="secondary"
Icon={IconPower}
disabled={isWaitingForWorkflowWithCurrentVersion}
onClick={() => {
assertWorkflowWithCurrentVersionIsDefined(
workflowWithCurrentVersion,
);
return activateWorkflowVersion(
workflowWithCurrentVersion.currentVersion.id,
);
}}
/>
) : workflowWithCurrentVersion?.currentVersion?.status === 'ACTIVE' ? (
<Button
title="Deactivate"
variant="secondary"
Icon={IconPlayerStop}
disabled={isWaitingForWorkflowWithCurrentVersion}
onClick={() => {
assertWorkflowWithCurrentVersionIsDefined(
workflowWithCurrentVersion,
);
return deactivateWorkflowVersion(
workflowWithCurrentVersion.currentVersion.id,
);
}}
/>
) : null}
</>
);
};

View File

@ -0,0 +1,666 @@
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 { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<typeof RecordShowPageWorkflowHeader> = {
title: 'Modules/Workflow/RecordShowPageWorkflowHeader',
component: RecordShowPageWorkflowHeader,
decorators: [
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
],
parameters: {
container: { width: 728 },
},
};
export default meta;
type Story = StoryObj<typeof RecordShowPageWorkflowHeader>;
const blankInitialVersionWorkflowId = '78fd5184-08f4-47b7-bb60-adb541608f65';
export const BlankInitialVersion: Story = {
args: {
workflowId: blankInitialVersionWorkflowId,
},
parameters: {
msw: {
handlers: [
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: blankInitialVersionWorkflowId,
},
},
],
},
},
});
}),
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,
},
},
],
},
},
},
});
}),
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: 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('FindManyWorkflows', () => {
return HttpResponse.json({
data: {
workflows: {
__typename: 'WorkflowConnection',
totalCount: 1,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'eyJwb3NpdGlvbiI6LTEsImlkIjoiN2JlM2E4MmMtNDRiNy00MTUwLWEyZTgtNDA4ODcxNDZmNGQ0In0=',
endCursor:
'eyJwb3NpdGlvbiI6LTEsImlkIjoiN2JlM2E4MmMtNDRiNy00MTUwLWEyZTgtNDA4ODcxNDZmNGQ0In0=',
},
edges: [
{
__typename: 'WorkflowEdge',
cursor:
'eyJwb3NpdGlvbiI6LTEsImlkIjoiN2JlM2E4MmMtNDRiNy00MTUwLWEyZTgtNDA4ODcxNDZmNGQ0In0=',
node: {
__typename: 'Workflow',
id: activeVersionWorkflowId,
},
},
],
},
},
});
}),
graphql.query('FindOneWorkflow', () => {
return HttpResponse.json({
data: {
workflow: {
__typename: 'Workflow',
name: 'test qqqq',
lastPublishedVersionId: 'b57e577a-ae55-4de2-ba08-fe361dcc1a57',
id: activeVersionWorkflowId,
deletedAt: null,
statuses: null,
createdAt: '2024-09-20T10:18:59.977Z',
updatedAt: '2024-09-20T16:59:37.212Z',
position: -1,
runs: {
__typename: 'WorkflowRunConnection',
edges: [],
},
favorites: {
__typename: 'FavoriteConnection',
edges: [],
},
eventListeners: {
__typename: 'WorkflowEventListenerConnection',
edges: [],
},
versions: {
__typename: 'WorkflowVersionConnection',
edges: [
{
__typename: 'WorkflowVersionEdge',
node: {
__typename: 'WorkflowVersion',
updatedAt: '2024-09-20T16:59:37.212Z',
status: 'ARCHIVED',
deletedAt: null,
steps: [
{
id: '93c41c1d-eff3-4c91-ac61-f56cc1a0df8a',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
],
workflowId: activeVersionWorkflowId,
trigger: {
type: 'DATABASE_EVENT',
settings: {
eventName: 'note.created',
},
},
name: 'v1',
id: '394cd0b5-bd48-41d7-a110-a92cafaf171d',
createdAt: '2024-09-20T10:19:00.141Z',
},
},
{
__typename: 'WorkflowVersionEdge',
node: {
__typename: 'WorkflowVersion',
updatedAt: '2024-09-20T17:01:15.637Z',
status: 'DRAFT',
deletedAt: null,
steps: [
{
id: '93c41c1d-eff3-4c91-ac61-f56cc1a0df8a',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
{
id: '4177d57d-35dc-4eb1-a467-07e25cb31da0',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
{
id: '0cc392d9-5f28-4d92-90a0-08180f264e68',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
],
workflowId: activeVersionWorkflowId,
trigger: {
type: 'DATABASE_EVENT',
settings: {
eventName: 'note.created',
},
},
name: 'v3',
id: '5eae34ef-9d62-4a9e-b827-3eb927481728',
createdAt: '2024-09-20T17:01:15.637Z',
},
},
{
__typename: 'WorkflowVersionEdge',
node: {
__typename: 'WorkflowVersion',
updatedAt: '2024-09-20T17:00:16.097Z',
status: 'ACTIVE',
deletedAt: null,
steps: [
{
id: '93c41c1d-eff3-4c91-ac61-f56cc1a0df8a',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
{
id: '4177d57d-35dc-4eb1-a467-07e25cb31da0',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
],
workflowId: activeVersionWorkflowId,
trigger: {
type: 'DATABASE_EVENT',
settings: {
eventName: 'note.created',
},
},
name: 'v2',
id: 'b57e577a-ae55-4de2-ba08-fe361dcc1a57',
createdAt: '2024-09-20T16:59:35.755Z',
},
},
],
},
},
},
});
}),
graphql.query('FindManyWorkflowVersions', () => {
return HttpResponse.json({
data: {
workflowVersions: {
__typename: 'WorkflowVersionConnection',
totalCount: 3,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: true,
hasPreviousPage: false,
startCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTIwVDE3OjAxOjE1LjYzN1oiLCJpZCI6IjVlYWUzNGVmLTlkNjItNGE5ZS1iODI3LTNlYjkyNzQ4MTcyOCJ9',
endCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTIwVDE3OjAxOjE1LjYzN1oiLCJpZCI6IjVlYWUzNGVmLTlkNjItNGE5ZS1iODI3LTNlYjkyNzQ4MTcyOCJ9',
},
edges: [
{
__typename: 'WorkflowVersionEdge',
cursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTIwVDE3OjAxOjE1LjYzN1oiLCJpZCI6IjVlYWUzNGVmLTlkNjItNGE5ZS1iODI3LTNlYjkyNzQ4MTcyOCJ9',
node: {
__typename: 'WorkflowVersion',
updatedAt: '2024-09-20T17:01:15.637Z',
status: 'ACTIVE',
deletedAt: null,
steps: [
{
id: '93c41c1d-eff3-4c91-ac61-f56cc1a0df8a',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
{
id: '4177d57d-35dc-4eb1-a467-07e25cb31da0',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
{
id: '0cc392d9-5f28-4d92-90a0-08180f264e68',
name: 'Code',
type: 'CODE',
valid: false,
settings: {
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
serverlessFunctionId: '',
},
},
],
workflowId: activeVersionWorkflowId,
trigger: {
type: 'DATABASE_EVENT',
settings: {
eventName: 'note.created',
},
},
name: 'v3',
id: '5eae34ef-9d62-4a9e-b827-3eb927481728',
createdAt: '2024-09-20T17:01:15.637Z',
},
},
],
},
},
});
}),
...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('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: draftVersionWithPreviousActiveVersionWorkflowId,
},
},
],
},
},
});
}),
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,
},
},
],
},
},
},
});
}),
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: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();
},
};

View File

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const ACTIVATE_WORKFLOW_VERSION = gql`
mutation ActivateWorkflowVersion($workflowVersionId: String!) {
activateWorkflowVersion(workflowVersionId: $workflowVersionId)
}
`;

View File

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DEACTIVATE_WORKFLOW_VERSION = gql`
mutation DeactivateWorkflowVersion($workflowVersionId: String!) {
deactivateWorkflowVersion(workflowVersionId: $workflowVersionId)
}
`;

View File

@ -0,0 +1,45 @@
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { ApolloClient, useApolloClient, useMutation } from '@apollo/client';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
import { ACTIVATE_WORKFLOW_VERSION } from '@/workflow/graphql/activateWorkflowVersion';
import {
ActivateWorkflowVersionMutation,
ActivateWorkflowVersionMutationVariables,
} from '~/generated/graphql';
export const useActivateWorkflowVersion = () => {
const apolloClient = useApolloClient();
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
ActivateWorkflowVersionMutation,
ActivateWorkflowVersionMutationVariables
>(ACTIVATE_WORKFLOW_VERSION, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const { findOneRecordQuery: findOneWorkflowVersionQuery } =
useFindOneRecordQuery({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const activateWorkflowVersion = async (workflowVersionId: string) => {
await mutate({
variables: {
workflowVersionId,
},
});
await apolloClient.query({
query: findOneWorkflowVersionQuery,
variables: {
objectRecordId: workflowVersionId,
},
fetchPolicy: 'network-only',
});
};
return { activateWorkflowVersion };
};

View File

@ -0,0 +1,30 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { WorkflowVersion } from '@/workflow/types/Workflow';
export const useCreateNewWorkflowVersion = ({
workflowId,
}: {
workflowId: string;
}) => {
const { createOneRecord: createOneWorkflowVersion } =
useCreateOneRecord<WorkflowVersion>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const createNewWorkflowVersion = (
workflowVersionData: Pick<
WorkflowVersion,
'name' | 'status' | 'trigger' | 'steps'
>,
) => {
return createOneWorkflowVersion({
workflowId,
...workflowVersionData,
});
};
return {
createNewWorkflowVersion,
};
};

View File

@ -1,5 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState';
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
import {
@ -31,7 +32,11 @@ export const useCreateStep = ({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const insertNodeAndSave = ({
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
workflowId: workflow.id,
});
const insertNodeAndSave = async ({
parentNodeId,
nodeToAdd,
}: {
@ -43,15 +48,28 @@ export const useCreateStep = ({
throw new Error("Can't add a node when there is no current version.");
}
return updateOneWorkflowVersion({
idToUpdate: currentVersion.id,
updateOneRecordInput: {
steps: insertStep({
steps: currentVersion.steps ?? [],
parentStepId: parentNodeId,
stepToAdd: nodeToAdd,
}),
},
const updatedSteps = insertStep({
steps: currentVersion.steps ?? [],
parentStepId: parentNodeId,
stepToAdd: nodeToAdd,
});
if (workflow.currentVersion.status === 'DRAFT') {
await updateOneWorkflowVersion({
idToUpdate: currentVersion.id,
updateOneRecordInput: {
steps: updatedSteps,
},
});
return;
}
await createNewWorkflowVersion({
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: workflow.currentVersion.trigger,
steps: updatedSteps,
});
};

View File

@ -0,0 +1,45 @@
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { ApolloClient, useApolloClient, useMutation } from '@apollo/client';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
import { DEACTIVATE_WORKFLOW_VERSION } from '@/workflow/graphql/deactivateWorkflowVersion';
import {
ActivateWorkflowVersionMutation,
ActivateWorkflowVersionMutationVariables,
} from '~/generated/graphql';
export const useDeactivateWorkflowVersion = () => {
const apolloClient = useApolloClient();
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
ActivateWorkflowVersionMutation,
ActivateWorkflowVersionMutationVariables
>(DEACTIVATE_WORKFLOW_VERSION, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const { findOneRecordQuery: findOneWorkflowVersionQuery } =
useFindOneRecordQuery({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const deactivateWorkflowVersion = async (workflowVersionId: string) => {
await mutate({
variables: {
workflowVersionId,
},
});
await apolloClient.query({
query: findOneWorkflowVersionQuery,
variables: {
objectRecordId: workflowVersionId,
},
fetchPolicy: 'network-only',
});
};
return { deactivateWorkflowVersion };
};

View File

@ -0,0 +1,37 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
import { useApolloClient } from '@apollo/client';
export const useDeleteOneWorkflowVersion = () => {
const apolloClient = useApolloClient();
const { findOneRecordQuery: findOneWorkflowRecordQuery } =
useFindOneRecordQuery({
objectNameSingular: CoreObjectNameSingular.Workflow,
});
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const deleteOneWorkflowVersion = async ({
workflowId,
workflowVersionId,
}: {
workflowId: string;
workflowVersionId: string;
}) => {
await deleteOneRecord(workflowVersionId);
await apolloClient.query({
query: findOneWorkflowRecordQuery,
variables: {
objectRecordId: workflowId,
},
fetchPolicy: 'network-only',
});
};
return { deleteOneWorkflowVersion };
};

View File

@ -1,5 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import {
WorkflowStep,
WorkflowVersion,
@ -20,20 +21,37 @@ export const useUpdateWorkflowVersionStep = ({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
workflowId: workflow.id,
});
const updateStep = async (updatedStep: WorkflowStep) => {
if (!isDefined(workflow.currentVersion)) {
throw new Error('Can not update an undefined workflow version.');
}
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
steps: replaceStep({
steps: workflow.currentVersion.steps ?? [],
stepId,
stepToReplace: updatedStep,
}),
},
const updatedSteps = replaceStep({
steps: workflow.currentVersion.steps ?? [],
stepId,
stepToReplace: updatedStep,
});
if (workflow.currentVersion.status === 'DRAFT') {
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
steps: updatedSteps,
},
});
return;
}
await createNewWorkflowVersion({
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: workflow.currentVersion.trigger,
steps: updatedSteps,
});
};

View File

@ -1,5 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
import {
WorkflowTrigger,
WorkflowVersion,
@ -17,16 +18,31 @@ export const useUpdateWorkflowVersionTrigger = ({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
workflowId: workflow.id,
});
const updateTrigger = async (updatedTrigger: WorkflowTrigger) => {
if (!isDefined(workflow.currentVersion)) {
throw new Error('Can not update an undefined workflow version.');
}
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
trigger: updatedTrigger,
},
if (workflow.currentVersion.status === 'DRAFT') {
await updateOneWorkflowVersion({
idToUpdate: workflow.currentVersion.id,
updateOneRecordInput: {
trigger: updatedTrigger,
},
});
return;
}
await createNewWorkflowVersion({
name: `v${workflow.versions.length + 1}`,
status: 'DRAFT',
trigger: updatedTrigger,
steps: workflow.currentVersion.steps,
});
};

View File

@ -19,6 +19,9 @@ export const useWorkflowWithCurrentVersion = (
id: true,
name: true,
statuses: true,
versions: {
totalCount: true,
},
},
skip: !isDefined(workflowId),
});

View File

@ -0,0 +1,15 @@
import {
Workflow,
WorkflowVersion,
WorkflowWithCurrentVersion,
} from '@/workflow/types/Workflow';
import { isDefined } from 'twenty-ui';
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function assertWorkflowWithCurrentVersionIsDefined(
workflow: WorkflowWithCurrentVersion | undefined,
): asserts workflow is Workflow & { currentVersion: WorkflowVersion } {
if (!isDefined(workflow) || !isDefined(workflow.currentVersion)) {
throw new Error('Expected workflow and its current version to be defined');
}
}

View File

@ -1,16 +1,16 @@
import { useParams } from 'react-router-dom';
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton';
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton';
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { RecordShowPageWorkflowHeader } from '@/workflow/components/RecordShowPageWorkflowHeader';
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
export const RecordShowPage = () => {
@ -46,22 +46,21 @@ export const RecordShowPage = () => {
headerIcon={headerIcon}
>
<>
<PageFavoriteButton
isFavorite={isFavorite}
onClick={handleFavoriteButtonClick}
/>
<ShowPageAddButton
key="add"
activityTargetObject={{
id: record?.id ?? '0',
targetObjectNameSingular: objectMetadataItem?.nameSingular,
}}
/>
<ShowPageMoreButton
key="more"
recordId={record?.id ?? '0'}
objectNameSingular={objectNameSingular}
/>
{objectNameSingular === CoreObjectNameSingular.Workflow ? (
<RecordShowPageWorkflowHeader
workflowId={parameters.objectRecordId}
/>
) : (
<RecordShowPageBaseHeader
{...{
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}}
/>
)}
</>
</RecordShowPageHeader>
<PageBody>

View File

@ -0,0 +1,40 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton';
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton';
export const RecordShowPageBaseHeader = ({
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}: {
isFavorite: boolean;
handleFavoriteButtonClick: () => void;
record: ObjectRecord | undefined;
objectMetadataItem: ObjectMetadataItem;
objectNameSingular: string;
}) => {
return (
<>
<PageFavoriteButton
isFavorite={isFavorite}
onClick={handleFavoriteButtonClick}
/>
<ShowPageAddButton
key="add"
activityTargetObject={{
id: record?.id ?? '0',
targetObjectNameSingular: objectMetadataItem.nameSingular,
}}
/>
<ShowPageMoreButton
key="more"
recordId={record?.id ?? '0'}
objectNameSingular={objectNameSingular}
/>
</>
);
};

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) => ({

View File

@ -28,7 +28,6 @@
}
},
"files": [],
"exclude": ["**/object-metadata/utils/getObjectMetadataItemsMock.ts"],
"include": [],
"references": [
{

View File

@ -12,6 +12,7 @@ import {
getDevSeedCompanyCustomFields,
getDevSeedPeopleCustomFields,
} from 'src/database/typeorm-seeds/metadata/fieldsMetadata';
import { getDevSeedCustomObjects } from 'src/database/typeorm-seeds/metadata/objectsMetadata';
import { seedCalendarChannels } from 'src/database/typeorm-seeds/workspace/calendar-channel';
import { seedCalendarChannelEventAssociations } from 'src/database/typeorm-seeds/workspace/calendar-channel-event-association';
import { seedCalendarEventParticipants } from 'src/database/typeorm-seeds/workspace/calendar-event-participants';
@ -150,6 +151,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
objectMetadataMap[STANDARD_OBJECT_IDS.person],
workspaceId,
);
await this.seedCustomObjects(workspaceId, dataSourceMetadata.id);
await workspaceDataSource.transaction(
async (entityManager: EntityManager) => {
@ -282,4 +284,15 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
});
}
}
async seedCustomObjects(workspaceId: string, dataSourceId: string) {
const devSeedCustomObjects = getDevSeedCustomObjects(
workspaceId,
dataSourceId,
);
for (const customObject of devSeedCustomObjects) {
await this.objectMetadataService.createOne(customObject);
}
}
}

View File

@ -0,0 +1,20 @@
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
export const getDevSeedCustomObjects = (
workspaceId: string,
dataSourceId: string,
): CreateObjectInput[] => {
return [
{
workspaceId,
dataSourceId,
labelPlural: 'Rockets',
labelSingular: 'Rocket',
namePlural: 'rockets',
nameSingular: 'rocket',
description: 'A rocket',
icon: 'IconRocket',
isRemote: false,
},
];
};

View File

@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import {
CreateOneResolverArgs,
DeleteOneResolverArgs,
@ -11,12 +13,12 @@ import {
WorkflowQueryValidationException,
WorkflowQueryValidationExceptionCode,
} from 'src/modules/workflow/common/exceptions/workflow-query-validation.exception';
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import {
WorkflowVersionStatus,
WorkflowVersionWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@Injectable()
export class WorkflowVersionValidationWorkspaceService {
@ -48,6 +50,8 @@ export class WorkflowVersionValidationWorkspaceService {
where: {
workflowId: payload.data.workflowId,
status: WorkflowVersionStatus.DRAFT,
// FIXME: soft-deleted rows selection will have to be improved globally
deletedAt: IsNull(),
},
});
@ -84,5 +88,25 @@ export class WorkflowVersionValidationWorkspaceService {
);
assertWorkflowVersionIsDraft(workflowVersion);
const workflowVersionRepository =
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
'workflowVersion',
);
const otherWorkflowVersionsExist = await workflowVersionRepository.exists({
where: {
workflowId: workflowVersion.workflowId,
deletedAt: IsNull(),
id: Not(workflowVersion.id),
},
});
if (!otherWorkflowVersionsExist) {
throw new WorkflowQueryValidationException(
'The initial version of a workflow can not be deleted',
WorkflowQueryValidationExceptionCode.FORBIDDEN,
);
}
}
}

View File

@ -203,6 +203,10 @@ export class WorkflowTriggerWorkspaceService {
workflowVersionNullable,
);
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
return;
}
await this.setDeactivatedVersionStatus(
workflowVersion,
workflowVersionRepository,

View File

@ -138,6 +138,8 @@ export {
IconPhotoUp,
IconPilcrow,
IconPlayerPlay,
IconPlayerStop,
IconPower,
IconPlaystationSquare,
IconPlug,
IconPlus,