-
Notifications
You must be signed in to change notification settings - Fork 5.7k
feat: add atomic upsertFieldsWidget mutation to replace multiple view field group/field API calls
#18137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add atomic upsertFieldsWidget mutation to replace multiple view field group/field API calls
#18137
Changes from 3 commits
1d41338
7652ca0
c4069c7
6ff7a5b
8148f92
0917fd0
72c2267
c556e1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment'; | ||
| import { gql } from '@apollo/client'; | ||
|
|
||
| export const UPSERT_FIELDS_WIDGET = gql` | ||
| ${VIEW_FIELD_GROUP_FRAGMENT} | ||
| mutation UpsertFieldsWidget($input: UpsertFieldsWidgetInput!) { | ||
| upsertFieldsWidget(input: $input) { | ||
| ...ViewFieldGroupFragment | ||
| } | ||
| } | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,31 @@ | ||
| import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; | ||
| import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState'; | ||
| import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; | ||
| import { computeFieldsWidgetFieldDiff } from '@/page-layout/utils/computeFieldsWidgetFieldDiff'; | ||
| import { computeFieldsWidgetGroupDiff } from '@/page-layout/utils/computeFieldsWidgetGroupDiff'; | ||
| import { UPSERT_FIELDS_WIDGET } from '@/page-layout/graphql/mutations/upsertFieldsWidget'; | ||
| import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; | ||
| import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; | ||
| import { usePerformViewFieldGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFieldGroupAPIPersist'; | ||
| import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews'; | ||
| import { useMutation } from '@apollo/client'; | ||
| import { useRecoilCallback } from 'recoil'; | ||
| import { isDefined } from 'twenty-shared/utils'; | ||
| import { | ||
| type FieldsConfiguration, | ||
| WidgetConfigurationType, | ||
| } from '~/generated-metadata/graphql'; | ||
| import { type CoreViewFieldGroup } from '~/generated-metadata/graphql'; | ||
|
|
||
| type UpsertFieldsWidgetInput = { | ||
| widgetId: string; | ||
| groups: { | ||
| id: string; | ||
| name: string; | ||
| position: number; | ||
| isVisible: boolean; | ||
| fields: { | ||
| viewFieldId: string; | ||
| isVisible: boolean; | ||
| position: number; | ||
| }[]; | ||
| }[]; | ||
| }; | ||
|
|
||
| type UpsertFieldsWidgetResult = { | ||
| upsertFieldsWidget: CoreViewFieldGroup[]; | ||
| }; | ||
|
|
||
| type UseSaveFieldsWidgetGroupsParams = { | ||
| pageLayoutId: string; | ||
|
|
@@ -31,52 +44,13 @@ export const useSaveFieldsWidgetGroups = ({ | |
| pageLayoutId, | ||
| ); | ||
|
|
||
| const pageLayoutPersistedState = useRecoilComponentCallbackState( | ||
| pageLayoutPersistedComponentState, | ||
| pageLayoutId, | ||
| ); | ||
|
|
||
| const { | ||
| performViewFieldGroupAPICreate, | ||
| performViewFieldGroupAPIUpdate, | ||
| performViewFieldGroupAPIDelete, | ||
| } = usePerformViewFieldGroupAPIPersist(); | ||
|
|
||
| const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist(); | ||
| const [upsertFieldsWidgetMutation] = useMutation< | ||
| UpsertFieldsWidgetResult, | ||
| { input: UpsertFieldsWidgetInput } | ||
| >(UPSERT_FIELDS_WIDGET); | ||
|
|
||
| const { refreshAllCoreViews } = useRefreshAllCoreViews(); | ||
|
|
||
| const getViewIdForWidget = useRecoilCallback( | ||
| ({ snapshot }) => | ||
| (widgetId: string): string | null => { | ||
| const pageLayoutPersisted = snapshot | ||
| .getLoadable(pageLayoutPersistedState) | ||
| .getValue(); | ||
|
|
||
| if (!isDefined(pageLayoutPersisted)) { | ||
| return null; | ||
| } | ||
|
|
||
| for (const tab of pageLayoutPersisted.tabs) { | ||
| for (const widget of tab.widgets) { | ||
| if ( | ||
| widget.id === widgetId && | ||
| isDefined(widget.configuration) && | ||
| widget.configuration.configurationType === | ||
| WidgetConfigurationType.FIELDS | ||
| ) { | ||
| return ( | ||
| (widget.configuration as FieldsConfiguration).viewId ?? null | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| }, | ||
| [pageLayoutPersistedState], | ||
| ); | ||
|
|
||
| const saveFieldsWidgetGroups = useRecoilCallback( | ||
| ({ set, snapshot }) => | ||
| async () => { | ||
|
|
@@ -94,73 +68,33 @@ export const useSaveFieldsWidgetGroups = ({ | |
|
|
||
| for (const widgetId of widgetIds) { | ||
| const draftGroups = allDraftGroups[widgetId] ?? []; | ||
| const persistedGroups = allPersistedGroups[widgetId] ?? []; | ||
|
|
||
| if (draftGroups.length === 0 && persistedGroups.length === 0) { | ||
| continue; | ||
| } | ||
|
|
||
| const viewId = getViewIdForWidget(widgetId); | ||
|
|
||
| if (!isDefined(viewId)) { | ||
| continue; | ||
| } | ||
|
|
||
| const { createdGroups, deletedGroups, updatedGroups } = | ||
| computeFieldsWidgetGroupDiff(persistedGroups, draftGroups); | ||
|
|
||
| if (createdGroups.length > 0) { | ||
| await performViewFieldGroupAPICreate({ | ||
| inputs: createdGroups.map((group) => ({ | ||
| id: group.id, | ||
| name: group.name, | ||
| position: group.position, | ||
| isVisible: group.isVisible, | ||
| viewId, | ||
| })), | ||
| }); | ||
| } | ||
|
|
||
| if (deletedGroups.length > 0) { | ||
| for (const group of deletedGroups) { | ||
| await performViewFieldGroupAPIDelete([ | ||
| { input: { id: group.id } }, | ||
| ]); | ||
| } | ||
| } | ||
|
|
||
| if (updatedGroups.length > 0) { | ||
| const updates = updatedGroups.map((group) => ({ | ||
|
|
||
| await upsertFieldsWidgetMutation({ | ||
| variables: { | ||
| input: { | ||
| id: group.id, | ||
| update: { | ||
| widgetId, | ||
| groups: draftGroups.map((group) => ({ | ||
| id: group.id, | ||
| name: group.name, | ||
| position: group.position, | ||
| isVisible: group.isVisible, | ||
| }, | ||
| fields: group.fields.flatMap((field) => { | ||
| if (!isDefined(field.viewFieldId)) { | ||
| return []; | ||
| } | ||
|
|
||
| return [ | ||
| { | ||
| viewFieldId: field.viewFieldId, | ||
| isVisible: field.isVisible, | ||
| position: field.position, | ||
| }, | ||
| ]; | ||
| }), | ||
|
Comment on lines
+86
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
| })), | ||
| }, | ||
| })); | ||
|
|
||
| await performViewFieldGroupAPIUpdate(updates); | ||
| } | ||
|
|
||
| const fieldUpdates = computeFieldsWidgetFieldDiff( | ||
| persistedGroups, | ||
| draftGroups, | ||
| ); | ||
|
|
||
| if (fieldUpdates.length > 0) { | ||
| const viewFieldUpdateInputs = fieldUpdates.map( | ||
| ({ viewFieldId, ...updates }) => ({ | ||
| input: { | ||
| id: viewFieldId, | ||
| update: updates, | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| await performViewFieldAPIUpdate(viewFieldUpdateInputs); | ||
| } | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| set(fieldsWidgetGroupsPersistedState, allDraftGroups); | ||
|
Comment on lines
74
to
105
|
||
|
|
@@ -172,11 +106,7 @@ export const useSaveFieldsWidgetGroups = ({ | |
| [ | ||
| fieldsWidgetGroupsDraftState, | ||
| fieldsWidgetGroupsPersistedState, | ||
| getViewIdForWidget, | ||
| performViewFieldGroupAPICreate, | ||
| performViewFieldGroupAPIDelete, | ||
| performViewFieldGroupAPIUpdate, | ||
| performViewFieldAPIUpdate, | ||
| upsertFieldsWidgetMutation, | ||
| refreshAllCoreViews, | ||
| ], | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { Field, InputType } from '@nestjs/graphql'; | ||
|
|
||
| import { IsBoolean, IsNotEmpty, IsNumber, IsUUID } from 'class-validator'; | ||
|
|
||
| import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; | ||
|
|
||
| @InputType() | ||
| export class UpsertFieldsWidgetFieldInput { | ||
| @IsUUID() | ||
| @IsNotEmpty() | ||
| @Field(() => UUIDScalarType, { description: 'The id of the view field' }) | ||
| viewFieldId: string; | ||
|
|
||
| @IsBoolean() | ||
| @Field() | ||
| isVisible: boolean; | ||
|
|
||
| @IsNumber() | ||
| @Field() | ||
| position: number; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { Field, InputType } from '@nestjs/graphql'; | ||
|
|
||
| import { Type } from 'class-transformer'; | ||
| import { | ||
| IsBoolean, | ||
| IsNotEmpty, | ||
| IsNumber, | ||
| IsString, | ||
| IsUUID, | ||
| ValidateNested, | ||
| } from 'class-validator'; | ||
|
|
||
| import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; | ||
| import { UpsertFieldsWidgetFieldInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/upsert-fields-widget-field.input'; | ||
|
|
||
| @InputType() | ||
| export class UpsertFieldsWidgetGroupInput { | ||
| @IsUUID() | ||
| @IsNotEmpty() | ||
| @Field(() => UUIDScalarType) | ||
| id: string; | ||
|
|
||
| @IsString() | ||
| @IsNotEmpty() | ||
| @Field() | ||
| name: string; | ||
|
|
||
| @IsNumber() | ||
| @Field() | ||
| position: number; | ||
|
|
||
| @IsBoolean() | ||
| @Field() | ||
| isVisible: boolean; | ||
|
|
||
| @ValidateNested({ each: true }) | ||
| @Type(() => UpsertFieldsWidgetFieldInput) | ||
| @Field(() => [UpsertFieldsWidgetFieldInput]) | ||
| fields: UpsertFieldsWidgetFieldInput[]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { Field, InputType } from '@nestjs/graphql'; | ||
|
|
||
| import { Type } from 'class-transformer'; | ||
| import { IsNotEmpty, IsUUID, ValidateNested } from 'class-validator'; | ||
|
|
||
| import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; | ||
| import { UpsertFieldsWidgetGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/upsert-fields-widget-group.input'; | ||
|
|
||
| @InputType() | ||
| export class UpsertFieldsWidgetInput { | ||
| @IsUUID() | ||
| @IsNotEmpty() | ||
| @Field(() => UUIDScalarType, { | ||
| description: | ||
| 'The id of the fields widget whose groups and fields to upsert', | ||
| }) | ||
| widgetId: string; | ||
|
|
||
| @ValidateNested({ each: true }) | ||
| @Type(() => UpsertFieldsWidgetGroupInput) | ||
| @Field(() => [UpsertFieldsWidgetGroupInput]) | ||
| groups: UpsertFieldsWidgetGroupInput[]; | ||
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't find this logic anymore. Just want to make sure that we are not calling mutations for unchanged groups 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you click on the Save button of a dashboard, it triggers a mutation even if you changed nothing. The behavior is similar for record page layouts. I think we can go this way.