Files
upstream-twenty/packages/twenty-server/test/integration/metadata/suites/view/upsert-view-widget.integration-spec.ts
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

1264 lines
36 KiB
TypeScript

import {
VIEW_FIELD_GQL_FIELDS,
VIEW_FILTER_GQL_FIELDS,
VIEW_FILTER_GROUP_GQL_FIELDS,
VIEW_GQL_FIELDS,
VIEW_SORT_GQL_FIELDS,
} from 'test/integration/constants/view-gql-fields.constants';
import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util';
import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util';
import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util';
import { destroyOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util';
import {
fetchTestFieldMetadataIds,
type TestFieldMetadataIds,
} from 'test/integration/metadata/suites/page-layout-widget/utils/fetch-test-field-metadata-ids.util';
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { findViewFields } from 'test/integration/metadata/suites/view-field/utils/find-view-fields.util';
import { findViewFilters } from 'test/integration/metadata/suites/view-filter/utils/find-view-filters.util';
import { createOneView } from 'test/integration/metadata/suites/view/utils/create-one-view.util';
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,
PageLayoutTabLayoutMode,
ViewFilterGroupLogicalOperator,
ViewFilterOperand,
ViewSortDirection,
ViewType,
WidgetType,
} from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
type ViewWidgetTestSetup = {
pageLayoutId: string;
pageLayoutTabId: string;
widgetId: string;
viewId: string;
labelIdentifierFieldMetadataId: string | null;
viewFields: Array<{
id: string;
fieldMetadataId: string;
position: number;
isVisible: boolean;
}>;
fieldMetadataIds: string[];
};
const VIEW_WITH_ALL_RELATIONS_GQL_FIELDS = `
${VIEW_GQL_FIELDS}
viewFields {
${VIEW_FIELD_GQL_FIELDS}
}
viewFilters {
${VIEW_FILTER_GQL_FIELDS}
}
viewFilterGroups {
${VIEW_FILTER_GROUP_GQL_FIELDS}
}
viewSorts {
${VIEW_SORT_GQL_FIELDS}
}
`;
describe('upsertViewWidget', () => {
let testSetup: ViewWidgetTestSetup;
beforeAll(async () => {
const testFieldMetadataIds: TestFieldMetadataIds =
await fetchTestFieldMetadataIds();
const { data: layoutData } = await createOnePageLayout({
expectToFail: false,
input: { name: 'Test Page Layout For View Widget' },
});
const pageLayoutId = layoutData.createPageLayout.id;
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Test Tab For View Widget',
pageLayoutId,
},
});
const pageLayoutTabId = tabData.createPageLayoutTab.id;
const { data: viewData } = await createOneView({
expectToFail: false,
input: {
name: 'testViewWidgetView',
objectMetadataId: testFieldMetadataIds.objectMetadataId,
icon: 'IconTable',
type: ViewType.TABLE,
},
});
const viewId = viewData.createView.id;
const { data: widgetData } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: 'Test Record Table Widget',
type: WidgetType.RECORD_TABLE,
pageLayoutTabId,
objectMetadataId: testFieldMetadataIds.objectMetadataId,
position: {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
configuration: {
configurationType: WidgetConfigurationType.RECORD_TABLE,
viewId,
},
},
});
const widgetId = widgetData.createPageLayoutWidget.id;
const objectMetadataRows = await global.testDataSource.query(
`SELECT "labelIdentifierFieldMetadataId"
FROM core."objectMetadata"
WHERE id = $1`,
[testFieldMetadataIds.objectMetadataId],
);
const labelIdentifierFieldMetadataId =
objectMetadataRows[0]?.labelIdentifierFieldMetadataId ?? null;
const allFieldMetadataRows = await global.testDataSource.query(
`SELECT id FROM core."fieldMetadata"
WHERE "objectMetadataId" = $1
AND "isActive" = true
ORDER BY id
LIMIT 10`,
[testFieldMetadataIds.objectMetadataId],
);
const allFieldMetadataIds: string[] = allFieldMetadataRows.map(
(row: { id: string }) => row.id,
);
if (labelIdentifierFieldMetadataId) {
const idx = allFieldMetadataIds.indexOf(labelIdentifierFieldMetadataId);
if (idx > 0) {
allFieldMetadataIds.splice(idx, 1);
allFieldMetadataIds.unshift(labelIdentifierFieldMetadataId);
} else if (idx === -1) {
allFieldMetadataIds.unshift(labelIdentifierFieldMetadataId);
}
}
const seedFieldMetadataIds = allFieldMetadataIds.slice(0, 3);
await upsertViewWidget({
expectToFail: false,
input: {
widgetId,
viewFields: seedFieldMetadataIds.map((fmId, index) => ({
fieldMetadataId: fmId,
isVisible: true,
position: index,
})),
},
});
const { data: fieldsData } = await findViewFields({
viewId,
gqlFields: 'id fieldMetadataId position isVisible',
expectToFail: false,
});
testSetup = {
pageLayoutId,
pageLayoutTabId,
widgetId,
viewId,
labelIdentifierFieldMetadataId,
viewFields: fieldsData.getViewFields,
fieldMetadataIds: allFieldMetadataIds,
};
});
afterAll(async () => {
await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: testSetup.widgetId },
});
await destroyOneView({
expectToFail: false,
viewId: testSetup.viewId,
});
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: testSetup.pageLayoutTabId },
});
await destroyOnePageLayout({
expectToFail: false,
input: { id: testSetup.pageLayoutId },
});
});
describe('view fields', () => {
it('should create a new view field via fieldMetadataId', async () => {
const existingFieldMetadataIds = new Set(
testSetup.viewFields.map((f) => f.fieldMetadataId),
);
const newFieldMetadataId = testSetup.fieldMetadataIds.find(
(id) => !existingFieldMetadataIds.has(id),
);
expect(newFieldMetadataId).toBeDefined();
const { data, errors } = await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFields: [
...testSetup.viewFields.map((f) => ({
viewFieldId: f.id,
fieldMetadataId: f.fieldMetadataId,
isVisible: f.isVisible,
position: f.position,
})),
{
fieldMetadataId: newFieldMetadataId!,
isVisible: true,
position: 99,
},
],
},
gqlFields: VIEW_WITH_ALL_RELATIONS_GQL_FIELDS,
});
expect(errors).toBeUndefined();
expect(data.upsertViewWidget).toBeDefined();
const { data: fieldsData } = await findViewFields({
viewId: testSetup.viewId,
gqlFields: 'id fieldMetadataId position isVisible',
expectToFail: false,
});
const createdField = fieldsData.getViewFields.find(
(f: { fieldMetadataId: string }) =>
f.fieldMetadataId === newFieldMetadataId!,
);
expect(createdField).toBeDefined();
expect(createdField!.isVisible).toBe(true);
expect(createdField!.position).toBe(99);
});
it('should update an existing view field position and visibility', async () => {
const targetField = testSetup.viewFields.find(
(field) =>
field.fieldMetadataId !== testSetup.labelIdentifierFieldMetadataId,
)!;
expect(targetField).toBeDefined();
const labelIdentifierField = testSetup.viewFields.find(
(field) =>
field.fieldMetadataId === testSetup.labelIdentifierFieldMetadataId,
);
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFields: [
...(labelIdentifierField
? [
{
viewFieldId: labelIdentifierField.id,
fieldMetadataId: labelIdentifierField.fieldMetadataId,
isVisible: true,
position: 0,
},
]
: []),
{
viewFieldId: targetField.id,
fieldMetadataId: targetField.fieldMetadataId,
isVisible: false,
position: 42,
},
],
},
});
const { data: fieldsData } = await findViewFields({
viewId: testSetup.viewId,
gqlFields: 'id isVisible position',
expectToFail: false,
});
const updatedField = fieldsData.getViewFields.find(
(f: { id: string }) => f.id === targetField.id,
);
expect(updatedField).toBeDefined();
expect(updatedField!.isVisible).toBe(false);
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,
gqlFields: 'id position isVisible',
expectToFail: false,
});
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [],
},
});
const { data: fieldsAfter } = await findViewFields({
viewId: testSetup.viewId,
gqlFields: 'id position isVisible',
expectToFail: false,
});
expect(fieldsAfter.getViewFields.length).toBe(
fieldsBefore.getViewFields.length,
);
});
});
describe('view filter groups', () => {
it('should create a new filter group', async () => {
const filterGroupId = uuidv4();
const { data, errors } = await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilterGroups: [
{
id: filterGroupId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
},
],
},
gqlFields: VIEW_WITH_ALL_RELATIONS_GQL_FIELDS,
});
expect(errors).toBeUndefined();
expect(data.upsertViewWidget).toBeDefined();
const filterGroups = await global.testDataSource.query(
`SELECT id, "logicalOperator"
FROM core."viewFilterGroup"
WHERE id = $1 AND "deletedAt" IS NULL`,
[filterGroupId],
);
expect(filterGroups.length).toBe(1);
expect(filterGroups[0].logicalOperator).toBe(
ViewFilterGroupLogicalOperator.AND,
);
});
it('should update an existing filter group logical operator', async () => {
const filterGroupId = uuidv4();
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilterGroups: [
{
id: filterGroupId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
},
],
},
});
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilterGroups: [
{
id: filterGroupId,
logicalOperator: ViewFilterGroupLogicalOperator.OR,
},
],
},
});
const filterGroups = await global.testDataSource.query(
`SELECT id, "logicalOperator"
FROM core."viewFilterGroup"
WHERE id = $1 AND "deletedAt" IS NULL`,
[filterGroupId],
);
expect(filterGroups.length).toBe(1);
expect(filterGroups[0].logicalOperator).toBe(
ViewFilterGroupLogicalOperator.OR,
);
});
it('should remove filter groups not included in the input', async () => {
const groupToKeepId = uuidv4();
const groupToRemoveId = uuidv4();
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilterGroups: [
{
id: groupToKeepId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
},
{
id: groupToRemoveId,
logicalOperator: ViewFilterGroupLogicalOperator.OR,
},
],
},
});
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilterGroups: [
{
id: groupToKeepId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
},
],
},
});
const removedGroup = await global.testDataSource.query(
`SELECT id FROM core."viewFilterGroup"
WHERE id = $1 AND "deletedAt" IS NULL`,
[groupToRemoveId],
);
expect(removedGroup.length).toBe(0);
const keptGroup = await global.testDataSource.query(
`SELECT id FROM core."viewFilterGroup"
WHERE id = $1 AND "deletedAt" IS NULL`,
[groupToKeepId],
);
expect(keptGroup.length).toBe(1);
});
it('should create a nested filter group with parent reference', async () => {
const parentGroupId = uuidv4();
const childGroupId = uuidv4();
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilterGroups: [
{
id: parentGroupId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
},
],
},
});
const { errors } = await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilterGroups: [
{
id: parentGroupId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
},
{
id: childGroupId,
logicalOperator: ViewFilterGroupLogicalOperator.OR,
parentViewFilterGroupId: parentGroupId,
},
],
},
});
expect(errors).toBeUndefined();
const childGroup = await global.testDataSource.query(
`SELECT id, "parentViewFilterGroupId"
FROM core."viewFilterGroup"
WHERE id = $1 AND "deletedAt" IS NULL`,
[childGroupId],
);
expect(childGroup.length).toBe(1);
expect(childGroup[0].parentViewFilterGroupId).toBe(parentGroupId);
});
});
describe('view filters', () => {
it('should create a new filter', async () => {
const fieldMetadataId = testSetup.fieldMetadataIds[0];
const { data, errors } = await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilters: [
{
fieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: 'test-value',
},
],
},
gqlFields: VIEW_WITH_ALL_RELATIONS_GQL_FIELDS,
});
expect(errors).toBeUndefined();
expect(data.upsertViewWidget).toBeDefined();
const { data: filtersData } = await findViewFilters({
viewId: testSetup.viewId,
gqlFields: 'id fieldMetadataId operand value',
expectToFail: false,
});
const createdFilter = filtersData.getViewFilters.find(
(f: { fieldMetadataId: string; value: unknown }) =>
f.fieldMetadataId === fieldMetadataId && f.value === 'test-value',
);
expect(createdFilter).toBeDefined();
expect(createdFilter!.operand).toBe(ViewFilterOperand.CONTAINS);
});
it('should update an existing filter value and operand', async () => {
const filterId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[0];
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilters: [
{
id: filterId,
fieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: 'initial',
},
],
},
});
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilters: [
{
id: filterId,
fieldMetadataId,
operand: ViewFilterOperand.DOES_NOT_CONTAIN,
value: 'updated',
},
],
},
});
const { data: filtersData } = await findViewFilters({
viewId: testSetup.viewId,
gqlFields: 'id operand value',
expectToFail: false,
});
const updatedFilter = filtersData.getViewFilters.find(
(f: { id: string }) => f.id === filterId,
);
expect(updatedFilter).toBeDefined();
expect(updatedFilter!.operand).toBe(ViewFilterOperand.DOES_NOT_CONTAIN);
expect(updatedFilter!.value).toBe('updated');
});
it('should remove filters not included in the input', async () => {
const filterToKeepId = uuidv4();
const filterToRemoveId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[0];
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilters: [
{
id: filterToKeepId,
fieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: 'keep',
},
{
id: filterToRemoveId,
fieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: 'remove',
},
],
},
});
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilters: [
{
id: filterToKeepId,
fieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: 'keep',
},
],
},
});
const { data: filtersData } = await findViewFilters({
viewId: testSetup.viewId,
gqlFields: 'id',
expectToFail: false,
});
const keptFilter = filtersData.getViewFilters.find(
(f: { id: string }) => f.id === filterToKeepId,
);
expect(keptFilter).toBeDefined();
const removedFilter = filtersData.getViewFilters.find(
(f: { id: string }) => f.id === filterToRemoveId,
);
expect(removedFilter).toBeUndefined();
});
it('should create a filter within a filter group', async () => {
const filterGroupId = uuidv4();
const filterId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[0];
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilterGroups: [
{
id: filterGroupId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
},
],
viewFilters: [
{
id: filterId,
fieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: 'grouped-filter',
viewFilterGroupId: filterGroupId,
},
],
},
});
const filters = await global.testDataSource.query(
`SELECT id, "viewFilterGroupId"
FROM core."viewFilter"
WHERE id = $1 AND "deletedAt" IS NULL`,
[filterId],
);
expect(filters.length).toBe(1);
expect(filters[0].viewFilterGroupId).toBe(filterGroupId);
});
});
describe('view sorts', () => {
afterEach(async () => {
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [],
},
});
});
it('should create a new sort', async () => {
const sortId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[0];
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [
{
id: sortId,
fieldMetadataId,
direction: ViewSortDirection.DESC,
},
],
},
});
const sorts = await global.testDataSource.query(
`SELECT id, "fieldMetadataId", direction
FROM core."viewSort"
WHERE id = $1 AND "deletedAt" IS NULL`,
[sortId],
);
expect(sorts.length).toBe(1);
expect(sorts[0].fieldMetadataId).toBe(fieldMetadataId);
expect(sorts[0].direction).toBe(ViewSortDirection.DESC);
});
it('should update an existing sort direction', async () => {
const sortId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[1];
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [
{
id: sortId,
fieldMetadataId,
direction: ViewSortDirection.ASC,
},
],
},
});
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [
{
id: sortId,
fieldMetadataId,
direction: ViewSortDirection.DESC,
},
],
},
});
const sorts = await global.testDataSource.query(
`SELECT id, direction
FROM core."viewSort"
WHERE id = $1 AND "deletedAt" IS NULL`,
[sortId],
);
expect(sorts.length).toBe(1);
expect(sorts[0].direction).toBe(ViewSortDirection.DESC);
});
it('should remove sorts not included in the input', async () => {
const sortToKeepId = uuidv4();
const sortToRemoveId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[0];
const secondFieldMetadataId = testSetup.fieldMetadataIds[1];
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [
{
id: sortToKeepId,
fieldMetadataId,
direction: ViewSortDirection.ASC,
},
{
id: sortToRemoveId,
fieldMetadataId: secondFieldMetadataId,
direction: ViewSortDirection.DESC,
},
],
},
});
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [
{
id: sortToKeepId,
fieldMetadataId,
direction: ViewSortDirection.ASC,
},
],
},
});
const keptSort = await global.testDataSource.query(
`SELECT id FROM core."viewSort"
WHERE id = $1 AND "deletedAt" IS NULL`,
[sortToKeepId],
);
expect(keptSort.length).toBe(1);
const removedSort = await global.testDataSource.query(
`SELECT id FROM core."viewSort"
WHERE id = $1 AND "deletedAt" IS NULL`,
[sortToRemoveId],
);
expect(removedSort.length).toBe(0);
});
});
describe('combined operations', () => {
it('should upsert view fields, filters, filter groups, and sorts in a single call', async () => {
const filterGroupId = uuidv4();
const filterId = uuidv4();
const sortId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[0];
const targetField = testSetup.viewFields.find(
(field) =>
field.fieldMetadataId !== testSetup.labelIdentifierFieldMetadataId,
)!;
expect(targetField).toBeDefined();
const labelIdentifierField = testSetup.viewFields.find(
(field) =>
field.fieldMetadataId === testSetup.labelIdentifierFieldMetadataId,
);
const { data, errors } = await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFields: [
...(labelIdentifierField
? [
{
viewFieldId: labelIdentifierField.id,
fieldMetadataId: labelIdentifierField.fieldMetadataId,
isVisible: true,
position: 0,
},
]
: []),
{
viewFieldId: targetField.id,
fieldMetadataId: targetField.fieldMetadataId,
isVisible: true,
position: 1,
},
],
viewFilterGroups: [
{
id: filterGroupId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
},
],
viewFilters: [
{
id: filterId,
fieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: 'combined-test',
viewFilterGroupId: filterGroupId,
},
],
viewSorts: [
{
id: sortId,
fieldMetadataId,
direction: ViewSortDirection.DESC,
},
],
},
gqlFields: VIEW_WITH_ALL_RELATIONS_GQL_FIELDS,
});
expect(errors).toBeUndefined();
expect(data.upsertViewWidget).toBeDefined();
expect(data.upsertViewWidget.id).toBeDefined();
const filterGroups = await global.testDataSource.query(
`SELECT id FROM core."viewFilterGroup"
WHERE id = $1 AND "deletedAt" IS NULL`,
[filterGroupId],
);
expect(filterGroups.length).toBe(1);
const filters = await global.testDataSource.query(
`SELECT id, value FROM core."viewFilter"
WHERE id = $1 AND "deletedAt" IS NULL`,
[filterId],
);
expect(filters.length).toBe(1);
expect(filters[0].value).toBe('combined-test');
const sorts = await global.testDataSource.query(
`SELECT id, direction FROM core."viewSort"
WHERE id = $1 AND "deletedAt" IS NULL`,
[sortId],
);
expect(sorts.length).toBe(1);
expect(sorts[0].direction).toBe(ViewSortDirection.DESC);
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [],
viewFilters: [],
viewFilterGroups: [],
},
});
});
});
describe('validation', () => {
it('should fail when widget id does not exist', async () => {
const { errors } = await upsertViewWidget({
expectToFail: true,
input: {
widgetId: uuidv4(),
viewFields: [
{
fieldMetadataId: testSetup.fieldMetadataIds[0],
isVisible: true,
position: 0,
},
],
},
});
expect(errors).toBeDefined();
expect(errors.length).toBeGreaterThan(0);
});
it('should return the view without changes when no sub-entity arrays are provided', async () => {
const { data, errors } = await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
},
gqlFields: VIEW_GQL_FIELDS,
});
expect(errors).toBeUndefined();
expect(data.upsertViewWidget).toBeDefined();
expect(data.upsertViewWidget.id).toBeDefined();
});
it('should remove all filters when an empty array is provided', async () => {
const filterId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[0];
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilters: [
{
id: filterId,
fieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: 'to-be-removed',
},
],
},
});
const { data: filtersBefore } = await findViewFilters({
viewId: testSetup.viewId,
gqlFields: 'id',
expectToFail: false,
});
const filterExistsBefore = filtersBefore.getViewFilters.find(
(f: { id: string }) => f.id === filterId,
);
expect(filterExistsBefore).toBeDefined();
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFilters: [],
},
});
const { data: filtersAfter } = await findViewFilters({
viewId: testSetup.viewId,
gqlFields: 'id',
expectToFail: false,
});
const filterExistsAfter = filtersAfter.getViewFilters.find(
(f: { id: string }) => f.id === filterId,
);
expect(filterExistsAfter).toBeUndefined();
});
it('should remove all sorts when an empty array is provided', async () => {
const sortId = uuidv4();
const fieldMetadataId = testSetup.fieldMetadataIds[3];
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [
{
id: sortId,
fieldMetadataId,
direction: ViewSortDirection.ASC,
},
],
},
});
await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewSorts: [],
},
});
const sortsAfter = await global.testDataSource.query(
`SELECT id FROM core."viewSort"
WHERE id = $1 AND "deletedAt" IS NULL`,
[sortId],
);
expect(sortsAfter.length).toBe(0);
});
});
describe('return type', () => {
it('should return a view with the expected fields and relations', async () => {
const targetField = testSetup.viewFields.find(
(field) =>
field.fieldMetadataId !== testSetup.labelIdentifierFieldMetadataId,
)!;
expect(targetField).toBeDefined();
const labelIdentifierField = testSetup.viewFields.find(
(field) =>
field.fieldMetadataId === testSetup.labelIdentifierFieldMetadataId,
);
const { data } = await upsertViewWidget({
expectToFail: false,
input: {
widgetId: testSetup.widgetId,
viewFields: [
...(labelIdentifierField
? [
{
viewFieldId: labelIdentifierField.id,
fieldMetadataId: labelIdentifierField.fieldMetadataId,
isVisible: true,
position: 0,
},
]
: []),
{
viewFieldId: targetField.id,
fieldMetadataId: targetField.fieldMetadataId,
isVisible: true,
position: 1,
},
],
},
gqlFields: VIEW_WITH_ALL_RELATIONS_GQL_FIELDS,
});
const view = data.upsertViewWidget;
expect(view.id).toBeDefined();
expect(view.name).toBeDefined();
expect(view.objectMetadataId).toBeDefined();
expect(view.workspaceId).toBeDefined();
expect(view.createdAt).toBeDefined();
expect(view.updatedAt).toBeDefined();
expect(Array.isArray(view.viewFields)).toBe(true);
expect(Array.isArray(view.viewFilters)).toBe(true);
expect(Array.isArray(view.viewFilterGroups)).toBe(true);
expect(Array.isArray(view.viewSorts)).toBe(true);
});
});
});