Fix dashboard record table widget aggregate persistence (#23008)

## Summary

- Update dashboard record-table widget aggregate changes to write into
the widget draft while page layout edit mode is active.
- Include `aggregateOperation` when saving record-table widget view
fields through `upsertViewWidget`.
- Persist aggregate operations server-side for widget view-field create,
update, and clear flows.
- Add frontend utility tests and backend integration coverage for widget
aggregate create/update/clear behavior.

Fixes #22934.

## Why

Dashboard record-table widgets use their own draft view state while a
page layout is being edited. The aggregate footer path was resolving
fields through the normal current-view flow and then trying to persist
immediately, which can miss widget draft fields and fail before the save
flow runs.

This change keeps aggregate edits in the widget draft during page layout
editing, then saves the aggregate operation with the rest of the widget
view configuration.

## Validation

- `npx nx lint twenty-front`
- `npx nx typecheck twenty-front`
- `npx nx test twenty-front --configuration=ci`
- `npx nx build twenty-front`
- `npx nx build twenty-server`
- `npx nx lint twenty-server --configuration=ci`
- `npx nx typecheck twenty-server`
- `npx nx test twenty-server --configuration=ci`
- `npx nx jest --config ./jest-integration.config.ts --logHeapUsage
--runTestsByPath
test/integration/metadata/suites/view/upsert-view-widget.integration-spec.ts`
- `git diff --check`

Disclosure: I used AI-assisted coding tools while preparing this PR. I
reviewed the changes myself, tested them, and take responsibility for
the implementation and any follow-up revisions needed.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23008?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Scarab Systems
2026-07-23 02:07:50 -04:00
committed by GitHub
parent 32041ce2e0
commit d1c70ab0bf
15 changed files with 392 additions and 76 deletions
@@ -3738,6 +3738,7 @@ input UpsertViewWidgetViewFieldInput {
isVisible: Boolean!
position: Float!
size: Float
aggregateOperation: AggregateOperations
}
input UpsertViewWidgetViewFilterInput {
@@ -6387,7 +6387,7 @@ export interface UpsertViewWidgetViewFieldInput {
/** The id of an existing view field to update. */
viewFieldId?: (Scalars['UUID'] | null),
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null),aggregateOperation?: (AggregateOperations | null)}
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null)}
@@ -9841,6 +9841,9 @@ export default {
"size": [
16
],
"aggregateOperation": [
55
],
"__typename": [
1
]
@@ -5894,6 +5894,7 @@ export type UpsertViewWidgetInput = {
};
export type UpsertViewWidgetViewFieldInput = {
aggregateOperation?: InputMaybe<AggregateOperations>;
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
fieldMetadataId?: InputMaybe<Scalars['UUID']['input']>;
isVisible: Scalars['Boolean']['input'];
@@ -8,10 +8,14 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
import { RecordTableWidgetContextStoreInitEffect } from '@/object-record/record-table-widget/components/RecordTableWidgetContextStoreInitEffect';
import { RecordTableWidgetViewLoadEffect } from '@/object-record/record-table-widget/components/RecordTableWidgetViewLoadEffect';
import { RecordTableWidgetContext } from '@/object-record/record-table-widget/contexts/RecordTableWidgetContext';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { isNonEmptyString } from '@sniptt/guards';
import { type PropsWithChildren, useCallback } from 'react';
import { type PropsWithChildren, useCallback, useMemo } from 'react';
import { AppPath } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
@@ -75,55 +79,76 @@ export const RecordTableWidgetProvider = ({
const handleIndexRecordsLoaded = useCallback(() => {}, []);
const pageLayoutComponentInstanceContext = useComponentInstanceStateContext(
PageLayoutComponentInstanceContext,
);
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const recordTableWidgetContext = useMemo(
() => ({
isPageLayoutInEditMode,
pageLayoutId: pageLayoutComponentInstanceContext?.instanceId,
widgetId,
}),
[
isPageLayoutInEditMode,
pageLayoutComponentInstanceContext?.instanceId,
widgetId,
],
);
if (!objectPermissions.canReadObjectRecords) {
return null;
}
return (
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: isNonEmptyString(instanceIdSuffix)
? `record-table-widget-${widgetId}-${instanceIdSuffix}`
: `record-table-widget-${widgetId}`,
}}
>
<RecordTableWidgetContextStoreInitEffect
objectMetadataItemId={objectMetadataItem.id}
viewId={viewId}
contextStoreViewType={contextStoreViewType}
/>
<RecordIndexContextProvider
<RecordTableWidgetContext.Provider value={recordTableWidgetContext}>
<ContextStoreComponentInstanceContext.Provider
value={{
objectPermissionsByObjectMetadataId,
recordIndexId,
viewBarInstanceId: recordIndexId,
objectNamePlural: objectMetadataItem.namePlural,
objectNameSingular,
objectMetadataItem,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
indexIdentifierUrl,
recordFieldByFieldMetadataItemId,
labelIdentifierFieldMetadataItem,
fieldMetadataItemByFieldMetadataItemId,
fieldDefinitionByFieldMetadataItemId,
recordLimit,
instanceId: isNonEmptyString(instanceIdSuffix)
? `record-table-widget-${widgetId}-${instanceIdSuffix}`
: `record-table-widget-${widgetId}`,
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<RecordTableWidgetContextStoreInitEffect
objectMetadataItemId={objectMetadataItem.id}
viewId={viewId}
contextStoreViewType={contextStoreViewType}
/>
<RecordIndexContextProvider
value={{
objectPermissionsByObjectMetadataId,
recordIndexId,
viewBarInstanceId: recordIndexId,
objectNamePlural: objectMetadataItem.namePlural,
objectNameSingular,
objectMetadataItem,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
indexIdentifierUrl,
recordFieldByFieldMetadataItemId,
labelIdentifierFieldMetadataItem,
fieldMetadataItemByFieldMetadataItemId,
fieldDefinitionByFieldMetadataItemId,
recordLimit,
}}
>
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordIndexId}
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordTableWidgetViewLoadEffect
viewId={viewId}
widgetId={widgetId}
objectMetadataItem={objectMetadataItem}
/>
{children}
</RecordComponentInstanceContextsWrapper>
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</ContextStoreComponentInstanceContext.Provider>
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordIndexId}
>
<RecordTableWidgetViewLoadEffect
viewId={viewId}
widgetId={widgetId}
objectMetadataItem={objectMetadataItem}
/>
{children}
</RecordComponentInstanceContextsWrapper>
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</ContextStoreComponentInstanceContext.Provider>
</RecordTableWidgetContext.Provider>
);
};
@@ -0,0 +1,10 @@
import { createContext } from 'react';
export type RecordTableWidgetContextValue = {
isPageLayoutInEditMode: boolean;
pageLayoutId?: string;
widgetId: string;
};
export const RecordTableWidgetContext =
createContext<RecordTableWidgetContextValue | null>(null);
@@ -0,0 +1,31 @@
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { computeRecordTableWidgetViewLoadContentSignature } from '@/object-record/record-table-widget/utils/computeRecordTableWidgetViewLoadContentSignature';
import { type View } from '@/views/types/View';
const makeView = (aggregateOperation: AggregateOperations | null): View =>
({
viewFields: [
{
id: 'view-field-id',
fieldMetadataId: 'field-metadata-id',
isVisible: true,
position: 0,
aggregateOperation,
},
],
viewFilters: [],
viewFilterGroups: [],
viewSorts: [],
}) as unknown as View;
describe('computeRecordTableWidgetViewLoadContentSignature', () => {
it('should include aggregate operations in the record table widget view load signature', () => {
expect(
computeRecordTableWidgetViewLoadContentSignature(
makeView(AggregateOperations.SUM),
),
).not.toEqual(
computeRecordTableWidgetViewLoadContentSignature(makeView(null)),
);
});
});
@@ -11,6 +11,7 @@ export const computeRecordTableWidgetViewLoadContentSignature = (
fieldMetadataId: field.fieldMetadataId,
isVisible: field.isVisible,
position: field.position,
aggregateOperation: field.aggregateOperation,
})),
filters: [...view.viewFilters]
.sort((filterA, filterB) => filterA.id.localeCompare(filterB.id))
@@ -2,29 +2,81 @@ import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { RecordTableWidgetContext } from '@/object-record/record-table-widget/contexts/RecordTableWidgetContext';
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { useRecordTableWidgetFieldUpdate } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetFieldUpdate';
import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
const MISSING_RECORD_TABLE_WIDGET_PAGE_LAYOUT_ID =
'__missing_record_table_widget_page_layout__';
export const useViewFieldAggregateOperation = () => {
const { fieldMetadataId } = useContext(
RecordTableColumnAggregateFooterDropdownContext,
);
const { currentView } = useGetCurrentViewOnly();
const recordTableWidgetContext = useContext(RecordTableWidgetContext);
const currentViewField = currentView?.viewFields?.find(
const recordTableWidgetViewDraft = useAtomComponentStateValue(
recordTableWidgetViewDraftComponentState,
recordTableWidgetContext?.pageLayoutId ??
MISSING_RECORD_TABLE_WIDGET_PAGE_LAYOUT_ID,
);
const draftSnapshot = !isDefined(recordTableWidgetContext)
? undefined
: recordTableWidgetViewDraft[recordTableWidgetContext.widgetId];
const shouldUseRecordTableWidgetDraft =
isDefined(recordTableWidgetContext) &&
recordTableWidgetContext.isPageLayoutInEditMode &&
isDefined(draftSnapshot);
const currentViewForAggregateOperation = shouldUseRecordTableWidgetDraft
? constructViewFromRecordTableWidgetViewSnapshot(draftSnapshot)
: currentView;
const currentViewField = currentViewForAggregateOperation?.viewFields?.find(
(viewField) => viewField.fieldMetadataId === fieldMetadataId,
);
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
const { handleFieldUpdated: handleRecordTableWidgetFieldUpdated } =
useRecordTableWidgetFieldUpdate({
pageLayoutId:
recordTableWidgetContext?.pageLayoutId ??
MISSING_RECORD_TABLE_WIDGET_PAGE_LAYOUT_ID,
widgetId: recordTableWidgetContext?.widgetId ?? '',
});
const updateViewFieldAggregateOperation = async (
aggregateOperation: ExtendedAggregateOperations | null,
) => {
if (!currentViewField) {
throw new Error('ViewField not found');
}
const aggregateOperationForPersistence =
aggregateOperation === null
? null
: convertExtendedAggregateOperationToAggregateOperation(
aggregateOperation,
);
if (shouldUseRecordTableWidgetDraft) {
handleRecordTableWidgetFieldUpdated(currentViewField.id, {
aggregateOperation: aggregateOperationForPersistence,
});
return;
}
await performViewFieldAPIUpdate([
{
input: {
@@ -33,11 +85,7 @@ export const useViewFieldAggregateOperation = () => {
isVisible: currentViewField.isVisible,
position: currentViewField.position,
size: currentViewField.size,
aggregateOperation: isDefined(aggregateOperation)
? convertExtendedAggregateOperationToAggregateOperation(
aggregateOperation,
)
: null,
aggregateOperation: aggregateOperationForPersistence,
},
},
},
@@ -115,6 +115,7 @@ export const useSaveRecordTableWidgetViews = () => {
isVisible: field.isVisible,
position: field.position,
size: field.size,
aggregateOperation: field.aggregateOperation ?? null,
})),
viewFilters: widgetViewDraft.viewFilters.map((filter) => ({
id: filter.id,
@@ -1,5 +1,6 @@
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { useRecordTableWidgetFieldUpdate } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetFieldUpdate';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
@@ -22,31 +23,10 @@ export const useRecordTableWidgetFieldCallbacks = ({
const store = useStore();
const handleFieldUpdated = (
viewFieldId: string,
update: Partial<{ position: number; isVisible: boolean }>,
) => {
store.set(recordTableWidgetViewDraftState, (prev) => {
const widgetViewDraft = prev[widgetId];
if (!isDefined(widgetViewDraft)) {
return prev;
}
return {
...prev,
[widgetId]: {
...widgetViewDraft,
viewFields: widgetViewDraft.viewFields.map((field) =>
field.id === viewFieldId ||
field.clientRecordFieldId === viewFieldId
? { ...field, ...update }
: field,
),
},
};
});
};
const { handleFieldUpdated } = useRecordTableWidgetFieldUpdate({
pageLayoutId,
widgetId,
});
const handleFieldCreated = (recordField: RecordField) => {
store.set(recordTableWidgetViewDraftState, (prev) => {
@@ -0,0 +1,57 @@
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { type RecordTableWidgetDraftViewField } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
type UseRecordTableWidgetFieldUpdateParams = {
pageLayoutId: string;
widgetId: string;
};
type RecordTableWidgetFieldUpdate = Partial<
Pick<
RecordTableWidgetDraftViewField,
'aggregateOperation' | 'isVisible' | 'position'
>
>;
export const useRecordTableWidgetFieldUpdate = ({
pageLayoutId,
widgetId,
}: UseRecordTableWidgetFieldUpdateParams) => {
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
recordTableWidgetViewDraftComponentState,
pageLayoutId,
);
const store = useStore();
const handleFieldUpdated = (
viewFieldId: string,
update: RecordTableWidgetFieldUpdate,
) => {
store.set(recordTableWidgetViewDraftState, (prev) => {
const widgetViewDraft = prev[widgetId];
if (!isDefined(widgetViewDraft)) {
return prev;
}
return {
...prev,
[widgetId]: {
...widgetViewDraft,
viewFields: widgetViewDraft.viewFields.map((field) =>
field.id === viewFieldId ||
field.clientRecordFieldId === viewFieldId
? { ...field, ...update }
: field,
),
},
};
});
};
return { handleFieldUpdated };
};
@@ -1,6 +1,13 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator';
import {
IsBoolean,
IsEnum,
IsNumber,
IsOptional,
IsUUID,
} from 'class-validator';
import { AggregateOperations } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AtLeastOneOf } from 'src/engine/metadata-modules/view-field-group/dtos/validators/at-least-one-of.validator';
@@ -37,4 +44,9 @@ export class UpsertViewWidgetViewFieldInput {
@IsNumber()
@Field({ nullable: true })
size?: number;
@IsOptional()
@IsEnum(AggregateOperations)
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations | null;
}
@@ -489,11 +489,17 @@ export class ViewWidgetUpsertService {
const resolvedSize = isDefined(existingField.overrides?.size)
? existingField.overrides.size
: existingField.size;
const resolvedAggregateOperation =
existingField.overrides?.aggregateOperation !== undefined
? existingField.overrides.aggregateOperation
: existingField.aggregateOperation;
const hasChanged =
resolvedIsVisible !== inputField.isVisible ||
resolvedPosition !== inputField.position ||
(isDefined(inputField.size) && resolvedSize !== inputField.size);
(isDefined(inputField.size) && resolvedSize !== inputField.size) ||
(inputField.aggregateOperation !== undefined &&
resolvedAggregateOperation !== inputField.aggregateOperation);
if (!hasChanged) {
continue;
@@ -516,6 +522,9 @@ export class ViewWidgetUpsertService {
isVisible: inputField.isVisible,
position: inputField.position,
...(isDefined(inputField.size) ? { size: inputField.size } : {}),
...(inputField.aggregateOperation !== undefined
? { aggregateOperation: inputField.aggregateOperation }
: {}),
},
shouldOverride,
});
@@ -582,7 +591,7 @@ export class ViewWidgetUpsertService {
isVisible: inputField.isVisible,
size: inputField.size ?? DEFAULT_VIEW_FIELD_SIZE,
position: inputField.position,
aggregateOperation: null,
aggregateOperation: inputField.aggregateOperation ?? null,
overrides: null,
universalOverrides: null,
isActive: true,
@@ -21,6 +21,7 @@ import { createOneView } from 'test/integration/metadata/suites/view/utils/creat
import { destroyOneView } from 'test/integration/metadata/suites/view/utils/destroy-one-view.util';
import { upsertViewWidget } from 'test/integration/metadata/suites/view/utils/upsert-view-widget.util';
import {
AggregateOperations,
ViewFilterGroupLogicalOperator,
ViewFilterOperand,
ViewSortDirection,
@@ -303,6 +304,142 @@ describe('upsertViewWidget', () => {
expect(updatedField!.position).toBe(42);
});
it('should create a new view field with an aggregate operation', async () => {
const { data: fieldsBefore } = await findViewFields({
viewId: testSetup.viewId,
gqlFields: 'id fieldMetadataId position isVisible aggregateOperation',
expectToFail: false,
});
const existingFieldMetadataIds = new Set(
fieldsBefore.getViewFields.map(
(field: { fieldMetadataId: string }) => field.fieldMetadataId,
),
);
const newFieldMetadataId = testSetup.fieldMetadataIds.find(
(id) => !existingFieldMetadataIds.has(id),
);
expect(newFieldMetadataId).toBeDefined();
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFields: [
...fieldsBefore.getViewFields.map(
(field: {
id: string;
fieldMetadataId: string;
isVisible: boolean;
position: number;
aggregateOperation?: AggregateOperations | null;
}) => ({
viewFieldId: field.id,
fieldMetadataId: field.fieldMetadataId,
isVisible: field.isVisible,
position: field.position,
aggregateOperation: field.aggregateOperation ?? null,
}),
),
{
fieldMetadataId: newFieldMetadataId!,
isVisible: true,
position: 99,
aggregateOperation: AggregateOperations.SUM,
},
],
},
});
const { data: fieldsAfter } = await findViewFields({
viewId: testSetup.viewId,
gqlFields: 'id fieldMetadataId aggregateOperation',
expectToFail: false,
});
const createdField = fieldsAfter.getViewFields.find(
(field: { fieldMetadataId: string }) =>
field.fieldMetadataId === newFieldMetadataId,
);
expect(createdField).toBeDefined();
expect(createdField!.aggregateOperation).toBe(AggregateOperations.SUM);
});
it('should update and clear an existing view field aggregate operation', async () => {
const { data: fieldsBefore } = await findViewFields({
viewId: testSetup.viewId,
gqlFields: 'id fieldMetadataId position isVisible aggregateOperation',
expectToFail: false,
});
const targetField = fieldsBefore.getViewFields.find(
(field: { fieldMetadataId: string }) =>
field.fieldMetadataId !== testSetup.labelIdentifierFieldMetadataId,
);
expect(targetField).toBeDefined();
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFields: [
{
viewFieldId: targetField!.id,
fieldMetadataId: targetField!.fieldMetadataId,
isVisible: targetField!.isVisible,
position: targetField!.position,
aggregateOperation: AggregateOperations.COUNT,
},
],
},
});
const { data: fieldsWithAggregate } = await findViewFields({
viewId: testSetup.viewId,
gqlFields: 'id aggregateOperation',
expectToFail: false,
});
const updatedField = fieldsWithAggregate.getViewFields.find(
(field: { id: string }) => field.id === targetField!.id,
);
expect(updatedField).toBeDefined();
expect(updatedField!.aggregateOperation).toBe(AggregateOperations.COUNT);
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFields: [
{
viewFieldId: targetField!.id,
fieldMetadataId: targetField!.fieldMetadataId,
isVisible: targetField!.isVisible,
position: targetField!.position,
aggregateOperation: null,
},
],
},
});
const { data: fieldsAfterClear } = await findViewFields({
viewId: testSetup.viewId,
gqlFields: 'id aggregateOperation',
expectToFail: false,
});
const clearedField = fieldsAfterClear.getViewFields.find(
(field: { id: string }) => field.id === targetField!.id,
);
expect(clearedField).toBeDefined();
expect(clearedField!.aggregateOperation).toBeNull();
});
it('should not modify fields when viewFields is omitted', async () => {
const { data: fieldsBefore } = await findViewFields({
viewId: testSetup.viewId,