Compare commits

...

18 Commits

Author SHA1 Message Date
Charles Bochet a800bbc20f Fix race condition 2026-02-26 16:06:22 +01:00
Charles Bochet e53fdf3e30 More 2026-02-26 14:21:27 +01:00
Charles Bochet a74d9d45fb Fix 2026-02-26 13:58:59 +01:00
Charles Bochet 26e5433636 Fix 2026-02-26 13:51:43 +01:00
Charles Bochet d11b5af46f Fix 2026-02-26 13:45:19 +01:00
Charles Bochet 3509620c2a Fix 2026-02-26 13:43:58 +01:00
Charles Bochet a0c5e4385a Fix 2026-02-26 13:26:50 +01:00
Charles Bochet 5a1f01df1f Fix flaky storybook tests by pre-warming mock metadata import
The dynamic import of the large generated metadata file in
WorkflowStepDecorator can be slow on first call in CI, causing
findByText to time out. Pre-warm the import in vitest.setup.ts
so the module is cached before any test runs.

Made-with: Cursor
2026-02-26 13:09:55 +01:00
Charles Bochet c4f50e8563 Regenerate mock data after server seed reset
Made-with: Cursor
2026-02-26 12:56:12 +01:00
Charles Bochet fb1b19ce66 Fix storybook test failures: await async metadata loading and use dynamic search results
WorkflowStepDecorator was calling loadMockedObjectMetadataItems() without
await, causing stories to render before metadata was loaded. Also update
Search query handler to derive results from actual mock company data
instead of hardcoded values that no longer match the regenerated data.

Made-with: Cursor
2026-02-26 12:52:13 +01:00
Charles Bochet 8fceadf6ef Fix 2026-02-26 12:15:35 +01:00
Charles Bochet 86b0b3c4ac Fix 2026-02-26 12:04:07 +01:00
Charles Bochet ef1be09a82 More 2026-02-26 11:54:25 +01:00
Charles Bochet ce3a7a55be Fix 2026-02-26 11:53:17 +01:00
Charles Bochet eab05aca4a More 2026-02-26 10:58:39 +01:00
Charles Bochet b9f33b9d49 Fix 2026-02-26 10:56:55 +01:00
Charles Bochet 505f7c9a38 Fix 2026-02-26 10:56:13 +01:00
Charles Bochet 5caecffda7 Improve test tooling 2026-02-26 10:46:42 +01:00
36 changed files with 23162 additions and 21592 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
src/generated
src/generated-metadata
src/generated-metadata
src/testing/mock-data/generated
@@ -1,6 +1,10 @@
import { setProjectAnnotations } from '@storybook/react-vite';
import * as projectAnnotations from './preview';
// Pre-warm the dynamic import used by WorkflowStepDecorator so the
// module is cached before any test runs (avoids flaky timeouts in CI).
import('~/testing/utils/generatedMockObjectMetadataItems');
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([projectAnnotations]);
+1 -1
View File
@@ -259,7 +259,7 @@
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "dotenv npx tsx scripts/generate-mock-data.ts"
"command": "dotenv npx vite-node scripts/generate-mock-data.ts"
}
},
"chromatic": {
@@ -1,281 +1,15 @@
/* eslint-disable no-console */
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const SERVER_BASE_URL =
process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000';
const AUTH_EMAIL = 'tim@apple.dev';
const AUTH_PASSWORD = 'tim@apple.dev';
const WORKSPACE_SUBDOMAIN = 'apple';
const serverUrl = new URL(SERVER_BASE_URL);
const WORKSPACE_ORIGIN = `${serverUrl.protocol}//${WORKSPACE_SUBDOMAIN}.${serverUrl.host}`;
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT_DIR = path.resolve(
currentDir,
'../src/testing/mock-data/generated',
);
const graphqlRequest = async (
endpoint: string,
query: string,
token?: string,
): Promise<unknown> => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Origin: WORKSPACE_ORIGIN,
};
if (token !== undefined) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${SERVER_BASE_URL}${endpoint}`, {
method: 'POST',
headers,
body: JSON.stringify({ query }),
});
const json = (await response.json()) as {
data?: unknown;
errors?: { message: string }[];
};
if (
json.errors !== undefined &&
json.errors !== null &&
json.errors.length > 0
) {
const errorDetails = json.errors.map((error) => error.message).join(', ');
throw new Error(`GraphQL error on ${endpoint}: ${errorDetails}`);
}
return json.data;
};
const authenticate = async (): Promise<string> => {
console.log(
`Authenticating as ${AUTH_EMAIL} on workspace ${WORKSPACE_SUBDOMAIN}...`,
);
const loginData = (await graphqlRequest(
'/metadata',
`mutation GetLoginTokenFromCredentials {
getLoginTokenFromCredentials(
email: "${AUTH_EMAIL}",
password: "${AUTH_PASSWORD}",
origin: "${WORKSPACE_ORIGIN}"
) {
loginToken { token }
}
}`,
)) as {
getLoginTokenFromCredentials: { loginToken: { token: string } };
};
const loginToken = loginData.getLoginTokenFromCredentials.loginToken.token;
const authData = (await graphqlRequest(
'/metadata',
`mutation GetAuthTokensFromLoginToken {
getAuthTokensFromLoginToken(
loginToken: "${loginToken}",
origin: "${WORKSPACE_ORIGIN}"
) {
tokens {
accessOrWorkspaceAgnosticToken { token }
}
}
}`,
)) as {
getAuthTokensFromLoginToken: {
tokens: { accessOrWorkspaceAgnosticToken: { token: string } };
};
};
const accessToken =
authData.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken
.token;
console.log('Authenticated successfully.');
return accessToken;
};
// Apollo Client automatically adds __typename to every object level;
// raw fetch does not, so we include it explicitly here.
const METADATA_QUERY = `
query ObjectMetadataItems {
objects(paging: { first: 1000 }) {
__typename
edges {
__typename
node {
__typename
id
universalIdentifier
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isRemote
isActive
isSystem
isUIReadOnly
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
applicationId
shortcut
isLabelSyncedWithName
isSearchable
duplicateCriteria
indexMetadataList {
__typename
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
isCustom
indexFieldMetadataList {
__typename
id
fieldMetadataId
createdAt
updatedAt
order
}
}
fieldsList {
__typename
id
universalIdentifier
type
name
label
description
icon
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
morphId
applicationId
relation {
__typename
type
sourceObjectMetadata {
__typename
id
nameSingular
namePlural
}
targetObjectMetadata {
__typename
id
nameSingular
namePlural
}
sourceFieldMetadata {
__typename
id
name
}
targetFieldMetadata {
__typename
id
name
}
}
morphRelations {
__typename
type
sourceObjectMetadata {
__typename
id
nameSingular
namePlural
}
targetObjectMetadata {
__typename
id
nameSingular
namePlural
}
sourceFieldMetadata {
__typename
id
name
}
targetFieldMetadata {
__typename
id
name
}
}
}
}
}
pageInfo {
__typename
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`;
import { generateMetadata } from './mock-data/generate-metadata.js';
import { generateRecordData } from './mock-data/generate-record-data.js';
import { authenticate } from './mock-data/utils.js';
const main = async () => {
console.log(`Server: ${SERVER_BASE_URL}`);
console.log(`Output: ${OUTPUT_DIR}`);
console.log('');
const token = await authenticate();
console.log('Fetching object metadata from /metadata ...');
const metadata = await graphqlRequest('/metadata', METADATA_QUERY, token);
const metadata = await generateMetadata(token);
await generateRecordData(token, metadata);
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const filePath = path.join(OUTPUT_DIR, 'mock-metadata-query-result.ts');
const content = [
'/* eslint-disable */',
'// @ts-nocheck',
"import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';",
'',
'// This file was automatically generated by scripts/generate-mock-data.ts',
'// Do not edit this file manually.',
'',
'// prettier-ignore',
'export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =',
JSON.stringify(metadata, null, 2) + ';',
'',
].join('\n');
fs.writeFileSync(filePath, content, 'utf-8');
console.log(`Written: ${filePath}`);
console.log('Done!');
console.log('All mock data generated!');
};
main().catch((error) => {
@@ -0,0 +1,160 @@
/* eslint-disable no-console */
import { graphqlRequest, writeGeneratedFile } from './utils.js';
// Apollo Client automatically adds __typename to every object level;
// raw fetch does not, so we include it explicitly here.
const METADATA_QUERY = `
query ObjectMetadataItems {
objects(paging: { first: 1000 }) {
__typename
edges {
__typename
node {
__typename
id
universalIdentifier
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isRemote
isActive
isSystem
isUIReadOnly
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
applicationId
shortcut
isLabelSyncedWithName
isSearchable
duplicateCriteria
indexMetadataList {
__typename
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
isCustom
indexFieldMetadataList {
__typename
id
fieldMetadataId
createdAt
updatedAt
order
}
}
fieldsList {
__typename
id
universalIdentifier
type
name
label
description
icon
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
morphId
applicationId
relation {
__typename
type
sourceObjectMetadata {
__typename
id
nameSingular
namePlural
}
targetObjectMetadata {
__typename
id
nameSingular
namePlural
}
sourceFieldMetadata {
__typename
id
name
}
targetFieldMetadata {
__typename
id
name
}
}
morphRelations {
__typename
type
sourceObjectMetadata {
__typename
id
nameSingular
namePlural
}
targetObjectMetadata {
__typename
id
nameSingular
namePlural
}
sourceFieldMetadata {
__typename
id
name
}
targetFieldMetadata {
__typename
id
name
}
}
}
}
}
pageInfo {
__typename
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`;
export const generateMetadata = async (token: string) => {
console.log('Fetching object metadata from /metadata ...');
const metadata = await graphqlRequest('/metadata', METADATA_QUERY, token);
writeGeneratedFile(
'metadata/objects/mock-objects-metadata.ts',
'mockedStandardObjectMetadataQueryResult',
'ObjectMetadataItemsQuery',
"import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';",
metadata,
);
return metadata as {
objects: { edges: { node: Record<string, unknown> }[] };
};
};
@@ -0,0 +1,85 @@
/* eslint-disable no-console */
import { print } from 'graphql';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromObject';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
import { graphqlRequest, writeGeneratedFile } from './utils.js';
const RECORDS_LIMIT = 10;
// Production query builders omit __typename on connection/edge wrappers
// since Apollo Client injects them automatically. Raw fetch needs them explicit.
const addTypenamesToSelections = (query: string): string =>
query.replace(/\{(?!\s*__typename\b)/g, '{\n__typename');
const toObjectMetadataItems = (rawMetadata: {
objects: { edges: { node: Record<string, unknown> }[] };
}): ObjectMetadataItem[] =>
rawMetadata.objects.edges.map((edge) => {
const { fieldsList, indexMetadataList, ...rest } = edge.node;
return {
...rest,
fields: fieldsList,
readableFields: fieldsList,
updatableFields: fieldsList,
indexMetadatas: ((indexMetadataList as unknown[]) ?? []).map(
(index: any) => ({
...index,
indexFieldMetadatas: index.indexFieldMetadataList ?? [],
}),
),
} as unknown as ObjectMetadataItem;
});
export const generateRecordData = async (
token: string,
rawMetadata: { objects: { edges: { node: Record<string, unknown> }[] } },
) => {
const objectMetadataItems = toObjectMetadataItems(rawMetadata);
const companyObject = objectMetadataItems.find(
(item) => item.nameSingular === 'company',
);
if (!companyObject) {
throw new Error('Company object metadata not found');
}
const recordGqlFields = generateDepthRecordGqlFieldsFromObject({
objectMetadataItems,
objectMetadataItem: companyObject,
depth: 1,
});
const queryDocument = generateFindManyRecordsQuery({
objectMetadataItem: companyObject,
objectMetadataItems,
recordGqlFields,
objectPermissionsByObjectMetadataId: {},
});
const query = addTypenamesToSelections(print(queryDocument));
console.log(
`Fetching ${companyObject.namePlural} (limit: ${RECORDS_LIMIT}) from /graphql ...`,
);
const data = (await graphqlRequest('/graphql', query, token, {
limit: RECORDS_LIMIT,
})) as Record<string, { edges: { node: Record<string, unknown> }[] }>;
const records = data[companyObject.namePlural].edges.map((edge) => edge.node);
console.log(` Got ${records.length} ${companyObject.namePlural}.`);
writeGeneratedFile(
'data/companies/mock-companies-data.ts',
'mockedCompanyRecords',
'Record<string, unknown>[]',
'',
records,
);
};
@@ -0,0 +1,134 @@
/* eslint-disable no-console */
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const SERVER_BASE_URL =
process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000';
const AUTH_EMAIL = 'jane.austen@apple.dev';
const AUTH_PASSWORD = 'tim@apple.dev';
const WORKSPACE_SUBDOMAIN = 'apple';
const serverUrl = new URL(SERVER_BASE_URL);
export const WORKSPACE_ORIGIN = `${serverUrl.protocol}//${WORKSPACE_SUBDOMAIN}.${serverUrl.host}`;
const currentDir = path.dirname(fileURLToPath(import.meta.url));
export const GENERATED_DIR = path.resolve(
currentDir,
'../../src/testing/mock-data/generated',
);
export const graphqlRequest = async (
endpoint: string,
query: string,
token?: string,
variables?: Record<string, unknown>,
): Promise<unknown> => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Origin: WORKSPACE_ORIGIN,
};
if (token !== undefined) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${SERVER_BASE_URL}${endpoint}`, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables }),
});
const json = (await response.json()) as {
data?: unknown;
errors?: { message: string }[];
};
if (
json.errors !== undefined &&
json.errors !== null &&
json.errors.length > 0
) {
const errorDetails = json.errors.map((error) => error.message).join(', ');
throw new Error(`GraphQL error on ${endpoint}: ${errorDetails}`);
}
return json.data;
};
export const authenticate = async (): Promise<string> => {
console.log(
`Authenticating as ${AUTH_EMAIL} on workspace ${WORKSPACE_SUBDOMAIN}...`,
);
const loginData = (await graphqlRequest(
'/metadata',
`mutation GetLoginTokenFromCredentials {
getLoginTokenFromCredentials(
email: "${AUTH_EMAIL}",
password: "${AUTH_PASSWORD}",
origin: "${WORKSPACE_ORIGIN}"
) {
loginToken { token }
}
}`,
)) as {
getLoginTokenFromCredentials: { loginToken: { token: string } };
};
const loginToken = loginData.getLoginTokenFromCredentials.loginToken.token;
const authData = (await graphqlRequest(
'/metadata',
`mutation GetAuthTokensFromLoginToken {
getAuthTokensFromLoginToken(
loginToken: "${loginToken}",
origin: "${WORKSPACE_ORIGIN}"
) {
tokens {
accessOrWorkspaceAgnosticToken { token }
}
}
}`,
)) as {
getAuthTokensFromLoginToken: {
tokens: { accessOrWorkspaceAgnosticToken: { token: string } };
};
};
const accessToken =
authData.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken
.token;
console.log('Authenticated successfully.');
return accessToken;
};
export const writeGeneratedFile = (
relativePath: string,
exportName: string,
typeName: string,
typeImport: string,
data: unknown,
) => {
const filePath = path.join(GENERATED_DIR, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const content = [
'/* eslint-disable */',
'// @ts-nocheck',
typeImport,
'',
'// This file was automatically generated — do not edit manually.',
'',
'// prettier-ignore',
`export const ${exportName}: ${typeName} =`,
JSON.stringify(data, null, 2) + ';',
'',
].join('\n');
fs.writeFileSync(filePath, content, 'utf-8');
console.log(`Written: ${filePath}`);
};
@@ -11,7 +11,7 @@ import {
QUERY_MAX_RECORDS,
} from 'twenty-shared/constants';
import { MessageParticipantRole } from 'twenty-shared/types';
import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode';
import { generateMockRecordNode } from '~/testing/utils/generateMockRecordNode';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
@@ -84,7 +84,7 @@ const mocks = [
messages: {
edges: [
{
node: generateEmptyJestRecordNode({
node: generateMockRecordNode({
objectNameSingular: 'message',
input: {
id: '1',
@@ -95,7 +95,7 @@ const mocks = [
cursor: '1',
},
{
node: generateEmptyJestRecordNode({
node: generateMockRecordNode({
objectNameSingular: 'message',
input: {
id: '2',
@@ -135,7 +135,7 @@ const mocks = [
messageParticipants: {
edges: [
{
node: generateEmptyJestRecordNode({
node: generateMockRecordNode({
objectNameSingular: 'messageParticipant',
input: {
id: 'messageParticipant-1',
@@ -146,7 +146,7 @@ const mocks = [
cursor: '1',
},
{
node: generateEmptyJestRecordNode({
node: generateMockRecordNode({
objectNameSingular: 'messageParticipant',
input: {
id: 'messageParticipant-2',
@@ -1,5 +1,5 @@
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata';
export const query = FIND_MANY_OBJECT_METADATA_ITEMS;
@@ -1,13 +1,16 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useCallback } from 'react';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
export const useLoadMockedObjectMetadataItems = () => {
const store = useStore();
const loadMockedObjectMetadataItems = useCallback(async () => {
const { generatedMockObjectMetadataItems } = await import(
'~/testing/utils/generatedMockObjectMetadataItems'
);
if (
!isDeeplyEqual(
store.get(objectMetadataItemsState.atom),
@@ -1,12 +1,15 @@
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/types/RecordGqlFields';
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
import { type ObjectPermissions } from 'twenty-shared/types';
import {
FieldMetadataType,
type ObjectPermissions,
RelationType,
} from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
type MapFieldMetadataToGraphQLQueryArgs = {
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`generateActivityTargetGqlFields snapshot tests should match snapshot for Note with loadRelations="activity" 1`] = `
{
@@ -5,7 +5,7 @@ import {
variables,
} from '@/object-record/hooks/__mocks__/useFindOneRecord';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode';
import { generateMockRecordNode } from '~/testing/utils/generateMockRecordNode';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const mocks = [
@@ -16,7 +16,7 @@ const mocks = [
},
result: jest.fn(() => ({
data: {
person: generateEmptyJestRecordNode({
person: generateMockRecordNode({
objectNameSingular: 'person',
input: { id: '6205681e-7c11-40b4-9e32-f523dbe54590' },
withDepthOneRelation: true,
@@ -12,7 +12,7 @@ import {
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { useToggleEditOnlyInput } from '@/object-record/record-field/ui/hooks/useToggleEditOnlyInput';
import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode';
import { generateMockRecordNode } from '~/testing/utils/generateMockRecordNode';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
@@ -39,7 +39,7 @@ const mocks: MockedResponse[] = [
result: jest.fn(() => ({
data: {
updateCompany: {
...generateEmptyJestRecordNode({
...generateMockRecordNode({
objectNameSingular: CoreObjectNameSingular.Company,
input: { id: recordId },
withDepthOneRelation: true,
@@ -27,8 +27,23 @@ import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSe
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks';
const personMetadata = getMockObjectMetadataItemOrThrow('person');
const companyMetadata = getMockObjectMetadataItemOrThrow('company');
const companyFieldOnPerson = getMockFieldMetadataItemOrThrow({
objectMetadataItem: personMetadata,
fieldName: 'company',
});
const peopleFieldOnCompany = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyMetadata,
fieldName: 'people',
});
const companiesMock = getCompaniesMock();
const RelationWorkspaceSetterEffect = () => {
const setRecordFieldInputLayoutDirectionLoading = useSetAtomComponentState(
recordFieldInputLayoutDirectionLoadingComponentState,
@@ -75,7 +90,7 @@ const RelationManyToOneFieldInputWithContext = ({
<FieldContext.Provider
value={{
fieldDefinition: {
fieldMetadataId: 'f6be42ac-ccb8-4df0-8c22-a9627d655c76',
fieldMetadataId: companyFieldOnPerson.id,
label: 'Company',
type: FieldMetadataType.RELATION,
iconName: 'IconLink',
@@ -84,9 +99,9 @@ const RelationManyToOneFieldInputWithContext = ({
relationObjectMetadataNamePlural: 'companies',
relationObjectMetadataNameSingular:
CoreObjectNameSingular.Company,
relationObjectMetadataId: '69e0aa5d-a9c5-486b-a99d-fe191195a19d',
relationObjectMetadataId: companyMetadata.id,
objectMetadataNameSingular: 'person',
relationFieldMetadataId: '94902a74-b8ca-4a56-8573-7469f0b664f6',
relationFieldMetadataId: peopleFieldOnCompany.id,
},
},
recordId: recordId,
@@ -154,7 +169,7 @@ export const Submit: Story = {
expect(handleSubmitMocked).toHaveBeenCalledTimes(0);
const item = await canvas.findByText('Linkedin', undefined, {
const item = await canvas.findByText(companiesMock[0].name, undefined, {
timeout: 3000,
});
@@ -171,7 +186,9 @@ export const Cancel: Story = {
const canvas = within(canvasElement);
expect(handleCancelMocked).toHaveBeenCalledTimes(0);
await canvas.findByText('Linkedin', undefined, { timeout: 3000 });
await canvas.findByText(companiesMock[0].name, undefined, {
timeout: 3000,
});
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
@@ -24,6 +24,19 @@ import { recordStoreFamilySelector } from '@/object-record/record-store/states/s
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from '~/generated-metadata/graphql';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
const personMetadata = getMockObjectMetadataItemOrThrow('person');
const companyMetadata = getMockObjectMetadataItemOrThrow('company');
const companyFieldOnPerson = getMockFieldMetadataItemOrThrow({
objectMetadataItem: personMetadata,
fieldName: 'company',
});
const peopleFieldOnCompany = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyMetadata,
fieldName: 'people',
});
const RelationWorkspaceSetterEffect = () => {
useEffect(() => {
@@ -39,7 +52,7 @@ const RelationOneToManyFieldInputWithContext = () => {
const fieldDefinition = useMemo(
() => ({
fieldMetadataId: '94902a74-b8ca-4a56-8573-7469f0b664f6',
fieldMetadataId: peopleFieldOnCompany.id,
label: 'People',
type: FieldMetadataType.RELATION,
iconName: 'IconLink',
@@ -49,7 +62,7 @@ const RelationOneToManyFieldInputWithContext = () => {
relationObjectMetadataNamePlural: 'people',
relationObjectMetadataNameSingular: CoreObjectNameSingular.Person,
objectMetadataNameSingular: 'company',
relationFieldMetadataId: 'f6be42ac-ccb8-4df0-8c22-a9627d655c76',
relationFieldMetadataId: companyFieldOnPerson.id,
},
}),
[],
@@ -61,7 +61,7 @@ describe('computeViewRecordGqlOperationFilter', () => {
expect(result).toEqual({
name: {
ilike: '%Linkedin%',
ilike: `%${companiesMock[0].name}%`,
},
});
});
@@ -116,7 +116,7 @@ describe('computeViewRecordGqlOperationFilter', () => {
and: [
{
name: {
ilike: '%Linkedin%',
ilike: `%${companiesMock[0].name}%`,
},
},
{
@@ -16,8 +16,8 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState';
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
import { unselectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { atom, useStore } from 'jotai';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@@ -42,17 +42,17 @@ export const RecordIndexFiltersToContextStoreEffect = () => {
recordIndexId,
);
const hasUserSelectedAllRowsAtom = useAtomComponentStateCallbackState(
const hasUserSelectedAllRows = useAtomComponentStateValue(
hasUserSelectedAllRowsComponentState,
recordIndexId,
);
const selectedRowIdsAtom = useAtomComponentSelectorCallbackState(
const selectedRowIds = useAtomComponentSelectorValue(
selectedRowIdsComponentSelector,
recordIndexId,
);
const unselectedRowIdsAtom = useAtomComponentSelectorCallbackState(
const unselectedRowIds = useAtomComponentSelectorValue(
unselectedRowIdsComponentSelector,
recordIndexId,
);
@@ -88,17 +88,14 @@ export const RecordIndexFiltersToContextStoreEffect = () => {
anyFieldFilterValue: string;
},
) => {
const hasUserSelectedAllRows = get(hasUserSelectedAllRowsAtom);
let newRule: ContextStoreTargetedRecordsRule;
if (hasUserSelectedAllRows) {
const unselectedRowIds = get(unselectedRowIdsAtom);
newRule = {
mode: 'exclusion',
excludedRecordIds: unselectedRowIds,
};
} else {
const selectedRowIds = get(selectedRowIdsAtom);
newRule = {
mode: 'selection',
selectedRecordIds: selectedRowIds,
@@ -132,9 +129,9 @@ export const RecordIndexFiltersToContextStoreEffect = () => {
},
),
[
hasUserSelectedAllRowsAtom,
selectedRowIdsAtom,
unselectedRowIdsAtom,
hasUserSelectedAllRows,
selectedRowIds,
unselectedRowIds,
contextStoreTargetedRecordsRuleAtom,
contextStoreFiltersAtom,
contextStoreFilterGroupsAtom,
@@ -321,6 +321,10 @@ export const useMultipleRecordPickerPerformSearch = () => {
},
}));
if (operationSignatures.length === 0) {
return;
}
performCombinedFindManyRecords({ operationSignatures }).then(
({ result }) => {
Object.values(result)
@@ -10,10 +10,13 @@ import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorato
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedViewsData } from '~/testing/mock-data/views';
import { sleep } from '~/utils/sleep';
const companiesMock = getCompaniesMock();
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTable',
component: RecordTableWithWrappers,
@@ -70,7 +73,7 @@ export const ScrolledLeft: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findAllByText('Linkedin', {}, { timeout: 3000 });
await canvas.findAllByText(companiesMock[0].name, {}, { timeout: 3000 });
const scrollWrapper = canvasElement.ownerDocument.body.querySelector(
'.scroll-wrapper-x-enabled',
@@ -88,7 +91,7 @@ export const ScrolledLeft: Story = {
},
});
await canvas.findByText('Facebook');
await canvas.findByText(companiesMock[1].name);
},
};
@@ -100,7 +103,7 @@ export const ScrolledBottom: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findAllByText('Linkedin', {}, { timeout: 3000 });
await canvas.findAllByText(companiesMock[0].name, {}, { timeout: 3000 });
const scrollWrapper = canvasElement.ownerDocument.body.querySelector(
'.scroll-wrapper-y-enabled',
@@ -118,6 +121,6 @@ export const ScrolledBottom: Story = {
},
});
await canvas.findByText('Facebook');
await canvas.findByText(companiesMock[1].name);
},
};
@@ -1,6 +1,5 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useRef } from 'react';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexHasRecordsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexHasRecordsComponentSelector';
@@ -9,7 +9,7 @@ import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScro
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { isNonEmptyString } from '@sniptt/guards';
import { useEffect, useState } from 'react';
import { useEffect, useRef } from 'react';
export const RecordTableNoRecordGroupScrollToPreviousRecordEffect = () => {
const { getScrollWrapperElement } = useScrollWrapperHTMLElement();
@@ -22,8 +22,6 @@ export const RecordTableNoRecordGroupScrollToPreviousRecordEffect = () => {
lastShowPageRecordIdState,
);
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
const { scrollTableToPosition } = useScrollTableToPosition();
const { triggerInitialRecordTableDataLoad } =
@@ -33,19 +31,49 @@ export const RecordTableNoRecordGroupScrollToPreviousRecordEffect = () => {
const { triggerFetchPagesWithoutDebounce } = useTriggerFetchPages();
const allRecordIdsRef = useRef(allRecordIds);
allRecordIdsRef.current = allRecordIds;
const scrollTableToPositionRef = useRef(scrollTableToPosition);
scrollTableToPositionRef.current = scrollTableToPosition;
const triggerInitialRecordTableDataLoadRef = useRef(
triggerInitialRecordTableDataLoad,
);
triggerInitialRecordTableDataLoadRef.current =
triggerInitialRecordTableDataLoad;
const processTreadmillScrollTopRef = useRef(processTreadmillScrollTop);
processTreadmillScrollTopRef.current = processTreadmillScrollTop;
const getScrollWrapperElementRef = useRef(getScrollWrapperElement);
getScrollWrapperElementRef.current = getScrollWrapperElement;
const triggerFetchPagesWithoutDebounceRef = useRef(
triggerFetchPagesWithoutDebounce,
);
triggerFetchPagesWithoutDebounceRef.current =
triggerFetchPagesWithoutDebounce;
useEffect(() => {
if (!isNonEmptyString(lastShowPageRecordId)) {
return;
}
const run = async () => {
setLastShowPageRecordId(null);
const recordPosition = allRecordIds.findIndex(
const recordPosition = allRecordIdsRef.current.findIndex(
(recordId) => recordId === lastShowPageRecordId,
);
await triggerInitialRecordTableDataLoad();
await triggerInitialRecordTableDataLoadRef.current();
const { scrollWrapperElement } = getScrollWrapperElement();
const { scrollWrapperElement } =
getScrollWrapperElementRef.current();
const tableScrollWrapperHeight = scrollWrapperElement?.clientHeight ?? 0;
const tableScrollWrapperHeight =
scrollWrapperElement?.clientHeight ?? 0;
const numberOfRowsDisplayedInTable = Math.min(
Math.floor(tableScrollWrapperHeight / (RECORD_TABLE_ROW_HEIGHT + 1)),
@@ -56,7 +84,8 @@ export const RecordTableNoRecordGroupScrollToPreviousRecordEffect = () => {
numberOfRowsDisplayedInTable / 2,
);
const recordPositionInPx = recordPosition * (RECORD_TABLE_ROW_HEIGHT + 1);
const recordPositionInPx =
recordPosition * (RECORD_TABLE_ROW_HEIGHT + 1);
const targetScrollPositionInPx = Math.max(
0,
@@ -64,32 +93,18 @@ export const RecordTableNoRecordGroupScrollToPreviousRecordEffect = () => {
halfNumberOfRowsVisible * (RECORD_TABLE_ROW_HEIGHT + 1),
);
scrollTableToPosition({
scrollTableToPositionRef.current({
horizontalScrollInPx: 0,
verticalScrollInPx: targetScrollPositionInPx,
});
processTreadmillScrollTop(targetScrollPositionInPx);
processTreadmillScrollTopRef.current(targetScrollPositionInPx);
setHasInitializedScroll(true);
await triggerFetchPagesWithoutDebounce();
await triggerFetchPagesWithoutDebounceRef.current();
};
if (isNonEmptyString(lastShowPageRecordId)) {
run();
}
}, [
hasInitializedScroll,
lastShowPageRecordId,
scrollTableToPosition,
allRecordIds,
setLastShowPageRecordId,
triggerInitialRecordTableDataLoad,
processTreadmillScrollTop,
getScrollWrapperElement,
triggerFetchPagesWithoutDebounce,
]);
run();
}, [lastShowPageRecordId, setLastShowPageRecordId]);
return <></>;
};
@@ -13,9 +13,8 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import isEmpty from 'lodash.isempty';
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
// TODO: see if we can merge the initial and load more processes, to have only one load at scroll index effect
export const RecordTableVirtualizedInitialDataLoadEffect = () => {
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
@@ -53,45 +52,81 @@ export const RecordTableVirtualizedInitialDataLoadEffect = () => {
const { currentView } = useGetCurrentViewOnly();
const currentViewId = currentView?.id ?? null;
const triggerInitialRecordTableDataLoadRef = useRef(
triggerInitialRecordTableDataLoad,
);
triggerInitialRecordTableDataLoadRef.current =
triggerInitialRecordTableDataLoad;
const setLastRecordTableQueryIdentifierRef = useRef(
setLastRecordTableQueryIdentifier,
);
setLastRecordTableQueryIdentifierRef.current =
setLastRecordTableQueryIdentifier;
const setLastContextStoreVirtualizedViewIdRef = useRef(
setLastContextStoreVirtualizedViewId,
);
setLastContextStoreVirtualizedViewIdRef.current =
setLastContextStoreVirtualizedViewId;
const setLastContextStoreVirtualizedVisibleRecordFieldsRef = useRef(
setLastContextStoreVirtualizedVisibleRecordFields,
);
setLastContextStoreVirtualizedVisibleRecordFieldsRef.current =
setLastContextStoreVirtualizedVisibleRecordFields;
const visibleRecordFieldsRef = useRef(visibleRecordFields);
visibleRecordFieldsRef.current = visibleRecordFields;
const queryIdentifierRef = useRef(queryIdentifier);
queryIdentifierRef.current = queryIdentifier;
useEffect(() => {
if (isInitializingVirtualTableDataLoading) {
return;
}
(async () => {
if ((currentView?.id ?? null) !== lastContextStoreVirtualizedViewId) {
// Wait for the atomic batch from loadRecordIndexStates to populate
// visibleRecordFields before triggering a fetch. On the next render
// after the batch, fields will be populated and we'll proceed.
if (isEmpty(visibleRecordFields)) {
if (currentViewId !== lastContextStoreVirtualizedViewId) {
if (isEmpty(visibleRecordFieldsRef.current)) {
return;
}
setLastContextStoreVirtualizedViewId(currentView?.id ?? null);
setLastRecordTableQueryIdentifier(queryIdentifier);
setLastContextStoreVirtualizedVisibleRecordFields(visibleRecordFields);
setLastContextStoreVirtualizedViewIdRef.current(currentViewId);
setLastRecordTableQueryIdentifierRef.current(
queryIdentifierRef.current,
);
setLastContextStoreVirtualizedVisibleRecordFieldsRef.current(
visibleRecordFieldsRef.current,
);
await triggerInitialRecordTableDataLoad();
await triggerInitialRecordTableDataLoadRef.current();
} else if (
queryIdentifier !== lastRecordTableQueryIdentifier &&
!isFetchingMoreRecords
) {
setLastRecordTableQueryIdentifier(queryIdentifier);
setLastRecordTableQueryIdentifierRef.current(queryIdentifier);
await triggerInitialRecordTableDataLoad();
await triggerInitialRecordTableDataLoadRef.current();
} else if (
JSON.stringify(lastContextStoreVirtualizedVisibleRecordFields) !==
JSON.stringify(visibleRecordFields)
) {
const lastFields = lastContextStoreVirtualizedVisibleRecordFields || [];
const lastFields =
lastContextStoreVirtualizedVisibleRecordFields || [];
const currentFields = visibleRecordFields || [];
setLastContextStoreVirtualizedVisibleRecordFields(visibleRecordFields);
setLastContextStoreVirtualizedVisibleRecordFieldsRef.current(
visibleRecordFields,
);
const shouldRefetchData = currentFields.length > lastFields.length;
if (shouldRefetchData) {
await triggerInitialRecordTableDataLoad({
await triggerInitialRecordTableDataLoadRef.current({
shouldScrollToStart: isEmpty(lastFields),
});
}
@@ -100,15 +135,11 @@ export const RecordTableVirtualizedInitialDataLoadEffect = () => {
}, [
queryIdentifier,
lastRecordTableQueryIdentifier,
triggerInitialRecordTableDataLoad,
setLastRecordTableQueryIdentifier,
isFetchingMoreRecords,
isInitializingVirtualTableDataLoading,
currentView,
currentViewId,
lastContextStoreVirtualizedViewId,
setLastContextStoreVirtualizedViewId,
lastContextStoreVirtualizedVisibleRecordFields,
setLastContextStoreVirtualizedVisibleRecordFields,
visibleRecordFields,
]);
@@ -4,9 +4,9 @@ import { type ColumnDefinition } from '@/object-record/record-table/types/Column
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { findByProperty } from 'twenty-shared/utils';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { getMockCompanyObjectMetadataItem } from '~/testing/mock-data/companies';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
const COMPANY_MOCK_OBJECT = getMockCompanyObjectMetadataItem();
const COMPANY_MOCK_OBJECT = getMockObjectMetadataItemOrThrow('company');
export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
[
@@ -12,7 +12,13 @@ import { PageDragDropProvider } from '@/navigation/components/PageDragDropProvid
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
import { SignInAppNavigationDrawerMock } from '@/sign-in-background-mock/components/SignInAppNavigationDrawerMock';
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
import { lazy, Suspense } from 'react';
const SignInBackgroundMockPage = lazy(() =>
import('@/sign-in-background-mock/components/SignInBackgroundMockPage').then(
(module) => ({ default: module.SignInBackgroundMockPage }),
),
);
import { useShowFullscreen } from '@/ui/layout/fullscreen/hooks/useShowFullscreen';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints';
@@ -106,7 +112,9 @@ export const DefaultLayout = () => {
{showAuthModal ? (
<>
<StyledMainContainer>
<SignInBackgroundMockPage />
<Suspense fallback={null}>
<SignInBackgroundMockPage />
</Suspense>
</StyledMainContainer>
<AnimatePresence mode="wait">
<LayoutGroup>
@@ -1,5 +1,11 @@
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
import { Trans, useLingui } from '@lingui/react/macro';
import { lazy, Suspense } from 'react';
const SignInBackgroundMockPage = lazy(() =>
import('@/sign-in-background-mock/components/SignInBackgroundMockPage').then(
(module) => ({ default: module.SignInBackgroundMockPage }),
),
);
import { AppPath } from 'twenty-shared/types';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
@@ -61,7 +67,9 @@ export const NotFound = () => {
</StyledButtonContainer>
</AnimatedPlaceholderErrorContainer>
</StyledBackDrop>
<SignInBackgroundMockPage />
<Suspense fallback={null}>
<SignInBackgroundMockPage />
</Suspense>
</>
);
};
@@ -10,69 +10,75 @@ import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema';
import { type Decorator } from '@storybook/react-vite';
import { useStore } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import { useAtomValue, useStore } from 'jotai';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
getWorkflowMock,
getWorkflowNodeIdMock,
mockedWorkflow,
mockedWorkflowNodeId,
mockedWorkflowVersion,
} from '~/testing/mock-data/workflow';
export const WorkflowStepDecorator: Decorator = (Story) => {
const workflowVisualizerComponentInstanceId = 'workflow-visualizer-test-id';
const workflowVersion = getWorkflowMock().versions.edges[0]
.node as WorkflowVersion;
const workflowVersion = mockedWorkflowVersion as WorkflowVersion;
const { populateStepsOutputSchema } = useStepsOutputSchema();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const [ready, setReady] = useState(false);
const store = useStore();
const handleMount = useCallback(async () => {
await loadMockedObjectMetadataItems();
useEffect(() => {
const setup = async () => {
await loadMockedObjectMetadataItems();
store.set(
workflowVisualizerWorkflowIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
getWorkflowMock().id,
);
store.set(
workflowVisualizerWorkflowVersionIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
workflowVersion.id,
);
store.set(
workflowVisualizerWorkflowRunIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
'123',
);
store.set(
workflowSelectedNodeComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
getWorkflowNodeIdMock(),
);
store.set(
flowComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
{
workflowVersionId: workflowVersion.id,
trigger: workflowVersion.trigger,
steps: workflowVersion.steps,
},
);
store.set(
commandMenuWorkflowIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
getWorkflowMock().id,
);
populateStepsOutputSchema(workflowVersion);
setReady(true);
store.set(
workflowVisualizerWorkflowIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
mockedWorkflow.id,
);
store.set(
workflowVisualizerWorkflowVersionIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
workflowVersion.id,
);
store.set(
workflowVisualizerWorkflowRunIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
'123',
);
store.set(
workflowSelectedNodeComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
mockedWorkflowNodeId,
);
store.set(
flowComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
{
workflowVersionId: workflowVersion.id,
trigger: workflowVersion.trigger,
steps: workflowVersion.steps,
},
);
store.set(
commandMenuWorkflowIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
mockedWorkflow.id,
);
populateStepsOutputSchema(workflowVersion);
setReady(true);
};
setup();
}, [
loadMockedObjectMetadataItems,
populateStepsOutputSchema,
@@ -80,9 +86,11 @@ export const WorkflowStepDecorator: Decorator = (Story) => {
store,
]);
useEffect(() => {
handleMount();
}, [handleMount]);
const workflowVersionId = useAtomValue(
workflowVisualizerWorkflowVersionIdComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
);
return (
<CommandMenuPageComponentInstanceContext.Provider
@@ -95,7 +103,7 @@ export const WorkflowStepDecorator: Decorator = (Story) => {
instanceId: workflowVisualizerComponentInstanceId,
}}
>
{ready && <Story />}
{ready && isDefined(workflowVersionId) && <Story />}
</WorkflowVisualizerComponentInstanceContext.Provider>
</CommandMenuPageComponentInstanceContext.Provider>
);
+59 -131
View File
@@ -26,7 +26,7 @@ import { LIST_PLANS } from '@/billing/graphql/queries/listPlans';
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
import { isDefined } from 'twenty-shared/utils';
import { mockBillingPlans } from '~/testing/mock-data/billing-plans';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata';
import { getRolesMock } from '~/testing/mock-data/roles';
import { mockedTasks } from '~/testing/mock-data/tasks';
import {
@@ -35,12 +35,31 @@ import {
workflowQueryResult,
} from '~/testing/mock-data/workflow';
import { oneSucceededWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename';
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
import { mockedViewFieldsData } from './mock-data/view-fields';
const peopleMock = getPeopleRecordConnectionMock();
const companiesMock = getCompaniesRecordConnectionMock();
const duplicateCompanyMock = getCompanyDuplicateMock();
// Wraps raw server-fetched records (which already have correct field shapes)
// into a GraphQL connection response structure.
const wrapRecordsAsConnection = (
objectNameSingular: string,
records: Record<string, unknown>[],
) => ({
__typename: getConnectionTypename(objectNameSingular),
edges: records.map((node) => ({
__typename: getEdgeTypename(objectNameSingular),
node,
cursor: '',
})),
pageInfo: getEmptyPageInfo(),
totalCount: records.length,
});
const getRootFieldNamesFromQuery = (query: string) => {
try {
const document = parse(query);
@@ -201,62 +220,48 @@ export const graphqlMocks = {
});
}),
graphql.query('Search', () => {
const personSearchEdges = peopleMock
.slice(0, 2)
.map((person: Record<string, unknown>, index: number) => ({
node: {
__typename: 'SearchRecordDTO',
recordId: person.id,
objectNameSingular: 'person',
objectLabelSingular: 'Person',
label:
`${(person.name as Record<string, string>)?.firstName ?? ''} ${(person.name as Record<string, string>)?.lastName ?? ''}`.trim(),
imageUrl: '',
tsRankCD: 0.2,
tsRank: 0.12158542,
},
cursor: `cursor-${index + 1}`,
}));
const companySearchEdges = companiesMock
.slice(0, 2)
.map((company: Record<string, unknown>, index: number) => ({
node: {
__typename: 'SearchRecordDTO',
recordId: company.id,
objectNameSingular: 'company',
objectLabelSingular: 'Company',
label: company.name,
imageUrl: '',
tsRankCD: 0.2,
tsRank: 0.12158542,
},
cursor: `cursor-${personSearchEdges.length + index + 1}`,
}));
const allEdges = [...personSearchEdges, ...companySearchEdges];
return HttpResponse.json({
data: {
search: {
edges: [
{
node: {
__typename: 'SearchRecordDTO',
recordId: '20202020-2d40-4e49-8df4-9c6a049191de',
objectNameSingular: 'person',
label: 'Louis Duss',
imageUrl: '',
tsRankCD: 0.2,
tsRank: 0.12158542,
},
cursor: 'cursor-1',
},
{
node: {
__typename: 'SearchRecordDTO',
recordId: '20202020-3ec3-4fe3-8997-b76aa0bfa408',
objectNameSingular: 'company',
label: 'Linkedin',
imageUrl: 'https://twenty-icons.com/linkedin.com',
tsRankCD: 0.2,
tsRank: 0.12158542,
},
cursor: 'cursor-2',
},
{
node: {
__typename: 'SearchRecordDTO',
recordId: '20202020-3f74-492d-a101-2a70f50a1645',
objectNameSingular: 'company',
label: 'Libeo',
imageUrl: 'https://twenty-icons.com/libeo.io',
tsRankCD: 0.2,
tsRank: 0.12158542,
},
cursor: 'cursor-3',
},
{
node: {
__typename: 'SearchRecordDTO',
recordId: '20202020-ac73-4797-824e-87a1f5aea9e0',
objectNameSingular: 'person',
label: 'Sylvie Palmer',
imageUrl: '',
tsRankCD: 0.1,
tsRank: 0.06079271,
},
cursor: 'cursor-4',
},
],
edges: allEdges,
pageInfo: {
hasNextPage: true,
endCursor: 'cursor-4',
endCursor: allEdges[allEdges.length - 1]?.cursor ?? null,
},
},
},
@@ -390,45 +395,7 @@ export const graphqlMocks = {
return HttpResponse.json({
data: {
companies: {
edges: mockedData.map((company) => ({
node: {
...company,
favorites: {
edges: [],
__typename: 'FavoriteConnection',
},
attachments: {
edges: [],
__typename: 'AttachmentConnection',
},
people: {
edges: [],
__typename: 'PersonConnection',
},
opportunities: {
edges: [],
__typename: 'OpportunityConnection',
},
taskTargets: {
edges: [],
__typename: 'TaskTargetConnection',
},
noteTargets: {
edges: [],
__typename: 'NoteTargetConnection',
},
},
cursor: null,
})),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
totalCount: mockedData.length,
},
companies: wrapRecordsAsConnection('company', mockedData),
},
});
}),
@@ -436,46 +403,7 @@ export const graphqlMocks = {
return HttpResponse.json({
data: {
companyDuplicates: [
{
edges: [
{
node: {
...duplicateCompanyMock,
favorites: {
edges: [],
__typename: 'FavoriteConnection',
},
attachments: {
edges: [],
__typename: 'AttachmentConnection',
},
people: {
edges: [],
__typename: 'PersonConnection',
},
opportunities: {
edges: [],
__typename: 'OpportunityConnection',
},
taskTargets: {
edges: [],
__typename: 'TaskTargetConnection',
},
noteTargets: {
edges: [],
__typename: 'NoteTargetConnection',
},
},
cursor: null,
},
],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
wrapRecordsAsConnection('company', [duplicateCompanyMock]),
],
},
});
@@ -1,784 +1,21 @@
import { type Company } from '@/companies/types/Company';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { mockedCompanyRecords } from '~/testing/mock-data/generated/data/companies/mock-companies-data';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
export const companiesQueryResult = {
companies: {
__typename: 'CompanyConnection',
totalCount: 13,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
startCursor:
'WzEsICIyMDIwMjAyMC0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
endCursor: 'WzEzLCAiMjAyMDIwMjAtMTQ1NS00YzU3LWFmYWYtZGQ1ZGMwODYzNjFkIl0=',
},
edges: [
{
__typename: 'CompanyEdge',
cursor: 'WzEsICIyMDIwMjAyMC0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
node: {
__typename: 'Company',
id: '20202020-3ec3-4fe3-8997-b76aa0bfa408',
employees: 100,
createdAt: '2025-01-05T09:00:20.412Z',
updatedAt: '2025-01-05T10:15:30.412Z',
deletedAt: null,
name: 'Linkedin',
accountOwnerId: null,
accountOwner: null,
domainName: {
__typename: 'Links',
primaryLinkLabel: 'https://linkedin.com',
primaryLinkUrl: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
position: 1,
idealCustomerProfile: true,
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
previousEmployees: {
__typename: 'Person',
id: '20202020-2d40-4e49-8df4-9c6a049191de',
email: 'louis.duss@google.com',
position: 14,
testJson: null,
testRating: null,
companyId: '20202020-c21e-4ec2-873b-de4264d89025',
avatarUrl: '',
updatedAt: '2025-01-05T11:36:42.400Z',
testMultiSelect: null,
testBoolean: true,
testSelect: 'OPTION_1',
testDateOnly: null,
bestCompanyId: null,
testUuid: null,
phone: '+33788901234',
createdAt: '2025-01-05T09:00:20.412Z',
city: 'Seattle',
testPhone: '',
jobTitle: 'CTO',
testCurrency: {
__typename: 'Currency',
amountMicros: null,
currencyCode: 'USD',
},
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: 'twitter.com',
},
testLinks: {
__typename: 'Links',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: [],
},
name: {
__typename: 'FullName',
firstName: 'Louis',
lastName: 'Duss',
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: 'linkedin.com',
},
testAddress: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
testLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzIsICIyMDIwMjAyMC01ZDgxLTQ2ZDYtYmY4My1mN2ZkMzNlYTYxMDIiXQ==',
node: {
__typename: 'Company',
id: '20202020-5d81-46d6-bf83-f7fd33ea6102',
employees: null,
createdAt: '2025-01-06T08:30:15.412Z',
updatedAt: '2025-01-06T14:45:22.412Z',
deletedAt: null,
name: 'Facebook',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://facebook.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 2,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzMsICIyMDIwMjAyMC0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
node: {
__typename: 'Company',
id: '20202020-0713-40a5-8216-82802401d33e',
employees: null,
createdAt: '2025-01-07T10:15:30.412Z',
updatedAt: '2025-01-07T16:20:45.412Z',
deletedAt: null,
name: 'Anthropic',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://anthropic.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 3,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzQsICIyMDIwMjAyMC1lZDg5LTQxM2EtYjMxYS05NjI5ODZlNjdiYjQiXQ==',
node: {
__typename: 'Company',
id: '20202020-ed89-413a-b31a-962986e67bb4',
employees: null,
createdAt: '2025-01-08T11:45:10.412Z',
updatedAt: '2025-01-08T17:30:25.412Z',
deletedAt: null,
name: 'Microsoft',
idealCustomerProfile: true,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://microsoft.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 4,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzUsICIyMDIwMjAyMC0xNzFlLTRiY2MtOWNmNy00MzQ0OGQ2ZmIyNzgiXQ==',
node: {
__typename: 'Company',
id: '20202020-171e-4bcc-9cf7-43448d6fb278',
employees: null,
createdAt: '2025-01-09T09:20:45.412Z',
updatedAt: '2025-01-09T15:10:30.412Z',
deletedAt: null,
name: 'Airbnb',
idealCustomerProfile: true,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://airbnb.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 5,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzYsICIyMDIwMjAyMC1jMjFlLTRlYzItODczYi1kZTQyNjRkODkwMjUiXQ==',
node: {
__typename: 'Company',
id: '20202020-c21e-4ec2-873b-de4264d89025',
employees: null,
createdAt: '2025-01-10T13:30:20.412Z',
updatedAt: '2025-01-10T18:45:35.412Z',
deletedAt: null,
name: 'Google',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://google.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 6,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzcsICIyMDIwMjAyMC03MDdlLTQ0ZGMtYTFkMi0zMDAzMGJmMWE5NDQiXQ==',
node: {
__typename: 'Company',
id: '20202020-707e-44dc-a1d2-30030bf1a944',
employees: null,
createdAt: '2025-01-11T07:15:50.412Z',
updatedAt: '2025-01-11T12:25:15.412Z',
deletedAt: null,
name: 'Netflix',
idealCustomerProfile: true,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://netflix.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 7,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzgsICIyMDIwMjAyMC0zZjc0LTQ5MmQtYTEwMS0yYTcwZjUwYTE2NDUiXQ==',
node: {
__typename: 'Company',
id: '20202020-3f74-492d-a101-2a70f50a1645',
employees: null,
createdAt: '2025-01-12T14:40:25.412Z',
updatedAt: '2025-01-12T19:55:40.412Z',
deletedAt: null,
name: 'Libeo',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://libeo.io',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 8,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzksICIyMDIwMjAyMC1jZmJmLTQxNTYtYTc5MC1lMzk4NTRkY2Q0ZWIiXQ==',
node: {
__typename: 'Company',
id: '20202020-cfbf-4156-a790-e39854dcd4eb',
employees: null,
createdAt: '2025-01-13T08:25:35.412Z',
updatedAt: '2025-01-13T13:40:50.412Z',
deletedAt: null,
name: 'Claap',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://claap.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 9,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzEwLCAiMjAyMDIwMjAtZjg2Yi00MTlmLWI3OTQtMDIzMTlhYmU4NjM3Il0=',
node: {
__typename: 'Company',
id: '20202020-f86b-419f-b794-02319abe8637',
employees: null,
createdAt: '2025-01-14T16:10:15.412Z',
updatedAt: '2025-01-14T21:30:25.412Z',
deletedAt: null,
name: 'Hasura',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://hasura.io',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 10,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzExLCAiMjAyMDIwMjAtNTUxOC00NTUzLTk0MzMtNDJkOGViODI4MzRiIl0=',
node: {
__typename: 'Company',
id: '20202020-5518-4553-9433-42d8eb82834b',
employees: null,
createdAt: '2025-01-15T12:50:40.412Z',
updatedAt: '2025-01-15T17:15:55.412Z',
deletedAt: null,
name: 'Wework',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://wework.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 11,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzEyLCAiMjAyMDIwMjAtZjc5ZS00MGRkLWJkMDYtYzM2ZTZhYmI0Njc4Il0=',
node: {
__typename: 'Company',
id: '20202020-f79e-40dd-bd06-c36e6abb4678',
employees: null,
createdAt: '2025-01-16T10:35:20.412Z',
updatedAt: '2025-01-16T15:50:35.412Z',
deletedAt: null,
name: 'Samsung',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://samsung.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 12,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
{
__typename: 'CompanyEdge',
cursor: 'WzEzLCAiMjAyMDIwMjAtMTQ1NS00YzU3LWFmYWYtZGQ1ZGMwODYzNjFkIl0=',
node: {
__typename: 'Company',
id: '20202020-1455-4c57-afaf-dd5dc086361d',
employees: null,
createdAt: '2025-01-17T11:20:10.412Z',
updatedAt: '2025-01-17T16:45:25.412Z',
deletedAt: null,
name: 'Algolia',
idealCustomerProfile: false,
accountOwner: null,
accountOwnerId: null,
domainName: {
__typename: 'Links',
primaryLinkUrl: 'https://algolia.com',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {
__typename: 'Address',
addressStreet1: '',
addressStreet2: '',
addressCity: '',
addressState: '',
addressCountry: '',
addressPostcode: '',
addressLat: null,
addressLng: null,
},
previousEmployees: null,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: null,
currencyCode: '',
},
position: 13,
xLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
linkedinLink: {
__typename: 'Links',
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
},
},
],
},
};
const allMockedCompanyRecords = getRecordsFromRecordConnection({
recordConnection: companiesQueryResult.companies,
}) as ObjectRecord[];
const allMockedCompanyRecords = mockedCompanyRecords as ObjectRecord[];
export const getCompaniesMock = () => {
return [...allMockedCompanyRecords] as Company[];
};
export const getCompaniesRecordConnectionMock = () => {
const companiesMock = companiesQueryResult.companies.edges.map(
(edge) => edge.node,
);
return companiesMock;
return [...allMockedCompanyRecords];
};
export const getMockCompanyObjectMetadataItem = () => {
const companyObjectMetadataItem = getMockObjectMetadataItemOrThrow('company');
return companyObjectMetadataItem;
return getMockObjectMetadataItemOrThrow('company');
};
export const getCompanyDuplicateMock = () => {
@@ -811,26 +48,3 @@ export const findMockCompanyRecord = ({
return company;
};
export const getEmptyCompanyMock = () => {
return {
id: '9231e6ee-4cc2-4c7b-8c55-dff16f4d968a',
name: '',
domainName: {
__typename: 'Links',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: [],
},
address: {},
accountOwner: null,
createdAt: null,
updatedAt: null,
employees: null,
idealCustomerProfile: null,
linkedinLink: null,
xLink: null,
_activityCount: null,
__typename: 'Company',
};
};
@@ -1,4 +1,4 @@
import { companiesQueryResult } from '~/testing/mock-data/companies';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { mockedWorkspaceMemberData } from '~/testing/mock-data/users';
import { mockedViewsData } from '~/testing/mock-data/views';
@@ -18,8 +18,8 @@ export const mockedFavoritesData = [
workflowVersion: null,
forWorkspaceMemberId: mockedWorkspaceMemberData.id,
forWorkspaceMember: mockedWorkspaceMemberData,
companyId: companiesQueryResult.companies.edges[0].node.id,
company: companiesQueryResult.companies.edges[0].node,
companyId: getCompaniesMock()[0].id,
company: getCompaniesMock()[0],
viewId: null,
view: null,
taskId: null,
@@ -1,29 +1,10 @@
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
export const getWorkflowMock = () => {
return workflowQueryResult.workflows.edges[0].node;
};
export const getWorkflowVersionsMock = () => {
return {
...getWorkflowMock().versions,
__typename: 'WorkflowVersionConnection',
totalCount: 1,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
endCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
},
};
};
export const getWorkflowNodeIdMock = () => {
return getWorkflowMock().versions.edges[0].node.steps[0].id;
};
// Keep function wrappers as aliases for backward compatibility — they just
// return the pre-computed constants so callers can be migrated gradually.
export const getWorkflowMock = () => mockedWorkflow;
export const getWorkflowVersionsMock = () => mockedWorkflowVersions;
export const getWorkflowNodeIdMock = () => mockedWorkflowNodeId;
export const MOCKED_STEP_ID = '04d5f3bf-9714-400d-ba27-644006a5fb1b';
@@ -1552,3 +1533,24 @@ export const workflowQueryResult = {
],
},
} satisfies { workflows: RecordGqlConnection };
export const mockedWorkflow = workflowQueryResult.workflows.edges[0].node;
export const mockedWorkflowVersion = mockedWorkflow.versions.edges[0].node;
export const mockedWorkflowVersions = {
...mockedWorkflow.versions,
__typename: 'WorkflowVersionConnection' as const,
totalCount: 1,
pageInfo: {
__typename: 'PageInfo' as const,
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
endCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
},
};
export const mockedWorkflowNodeId = mockedWorkflowVersion.steps[0].id;
@@ -0,0 +1,28 @@
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
import { generateMockRecord } from '~/testing/utils/generateMockRecordNode';
export const generateMockRecordConnection = ({
objectNameSingular,
records,
computeReferences = false,
}: {
objectNameSingular: string;
records: Record<string, unknown>[];
computeReferences?: boolean;
}) => {
const objectMetadataItem =
getMockObjectMetadataItemOrThrow(objectNameSingular);
const prefilledRecords = records.map((recordInput) =>
generateMockRecord({ objectNameSingular, input: recordInput }),
);
return getRecordConnectionFromRecords({
objectMetadataItems: generatedMockObjectMetadataItems,
objectMetadataItem,
records: prefilledRecords,
computeReferences,
});
};
@@ -1,37 +1,44 @@
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromObject';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { prefillRecord } from '@/object-record/utils/prefillRecord';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
type GenerateEmptyJestRecordNodeArgs = {
objectNameSingular: string;
input: Record<string, unknown>;
withDepthOneRelation?: boolean;
};
export const generateEmptyJestRecordNode = ({
export const generateMockRecord = ({
objectNameSingular,
input,
withDepthOneRelation = false,
}: GenerateEmptyJestRecordNodeArgs) => {
}: {
objectNameSingular: string;
input: Record<string, unknown>;
}): ObjectRecord => {
const objectMetadataItem =
getMockObjectMetadataItemOrThrow(objectNameSingular);
if (!objectMetadataItem) {
throw new Error(
`ObjectMetadataItem not found for objectNameSingular: ${objectNameSingular} while generating empty Jest record node`,
);
}
return prefillRecord({ objectMetadataItem, input });
};
const prefilledRecord = prefillRecord({
objectMetadataItem,
input,
});
export const generateMockRecordNode = ({
objectNameSingular,
input,
withDepthOneRelation = false,
computeReferences,
}: {
objectNameSingular: string;
input: Record<string, unknown>;
withDepthOneRelation?: boolean;
computeReferences?: boolean;
}) => {
const objectMetadataItem =
getMockObjectMetadataItemOrThrow(objectNameSingular);
const record = generateMockRecord({ objectNameSingular, input });
return getRecordNodeFromRecord({
record: prefilledRecord,
record,
objectMetadataItem,
objectMetadataItems: generatedMockObjectMetadataItems,
computeReferences,
recordGqlFields: withDepthOneRelation
? generateDepthRecordGqlFieldsFromObject({
objectMetadataItem,
@@ -2,7 +2,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata';
export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
mockedStandardObjectMetadataQueryResult.objects.edges.map((edge) => {