import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { destroyOneViewGroup } from 'test/integration/metadata/suites/view-group/utils/destroy-one-view-group.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { assertMetadataRestListResponse, assertRestApiErrorNotFoundResponse, assertRestApiSuccessfulResponse, } from 'test/integration/rest/utils/rest-test-assertions.util'; import { createTestViewGroupWithRestApi, createTestViewWithRestApi, } from 'test/integration/rest/utils/view-rest-api.util'; import { assertViewGroupStructure } from 'test/integration/utils/view-test.util'; import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; import { FieldMetadataType } from 'twenty-shared/types'; import { type ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto'; describe('View Group REST API', () => { let testObjectMetadataId: string; let testFieldMetadataId: string; let testViewId: string; let testViewGroupId: string | undefined; beforeAll(async () => { const { data: { createOneObject: { id: objectMetadataId }, }, } = await createOneObjectMetadata({ expectToFail: false, input: { nameSingular: 'testViewGroupObject', namePlural: 'testViewGroupObjects', labelSingular: 'Test View Group Object', labelPlural: 'Test View Group Objects', icon: 'IconGroup', }, }); testObjectMetadataId = objectMetadataId; const createFieldInput = { name: 'testField', label: 'Test Field', type: FieldMetadataType.SELECT, objectMetadataId: testObjectMetadataId, isLabelSyncedWithName: true, options: [ { label: 'Option 1', value: 'OPTION_1', color: 'blue', position: 0 }, { label: 'Option 2', value: 'OPTION_2', color: 'red', position: 1 }, { label: 'Option 3', value: 'OPTION_3', color: 'green', position: 2 }, ], }; const { data: { createOneField: { id: fieldMetadataId }, }, } = await createOneFieldMetadata({ expectToFail: false, input: createFieldInput, gqlFields: ` id name label isLabelSyncedWithName `, }); testFieldMetadataId = fieldMetadataId; const testView = await createTestViewWithRestApi({ name: 'Test View for Group Integration', objectMetadataId: testObjectMetadataId, mainGroupByFieldMetadataId: testFieldMetadataId, }); testViewId = testView.id; }); afterAll(async () => { await updateOneObjectMetadata({ expectToFail: false, input: { idToUpdate: testObjectMetadataId, updatePayload: { isActive: false, }, }, }); await deleteOneObjectMetadata({ expectToFail: false, input: { idToDelete: testObjectMetadataId }, }); }); afterEach(async () => { if (!testViewGroupId) return; await destroyOneViewGroup({ input: { id: testViewGroupId, }, expectToFail: false, }); testViewGroupId = undefined; }); describe('GET /metadata/viewGroups', () => { it('should return all view groups for workspace when no viewId provided', async () => { const response = await makeRestAPIRequest({ method: 'get', path: '/metadata/viewGroups', bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertMetadataRestListResponse(response); }); it('should return view groups for a specific view after creating one', async () => { const response = await makeRestAPIRequest({ method: 'get', path: `/metadata/viewGroups?viewId=${testViewId}`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); const returnedViewGroups = assertMetadataRestListResponse(response); expect(returnedViewGroups).toHaveLength(4); // For a nullable field with three options, we expect groups for OPTION_1, OPTION_2, OPTION_3, and '' (empty string) const expectedFieldValues = ['OPTION_1', 'OPTION_2', 'OPTION_3', '']; expectedFieldValues.forEach((expectedFieldValue) => { const group = returnedViewGroups.find( (group) => group.fieldValue === expectedFieldValue, ); jestExpectToBeDefined(group); expect(group.isVisible).toBe(true); expect(group.viewId).toBe(testViewId); }); }); it('paginates the position-ordered result in both directions', async () => { const firstResponse = await makeRestAPIRequest({ method: 'get', path: `/metadata/viewGroups?viewId=${testViewId}&limit=2`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); const firstPage = assertMetadataRestListResponse(firstResponse); expect(firstPage).toHaveLength(2); expect(firstResponse.body.totalCount).toBe(4); expect(firstResponse.body.pageInfo).toMatchObject({ hasNextPage: true, hasPreviousPage: false, }); const secondResponse = await makeRestAPIRequest({ method: 'get', path: `/metadata/viewGroups?viewId=${testViewId}&limit=2&starting_after=${firstResponse.body.pageInfo.endCursor}`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); const secondPage = assertMetadataRestListResponse(secondResponse); expect(secondPage).toHaveLength(2); expect(secondResponse.body.pageInfo).toMatchObject({ hasNextPage: false, hasPreviousPage: true, }); const previousResponse = await makeRestAPIRequest({ method: 'get', path: `/metadata/viewGroups?viewId=${testViewId}&limit=2&ending_before=${secondResponse.body.pageInfo.startCursor}`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); const previousPage = assertMetadataRestListResponse(previousResponse); expect(previousPage.map(({ id }) => id)).toEqual( firstPage.map(({ id }) => id), ); }); }); describe('GET /metadata/viewGroups/:id', () => { it('should return a specific view group by id', async () => { const viewGroupsFromViewReponse = await makeRestAPIRequest({ method: 'get', path: `/metadata/viewGroups?viewId=${testViewId}`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); const viewGroup = assertMetadataRestListResponse( viewGroupsFromViewReponse, ).find((group) => group.fieldValue === 'OPTION_1'); jestExpectToBeDefined(viewGroup); const response = await makeRestAPIRequest({ method: 'get', path: `/metadata/viewGroups/${viewGroup.id}`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiSuccessfulResponse(response); assertViewGroupStructure(response.body, { id: viewGroup.id, viewId: testViewId, fieldValue: 'OPTION_1', isVisible: true, }); testViewGroupId = viewGroup.id; }); }); describe('POST /metadata/viewGroups', () => { it('should create a new view group', async () => { const viewGroupData = { viewId: testViewId, fieldValue: 'new-group-value', isVisible: true, position: 5, }; const response = await makeRestAPIRequest({ method: 'post', path: '/metadata/viewGroups', body: viewGroupData, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiSuccessfulResponse(response, 201); assertViewGroupStructure(response.body, { viewId: testViewId, fieldValue: 'new-group-value', isVisible: true, position: 5, }); testViewGroupId = response.body.id; }); it('should create view group with minimal required fields', async () => { const viewGroupData = { viewId: testViewId, fieldMetadataId: testFieldMetadataId, fieldValue: 'minimal-group', }; const response = await makeRestAPIRequest({ method: 'post', path: '/metadata/viewGroups', body: viewGroupData, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiSuccessfulResponse(response, 201); assertViewGroupStructure(response.body, { viewId: testViewId, fieldValue: 'minimal-group', isVisible: true, position: 0, }); testViewGroupId = response.body.id; }); it('should fail to create view group with missing required fields', async () => { const invalidData = { viewId: testViewId, }; const response = await makeRestAPIRequest({ method: 'post', path: '/metadata/viewGroups', body: invalidData, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); expect(response.status).toBe(400); const errorResponse = JSON.parse(response.text); expect(errorResponse).toMatchSnapshot( extractRecordIdsAndDatesAsExpectAny(errorResponse), ); }); }); describe('PATCH /metadata/viewGroups/:id', () => { it('should update an existing view group', async () => { const viewGroup = await createTestViewGroupWithRestApi({ viewId: testViewId, fieldMetadataId: testFieldMetadataId, fieldValue: 'original-value', isVisible: true, position: 1, }); testViewGroupId = viewGroup.id; const updateData = { fieldValue: 'updated-value', isVisible: false, position: 2, }; const response = await makeRestAPIRequest({ method: 'patch', path: `/metadata/viewGroups/${viewGroup.id}`, body: updateData, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiSuccessfulResponse(response); assertViewGroupStructure(response.body, { id: viewGroup.id, viewId: testViewId, fieldValue: 'updated-value', isVisible: false, position: 2, }); }); it('should update only specific fields', async () => { const viewGroup = await createTestViewGroupWithRestApi({ viewId: testViewId, fieldMetadataId: testFieldMetadataId, fieldValue: 'original-value', isVisible: true, position: 1, }); testViewGroupId = viewGroup.id; const updateData = { fieldValue: 'partially-updated', }; const response = await makeRestAPIRequest({ method: 'patch', path: `/metadata/viewGroups/${viewGroup.id}`, body: updateData, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiSuccessfulResponse(response); assertViewGroupStructure(response.body, { id: viewGroup.id, fieldValue: 'partially-updated', isVisible: true, position: 1, }); }); it('should return 404 for non-existent view group', async () => { const updateData = { fieldValue: 'test-update', }; const response = await makeRestAPIRequest({ method: 'patch', path: `/metadata/viewGroups/20202020-9c8b-4a7e-9f2d-1a2b3c4d5e6f`, body: updateData, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiErrorNotFoundResponse(response); }); }); describe('DELETE /metadata/viewGroups/:id', () => { it('should delete an existing view group', async () => { const viewGroup = await createTestViewGroupWithRestApi({ viewId: testViewId, fieldMetadataId: testFieldMetadataId, fieldValue: 'to-be-deleted', }); testViewGroupId = viewGroup.id; const deleteResponse = await makeRestAPIRequest({ method: 'delete', path: `/metadata/viewGroups/${viewGroup.id}`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiSuccessfulResponse(deleteResponse); expect(deleteResponse.body).toEqual({ success: true }); }); it('should return 404 for non-existent view group', async () => { const response = await makeRestAPIRequest({ method: 'delete', path: `/metadata/viewGroups/20202020-9c8b-4a7e-9f2d-1a2b3c4d5e6f`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiErrorNotFoundResponse(response); }); it('should return success even when group is already deleted', async () => { const viewGroup = await createTestViewGroupWithRestApi({ viewId: testViewId, fieldMetadataId: testFieldMetadataId, fieldValue: 'double-delete-test', }); testViewGroupId = viewGroup.id; const deleteResponse = await makeRestAPIRequest({ method: 'delete', path: `/metadata/viewGroups/${viewGroup.id}`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiSuccessfulResponse(deleteResponse); const deleteResponse2 = await makeRestAPIRequest({ method: 'delete', path: `/metadata/viewGroups/${viewGroup.id}`, bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); assertRestApiSuccessfulResponse(deleteResponse2); }); }); });