9f30915f6f
## Context Follow-up to #21228, which deprecated `isCustom` on object/field metadata but kept it exposed because the frontend still relied on it. This removes it from the GraphQL API and the frontend entirely. ## Implementation ### Server - Remove `isCustom` `@Field` from the `Object`, `Field`, and `MinimalObjectMetadata` GraphQL types - Remove the `isCustom` `@ResolveField` resolvers and the `isCustomLoader` dataloader (+ payload/interface) - Remove `isCustom` as an internal `@HideField()` on the Object/Field DTOs used by the i18n standard-override gate > Use an explicit isStandard instead (which is the correct gating) ### Frontend - Add `getIsMetadataItemCustom` helper + `useGetIsMetadataItemCustom` hook: an item is custom when `applicationId === currentWorkspace.workspaceCustomApplication.id` - Migrate all consumers off `objectMetadataItem.isCustom` / `fieldMetadataItem.isCustom`; `isRecordFieldReadOnly` now takes a precomputed `isFieldCustom` - Drop `isCustom` from the metadata fragment/mutations/minimal query, FE types, zod schemas, and mock generators; regenerate GraphQL types ## Notes - Breaking change on the (already-deprecated) `Object.isCustom` / `Field.isCustom` GraphQL fields and the `isCustom` filter - FE semantic is "belongs to the workspace custom app" (third-party-app objects/fields are treated as non-custom) - `isCustom` on IndexMetadata / View / Skill / Agent is a separate column and is untouched - Breaking changes on REST metadata API
224 lines
6.9 KiB
TypeScript
224 lines
6.9 KiB
TypeScript
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
|
import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util';
|
|
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
|
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
|
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
|
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
|
import { type Manifest } from 'twenty-shared/application';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
const TEST_APP_ID = uuidv4();
|
|
const TEST_ROLE_ID = uuidv4();
|
|
|
|
const buildManifest = (
|
|
overrides?: Partial<Pick<Manifest, 'objects' | 'fields'>>,
|
|
) => buildBaseManifest({ appId: TEST_APP_ID, roleId: TEST_ROLE_ID, overrides });
|
|
|
|
const OBJECT_GQL_FIELDS =
|
|
'id nameSingular namePlural labelSingular labelPlural description icon isActive universalIdentifier';
|
|
|
|
const findCustomObjects = async () => {
|
|
const { objects } = await findManyObjectMetadata({
|
|
input: {
|
|
filter: {},
|
|
paging: { first: 100 },
|
|
},
|
|
gqlFields: OBJECT_GQL_FIELDS,
|
|
expectToFail: false,
|
|
});
|
|
|
|
return objects;
|
|
};
|
|
|
|
describe('Manifest update - objects', () => {
|
|
beforeEach(async () => {
|
|
await setupApplicationForSync({
|
|
applicationUniversalIdentifier: TEST_APP_ID,
|
|
name: 'Test Application',
|
|
description: 'App for testing object manifest updates',
|
|
sourcePath: 'test-manifest-update-object',
|
|
});
|
|
}, 60000);
|
|
|
|
afterEach(async () => {
|
|
await cleanupApplicationAndAppRegistration({
|
|
applicationUniversalIdentifier: TEST_APP_ID,
|
|
});
|
|
});
|
|
|
|
it('should create a new object when added to manifest on second sync', async () => {
|
|
const ticketObject = buildDefaultObjectManifest({
|
|
nameSingular: 'ticket',
|
|
namePlural: 'tickets',
|
|
labelSingular: 'Ticket',
|
|
labelPlural: 'Tickets',
|
|
description: 'A support ticket',
|
|
icon: 'IconTicket',
|
|
});
|
|
|
|
await syncApplication({
|
|
manifest: buildManifest({ objects: [ticketObject] }),
|
|
expectToFail: false,
|
|
});
|
|
|
|
const objectsAfterFirstSync = await findCustomObjects();
|
|
const ticket = objectsAfterFirstSync.find(
|
|
(obj) => obj.nameSingular === 'ticket',
|
|
);
|
|
|
|
expect(ticket).toBeDefined();
|
|
expect(ticket).toMatchObject({
|
|
nameSingular: 'ticket',
|
|
labelSingular: 'Ticket',
|
|
description: 'A support ticket',
|
|
icon: 'IconTicket',
|
|
});
|
|
|
|
const invoiceObject = buildDefaultObjectManifest({
|
|
nameSingular: 'invoice',
|
|
namePlural: 'invoices',
|
|
labelSingular: 'Invoice',
|
|
labelPlural: 'Invoices',
|
|
description: 'A billing invoice',
|
|
icon: 'IconFileInvoice',
|
|
});
|
|
|
|
await syncApplication({
|
|
manifest: buildManifest({
|
|
objects: [ticketObject, invoiceObject],
|
|
}),
|
|
expectToFail: false,
|
|
});
|
|
|
|
const objectsAfterSecondSync = await findCustomObjects();
|
|
const ticketAfter = objectsAfterSecondSync.find(
|
|
(obj) => obj.nameSingular === 'ticket',
|
|
);
|
|
const invoiceAfter = objectsAfterSecondSync.find(
|
|
(obj) => obj.nameSingular === 'invoice',
|
|
);
|
|
|
|
expect(ticketAfter).toBeDefined();
|
|
expect(invoiceAfter).toBeDefined();
|
|
expect(invoiceAfter).toMatchObject({
|
|
nameSingular: 'invoice',
|
|
namePlural: 'invoices',
|
|
labelSingular: 'Invoice',
|
|
labelPlural: 'Invoices',
|
|
description: 'A billing invoice',
|
|
icon: 'IconFileInvoice',
|
|
});
|
|
}, 60000);
|
|
|
|
it('should update object properties when changed in manifest on second sync', async () => {
|
|
const universalIdentifier = uuidv4();
|
|
const ticketObject = buildDefaultObjectManifest({
|
|
nameSingular: 'ticket',
|
|
namePlural: 'tickets',
|
|
labelSingular: 'Ticket',
|
|
labelPlural: 'Tickets',
|
|
description: 'A support ticket',
|
|
icon: 'IconTicket',
|
|
universalIdentifier,
|
|
});
|
|
|
|
await syncApplication({
|
|
manifest: buildManifest({ objects: [ticketObject] }),
|
|
expectToFail: false,
|
|
});
|
|
|
|
const objectsAfterFirstSync = await findCustomObjects();
|
|
const ticketBefore = objectsAfterFirstSync.find(
|
|
(obj) => obj.universalIdentifier === universalIdentifier,
|
|
);
|
|
|
|
expect(ticketBefore).toBeDefined();
|
|
expect(ticketBefore).toMatchObject({
|
|
nameSingular: 'ticket',
|
|
labelSingular: 'Ticket',
|
|
description: 'A support ticket',
|
|
icon: 'IconTicket',
|
|
});
|
|
|
|
const updatedTicketObject = {
|
|
...ticketObject,
|
|
nameSingular: 'ticket2',
|
|
namePlural: 'tickets2',
|
|
labelSingular: 'Support Ticket',
|
|
labelPlural: 'Support Tickets',
|
|
description: 'A customer support ticket',
|
|
icon: 'IconHeadset',
|
|
};
|
|
|
|
await syncApplication({
|
|
manifest: buildManifest({ objects: [updatedTicketObject] }),
|
|
expectToFail: false,
|
|
});
|
|
|
|
const objectsAfterSecondSync = await findCustomObjects();
|
|
const ticketAfter = objectsAfterSecondSync.find(
|
|
(obj) => obj.universalIdentifier === universalIdentifier,
|
|
);
|
|
|
|
expect(ticketAfter).toBeDefined();
|
|
expect(ticketAfter).toMatchObject({
|
|
nameSingular: 'ticket2',
|
|
namePlural: 'tickets2',
|
|
labelSingular: 'Support Ticket',
|
|
labelPlural: 'Support Tickets',
|
|
description: 'A customer support ticket',
|
|
icon: 'IconHeadset',
|
|
});
|
|
}, 60000);
|
|
|
|
it('should delete an object when removed from manifest on second sync', async () => {
|
|
const ticketObject = buildDefaultObjectManifest({
|
|
nameSingular: 'ticket',
|
|
namePlural: 'tickets',
|
|
labelSingular: 'Ticket',
|
|
labelPlural: 'Tickets',
|
|
description: 'A support ticket',
|
|
icon: 'IconTicket',
|
|
});
|
|
|
|
const invoiceObject = buildDefaultObjectManifest({
|
|
nameSingular: 'invoice',
|
|
namePlural: 'invoices',
|
|
labelSingular: 'Invoice',
|
|
labelPlural: 'Invoices',
|
|
description: 'A billing invoice',
|
|
icon: 'IconFileInvoice',
|
|
});
|
|
|
|
await syncApplication({
|
|
manifest: buildManifest({
|
|
objects: [ticketObject, invoiceObject],
|
|
}),
|
|
expectToFail: false,
|
|
});
|
|
|
|
const objectsAfterFirstSync = await findCustomObjects();
|
|
|
|
expect(
|
|
objectsAfterFirstSync.find((obj) => obj.nameSingular === 'ticket'),
|
|
).toBeDefined();
|
|
expect(
|
|
objectsAfterFirstSync.find((obj) => obj.nameSingular === 'invoice'),
|
|
).toBeDefined();
|
|
|
|
await syncApplication({
|
|
manifest: buildManifest({ objects: [ticketObject] }),
|
|
expectToFail: false,
|
|
});
|
|
|
|
const objectsAfterSecondSync = await findCustomObjects();
|
|
|
|
expect(
|
|
objectsAfterSecondSync.find((obj) => obj.nameSingular === 'ticket'),
|
|
).toBeDefined();
|
|
expect(
|
|
objectsAfterSecondSync.find((obj) => obj.nameSingular === 'invoice'),
|
|
).toBeUndefined();
|
|
}, 60000);
|
|
});
|