Recursively turn relation connection into records (#3334)

* Use new ObjectRecordConnection

* Use new records without connection in GraphQLView

* Added playwright for storybook tests

* Fixed lint

* Fixed test and tsc

* Fixed storybook tests

* wip tests

* Added useMapConnectionToRecords unit test

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau 2024-01-11 20:27:59 +01:00 committed by GitHub
parent 299bed511f
commit e2bdf0ce45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1252 additions and 266 deletions

View File

@ -241,6 +241,7 @@
"msw": "^2.0.11",
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
"nx": "^17.2.8",
"playwright": "^1.40.1",
"prettier": "^3.1.1",
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",

View File

@ -9,14 +9,16 @@
"start:clean": "yarn start --force",
"build": "tsc && vite build && yarn build:inject-runtime-env",
"build:inject-runtime-env": "sh ./scripts/inject-runtime-env.sh",
"tsc:spec": "tsc --project tsconfig.spec.json --noEmit",
"tsc": "tsc --project tsconfig.app.json --watch",
"tsc:ci": "tsc --project tsconfig.app.json",
"tsc:ci": "tsc --project tsconfig.app.json --noEmit && tsc --project tsconfig.spec.json --noEmit && tsc --project tsconfig.node.json --noEmit",
"preview": "vite preview",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0 --config .eslintrc.cjs",
"lint:ci": "yarn lint --config .eslintrc-ci.cjs",
"fmt:fix": "prettier --cache --write \"src/**/*.ts\" \"src/**/*.tsx\"",
"fmt": "prettier --check \"src/**/*.ts\" \"src/**/*.tsx\"",
"test": "jest",
"test-watch": "jest --coverage=false --watch",
"tsup": "tsup",
"coverage": "jest --coverage",
"storybook:modules:dev": "STORYBOOK_SCOPE=modules yarn storybook:dev",
@ -29,6 +31,7 @@
"storybook:docs:build": "STORYBOOK_SCOPE=ui-docs yarn storybook:build",
"storybook:test": "test-storybook",
"storybook:test-slow": "test-storybook --maxWorkers=3",
"storybook:test-single-worker": "test-storybook --maxWorkers=1",
"storybook:coverage": "yarn storybook:test-slow --coverage && npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook --check-coverage",
"storybook:modules:coverage": "STORYBOOK_SCOPE=modules yarn storybook:coverage",
"storybook:pages:coverage": "STORYBOOK_SCOPE=pages yarn storybook:coverage",

View File

@ -4,7 +4,7 @@ import { produce } from 'immer';
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { isDefined } from '~/utils/isDefined';
import { capitalize } from '~/utils/string/capitalize';
@ -21,77 +21,39 @@ export const getRecordOptimisticEffectDefinition = ({
deletedRecordIds,
variables,
}) => {
const newRecordPaginatedCacheField = produce<
PaginatedRecordTypeResults<any>
>(currentData as PaginatedRecordTypeResults<any>, (draft) => {
const existingDataIsEmpty = !draft || !draft.edges || !draft.edges[0];
const newRecordPaginatedCacheField = produce<ObjectRecordConnection<any>>(
currentData as ObjectRecordConnection<any>,
(draft) => {
const existingDataIsEmpty = !draft || !draft.edges || !draft.edges[0];
if (isNonEmptyArray(createdRecords)) {
if (existingDataIsEmpty) {
return {
__typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
edges: createdRecords.map((createdRecord) => ({
node: createdRecord,
cursor: '',
})),
pageInfo: {
endCursor: '',
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
},
};
} else {
for (const createdRecord of createdRecords) {
const existingRecord = draft.edges.find(
(edge) => edge.node.id === createdRecord.id,
);
if (existingRecord) {
existingRecord.node = createdRecord;
continue;
}
draft.edges.unshift({
node: createdRecord,
cursor: '',
if (isNonEmptyArray(createdRecords)) {
if (existingDataIsEmpty) {
return {
__typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
});
}
}
}
if (isNonEmptyArray(deletedRecordIds)) {
draft.edges = draft.edges.filter(
(edge) => !deletedRecordIds.includes(edge.node.id),
);
}
if (isNonEmptyArray(updatedRecords)) {
for (const updatedRecord of updatedRecords) {
const updatedRecordIsOutOfQueryFilter =
isDefined(variables.filter) &&
!isRecordMatchingFilter({
record: updatedRecord,
filter: variables.filter,
objectMetadataItem,
});
if (updatedRecordIsOutOfQueryFilter) {
draft.edges = draft.edges.filter(
(edge) => edge.node.id !== updatedRecord.id,
);
edges: createdRecords.map((createdRecord) => ({
node: createdRecord,
cursor: '',
})),
pageInfo: {
endCursor: '',
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
},
};
} else {
const foundUpdatedRecordInCacheQuery = draft.edges.find(
(edge) => edge.node.id === updatedRecord.id,
);
for (const createdRecord of createdRecords) {
const existingRecord = draft.edges.find(
(edge) => edge.node.id === createdRecord.id,
);
if (foundUpdatedRecordInCacheQuery) {
foundUpdatedRecordInCacheQuery.node = updatedRecord;
} else {
// TODO: add order by
draft.edges.push({
node: updatedRecord,
if (existingRecord) {
existingRecord.node = createdRecord;
continue;
}
draft.edges.unshift({
node: createdRecord,
cursor: '',
__typename: `${capitalize(
objectMetadataItem.nameSingular,
@ -100,8 +62,49 @@ export const getRecordOptimisticEffectDefinition = ({
}
}
}
}
});
if (isNonEmptyArray(deletedRecordIds)) {
draft.edges = draft.edges.filter(
(edge) => !deletedRecordIds.includes(edge.node.id),
);
}
if (isNonEmptyArray(updatedRecords)) {
for (const updatedRecord of updatedRecords) {
const updatedRecordIsOutOfQueryFilter =
isDefined(variables.filter) &&
!isRecordMatchingFilter({
record: updatedRecord,
filter: variables.filter,
objectMetadataItem,
});
if (updatedRecordIsOutOfQueryFilter) {
draft.edges = draft.edges.filter(
(edge) => edge.node.id !== updatedRecord.id,
);
} else {
const foundUpdatedRecordInCacheQuery = draft.edges.find(
(edge) => edge.node.id === updatedRecord.id,
);
if (foundUpdatedRecordInCacheQuery) {
foundUpdatedRecordInCacheQuery.node = updatedRecord;
} else {
// TODO: add order by
draft.edges.push({
node: updatedRecord,
cursor: '',
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Edge`,
});
}
}
}
}
},
);
return newRecordPaginatedCacheField;
},

View File

@ -0,0 +1,752 @@
import { Company } from '@/companies/types/Company';
import { Favorite } from '@/favorites/types/Favorite';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { Person } from '@/people/types/Person';
export const emptyConnectionMock: ObjectRecordConnection = {
edges: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
__typename: 'ObjectRecordConnection',
};
export const companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock: ObjectRecordConnection<
Partial<Company> &
Pick<Company, 'id'> & {
people: ObjectRecordConnection<
Pick<Person, 'id' | 'name'> & {
favorites: ObjectRecordConnection<
Pick<Favorite, 'id' | 'personId' | 'companyId' | 'position'>
>;
}
>;
}
> = {
pageInfo: {
endCursor: 'WyJmZTI1NmIzOS0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'WyIwNGIyZTlmNS0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
},
edges: [
{
cursor: 'WyIwNGIyZTlmNS0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
node: {
id: '04b2e9f5-0713-40a5-8216-82802401d33e',
name: 'Qonto',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor: 'WyIwZDk0MDk5Ny1jMjFlLTRlYzItODczYi1kZTQyNjRkODkwMjUiXQ==',
node: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
people: {
edges: [
{
cursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190df',
name: {
firstName: 'Bertrand',
lastName: 'Voulzy',
},
favorites: {
edges: [
{
cursor:
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
node: {
id: 'c85a867c-5a8f-4861-8ed2-96c390248423',
personId: '240da2ec-2d40-4e49-8df4-9c6a049190df',
companyId: null,
position: 2,
},
},
],
pageInfo: {
endCursor:
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
},
},
},
},
{
cursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZWYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190ef',
name: {
firstName: 'Madison',
lastName: 'Perez',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor:
'WyI1Njk1NTQyMi01ZDU0LTQxYjctYmEzNi1mMGQyMGUxNDE3YWUiXQ==',
node: {
id: '56955422-5d54-41b7-ba36-f0d20e1417ae',
name: {
firstName: 'Avery',
lastName: 'Carter',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor:
'WyI3NTUwMzVkYi02MjNkLTQxZmUtOTJlNy1kZDQ1YjdjNTY4ZTEiXQ==',
node: {
id: '755035db-623d-41fe-92e7-dd45b7c568e1',
name: {
firstName: 'Ethan',
lastName: 'Mitchell',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor:
'WyJhMmU3OGE1Zi0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==',
node: {
id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35',
name: {
firstName: 'Elizabeth',
lastName: 'Baker',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor:
'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
node: {
id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016',
name: {
firstName: 'Christopher',
lastName: 'Nelson',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
],
pageInfo: {
endCursor:
'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
},
},
},
},
{
cursor: 'WyIxMTg5OTVmMy01ZDgxLTQ2ZDYtYmY4My1mN2ZkMzNlYTYxMDIiXQ==',
node: {
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
name: 'Facebook',
people: {
edges: [
{
cursor:
'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
node: {
id: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
name: {
firstName: 'Christopher',
lastName: 'Gonzalez',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor:
'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
node: {
id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e',
name: {
firstName: 'Ashley',
lastName: 'Parker',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
],
pageInfo: {
endCursor:
'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
},
},
},
},
{
cursor: 'WyIxZDNhMWM2ZS03MDdlLTQ0ZGMtYTFkMi0zMDAzMGJmMWE5NDQiXQ==',
node: {
id: '1d3a1c6e-707e-44dc-a1d2-30030bf1a944',
name: 'Netflix',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor: 'WyI0NjBiNmZiMS1lZDg5LTQxM2EtYjMxYS05NjI5ODZlNjdiYjQiXQ==',
node: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
people: {
edges: [
{
cursor:
'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
node: {
id: '1d151852-490f-4466-8391-733cfd66a0c8',
name: {
firstName: 'Isabella',
lastName: 'Scott',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor:
'WyI5ODQwNmUyNi04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==',
node: {
id: '98406e26-80f1-4dff-b570-a74942528de3',
name: {
firstName: 'Matthew',
lastName: 'Green',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor:
'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
node: {
id: '9b324a88-6784-4449-afdf-dc62cb8702f2',
name: {
firstName: 'Nicholas',
lastName: 'Wright',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
],
pageInfo: {
endCursor:
'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
},
},
},
},
{
cursor: 'WyI3YTkzZDFlNS0zZjc0LTQ5MmQtYTEwMS0yYTcwZjUwYTE2NDUiXQ==',
node: {
id: '7a93d1e5-3f74-492d-a101-2a70f50a1645',
name: 'Libeo',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor: 'WyI4OWJiODI1Yy0xNzFlLTRiY2MtOWNmNy00MzQ0OGQ2ZmIyNzgiXQ==',
node: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
name: 'Airbnb',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor: 'WyI5ZDE2MmRlNi1jZmJmLTQxNTYtYTc5MC1lMzk4NTRkY2Q0ZWIiXQ==',
node: {
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
name: 'Claap',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor: 'WyJhNjc0ZmE2Yy0xNDU1LTRjNTctYWZhZi1kZDVkYzA4NjM2MWQiXQ==',
node: {
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
name: 'Algolia',
people: {
edges: [
{
cursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049191df',
name: {
firstName: 'Lorie',
lastName: 'Vladim',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
],
pageInfo: {
endCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
},
},
},
},
{
cursor: 'WyJhN2JjNjhkNS1mNzllLTQwZGQtYmQwNi1jMzZlNmFiYjQ2NzgiXQ==',
node: {
id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678',
name: 'Samsung',
people: {
edges: [
{
cursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049191de',
name: {
firstName: 'Louis',
lastName: 'Duss',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
],
pageInfo: {
endCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
},
},
},
},
{
cursor: 'WyJhYWZmY2ZiZC1mODZiLTQxOWYtYjc5NC0wMjMxOWFiZTg2MzciXQ==',
node: {
id: 'aaffcfbd-f86b-419f-b794-02319abe8637',
name: 'Hasura',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor: 'WyJmMzNkYzI0Mi01NTE4LTQ1NTMtOTQzMy00MmQ4ZWI4MjgzNGIiXQ==',
node: {
id: 'f33dc242-5518-4553-9433-42d8eb82834b',
name: 'Wework',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
{
cursor: 'WyJmZTI1NmIzOS0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
node: {
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
name: 'Linkedin',
people: {
edges: [
{
cursor:
'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
node: {
id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
name: {
firstName: 'Sylvie',
lastName: 'Palmer',
},
favorites: {
edges: [
{
cursor:
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
node: {
id: '37b97140-26b9-498c-837b-4f3de499ad83',
personId: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
companyId: null,
position: 1,
},
},
],
pageInfo: {
endCursor:
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
},
},
},
},
{
cursor:
'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
node: {
id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
name: {
firstName: 'Christoph',
lastName: 'Callisto',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
},
},
},
],
pageInfo: {
endCursor:
'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
},
},
},
},
],
};
export const peopleWithTheirUniqueCompanies: ObjectRecordConnection<
Pick<Person, 'id'> & { company: Pick<Company, 'id' | 'name'> }
> = {
pageInfo: {
endCursor: 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
},
edges: [
{
cursor: 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
node: {
id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
company: {
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
name: 'Linkedin',
},
},
},
{
cursor: 'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
node: {
id: '1d151852-490f-4466-8391-733cfd66a0c8',
company: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
},
},
},
{
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190df',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZWYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190ef',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049191de',
company: {
id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678',
name: 'Samsung',
},
},
},
{
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049191df',
company: {
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
name: 'Algolia',
},
},
},
{
cursor: 'WyI1Njk1NTQyMi01ZDU0LTQxYjctYmEzNi1mMGQyMGUxNDE3YWUiXQ==',
node: {
id: '56955422-5d54-41b7-ba36-f0d20e1417ae',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyI3NTUwMzVkYi02MjNkLTQxZmUtOTJlNy1kZDQ1YjdjNTY4ZTEiXQ==',
node: {
id: '755035db-623d-41fe-92e7-dd45b7c568e1',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
node: {
id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
company: {
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
name: 'Linkedin',
},
},
},
{
cursor: 'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
node: {
id: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
company: {
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
name: 'Facebook',
},
},
},
{
cursor: 'WyI5ODQwNmUyNi04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==',
node: {
id: '98406e26-80f1-4dff-b570-a74942528de3',
company: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
},
},
},
{
cursor: 'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
node: {
id: '9b324a88-6784-4449-afdf-dc62cb8702f2',
company: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
},
},
},
{
cursor: 'WyJhMmU3OGE1Zi0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==',
node: {
id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
node: {
id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
node: {
id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e',
company: {
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
name: 'Facebook',
},
},
},
],
};

View File

@ -0,0 +1,190 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { renderHook } from '@testing-library/react';
import { Company } from '@/companies/types/Company';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import {
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
emptyConnectionMock,
peopleWithTheirUniqueCompanies,
} from '@/object-record/hooks/__mocks__/useMapConnectionToRecords';
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
import { Person } from '@/people/types/Person';
import { getJestHookWrapper } from '~/testing/jest/getJestHookWrapper';
import { isDefined } from '~/utils/isDefined';
const Wrapper = getJestHookWrapper({
apolloMocks: [],
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(objectMetadataItemsState, getObjectMetadataItemsMock());
},
});
describe('useMapConnectionToRecords', () => {
it('Empty edges - should return an empty array if no edge', async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Company,
objectRecordConnection: emptyConnectionMock,
depth: 5,
});
return records;
},
{
wrapper: Wrapper,
},
);
expect(Array.isArray(result.current)).toBe(true);
});
it('No relation fields - should return an array of company records', async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Company,
objectRecordConnection:
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
depth: 5,
});
return records;
},
{
wrapper: Wrapper,
},
);
expect(Array.isArray(result.current)).toBe(true);
});
it('n+1 relation fields - should return an array of company records with their people records', async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Company,
objectRecordConnection:
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
depth: 5,
});
return records;
},
{
wrapper: Wrapper,
},
);
const secondCompanyMock =
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock
.edges[1];
const secondCompanyPeopleMock = secondCompanyMock.node.people.edges.map(
(edge) => edge.node,
);
const companiesResult = result.current;
const secondCompanyResult = result.current[1];
const secondCompanyPeopleResult = secondCompanyResult.people;
expect(isNonEmptyArray(companiesResult)).toBe(true);
expect(secondCompanyResult.id).toBe(secondCompanyMock.node.id);
expect(isNonEmptyArray(secondCompanyPeopleResult)).toBe(true);
expect(secondCompanyPeopleResult[0].id).toEqual(
secondCompanyPeopleMock[0].id,
);
});
it('n+2 relation fields - should return an array of company records with their people records with their favorites records', async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Company,
objectRecordConnection:
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
depth: 5,
});
return records;
},
{
wrapper: Wrapper,
},
);
const secondCompanyMock =
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock
.edges[1];
const secondCompanyPeopleMock = secondCompanyMock.node.people;
const secondCompanyFirstPersonMock = secondCompanyPeopleMock.edges[0].node;
const secondCompanyFirstPersonFavoritesMock =
secondCompanyFirstPersonMock.favorites;
const companiesResult = result.current;
const secondCompanyResult = companiesResult[1];
const secondCompanyPeopleResult = secondCompanyResult.people;
const secondCompanyFirstPersonResult = secondCompanyPeopleResult[0];
const secondCompanyFirstPersonFavoritesResult =
secondCompanyFirstPersonResult.favorites;
expect(isNonEmptyArray(companiesResult)).toBe(true);
expect(secondCompanyResult.id).toBe(secondCompanyMock.node.id);
expect(isNonEmptyArray(secondCompanyPeopleResult)).toBe(true);
expect(secondCompanyFirstPersonResult.id).toEqual(
secondCompanyFirstPersonMock.id,
);
expect(isNonEmptyArray(secondCompanyFirstPersonFavoritesResult)).toBe(true);
expect(secondCompanyFirstPersonFavoritesResult[0].id).toEqual(
secondCompanyFirstPersonFavoritesMock.edges[0].node.id,
);
});
it("n+1 relation field TO_ONE_OBJECT - should return an array of people records with their company, mapConnectionToRecords shouldn't try to parse TO_ONE_OBJECT", async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Person,
objectRecordConnection: peopleWithTheirUniqueCompanies,
depth: 5,
});
return records as (Person & { company: Company })[];
},
{
wrapper: Wrapper,
},
);
const firstPersonMock = peopleWithTheirUniqueCompanies.edges[0].node;
const firstPersonsCompanyMock = firstPersonMock.company;
const peopleResult = result.current;
const firstPersonResult = result.current[0];
const firstPersonsCompanyresult = firstPersonResult.company;
expect(isNonEmptyArray(peopleResult)).toBe(true);
expect(firstPersonResult.id).toBe(firstPersonMock.id);
expect(isDefined(firstPersonsCompanyresult)).toBe(true);
expect(firstPersonsCompanyresult.id).toEqual(firstPersonsCompanyMock.id);
});
});

View File

@ -9,7 +9,11 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useRecordOptimisticEffect } from '@/object-metadata/hooks/useRecordOptimisticEffect';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@ -19,28 +23,24 @@ import { capitalize } from '~/utils/string/capitalize';
import { cursorFamilyState } from '../states/cursorFamilyState';
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState';
import { PaginatedRecordType } from '../types/PaginatedRecordType';
import {
PaginatedRecordTypeEdge,
PaginatedRecordTypeResults,
} from '../types/PaginatedRecordTypeResults';
import { ObjectRecordQueryResult } from '../types/ObjectRecordQueryResult';
import { mapPaginatedRecordsToRecords } from '../utils/mapPaginatedRecordsToRecords';
export const useFindManyRecords = <
RecordType extends { id: string } & Record<string, any>,
>({
export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
filter,
orderBy,
limit = DEFAULT_SEARCH_REQUEST_LIMIT,
onCompleted,
skip,
useRecordsWithoutConnection = false,
}: ObjectMetadataItemIdentifier & {
filter?: ObjectRecordQueryFilter;
orderBy?: OrderByField;
limit?: number;
onCompleted?: (data: PaginatedRecordTypeResults<RecordType>) => void;
onCompleted?: (data: ObjectRecordConnection<T>) => void;
skip?: boolean;
useRecordsWithoutConnection?: boolean;
}) => {
const findManyQueryStateIdentifier =
objectNameSingular +
@ -75,7 +75,7 @@ export const useFindManyRecords = <
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { data, loading, error, fetchMore } = useQuery<
PaginatedRecordType<RecordType>
ObjectRecordQueryResult<T>
>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem || !currentWorkspace,
variables: {
@ -88,10 +88,10 @@ export const useFindManyRecords = <
if (data?.[objectMetadataItem.namePlural]) {
setLastCursor(
data?.[objectMetadataItem.namePlural]?.pageInfo.endCursor,
data?.[objectMetadataItem.namePlural]?.pageInfo.endCursor ?? '',
);
setHasNextPage(
data?.[objectMetadataItem.namePlural]?.pageInfo.hasNextPage,
data?.[objectMetadataItem.namePlural]?.pageInfo.hasNextPage ?? false,
);
}
},
@ -125,7 +125,7 @@ export const useFindManyRecords = <
const nextEdges =
fetchMoreResult?.[objectMetadataItem.namePlural]?.edges;
let newEdges: PaginatedRecordTypeEdge<RecordType>[] = [];
let newEdges: ObjectRecordEdge<T>[] = [];
if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) {
newEdges = filterUniqueRecordEdgesByCursor([
@ -138,11 +138,11 @@ export const useFindManyRecords = <
if (data?.[objectMetadataItem.namePlural]) {
setLastCursor(
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo
.endCursor,
.endCursor ?? '',
);
setHasNextPage(
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo
.hasNextPage,
.hasNextPage ?? false,
);
}
@ -164,7 +164,7 @@ export const useFindManyRecords = <
pageInfo:
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
},
} as PaginatedRecordType<RecordType>);
} as ObjectRecordQueryResult<T>);
},
});
} catch (error) {
@ -198,18 +198,39 @@ export const useFindManyRecords = <
enqueueSnackBar,
]);
// TODO: remove this and use only mapConnectionToRecords when we've finished the refactor
const records = useMemo(
() =>
mapPaginatedRecordsToRecords({
pagedRecords: data,
objectNamePlural: objectMetadataItem.namePlural,
}),
}) as T[],
[data, objectMetadataItem],
);
const mapConnectionToRecords = useMapConnectionToRecords();
const recordsWithoutConnection = useMemo(
() =>
useRecordsWithoutConnection
? (mapConnectionToRecords({
objectRecordConnection: data?.[objectMetadataItem.namePlural],
objectNameSingular,
depth: 5,
}) as T[])
: [],
[
data,
objectNameSingular,
objectMetadataItem.namePlural,
mapConnectionToRecords,
useRecordsWithoutConnection,
],
);
return {
objectMetadataItem,
records: records as RecordType[],
records: useRecordsWithoutConnection ? recordsWithoutConnection : records,
loading,
error,
fetchMoreRecords,

View File

@ -0,0 +1,113 @@
import { useCallback } from 'react';
import { isNonEmptyArray } from '@sniptt/guards';
import { produce } from 'immer';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
export const useMapConnectionToRecords = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const mapConnectionToRecords = useCallback(
<T extends ObjectRecord>({
objectRecordConnection,
objectNameSingular,
objectNamePlural,
depth,
}: {
objectRecordConnection: ObjectRecordConnection<T> | undefined | null;
objectNameSingular?: string;
objectNamePlural?: string;
depth: number;
}): ObjectRecord[] => {
if (
!isDefined(objectRecordConnection) ||
!isNonEmptyArray(objectMetadataItems)
) {
return [];
}
const currentLevelObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular ||
objectMetadataItem.namePlural === objectNamePlural,
);
if (!currentLevelObjectMetadataItem) {
throw new Error(
`Could not find object metadata item for object name singular "${objectNameSingular}" in mapConnectionToRecords`,
);
}
const relationFields = currentLevelObjectMetadataItem.fields.filter(
(field) => field.type === FieldMetadataType.Relation,
);
const objectRecords = [
...(objectRecordConnection.edges?.map((edge) => edge.node) ?? []),
];
return produce(objectRecords, (objectRecordsDraft) => {
for (const objectRecordDraft of objectRecordsDraft) {
for (const relationField of relationFields) {
const relationType = parseFieldRelationType(relationField);
if (
relationType === 'TO_ONE_OBJECT' ||
relationType === 'FROM_ONE_OBJECT'
) {
continue;
}
const relatedObjectMetadataSingularName =
relationField.toRelationMetadata?.fromObjectMetadata
.nameSingular ??
relationField.fromRelationMetadata?.toObjectMetadata
.nameSingular ??
null;
const relationFieldMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular ===
relatedObjectMetadataSingularName,
);
if (
!relationFieldMetadataItem ||
!isDefined(relatedObjectMetadataSingularName)
) {
throw new Error(
`Could not find relation object metadata item for object name plural ${relationField.name} in mapConnectionToRecords`,
);
}
const relationConnection = objectRecordDraft?.[
relationField.name
] as ObjectRecordConnection | undefined | null;
if (!isDefined(relationConnection)) {
continue;
}
const relationConnectionMappedToRecords = mapConnectionToRecords({
objectRecordConnection: relationConnection,
objectNameSingular: relatedObjectMetadataSingularName,
depth: depth - 1,
});
(objectRecordDraft as any)[relationField.name] =
relationConnectionMappedToRecords;
}
}
}) as ObjectRecord[];
},
[objectMetadataItems],
);
return mapConnectionToRecords;
};

View File

@ -7,7 +7,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
@ -57,7 +57,7 @@ export const useObjectRecordBoard = () => {
objectNameSingular: CoreObjectNameSingular.PipelineStep,
filter: {},
onCompleted: useCallback(
(data: PaginatedRecordTypeResults<PipelineStep>) => {
(data: ObjectRecordConnection<PipelineStep>) => {
setSavedPipelineSteps(data.edges.map((edge) => edge.node));
},
[setSavedPipelineSteps],
@ -89,7 +89,7 @@ export const useObjectRecordBoard = () => {
},
},
onCompleted: useCallback(
(data: PaginatedRecordTypeResults<Company>) => {
(data: ObjectRecordConnection<Company>) => {
setSavedCompanies(data.edges.map((edge) => edge.node));
},
[setSavedCompanies],

View File

@ -1,11 +1,14 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
import { Nullable } from '~/types/Nullable';
export type ObjectRecordConnection = {
edges: ObjectRecordEdge[];
export type ObjectRecordConnection<T extends ObjectRecord = ObjectRecord> = {
__typename?: string;
edges: ObjectRecordEdge<T>[];
pageInfo: {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
startCursor?: string;
endCursor?: string;
startCursor?: Nullable<string>;
endCursor?: Nullable<string>;
};
};

View File

@ -1,6 +1,7 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export type ObjectRecordEdge = {
node: ObjectRecord;
export type ObjectRecordEdge<T extends ObjectRecord> = {
__typename?: string;
node: T;
cursor: string;
};

View File

@ -0,0 +1,6 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export type ObjectRecordQueryResult<T extends ObjectRecord> = {
[objectNamePlural: string]: ObjectRecordConnection<T>;
};

View File

@ -1,5 +0,0 @@
import { PaginatedRecordTypeResults } from './PaginatedRecordTypeResults';
export type PaginatedRecordType<RecordType extends { id: string }> = {
[objectNamePlural: string]: PaginatedRecordTypeResults<RecordType>;
};

View File

@ -1,19 +0,0 @@
export type PaginatedRecordTypeEdge<
RecordType extends { id: string } & Record<string, any>,
> = {
node: RecordType;
cursor: string;
__typename?: string;
};
export type PaginatedRecordTypeResults<
RecordType extends { id: string } & Record<string, any>,
> = {
__typename?: string;
edges: PaginatedRecordTypeEdge<RecordType>[];
pageInfo: {
hasNextPage: boolean;
startCursor: string;
endCursor: string;
};
};

View File

@ -1,9 +1,9 @@
import { PaginatedRecordTypeEdge } from '@/object-record/types/PaginatedRecordTypeResults';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
export const filterUniqueRecordEdgesByCursor = <
RecordType extends { id: string },
>(
arrayToFilter: PaginatedRecordTypeEdge<RecordType>[],
arrayToFilter: ObjectRecordEdge<RecordType>[],
) => {
const seenCursors = new Set();

View File

@ -41,6 +41,7 @@ export const ViewBarEffect = () => {
type: { eq: viewType },
objectMetadataId: { eq: viewObjectMetadataId },
},
useRecordsWithoutConnection: true,
});
useEffect(() => {

View File

@ -284,33 +284,11 @@ describe('useViewBar', () => {
viewBar.setAvailableSortDefinitions([sortDefinition]);
viewBar.loadViewSorts(
{
edges: [
{
node: viewSort,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
mockedUuid,
);
viewBar.loadViewSorts([viewSort], mockedUuid);
viewBar.setAvailableFilterDefinitions([filterDefinition]);
viewBar.loadViewFilters(
{
edges: [
{
node: viewFilter,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
mockedUuid,
);
viewBar.loadViewFilters([viewFilter], mockedUuid);
return { viewBar };
}, renderHookConfig);

View File

@ -159,18 +159,7 @@ describe('useViewBar > viewFields', () => {
await act(async () => {
result.current.viewBar.setAvailableFieldDefinitions([fieldDefinition]);
await result.current.viewBar.loadViewFields(
{
edges: [
{
node: viewField,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
currentViewId,
);
await result.current.viewBar.loadViewFields([viewField], currentViewId);
result.current.viewBar.setCurrentViewId(currentViewId);
});

View File

@ -69,18 +69,7 @@ describe('useViewBar > viewFilters', () => {
await act(async () => {
result.current.viewBar.setAvailableFilterDefinitions([filterDefinition]);
await result.current.viewBar.loadViewFilters(
{
edges: [
{
node: viewFilter,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
currentViewId,
);
await result.current.viewBar.loadViewFilters([viewFilter], currentViewId);
result.current.viewBar.setCurrentViewId(currentViewId);
});
@ -93,18 +82,7 @@ describe('useViewBar > viewFilters', () => {
viewBar.setAvailableFilterDefinitions([filterDefinition]);
viewBar.loadViewFilters(
{
edges: [
{
node: viewFilter,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
currentViewId,
);
viewBar.loadViewFilters([viewFilter], currentViewId);
viewBar.setCurrentViewId(currentViewId);
const { currentViewFiltersState } = useViewScopedStates({
@ -174,18 +152,7 @@ describe('useViewBar > viewFilters', () => {
viewBar.setAvailableFilterDefinitions([filterDefinition]);
viewBar.loadViewFilters(
{
edges: [
{
node: viewFilter,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
currentViewId,
);
viewBar.loadViewFilters([viewFilter], currentViewId);
viewBar.setCurrentViewId(currentViewId);
const { currentViewFiltersState } = useViewScopedStates({

View File

@ -65,18 +65,7 @@ describe('View Sorts', () => {
await act(async () => {
result.current.viewBar.setAvailableSortDefinitions([sortDefinition]);
await result.current.viewBar.loadViewSorts(
{
edges: [
{
node: viewSort,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
currentViewId,
);
await result.current.viewBar.loadViewSorts([viewSort], currentViewId);
result.current.viewBar.setCurrentViewId(currentViewId);
});
@ -89,18 +78,7 @@ describe('View Sorts', () => {
viewBar.setAvailableSortDefinitions([sortDefinition]);
viewBar.loadViewSorts(
{
edges: [
{
node: viewSort,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
currentViewId,
);
viewBar.loadViewSorts([viewSort], currentViewId);
viewBar.setCurrentViewId(currentViewId);
const { currentViewSortsState } = useViewScopedStates({
@ -161,18 +139,7 @@ describe('View Sorts', () => {
viewBar.setAvailableSortDefinitions([sortDefinition]);
viewBar.loadViewSorts(
{
edges: [
{
node: viewSort,
cursor: '',
},
],
pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
},
currentViewId,
);
viewBar.loadViewSorts([viewSort], currentViewId);
viewBar.setCurrentViewId(currentViewId);
const { currentViewSortsState } = useViewScopedStates({

View File

@ -3,7 +3,6 @@ import { useSearchParams } from 'react-router-dom';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { ViewField } from '@/views/types/ViewField';
import { ViewFilter } from '@/views/types/ViewFilter';
@ -93,10 +92,7 @@ export const useViewBar = (props?: UseViewProps) => {
const loadViewFields = useRecoilCallback(
({ snapshot, set }) =>
async (
data: PaginatedRecordTypeResults<ViewField>,
currentViewId: string,
) => {
async (viewFields: ViewField[], currentViewId: string) => {
const {
availableFieldDefinitions,
onViewFieldsChange,
@ -119,8 +115,8 @@ export const useViewBar = (props?: UseViewProps) => {
return;
}
const queriedViewFields = data.edges
.map((viewField) => viewField.node)
const queriedViewFields = viewFields
.map((viewField) => viewField)
.filter(assertNotNull);
if (isPersistingView) {
@ -138,10 +134,7 @@ export const useViewBar = (props?: UseViewProps) => {
const loadViewFilters = useRecoilCallback(
({ snapshot, set }) =>
async (
data: PaginatedRecordTypeResults<Required<ViewFilter>>,
currentViewId: string,
) => {
async (viewFilters: ViewFilter[], currentViewId: string) => {
const {
availableFilterDefinitions,
savedViewFilters,
@ -163,18 +156,18 @@ export const useViewBar = (props?: UseViewProps) => {
return;
}
const queriedViewFilters = data.edges
.map(({ node }) => {
const queriedViewFilters = viewFilters
.map((viewFilter) => {
const availableFilterDefinition = availableFilterDefinitions.find(
(filterDefinition) =>
filterDefinition.fieldMetadataId === node.fieldMetadataId,
filterDefinition.fieldMetadataId === viewFilter.fieldMetadataId,
);
if (!availableFilterDefinition) return null;
return {
...node,
displayValue: node.displayValue ?? node.value,
...viewFilter,
displayValue: viewFilter.displayValue ?? viewFilter.value,
definition: availableFilterDefinition,
};
})
@ -191,10 +184,7 @@ export const useViewBar = (props?: UseViewProps) => {
const loadViewSorts = useRecoilCallback(
({ snapshot, set }) =>
async (
data: PaginatedRecordTypeResults<Required<ViewSort>>,
currentViewId: string,
) => {
async (viewSorts: Required<ViewSort>[], currentViewId: string) => {
const { availableSortDefinitions, savedViewSorts, onViewSortsChange } =
getViewScopedStateValuesFromSnapshot({
snapshot,
@ -213,18 +203,18 @@ export const useViewBar = (props?: UseViewProps) => {
return;
}
const queriedViewSorts = data.edges
.map(({ node }) => {
const queriedViewSorts = viewSorts
.map((viewSort) => {
const availableSortDefinition = availableSortDefinitions.find(
(sort) => sort.fieldMetadataId === node.fieldMetadataId,
(sort) => sort.fieldMetadataId === viewSort.fieldMetadataId,
);
if (!availableSortDefinition) return null;
return {
id: node.id,
fieldMetadataId: node.fieldMetadataId,
direction: node.direction,
id: viewSort.id,
fieldMetadataId: viewSort.fieldMetadataId,
direction: viewSort.direction,
definition: availableSortDefinition,
};
})

View File

@ -1,4 +1,3 @@
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { ViewField } from '@/views/types/ViewField';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewSort } from '@/views/types/ViewSort';
@ -7,7 +6,7 @@ export type GraphQLView = {
id: string;
name: string;
objectMetadataId: string;
viewFields: PaginatedRecordTypeResults<ViewField>;
viewFilters: PaginatedRecordTypeResults<ViewFilter>;
viewSorts: PaginatedRecordTypeResults<ViewSort>;
viewFields: ViewField[];
viewFilters: ViewFilter[];
viewSorts: ViewSort[];
};

View File

@ -7,7 +7,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -93,7 +93,7 @@ export const SettingsObjectNewFieldStep2 = () => {
type: { eq: ViewType.Table },
objectMetadataId: { eq: activeObjectMetadataItem?.id },
},
onCompleted: async (data: PaginatedRecordTypeResults<View>) => {
onCompleted: async (data: ObjectRecordConnection<View>) => {
const views = data.edges;
if (!views) return;
@ -109,7 +109,7 @@ export const SettingsObjectNewFieldStep2 = () => {
type: { eq: ViewType.Table },
objectMetadataId: { eq: formValues.relation?.objectMetadataId },
},
onCompleted: async (data: PaginatedRecordTypeResults<View>) => {
onCompleted: async (data: ObjectRecordConnection<View>) => {
const views = data.edges;
if (!views) return;

View File

@ -0,0 +1,25 @@
import { ReactNode } from 'react';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { MutableSnapshot, RecoilRoot } from 'recoil';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
export const getJestHookWrapper = ({
apolloMocks,
onInitializeRecoilSnapshot,
}: {
apolloMocks:
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
| undefined;
onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void;
}) => {
return ({ children }: { children: ReactNode }) => (
<RecoilRoot initializeState={onInitializeRecoilSnapshot}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<MockedProvider mocks={apolloMocks} addTypename={false}>
{children}
</MockedProvider>
</SnackBarProviderScope>
</RecoilRoot>
);
};

View File

@ -35183,7 +35183,7 @@ __metadata:
languageName: node
linkType: hard
"playwright@npm:^1.14.0":
"playwright@npm:^1.14.0, playwright@npm:^1.40.1":
version: 1.40.1
resolution: "playwright@npm:1.40.1"
dependencies:
@ -41675,6 +41675,7 @@ __metadata:
patch-package: "npm:^8.0.0"
pg: "npm:^8.11.3"
pg-boss: "npm:^9.0.3"
playwright: "npm:^1.40.1"
prettier: "npm:^3.1.1"
prism-react-renderer: "npm:^2.1.0"
raw-loader: "npm:^4.0.2"