[REQUIRES_CACHE_FLUSH][GQL_VIEW_GROUP_API_BREAKING_CHANGE] ViewGroup in v2 (#15052)

# Introduction
Adding view-group to core engine v2
Following https://github.com/twentyhq/twenty/pull/15010 ( same pattern )

## What's done
- Created flat-view-group
- flat view group runner
- flat view group builder
- create view group service v2 and input transpilers
- refactor the existing view group resolver to fix standard (
BREAKING_CHANGE on graphql api update especially ) REST stays the same
- refactored the front to consume the mutations autogenerated

close https://github.com/twentyhq/core-team-issues/issues/1665
This commit is contained in:
Paul Rastoin
2025-10-14 15:55:50 +02:00
committed by GitHub
parent 5fec6a9afc
commit 7d747c9876
88 changed files with 2640 additions and 497 deletions
@@ -58,10 +58,14 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
fieldMetadataId: randomFieldForKanban,
} satisfies ViewGroup);
await createViewGroupRecords({
viewGroupsToCreate,
viewId: currentViewId,
});
await createViewGroupRecords(
viewGroupsToCreate.map((viewGroup) => ({
input: {
...viewGroup,
viewId: currentViewId,
},
})),
);
return viewGroupsToCreate;
},
@@ -106,14 +106,22 @@ export const useHandleRecordGroupField = () => {
);
if (viewGroupsToCreate.length > 0) {
await createViewGroupRecords({ viewGroupsToCreate, viewId: view.id });
await createViewGroupRecords(
viewGroupsToCreate.map((viewGroup) => ({
input: {
...viewGroup,
viewId: view.id,
},
})),
);
}
if (viewGroupsToDelete.length > 0) {
await deleteViewGroupRecords(
viewGroupsToDelete.map((group) => ({
id: group.id,
viewId: view.id,
input: {
id: group.id,
},
})),
);
}
@@ -151,8 +159,9 @@ export const useHandleRecordGroupField = () => {
await deleteViewGroupRecords(
view.viewGroups.map((group) => ({
id: group.id,
viewId: view.id,
input: {
id: group.id,
},
})),
);
@@ -8,5 +8,8 @@ export const VIEW_GROUP_FRAGMENT = gql`
fieldValue
position
viewId
createdAt
updatedAt
deletedAt
}
`;
@@ -1,7 +1,11 @@
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { gql } from '@apollo/client';
export const DELETE_CORE_VIEW_GROUP = gql`
mutation DeleteCoreViewGroup($id: String!) {
deleteCoreViewGroup(id: $id)
${VIEW_GROUP_FRAGMENT}
mutation DeleteCoreViewGroup($input: DeleteViewGroupInput!) {
deleteCoreViewGroup(input: $input) {
...ViewGroupFragment
}
}
`;
@@ -1,7 +1,11 @@
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { gql } from '@apollo/client';
export const DESTROY_CORE_VIEW_GROUP = gql`
mutation DestroyCoreViewGroup($id: String!) {
destroyCoreViewGroup(id: $id)
${VIEW_GROUP_FRAGMENT}
mutation DestroyCoreViewGroup($input: DestroyViewGroupInput!) {
destroyCoreViewGroup(input: $input) {
...ViewGroupFragment
}
}
`;
@@ -3,8 +3,8 @@ import { gql } from '@apollo/client';
export const UPDATE_CORE_VIEW_GROUP = gql`
${VIEW_GROUP_FRAGMENT}
mutation UpdateCoreViewGroup($id: String!, $input: UpdateViewGroupInput!) {
updateCoreViewGroup(id: $id, input: $input) {
mutation UpdateCoreViewGroup($input: UpdateViewGroupInput!) {
updateCoreViewGroup(input: $input) {
...ViewGroupFragment
}
}
@@ -1,115 +1,128 @@
import { useCallback } from 'react';
import { CREATE_CORE_VIEW_GROUP } from '@/views/graphql/mutations/createCoreViewGroup';
import { DESTROY_CORE_VIEW_GROUP } from '@/views/graphql/mutations/destroyCoreViewGroup';
import { UPDATE_CORE_VIEW_GROUP } from '@/views/graphql/mutations/updateCoreViewGroup';
import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { useApolloClient } from '@apollo/client';
import { type CoreViewGroup } from '~/generated/graphql';
type CreateViewGroupRecordsArgs = {
viewGroupsToCreate: ViewGroup[];
viewId: string;
};
import { isDefined } from 'twenty-shared/utils';
import {
type CreateCoreViewGroupMutationVariables,
type DeleteCoreViewGroupMutationVariables,
type DestroyCoreViewGroupMutationVariables,
type UpdateCoreViewGroupMutationVariables,
useCreateCoreViewGroupMutation,
useDeleteCoreViewGroupMutation,
useDestroyCoreViewGroupMutation,
useUpdateCoreViewGroupMutation,
} from '~/generated/graphql';
export const usePersistViewGroupRecords = () => {
const apolloClient = useApolloClient();
const { triggerViewGroupOptimisticEffect } =
useTriggerViewGroupOptimisticEffect();
const [createCoreViewGroupMutation] = useCreateCoreViewGroupMutation();
const [updateCoreViewGroupMutation] = useUpdateCoreViewGroupMutation();
const [deleteCoreViewGroupMutation] = useDeleteCoreViewGroupMutation();
const [destroyCoreViewGroupMutation] = useDestroyCoreViewGroupMutation();
const createCoreViewGroupRecords = useCallback(
({ viewGroupsToCreate, viewId }: CreateViewGroupRecordsArgs) => {
if (viewGroupsToCreate.length === 0) return;
(createCoreViewGroupInputs: CreateCoreViewGroupMutationVariables[]) => {
if (createCoreViewGroupInputs.length === 0) {
return;
}
return Promise.all(
viewGroupsToCreate.map((viewGroup) =>
apolloClient.mutate({
mutation: CREATE_CORE_VIEW_GROUP,
variables: {
input: {
id: viewGroup.id,
viewId,
fieldMetadataId: viewGroup.fieldMetadataId,
fieldValue: viewGroup.fieldValue,
isVisible: viewGroup.isVisible,
position: viewGroup.position,
},
},
createCoreViewGroupInputs.map((variables) =>
createCoreViewGroupMutation({
variables,
update: (_cache, { data }) => {
const record = data?.['createCoreViewGroup'];
if (!record) return;
const createdViewGroup = data?.createCoreViewGroup;
if (!isDefined(createdViewGroup)) {
return;
}
triggerViewGroupOptimisticEffect({
createdViewGroups: [record],
createdViewGroups: [createdViewGroup],
});
},
}),
),
);
},
[apolloClient, triggerViewGroupOptimisticEffect],
[triggerViewGroupOptimisticEffect, createCoreViewGroupMutation],
);
const updateCoreViewGroupRecords = useCallback(
async (viewGroupsToUpdate: ViewGroup[]) => {
if (!viewGroupsToUpdate.length) return;
const mutationPromises = viewGroupsToUpdate.map((viewGroup) =>
apolloClient.mutate<{ updateCoreViewGroup: CoreViewGroup }>({
mutation: UPDATE_CORE_VIEW_GROUP,
variables: {
id: viewGroup.id,
input: {
isVisible: viewGroup.isVisible,
position: viewGroup.position,
},
},
// Avoid cache being updated with stale data
fetchPolicy: 'no-cache',
update: (_cache, { data }) => {
const record = data?.['updateCoreViewGroup'];
if (!record) return;
triggerViewGroupOptimisticEffect({
updatedViewGroups: [record],
});
},
}),
);
return Promise.all(mutationPromises);
},
[apolloClient, triggerViewGroupOptimisticEffect],
);
const deleteCoreViewGroupRecords = useCallback(
async (viewGroupsToDelete: Pick<CoreViewGroup, 'id' | 'viewId'>[]) => {
if (!viewGroupsToDelete.length) return;
(updateCoreViewGroupInputs: UpdateCoreViewGroupMutationVariables[]) => {
if (updateCoreViewGroupInputs.length === 0) {
return;
}
return Promise.all(
viewGroupsToDelete.map((viewGroup) =>
apolloClient.mutate({
mutation: DESTROY_CORE_VIEW_GROUP,
variables: {
id: viewGroup.id,
},
update: () => {
updateCoreViewGroupInputs.map((variables) =>
updateCoreViewGroupMutation({
variables,
update: (_cache, { data }) => {
const updatedViewGroup = data?.updateCoreViewGroup;
if (!isDefined(updatedViewGroup)) {
return;
}
triggerViewGroupOptimisticEffect({
deletedViewGroups: [viewGroup],
updatedViewGroups: [updatedViewGroup],
});
},
}),
),
);
},
[apolloClient, triggerViewGroupOptimisticEffect],
[triggerViewGroupOptimisticEffect, updateCoreViewGroupMutation],
);
const deleteCoreViewGroupRecords = useCallback(
(deleteCoreViewGroupInputs: DeleteCoreViewGroupMutationVariables[]) => {
if (deleteCoreViewGroupInputs.length === 0) {
return;
}
return Promise.all(
deleteCoreViewGroupInputs.map((variables) =>
deleteCoreViewGroupMutation({
variables,
update: (_cache, { data }) => {
const deletedViewGroup = data?.deleteCoreViewGroup;
if (!isDefined(deletedViewGroup)) {
return;
}
triggerViewGroupOptimisticEffect({
deletedViewGroups: [deletedViewGroup],
});
},
}),
),
);
},
[triggerViewGroupOptimisticEffect, deleteCoreViewGroupMutation],
);
const destroyCoreViewGroupRecords = useCallback(
(destroyCoreViewGroupInputs: DestroyCoreViewGroupMutationVariables[]) => {
if (destroyCoreViewGroupInputs.length === 0) {
return;
}
return Promise.all(
destroyCoreViewGroupInputs.map((variables) =>
destroyCoreViewGroupMutation({
variables,
}),
),
);
},
[destroyCoreViewGroupMutation],
);
return {
createViewGroupRecords: createCoreViewGroupRecords,
updateViewGroupRecords: updateCoreViewGroupRecords,
deleteViewGroupRecords: deleteCoreViewGroupRecords,
destroyViewGroupRecords: destroyCoreViewGroupRecords,
};
};
@@ -194,10 +194,14 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
fieldMetadataId: kanbanFieldMetadataId,
} satisfies ViewGroup);
await createViewGroupRecords({
viewGroupsToCreate,
viewId: newViewId,
});
await createViewGroupRecords(
viewGroupsToCreate.map((viewGroup) => ({
input: {
...viewGroup,
viewId: newViewId,
},
})),
);
}
if (shouldCopyFiltersAndSortsAndAggregate === true) {
@@ -63,7 +63,17 @@ export const useSaveCurrentViewGroups = () => {
}
await updateViewGroupRecords([
{ ...viewGroupToSave, id: existingField.id },
{
input: {
id: existingField.id,
update: {
isVisible: viewGroupToSave.isVisible,
position: viewGroupToSave.position,
fieldMetadataId: viewGroupToSave.fieldMetadataId,
fieldValue: viewGroupToSave.fieldValue,
},
},
},
]);
},
[
@@ -118,7 +128,17 @@ export const useSaveCurrentViewGroups = () => {
return undefined;
}
return { ...viewGroupToSave, id: existingField.id };
return {
input: {
id: existingField.id,
update: {
isVisible: viewGroupToSave.isVisible,
position: viewGroupToSave.position,
fieldMetadataId: viewGroupToSave.fieldMetadataId,
fieldValue: viewGroupToSave.fieldValue,
},
},
};
})
.filter(isDefined);
@@ -131,7 +151,14 @@ export const useSaveCurrentViewGroups = () => {
);
await Promise.all([
createViewGroupRecords({ viewGroupsToCreate, viewId: view.id }),
createViewGroupRecords(
viewGroupsToCreate.map((viewGroup) => ({
input: {
...viewGroup,
viewId: view.id,
},
})),
),
updateViewGroupRecords(viewGroupsToUpdate),
]);
},
@@ -6,7 +6,11 @@ import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewGroup } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type UpdatedDeletedCoreViewGroup = {
createdViewGroups?: Omit<CoreViewGroup, 'workspaceId'>[];
updatedViewGroups?: Omit<CoreViewGroup, 'workspaceId'>[];
deletedViewGroups?: Pick<CoreViewGroup, 'id' | 'viewId'>[];
};
export const useTriggerViewGroupOptimisticEffect = () => {
const apolloClient = useApolloClient();
@@ -18,11 +22,7 @@ export const useTriggerViewGroupOptimisticEffect = () => {
createdViewGroups = [],
updatedViewGroups = [],
deletedViewGroups = [],
}: {
createdViewGroups?: CoreViewGroup[];
updatedViewGroups?: CoreViewGroup[];
deletedViewGroups?: Pick<CoreViewGroup, 'id' | 'viewId'>[];
}) => {
}: UpdatedDeletedCoreViewGroup) => {
const coreViews = getSnapshotValue(snapshot, coreViewsState);
let newCoreViews = [...coreViews];