fix(server): preserve kanban/calendar fields in view manifest sync (#19946)

## Summary

The `fromViewManifestToUniversalFlatView` converter hardcoded five view
fields to `null` instead of reading them from the manifest:
- `mainGroupByFieldMetadataUniversalIdentifier`
- `kanbanAggregateOperation`
- `kanbanAggregateOperationFieldMetadataUniversalIdentifier`
- `calendarLayout`
- `calendarFieldMetadataUniversalIdentifier`

As a result **any** Kanban view in an app manifest is rejected by
`validateFlatViewCreation` with `"Kanban view must have a main group by
field"`, and any Calendar view would trip the `view.entity.ts` check
constraint requiring `calendarLayout` + `calendarFieldMetadataId` to be
non-null. Discovered while trying to install
[`twenty-crm-meeting-baas`](https://github.com/Meeting-BaaS/twenty-crm-meeting-baas)
which ships a Kanban view.

## Changes

- **Server converter**: read all five fields from the manifest (with `??
null` fallback).
- **`ViewManifest` type** (`twenty-shared`): add the five fields so SDK
users can set them type-safely.
- **Move `ViewCalendarLayout`** from `twenty-server` to `twenty-shared`
so the manifest type can reference it. Seven import sites updated; the
front-end imports via generated GraphQL types and is unaffected.
- **Unit tests**: extend
`from-view-manifest-to-universal-flat-view.util.spec.ts` with
preservation + null-default cases for both Kanban and Calendar (5 tests
total).
- **Regression coverage**: add a Kanban view
(`post-cards-by-status.view.ts`) to the `rich-app` fixture grouped by
the existing `status` SELECT field. The existing
`applications-install-delete-reinstall` e2e test now exercises the
Kanban path end-to-end — a future regression here would fail CI.

Note: `expected-manifest.ts` and the `views.length` assertion in
`manifest.tests.ts` were updated to reflect the new fixture view.

## Test plan

- [x] `nx test twenty-server --
from-view-manifest-to-universal-flat-view` → 5/5 pass
- [x] `nx typecheck twenty-shared` / `twenty-sdk` / `twenty-server` → no
new errors (one pre-existing unrelated error in
`admin-panel.module-factory.ts`)
- [x] `nx lint twenty-shared` / `twenty-sdk` → clean
- [x] Manual install of the Meeting BaaS app on a dev workspace succeeds
with the Kanban view after this fix
- [ ] CI: SDK e2e `applications-install-delete-reinstall` passes against
the new fixture view
- [ ] CI: integration test `calendar-field-deactivation-deletes-views`
still passes after the enum move

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2026-04-22 16:31:00 +02:00
committed by GitHub
parent c2cf3eac50
commit b010599000
18 changed files with 276 additions and 20 deletions
@@ -1,4 +1,6 @@
import {
AggregateOperations,
ViewCalendarLayout,
ViewOpenRecordIn,
ViewType,
ViewVisibility,
@@ -65,4 +67,69 @@ describe('fromViewManifestToUniversalFlatView', () => {
expect(result.visibility).toBe(ViewVisibility.UNLISTED);
expect(result.openRecordIn).toBe(ViewOpenRecordIn.RECORD_PAGE);
});
it('should preserve kanban fields from the manifest', () => {
const result = fromViewManifestToUniversalFlatView({
viewManifest: {
universalIdentifier: 'view-uuid-3',
name: 'Kanban Board',
objectUniversalIdentifier: 'object-uuid-1',
type: ViewType.KANBAN,
mainGroupByFieldMetadataUniversalIdentifier: 'field-uuid-status',
kanbanAggregateOperation: AggregateOperations.COUNT,
kanbanAggregateOperationFieldMetadataUniversalIdentifier:
'field-uuid-amount',
},
applicationUniversalIdentifier,
now,
});
expect(result.mainGroupByFieldMetadataUniversalIdentifier).toBe(
'field-uuid-status',
);
expect(result.kanbanAggregateOperation).toBe(AggregateOperations.COUNT);
expect(
result.kanbanAggregateOperationFieldMetadataUniversalIdentifier,
).toBe('field-uuid-amount');
});
it('should default kanban and calendar fields to null when omitted', () => {
const result = fromViewManifestToUniversalFlatView({
viewManifest: {
universalIdentifier: 'view-uuid-4',
name: 'All Records',
objectUniversalIdentifier: 'object-uuid-1',
},
applicationUniversalIdentifier,
now,
});
expect(result.mainGroupByFieldMetadataUniversalIdentifier).toBeNull();
expect(result.kanbanAggregateOperation).toBeNull();
expect(
result.kanbanAggregateOperationFieldMetadataUniversalIdentifier,
).toBeNull();
expect(result.calendarLayout).toBeNull();
expect(result.calendarFieldMetadataUniversalIdentifier).toBeNull();
});
it('should preserve calendar fields from the manifest', () => {
const result = fromViewManifestToUniversalFlatView({
viewManifest: {
universalIdentifier: 'view-uuid-5',
name: 'Calendar View',
objectUniversalIdentifier: 'object-uuid-1',
type: ViewType.CALENDAR,
calendarLayout: ViewCalendarLayout.WEEK,
calendarFieldMetadataUniversalIdentifier: 'field-uuid-date',
},
applicationUniversalIdentifier,
now,
});
expect(result.calendarLayout).toBe(ViewCalendarLayout.WEEK);
expect(result.calendarFieldMetadataUniversalIdentifier).toBe(
'field-uuid-date',
);
});
});
@@ -29,11 +29,15 @@ export const fromViewManifestToUniversalFlatView = ({
visibility: viewManifest.visibility ?? ViewVisibility.WORKSPACE,
openRecordIn: viewManifest.openRecordIn ?? ViewOpenRecordIn.SIDE_PANEL,
key: viewManifest.key ?? null,
kanbanAggregateOperation: null,
kanbanAggregateOperationFieldMetadataUniversalIdentifier: null,
calendarLayout: null,
calendarFieldMetadataUniversalIdentifier: null,
mainGroupByFieldMetadataUniversalIdentifier: null,
kanbanAggregateOperation: viewManifest.kanbanAggregateOperation ?? null,
kanbanAggregateOperationFieldMetadataUniversalIdentifier:
viewManifest.kanbanAggregateOperationFieldMetadataUniversalIdentifier ??
null,
calendarLayout: viewManifest.calendarLayout ?? null,
calendarFieldMetadataUniversalIdentifier:
viewManifest.calendarFieldMetadataUniversalIdentifier ?? null,
mainGroupByFieldMetadataUniversalIdentifier:
viewManifest.mainGroupByFieldMetadataUniversalIdentifier ?? null,
shouldHideEmptyGroups: false,
anyFieldFilterValue: null,
createdByUserWorkspaceId: null,