breaking (soft) - Migrate viewGroup.fieldMetadataId -> view.mainGroupByFieldMetadataId (2/3) (#16277)

Should be merged once https://github.com/twentyhq/twenty/pull/16206 has
been released + command run to prod

In this PR
- Remove usage of viewGroup.fieldMetadataId, both in BE and FE states. 
- But we still need to properly populate it until we fully remove
viewGroup.fieldMetadataId from db and ORM entity (upcoming 3rd PR out of
3). fieldMetadataId was removed from CoreViewGroup type and
CreateViewGroupInput and is determined BE-side based on the associated
view's mainGroupByFieldMetadataId. **I expect this means a downtime on
viewGroup creation, until both FE and BE are deployed and cache is
flushed.** This seems acceptable to me as it only regards viewGroup
creation.
    - this information is replaced by view.mainGroupByFieldMetadataID
- Handle view group creation, update and deletion in the BE as a
side-effect of a view creation, update or deletion. Optimistic effects
are still used
- Add validation at view creation or update regarding
mainGroupByFieldMetadata

Left to do in 3rd PR
- Remove viewGroup.fieldMetadataId from db and ORM entity
- Restore feature allowing to update an existing grouped view's group by
field (already OK on BE side but need to rebuild FE optimistic)
This commit is contained in:
Marie
2025-12-05 15:00:03 +01:00
committed by GitHub
parent 17d88a074b
commit 7ce22d5c7e
87 changed files with 1338 additions and 1067 deletions
@@ -4,6 +4,7 @@ export const VIEW_GQL_FIELDS = `
objectMetadataId
type
key
mainGroupByFieldMetadataId
icon
position
isCompact
@@ -50,7 +51,6 @@ export const VIEW_FILTER_GQL_FIELDS = `
export const VIEW_GROUP_GQL_FIELDS = `
id
fieldMetadataId
fieldValue
isVisible
position
@@ -9,6 +9,7 @@ exports[`successful find view with all sub-relations (e2e) Company View Structur
"id": Any<String>,
"isCompact": false,
"key": "INDEX",
"mainGroupByFieldMetadataId": null,
"name": "All Companies",
"objectMetadataId": Any<String>,
"openRecordIn": "SIDE_PANEL",
@@ -1,3 +1,4 @@
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.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';
@@ -9,6 +10,7 @@ import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'
describe('Create core view', () => {
let testObjectMetadataId: string;
let testSelectFieldMetadataId: string;
beforeAll(async () => {
const {
@@ -27,6 +29,15 @@ describe('Create core view', () => {
});
testObjectMetadataId = objectMetadataId;
const { selectFieldMetadataId } =
await createOneSelectFieldMetadataForIntegrationTests({
input: {
objectMetadataId,
},
});
testSelectFieldMetadataId = selectFieldMetadataId;
});
afterAll(async () => {
@@ -52,6 +63,7 @@ describe('Create core view', () => {
objectMetadataId: testObjectMetadataId,
icon: 'IconDeal',
type: ViewType.KANBAN,
mainGroupByFieldMetadataId: testSelectFieldMetadataId,
position: 1,
isCompact: true,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
@@ -63,6 +75,7 @@ describe('Create core view', () => {
assertViewStructure(data.createCoreView, {
name: 'Kanban View',
objectMetadataId: testObjectMetadataId,
mainGroupByFieldMetadataId: testSelectFieldMetadataId,
type: ViewType.KANBAN,
key: null,
icon: 'IconDeal',
@@ -90,6 +103,7 @@ describe('Create core view', () => {
objectMetadataId: input.objectMetadataId,
icon: input.icon,
type: ViewType.TABLE,
mainGroupByFieldMetadataId: null,
key: null,
position: 0,
isCompact: false,
@@ -1,4 +1,5 @@
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.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';
@@ -11,6 +12,7 @@ const TEST_NOT_EXISTING_VIEW_ID = '20202020-0000-4000-8000-000000000000';
describe('Update core view', () => {
let testObjectMetadataId: string;
let testSelectFieldMetadataId: string;
beforeAll(async () => {
const {
@@ -28,7 +30,31 @@ describe('Update core view', () => {
},
});
const { selectFieldMetadataId } =
await createOneSelectFieldMetadataForIntegrationTests({
expectToFail: false,
input: {
objectMetadataId,
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,
},
],
},
});
testObjectMetadataId = objectMetadataId;
testSelectFieldMetadataId = selectFieldMetadataId;
});
afterAll(async () => {
@@ -65,6 +91,7 @@ describe('Update core view', () => {
id: view.id,
name: 'Updated View',
type: ViewType.KANBAN,
mainGroupByFieldMetadataId: testSelectFieldMetadataId,
isCompact: true,
};
@@ -1,17 +1,15 @@
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createTestViewWithGraphQL } from 'test/integration/graphql/utils/view-graphql.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.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 { createOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/create-one-core-view-group.util';
import { deleteOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/delete-one-core-view-group.util';
import { destroyOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/destroy-one-core-view-group.util';
import { findCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/find-core-view-groups.util';
import { updateOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/update-one-core-view-group.util';
import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util';
import { assertViewGroupStructure } from 'test/integration/utils/view-test.util';
import { FieldMetadataType } from 'twenty-shared/types';
const TEST_NOT_EXISTING_VIEW_GROUP_ID = '20202020-0000-4000-8000-000000000003';
@@ -38,22 +36,14 @@ describe('View Group Resolver', () => {
testObjectMetadataId = objectMetadataId;
const {
data: {
createOneField: { id: fieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail: false,
input: {
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: testObjectMetadataId,
isLabelSyncedWithName: true,
},
});
const { selectFieldMetadataId } =
await createOneSelectFieldMetadataForIntegrationTests({
input: {
objectMetadataId: testObjectMetadataId,
},
});
testFieldMetadataId = fieldMetadataId;
testFieldMetadataId = selectFieldMetadataId;
});
afterAll(async () => {
@@ -76,6 +66,7 @@ describe('View Group Resolver', () => {
const view = await createTestViewWithGraphQL({
name: 'Test View for Groups',
objectMetadataId: testObjectMetadataId,
mainGroupByFieldMetadataId: testFieldMetadataId,
});
testViewId = view.id;
@@ -88,51 +79,12 @@ describe('View Group Resolver', () => {
});
});
describe('getCoreViewGroups', () => {
it('should return empty array when no view groups exist', async () => {
const { data } = await findCoreViewGroups({
viewId: testViewId,
expectToFail: false,
});
expect(data.getCoreViewGroups).toEqual([]);
});
it('should return view groups for a specific view', async () => {
await createOneCoreViewGroup({
expectToFail: false,
input: {
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
isVisible: true,
fieldValue: 'active',
position: 0,
},
});
const { data } = await findCoreViewGroups({
viewId: testViewId,
expectToFail: false,
});
expect(data.getCoreViewGroups).toHaveLength(1);
assertViewGroupStructure(data.getCoreViewGroups[0], {
fieldMetadataId: testFieldMetadataId,
isVisible: true,
fieldValue: 'active',
position: 0,
viewId: testViewId,
});
});
});
describe('createCoreViewGroup', () => {
it('should create a new view group', async () => {
const { data } = await createOneCoreViewGroup({
expectToFail: false,
input: {
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
isVisible: false,
fieldValue: 'inactive',
position: 1,
@@ -140,7 +92,6 @@ describe('View Group Resolver', () => {
});
assertViewGroupStructure(data.createCoreViewGroup, {
fieldMetadataId: testFieldMetadataId,
isVisible: false,
fieldValue: 'inactive',
position: 1,
@@ -153,7 +104,6 @@ describe('View Group Resolver', () => {
expectToFail: false,
input: {
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
isVisible: true,
fieldValue: '',
position: 2,
@@ -161,7 +111,6 @@ describe('View Group Resolver', () => {
});
assertViewGroupStructure(data.createCoreViewGroup, {
fieldMetadataId: testFieldMetadataId,
isVisible: true,
fieldValue: '',
position: 2,
@@ -175,7 +124,6 @@ describe('View Group Resolver', () => {
expectToFail: false,
input: {
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
isVisible: true,
fieldValue: 'original',
position: 0,
@@ -224,7 +172,6 @@ describe('View Group Resolver', () => {
expectToFail: false,
input: {
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
isVisible: true,
fieldValue: 'to delete',
position: 0,
@@ -263,7 +210,6 @@ describe('View Group Resolver', () => {
expectToFail: false,
input: {
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
isVisible: true,
fieldValue: 'to destroy',
position: 0,
@@ -1,104 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`View Group Resolver - Failing Create Many Operations - v2 should accumulate multiple validation errors when some inputs are invalid 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"agent": [],
"cronTrigger": [],
"databaseEventTrigger": [],
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"pageLayoutTab": [],
"pageLayoutWidget": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [],
"viewFilter": [],
"viewGroup": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_group",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_group",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_group",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidAgent": 0,
"invalidCronTrigger": 0,
"invalidDatabaseEventTrigger": 0,
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidPageLayoutTab": 0,
"invalidPageLayoutWidget": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
"invalidViewField": 0,
"invalidViewFilter": 0,
"invalidViewGroup": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating view groups",
"name": "GraphQLError",
}
`;
@@ -1,152 +0,0 @@
import { createManyCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/create-many-core-view-groups.util';
import { v4 as uuidv4 } from 'uuid';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
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 { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util';
import { FieldMetadataType } from 'twenty-shared/types';
import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
describe('View Group Resolver - Failing Create Many Operations - v2', () => {
let testSetup: {
testViewId: string;
testObjectMetadataId: string;
firstTestFieldMetadataId: string;
secondTestFieldMetadataId: string;
};
beforeAll(async () => {
const {
data: {
createOneObject: { id: objectMetadataId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'myGroupTestObjectV2',
namePlural: 'myGroupTestObjectsV2',
labelSingular: 'My Group Test Object v2',
labelPlural: 'My Group Test Objects v2',
icon: 'Icon123',
},
});
const {
data: {
createOneField: { id: firstTestFieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail: false,
input: {
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId,
isLabelSyncedWithName: true,
},
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
const {
data: {
createOneField: { id: secondTestFieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail: false,
input: {
name: 'secondTestField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId,
isLabelSyncedWithName: false,
},
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
const {
data: {
createCoreView: { id: testViewId },
},
} = await createOneCoreView({
input: {
icon: 'icon123',
objectMetadataId,
name: 'TestViewForGroups',
},
expectToFail: false,
});
testSetup = {
testViewId,
testObjectMetadataId: objectMetadataId,
firstTestFieldMetadataId,
secondTestFieldMetadataId,
};
});
afterAll(async () => {
await updateOneObjectMetadata({
input: {
idToUpdate: testSetup.testObjectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: testSetup.testObjectMetadataId },
});
});
it('should accumulate multiple validation errors when some inputs are invalid', async () => {
const invalidViewId = uuidv4();
const invalidFieldMetadataId = uuidv4();
const inputs: CreateViewGroupInput[] = [
{
fieldMetadataId: invalidFieldMetadataId,
viewId: testSetup.testViewId,
position: 0,
isVisible: true,
fieldValue: 'Invalid Group A',
},
{
fieldMetadataId: testSetup.firstTestFieldMetadataId,
viewId: invalidViewId,
position: 1,
isVisible: true,
fieldValue: 'Invalid Group B',
},
{
fieldMetadataId: invalidFieldMetadataId,
viewId: invalidViewId,
position: 2,
isVisible: true,
fieldValue: 'Invalid Group C',
},
];
const { errors } = await createManyCoreViewGroups({
inputs,
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
});
});
@@ -1,4 +1,5 @@
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.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';
@@ -100,6 +101,13 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => {
`,
});
const { selectFieldMetadataId } =
await createOneSelectFieldMetadataForIntegrationTests({
input: {
objectMetadataId,
},
});
const {
data: {
createCoreView: { id: testViewId },
@@ -109,6 +117,7 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => {
icon: 'icon123',
objectMetadataId,
name: 'TestViewForGroups',
mainGroupByFieldMetadataId: selectFieldMetadataId,
},
expectToFail: false,
});
@@ -164,21 +173,18 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => {
it('should successfully create multiple view groups in batch', async () => {
const inputs: CreateViewGroupInput[] = [
{
fieldMetadataId: testSetup.firstTestFieldMetadataId,
viewId: testSetup.testViewId,
position: 0,
isVisible: true,
fieldValue: 'Group A',
},
{
fieldMetadataId: testSetup.secondTestFieldMetadataId,
viewId: testSetup.testViewId,
position: 1,
isVisible: false,
fieldValue: 'Group B',
},
{
fieldMetadataId: testSetup.thirdTestFieldMetadataId,
viewId: testSetup.testViewId,
position: 2,
isVisible: true,
@@ -200,7 +206,6 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => {
createdViewGroups.forEach((viewGroup, index) => {
expect(viewGroup).toMatchObject({
fieldMetadataId: inputs[index].fieldMetadataId,
viewId: testSetup.testViewId,
position: inputs[index].position,
isVisible: inputs[index].isVisible,
@@ -214,7 +219,6 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => {
it('should successfully create single view group using batch endpoint', async () => {
const inputs: CreateViewGroupInput[] = [
{
fieldMetadataId: testSetup.firstTestFieldMetadataId,
viewId: testSetup.testViewId,
position: 5,
isVisible: true,
@@ -237,7 +241,6 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => {
const viewGroup = createdViewGroups[0];
expect(viewGroup).toMatchObject({
fieldMetadataId: testSetup.firstTestFieldMetadataId,
viewId: testSetup.testViewId,
position: 5,
isVisible: true,
@@ -15,6 +15,8 @@ export const createTestViewWithGraphQL = async (
icon: viewData.icon,
type: viewData.type,
position: viewData.position,
mainGroupByFieldMetadataId:
viewData.mainGroupByFieldMetadataId ?? undefined,
isCompact: viewData.isCompact,
openRecordIn: viewData.openRecordIn,
visibility: viewData.visibility,
@@ -1,4 +1,5 @@
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util';
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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';
@@ -271,12 +272,21 @@ describe('calendar-field-deactivation-deletes-views', () => {
});
it('should delete calendar view but not other view types when calendar field is deactivated', async () => {
const { selectFieldMetadataId } =
await createOneSelectFieldMetadataForIntegrationTests({
input: {
objectMetadataId: testSetup.objectMetadataId,
name: 'selectField',
},
});
const {
data: { createCoreView: kanbanViewWithSameObject },
} = await createOneCoreView({
input: {
name: generateRecordName('Kanban View'),
objectMetadataId: testSetup.objectMetadataId,
mainGroupByFieldMetadataId: selectFieldMetadataId,
type: ViewType.KANBAN,
icon: 'IconLayoutKanban',
},
@@ -3,7 +3,6 @@ import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-m
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 { createOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/create-one-core-view-group.util';
import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util';
import { findOneCoreView } from 'test/integration/metadata/suites/view/utils/find-one-core-view.util';
import { generateRecordName } from 'test/integration/utils/generate-record-name';
@@ -29,7 +28,7 @@ type TestSetup = {
viewWithoutGroupId: string;
};
describe('view-group-field-deactivation-deletes-views', () => {
describe('field-for-group-by-deactivation-deletes-views', () => {
let testSetup: TestSetup;
const verifyViewExists = async (viewId: string, shouldExist: boolean) => {
@@ -126,22 +125,13 @@ describe('view-group-field-deactivation-deletes-views', () => {
name: generateRecordName('View With Group'),
objectMetadataId,
type: ViewType.TABLE,
mainGroupByFieldMetadataId: groupByFieldMetadataId,
icon: 'IconTable',
},
gqlFields: VIEW_FIELDS,
expectToFail: false,
});
await createOneCoreViewGroup({
input: {
fieldMetadataId: groupByFieldMetadataId,
fieldValue: 'OPTION_1',
viewId: viewWithGroup.id,
},
gqlFields: 'id',
expectToFail: false,
});
const {
data: { createCoreView: viewWithoutGroup },
} = await createOneCoreView({
@@ -180,7 +170,7 @@ describe('view-group-field-deactivation-deletes-views', () => {
});
});
it('should delete view when field used in view group is deactivated', async () => {
it('should delete view when mainGroupByFieldMetadata is deactivated', async () => {
await verifyViewExists(testSetup.viewWithGroupId, true);
await verifyViewExists(testSetup.viewWithoutGroupId, true);
@@ -208,22 +198,13 @@ describe('view-group-field-deactivation-deletes-views', () => {
name: generateRecordName('Second View With Group'),
objectMetadataId: testSetup.objectMetadataId,
type: ViewType.TABLE,
mainGroupByFieldMetadataId: testSetup.groupByFieldMetadataId,
icon: 'IconTable',
},
gqlFields: VIEW_FIELDS,
expectToFail: false,
});
await createOneCoreViewGroup({
input: {
fieldMetadataId: testSetup.groupByFieldMetadataId,
fieldValue: 'OPTION_2',
viewId: secondViewWithGroup.id,
},
gqlFields: 'id',
expectToFail: false,
});
await verifyViewExists(testSetup.viewWithGroupId, true);
await verifyViewExists(secondViewWithGroup.id, true);
await verifyViewExists(testSetup.viewWithoutGroupId, true);
@@ -234,74 +215,4 @@ describe('view-group-field-deactivation-deletes-views', () => {
await verifyViewExists(secondViewWithGroup.id, false);
await verifyViewExists(testSetup.viewWithoutGroupId, true);
});
it('should handle deactivation when view has multiple view groups with different fields', async () => {
const {
data: {
createOneField: { id: anotherGroupByFieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail: false,
input: {
name: 'anotherGroupByField',
type: FieldMetadataType.SELECT,
label: 'Another Group By Field',
objectMetadataId: testSetup.objectMetadataId,
options: [
{
label: 'Status A',
value: 'STATUS_A',
color: 'purple',
position: 0,
},
{
label: 'Status B',
value: 'STATUS_B',
color: 'orange',
position: 1,
},
],
},
gqlFields: 'id',
});
const {
data: { createCoreView: viewWithMultipleGroups },
} = await createOneCoreView({
input: {
name: generateRecordName('View With Multiple Groups'),
objectMetadataId: testSetup.objectMetadataId,
type: ViewType.TABLE,
icon: 'IconTable',
},
gqlFields: VIEW_FIELDS,
expectToFail: false,
});
await createOneCoreViewGroup({
input: {
fieldMetadataId: testSetup.groupByFieldMetadataId,
fieldValue: 'OPTION_1',
viewId: viewWithMultipleGroups.id,
},
gqlFields: 'id',
expectToFail: false,
});
await createOneCoreViewGroup({
input: {
fieldMetadataId: anotherGroupByFieldMetadataId,
fieldValue: 'STATUS_A',
viewId: viewWithMultipleGroups.id,
},
gqlFields: 'id',
expectToFail: false,
});
await verifyViewExists(viewWithMultipleGroups.id, true);
await deactivateFieldAndVerify(testSetup.groupByFieldMetadataId);
await verifyViewExists(viewWithMultipleGroups.id, false);
});
});
@@ -1,4 +1,5 @@
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util';
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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';
@@ -28,6 +29,7 @@ type TestSetup = {
objectMetadataId: string;
aggregateFieldMetadataId: string;
nonAggregateFieldMetadataId: string;
selectFieldMetadataId: string;
viewWithAggregateId: string;
viewWithoutAggregateId: string;
};
@@ -115,6 +117,29 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () =
gqlFields: 'id',
});
const { selectFieldMetadataId } =
await createOneSelectFieldMetadataForIntegrationTests({
input: {
name: 'selectField',
objectMetadataId,
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: { createCoreView: viewWithAggregate },
} = await createOneCoreView({
@@ -122,6 +147,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () =
name: generateRecordName('Kanban View With Aggregate'),
objectMetadataId,
type: ViewType.KANBAN,
mainGroupByFieldMetadataId: selectFieldMetadataId,
kanbanAggregateOperationFieldMetadataId: aggregateFieldMetadataId,
kanbanAggregateOperation: AggregateOperations.SUM,
icon: 'IconLayoutKanban',
@@ -136,6 +162,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () =
input: {
name: generateRecordName('Kanban View Without Aggregate'),
objectMetadataId,
mainGroupByFieldMetadataId: selectFieldMetadataId,
type: ViewType.KANBAN,
icon: 'IconLayoutKanban',
},
@@ -147,6 +174,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () =
objectMetadataId,
aggregateFieldMetadataId,
nonAggregateFieldMetadataId,
selectFieldMetadataId,
viewWithAggregateId: viewWithAggregate.id,
viewWithoutAggregateId: viewWithoutAggregate.id,
};
@@ -253,6 +281,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () =
name: generateRecordName('Second Kanban View With Aggregate'),
objectMetadataId: testSetup.objectMetadataId,
type: ViewType.KANBAN,
mainGroupByFieldMetadataId: testSetup.selectFieldMetadataId,
kanbanAggregateOperationFieldMetadataId:
testSetup.aggregateFieldMetadataId,
kanbanAggregateOperation: AggregateOperations.MAX,
@@ -318,6 +347,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () =
name: generateRecordName('Kanban View With MIN'),
objectMetadataId: testSetup.objectMetadataId,
type: ViewType.KANBAN,
mainGroupByFieldMetadataId: testSetup.selectFieldMetadataId,
kanbanAggregateOperationFieldMetadataId:
testSetup.aggregateFieldMetadataId,
kanbanAggregateOperation: AggregateOperations.MIN,
@@ -334,6 +364,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () =
name: generateRecordName('Kanban View With AVG'),
objectMetadataId: testSetup.objectMetadataId,
type: ViewType.KANBAN,
mainGroupByFieldMetadataId: testSetup.selectFieldMetadataId,
kanbanAggregateOperationFieldMetadataId:
testSetup.aggregateFieldMetadataId,
kanbanAggregateOperation: AggregateOperations.AVG,
@@ -5,13 +5,9 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { createOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/create-one-core-view-group.util';
import { findCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/find-core-view-groups.util';
import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util';
import {
FieldMetadataType,
type EnumFieldMetadataType,
} from 'twenty-shared/types';
import { FieldMetadataType } from 'twenty-shared/types';
import {
type FieldMetadataComplexOption,
@@ -37,16 +33,10 @@ const fakeOptionUpdate = ({ value, label, ...option }: Option) => ({
label: `${label} updated`,
});
const testFieldMetadataTypes: EnumFieldMetadataType[] = [
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
];
describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
let objectMetadataIdToDelete: string;
const createObjectWithSelectFieldAndView = async (
fieldType: EnumFieldMetadataType,
initialOptions: Option[],
) => {
const singular = 'sideEffect';
@@ -69,11 +59,11 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
const {
data: { createOneField },
} = await createOneFieldMetadata<typeof fieldType>({
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createOneObject.id,
type: fieldType,
type: FieldMetadataType.SELECT,
name: 'statusField',
label: 'Status Field',
isLabelSyncedWithName: true,
@@ -90,6 +80,7 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
icon: 'IconTable',
name: 'Test View',
objectMetadataId: createOneObject.id,
mainGroupByFieldMetadataId: createOneField.id,
type: ViewType.TABLE,
},
expectToFail: false,
@@ -120,38 +111,30 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
});
});
describe.each(testFieldMetadataTypes)('%s field type', (fieldType) => {
it('should delete all view groups when all enum field options are deleted', async () => {
describe('SELECT field type', () => {
it('deleted and adds view groups when options are deleted and added', async () => {
const initialOptions = generateOptions(3);
const { fieldMetadataId, viewId } =
await createObjectWithSelectFieldAndView(fieldType, initialOptions);
for (const [index, option] of initialOptions.entries()) {
await createOneCoreViewGroup({
input: {
viewId,
fieldMetadataId,
fieldValue: option.value,
isVisible: true,
position: index,
},
expectToFail: false,
gqlFields: 'id fieldValue',
});
}
await createObjectWithSelectFieldAndView(initialOptions);
const {
data: { getCoreViewGroups: initialViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue fieldMetadataId',
gqlFields: 'id fieldValue',
expectToFail: false,
});
expect(
initialViewGroups.filter((vg) => vg.fieldMetadataId === fieldMetadataId)
.length,
).toBe(3);
expect(initialViewGroups.length).toBe(4);
const initialFieldValues = [...initialViewGroups]
.map((vg) => vg.fieldValue)
.sort();
const expectedFieldValues = [
...initialOptions.map((opt) => opt.value),
'',
].sort();
expect(initialFieldValues).toEqual(expectedFieldValues);
await updateOneFieldMetadata({
input: {
@@ -175,82 +158,22 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue fieldMetadataId',
gqlFields: 'id fieldValue viewId',
expectToFail: false,
});
expect(
updatedViewGroups.filter((vg) => vg.fieldMetadataId === fieldMetadataId)
.length,
).toBe(0);
expect(updatedViewGroups.length).toBe(2);
const updatedFieldValues = updatedViewGroups
.map((vg) => vg.fieldValue)
.sort();
const expectedUpdatedFieldValues = ['', 'NEW_OPTION_VALUE'].sort();
expect(updatedFieldValues).toEqual(expectedUpdatedFieldValues);
});
it('should update view group when option value is updated', async () => {
const initialOptions = generateOptions(1);
const { fieldMetadataId, fieldOptions, viewId } =
await createObjectWithSelectFieldAndView(fieldType, initialOptions);
await createOneCoreViewGroup({
input: {
viewId,
fieldMetadataId,
fieldValue: initialOptions[0].value,
isVisible: true,
position: 0,
},
expectToFail: false,
gqlFields: 'id fieldValue',
});
const updatedOptions = [fakeOptionUpdate(fieldOptions[0])];
await updateOneFieldMetadata({
input: {
idToUpdate: fieldMetadataId,
updatePayload: {
options: updatedOptions,
},
},
gqlFields: 'id options',
expectToFail: false,
});
const {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue fieldMetadataId',
expectToFail: false,
});
const viewGroupsForField = updatedViewGroups.filter(
(vg) => vg.fieldMetadataId === fieldMetadataId,
);
expect(viewGroupsForField.length).toBe(1);
expect(viewGroupsForField[0].fieldValue).toBe(
`${initialOptions[0].value}_UPDATED`,
);
});
it('should update multiple view groups when multiple option values are updated', async () => {
it('updates specific view groups when some options are updated', async () => {
const initialOptions = generateOptions(3);
const { fieldMetadataId, fieldOptions, viewId } =
await createObjectWithSelectFieldAndView(fieldType, initialOptions);
for (const [index, option] of initialOptions.entries()) {
await createOneCoreViewGroup({
input: {
viewId,
fieldMetadataId,
fieldValue: option.value,
isVisible: true,
position: index,
},
expectToFail: false,
gqlFields: 'id fieldValue',
});
}
await createObjectWithSelectFieldAndView(initialOptions);
const updatedOptions = fieldOptions.map((opt) => fakeOptionUpdate(opt));
@@ -269,43 +192,26 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue fieldMetadataId',
gqlFields: 'id fieldValue',
expectToFail: false,
});
const viewGroupsForField = updatedViewGroups.filter(
(vg) => vg.fieldMetadataId === fieldMetadataId,
);
expect(viewGroupsForField.length).toBe(3);
const actualFieldValues = viewGroupsForField
expect(updatedViewGroups.length).toBe(4);
const updatedFieldValues = updatedViewGroups
.map((vg) => vg.fieldValue)
.sort();
const expectedFieldValues = initialOptions
.map((opt) => `${opt.value}_UPDATED`)
.sort();
const expectedUpdatedFieldValues = [
...updatedOptions.map((opt) => opt.value),
'',
].sort();
expect(actualFieldValues).toEqual(expectedFieldValues);
expect(updatedFieldValues).toEqual(expectedUpdatedFieldValues);
});
it('should delete specific view groups when their options are removed', async () => {
it('deletes specific view groups when some of all options are removed', async () => {
const initialOptions = generateOptions(5);
const { fieldMetadataId, fieldOptions, viewId } =
await createObjectWithSelectFieldAndView(fieldType, initialOptions);
for (const [index, option] of initialOptions.entries()) {
await createOneCoreViewGroup({
input: {
viewId,
fieldMetadataId,
fieldValue: option.value,
isVisible: true,
position: index,
},
expectToFail: false,
gqlFields: 'id fieldValue',
});
}
await createObjectWithSelectFieldAndView(initialOptions);
const updatedOptions = fieldOptions.slice(2);
@@ -324,44 +230,26 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue fieldMetadataId',
gqlFields: 'id fieldValue',
expectToFail: false,
});
const viewGroupsForField = updatedViewGroups.filter(
(vg) => vg.fieldMetadataId === fieldMetadataId,
);
expect(updatedViewGroups.length).toBe(4);
const remainingFieldValues = updatedViewGroups
.map((vg) => vg.fieldValue)
.sort();
const expectedRemainingFieldValues = [
...updatedOptions.map((opt) => opt.value),
'',
].sort();
expect(viewGroupsForField.length).toBe(3);
const remainingFieldValues = viewGroupsForField.map(
(vg) => vg.fieldValue,
);
expect(remainingFieldValues).not.toContain(initialOptions[0].value);
expect(remainingFieldValues).not.toContain(initialOptions[1].value);
expect(remainingFieldValues).toContain(initialOptions[2].value);
expect(remainingFieldValues).toContain(initialOptions[3].value);
expect(remainingFieldValues).toContain(initialOptions[4].value);
expect(remainingFieldValues).toEqual(expectedRemainingFieldValues);
});
it('should preserve view groups when no options are changed', async () => {
it('preserves view groups when no options are changed', async () => {
const initialOptions = generateOptions(3);
const { fieldMetadataId, fieldOptions, viewId } =
await createObjectWithSelectFieldAndView(fieldType, initialOptions);
for (const [index, option] of initialOptions.entries()) {
await createOneCoreViewGroup({
input: {
viewId,
fieldMetadataId,
fieldValue: option.value,
isVisible: true,
position: index,
},
expectToFail: false,
gqlFields: 'id fieldValue',
});
}
await createObjectWithSelectFieldAndView(initialOptions);
await updateOneFieldMetadata({
input: {
@@ -378,41 +266,26 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue fieldMetadataId',
gqlFields: 'id fieldValue',
expectToFail: false,
});
const viewGroupsForField = updatedViewGroups.filter(
(vg) => vg.fieldMetadataId === fieldMetadataId,
);
expect(viewGroupsForField.length).toBe(3);
const actualFieldValues = viewGroupsForField
expect(updatedViewGroups.length).toBe(4);
const actualFieldValues = updatedViewGroups
.map((vg) => vg.fieldValue)
.sort();
const expectedFieldValues = initialOptions.map((opt) => opt.value).sort();
const expectedFieldValues = [
...initialOptions.map((opt) => opt.value),
'',
].sort();
expect(actualFieldValues).toEqual(expectedFieldValues);
});
it('should handle adding new options while maintaining existing view groups', async () => {
it('adds new view groups when new options are added', async () => {
const initialOptions = generateOptions(3);
const { fieldMetadataId, fieldOptions, viewId } =
await createObjectWithSelectFieldAndView(fieldType, initialOptions);
for (const [index, option] of initialOptions.entries()) {
await createOneCoreViewGroup({
input: {
viewId,
fieldMetadataId,
fieldValue: option.value,
isVisible: true,
position: index,
},
expectToFail: false,
gqlFields: 'id fieldValue',
});
}
await createObjectWithSelectFieldAndView(initialOptions);
const newOptions = generateOptions(6).slice(3);
const updatedOptions = [...fieldOptions, ...newOptions];
@@ -432,19 +305,18 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue fieldMetadataId',
gqlFields: 'id fieldValue',
expectToFail: false,
});
const viewGroupsForField = updatedViewGroups.filter(
(vg) => vg.fieldMetadataId === fieldMetadataId,
);
expect(viewGroupsForField.length).toBe(6);
const actualFieldValues = viewGroupsForField
expect(updatedViewGroups.length).toBe(7);
const actualFieldValues = updatedViewGroups
.map((vg) => vg.fieldValue)
.sort();
const expectedFieldValues = updatedOptions.map((opt) => opt.value).sort();
const expectedFieldValues = [
...updatedOptions.map((opt) => opt.value),
'',
].sort();
expect(actualFieldValues).toEqual(expectedFieldValues);
});
@@ -0,0 +1,41 @@
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import {
type FieldMetadataComplexOption,
FieldMetadataType,
} from 'twenty-shared/types';
import { type CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
export const createOneSelectFieldMetadataForIntegrationTests = async ({
input,
expectToFail = false,
}: {
input: Partial<CreateOneFieldMetadataInput> & {
objectMetadataId: string;
options?: FieldMetadataComplexOption[];
name?: string;
};
expectToFail?: boolean;
}) => {
const {
data: {
createOneField: { id: selectFieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail,
input: {
name: 'normalField',
type: FieldMetadataType.SELECT,
label: 'Select Field',
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 },
],
...input,
},
gqlFields: 'id',
});
return { selectFieldMetadataId };
};
@@ -0,0 +1,285 @@
import { faker } from '@faker-js/faker';
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 { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { findCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/find-core-view-groups.util';
import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util';
import { updateOneCoreView } from 'test/integration/metadata/suites/view/utils/update-one-core-view.util';
import {
FieldMetadataType,
type EnumFieldMetadataType,
} from 'twenty-shared/types';
import {
type FieldMetadataComplexOption,
type FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption;
const generateOption = (index: number): Option => ({
label: `Option ${index}`,
value: `OPTION_${index}`,
color: 'green',
position: index,
});
const generateOptions = (length: number) =>
Array.from({ length }, (_value, index) => generateOption(index));
const testFieldMetadataTypes: EnumFieldMetadataType[] = [
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
];
describe('update-one-view-view-groups-side-effect-v2', () => {
let objectMetadataIdToDelete: string;
const createObjectWithTwoSelectFieldsAndKanbanViewAndGroupedTableView =
async (initialOptions: Option[]) => {
const singular = 'viewSideEffect';
const plural = 'viewSideEffects';
const {
data: { createOneObject },
} = await createOneObjectMetadata({
expectToFail: false,
input: getMockCreateObjectInput({
labelSingular: singular,
labelPlural: plural,
nameSingular: singular,
namePlural: plural,
isLabelSyncedWithName: false,
}),
});
objectMetadataIdToDelete = createOneObject.id;
const {
data: { createOneField: createOneStatusField },
} = await createOneFieldMetadata<typeof FieldMetadataType.SELECT>({
expectToFail: false,
input: {
objectMetadataId: createOneObject.id,
type: FieldMetadataType.SELECT,
name: 'statusField',
label: 'Status Field',
isLabelSyncedWithName: true,
options: initialOptions,
},
gqlFields: 'id options',
});
const phaseFieldOptions = generateOptions(2);
const {
data: { createOneField: createOnePhaseField },
} = await createOneFieldMetadata<typeof FieldMetadataType.SELECT>({
expectToFail: false,
input: {
objectMetadataId: createOneObject.id,
type: FieldMetadataType.SELECT,
name: 'phaseField',
label: 'Phase Field',
isLabelSyncedWithName: true,
options: phaseFieldOptions,
},
gqlFields: 'id options',
});
const {
data: { createCoreView: view },
} = await createOneCoreView({
input: {
id: faker.string.uuid(),
icon: 'IconKanban',
name: 'Kanban View',
objectMetadataId: createOneObject.id,
type: ViewType.KANBAN,
mainGroupByFieldMetadataId: createOneStatusField.id,
},
expectToFail: false,
gqlFields: 'id',
});
const {
data: { createCoreView: groupedTableView },
} = await createOneCoreView({
input: {
id: faker.string.uuid(),
icon: 'IconKanban',
name: 'Grouped Table View',
objectMetadataId: createOneObject.id,
type: ViewType.TABLE,
mainGroupByFieldMetadataId: createOneStatusField.id,
},
expectToFail: false,
gqlFields: 'id',
});
return {
objectMetadataId: createOneObject.id,
statusFieldMetadataId: createOneStatusField.id,
phaseFieldMetadataId: createOnePhaseField.id,
statusFieldOptions: createOneStatusField.options ?? [],
viewId: view.id,
groupedTableViewId: groupedTableView.id,
phaseFieldOptions,
};
};
afterEach(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: objectMetadataIdToDelete,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: objectMetadataIdToDelete },
});
});
describe.each(testFieldMetadataTypes)('%s field type', () => {
it('should delete all view groups when mainGroupByFieldMetadataId is removed', async () => {
const initialOptions = generateOptions(3);
const { groupedTableViewId } =
await createObjectWithTwoSelectFieldsAndKanbanViewAndGroupedTableView(
initialOptions,
);
const {
data: { getCoreViewGroups: initialViewGroups },
} = await findCoreViewGroups({
viewId: groupedTableViewId,
gqlFields: 'id fieldValue',
expectToFail: false,
});
expect(initialViewGroups.length).toBe(4);
await updateOneCoreView({
viewId: groupedTableViewId,
input: {
id: groupedTableViewId,
mainGroupByFieldMetadataId: null,
},
gqlFields: 'id mainGroupByFieldMetadataId',
expectToFail: false,
});
const {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId: groupedTableViewId,
gqlFields: 'id fieldValue',
expectToFail: false,
});
expect(updatedViewGroups.length).toBe(0);
});
it('should delete and recreate view groups when mainGroupByFieldMetadataId changes', async () => {
const initialOptions = generateOptions(3);
const { phaseFieldOptions, phaseFieldMetadataId, viewId } =
await createObjectWithTwoSelectFieldsAndKanbanViewAndGroupedTableView(
initialOptions,
);
const {
data: { getCoreViewGroups: initialViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue',
expectToFail: false,
});
expect(initialViewGroups.length).toBe(initialOptions.length + 1); // null option
await updateOneCoreView({
viewId,
input: {
id: viewId,
mainGroupByFieldMetadataId: phaseFieldMetadataId,
},
gqlFields: 'id mainGroupByFieldMetadataId',
expectToFail: false,
});
const {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue',
expectToFail: false,
});
expect(updatedViewGroups.length).toBe(phaseFieldOptions.length + 1);
const actualFieldValues = updatedViewGroups
.map((viewGroup) => viewGroup.fieldValue)
.sort();
const expectedFieldValues = [...phaseFieldOptions, { value: '' }]
.map((option) => option.value)
.sort();
expect(actualFieldValues).toEqual(expectedFieldValues);
});
it('should preserve view groups when mainGroupByFieldMetadataId does not change', async () => {
const initialOptions = generateOptions(3);
const { statusFieldOptions, viewId } =
await createObjectWithTwoSelectFieldsAndKanbanViewAndGroupedTableView(
initialOptions,
);
const {
data: { getCoreViewGroups: initialViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue',
expectToFail: false,
});
expect(initialViewGroups.length).toBe(statusFieldOptions.length + 1);
await updateOneCoreView({
viewId,
input: {
id: viewId,
name: 'Renamed Kanban View',
},
gqlFields: 'id name mainGroupByFieldMetadataId',
expectToFail: false,
});
const {
data: { getCoreViewGroups: updatedViewGroups },
} = await findCoreViewGroups({
viewId,
gqlFields: 'id fieldValue',
expectToFail: false,
});
expect(updatedViewGroups.length).toBe(statusFieldOptions.length + 1);
const actualFieldValues = updatedViewGroups
.map((viewGroup) => viewGroup.fieldValue)
.sort();
const expectedFieldValues = [...statusFieldOptions, { value: '' }]
.map((option) => option.value)
.sort();
expect(actualFieldValues).toEqual(expectedFieldValues);
});
});
});
@@ -24,8 +24,8 @@ exports[`View Group REST API POST /metadata/viewGroups should fail to create vie
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
"message": "Field value is required",
"userFriendlyMessage": "Field value is required",
},
],
"flatEntityMinimalInformation": {
@@ -14,12 +14,8 @@ import {
} 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';
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
describe('View Group REST API', () => {
let testObjectMetadataId: string;
let testFieldMetadataId: string;
@@ -47,9 +43,14 @@ describe('View Group REST API', () => {
const createFieldInput = {
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
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 {
@@ -72,6 +73,7 @@ describe('View Group REST API', () => {
const testView = await createTestViewWithRestApi({
name: 'Test View for Group Integration',
objectMetadataId: testObjectMetadataId,
mainGroupByFieldMetadataId: testFieldMetadataId,
});
testViewId = testView.id;
@@ -106,17 +108,6 @@ describe('View Group REST API', () => {
});
describe('GET /metadata/viewGroups', () => {
it('should return empty array when no view groups exist', async () => {
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/viewGroups?viewId=${testViewId}`,
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
});
assertRestApiSuccessfulResponse(response);
expect(response.body).toEqual([]);
});
it('should return all view groups for workspace when no viewId provided', async () => {
const response = await makeRestAPIRequest({
method: 'get',
@@ -129,16 +120,6 @@ describe('View Group REST API', () => {
});
it('should return view groups for a specific view after creating one', async () => {
const viewGroup = await createTestViewGroupWithRestApi({
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
fieldValue: 'test-field-value',
isVisible: true,
position: 0,
});
testViewGroupId = viewGroup.id;
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/viewGroups?viewId=${testViewId}`,
@@ -148,79 +129,36 @@ describe('View Group REST API', () => {
assertRestApiSuccessfulResponse(response);
expect(Array.isArray(response.body)).toBe(true);
const returnedViewGroup = response.body.find(
(el: ViewGroupDTO) => el.id === viewGroup.id,
);
const returnedViewGroups = response.body;
jestExpectToBeDefined(returnedViewGroup);
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', ''];
assertViewGroupStructure(returnedViewGroup, {
id: viewGroup.id,
viewId: testViewId,
fieldValue: 'test-field-value',
isVisible: true,
position: 0,
// Check structure and visibility for each group
expectedFieldValues.forEach((expectedFieldValue) => {
const group = returnedViewGroups.find(
(group: any) => group.fieldValue === expectedFieldValue,
);
expect(group).toBeDefined();
expect(group.isVisible).toBe(true);
expect(group.viewId).toBe(testViewId);
});
testViewGroupId = viewGroup.id;
});
it('should return multiple view groups for a view', async () => {
const viewGroup1 = await createTestViewGroupWithRestApi({
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
fieldValue: 'group-1',
position: 0,
});
const viewGroup2 = await createTestViewGroupWithRestApi({
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
fieldValue: 'group-2',
position: 1,
});
const response = await makeRestAPIRequest({
method: 'get',
path: `/metadata/viewGroups?viewId=${testViewId}`,
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
});
assertRestApiSuccessfulResponse(response);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body).toHaveLength(2);
const group1 = response.body.find(
(group: ViewGroupEntity) => group.id === viewGroup1.id,
);
const group2 = response.body.find(
(group: ViewGroupEntity) => group.id === viewGroup2.id,
);
assertViewGroupStructure(group1, {
fieldValue: 'group-1',
position: 0,
});
assertViewGroupStructure(group2, {
fieldValue: 'group-2',
position: 1,
});
testViewGroupId = viewGroup2.id;
});
});
describe('GET /metadata/viewGroups/:id', () => {
it('should return a specific view group by id', async () => {
const viewGroup = await createTestViewGroupWithRestApi({
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
fieldValue: 'specific-group',
isVisible: false,
const viewGroupsFromViewReponse = await makeRestAPIRequest({
method: 'get',
path: `/metadata/viewGroups?viewId=${testViewId}`,
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
});
testViewGroupId = viewGroup.id;
const viewGroup = viewGroupsFromViewReponse.body.find(
(group: any) => group.fieldValue === 'OPTION_1',
);
const response = await makeRestAPIRequest({
method: 'get',
@@ -232,8 +170,8 @@ describe('View Group REST API', () => {
assertViewGroupStructure(response.body, {
id: viewGroup.id,
viewId: testViewId,
fieldValue: 'specific-group',
isVisible: false,
fieldValue: 'OPTION_1',
isVisible: true,
});
testViewGroupId = viewGroup.id;
@@ -244,7 +182,6 @@ describe('View Group REST API', () => {
it('should create a new view group', async () => {
const viewGroupData = {
viewId: testViewId,
fieldMetadataId: testFieldMetadataId,
fieldValue: 'new-group-value',
isVisible: true,
position: 5,
@@ -1,3 +1,4 @@
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.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';
@@ -17,6 +18,7 @@ import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'
describe('View REST API', () => {
let testObjectMetadataId: string;
let testSelectFieldMetadataId: string;
let testViewId: string | undefined;
beforeAll(async () => {
@@ -35,6 +37,15 @@ describe('View REST API', () => {
});
testObjectMetadataId = objectMetadataId;
const { selectFieldMetadataId } =
await createOneSelectFieldMetadataForIntegrationTests({
input: {
objectMetadataId,
},
});
testSelectFieldMetadataId = selectFieldMetadataId;
});
afterAll(async () => {
@@ -129,6 +140,7 @@ describe('View REST API', () => {
isCompact: true,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
objectMetadataId: testObjectMetadataId,
mainGroupByFieldMetadataId: testSelectFieldMetadataId,
});
testViewId = kanbanView.id;
@@ -206,6 +218,7 @@ describe('View REST API', () => {
type: ViewType.KANBAN,
isCompact: true,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
mainGroupByFieldMetadataId: testSelectFieldMetadataId,
};
const response = await makeRestAPIRequest({
@@ -78,7 +78,6 @@ export const assertViewGroupStructure = (
) => {
expect(viewGroup).toBeDefined();
expect(viewGroup.id).toBeDefined();
expect(viewGroup.fieldMetadataId).toBeDefined();
expect(viewGroup.viewId).toBeDefined();
expect(viewGroup.fieldValue).toBeDefined();
expect(typeof viewGroup.isVisible).toBe('boolean');