feat: configure standard views and migrate attachment seeds to FILES field (#17958)

## Summary

- Add default visible view fields for `timelineActivity`, `attachment`,
`noteTarget`, `taskTarget`, and `workspaceMember` objects so they
display useful columns out of the box
- Standardize morph relation field labels to "Target" with
`IconArrowUpRight` for consistency across all pivot/junction tables
- Mark deprecated fields (`fullPath`, `fileCategory`,
`linkedRecordCachedName`, `linkedRecordId`, `linkedObjectMetadataId`) as
`isSystem` to hide them from the UI column picker
- Fix morph field deduplication logic (`pickMorphGroupSurvivor`) to
prefer active, non-system fields over auto-generated system fields from
custom objects
- Migrate attachment seeds from legacy `fullPath`/`fileCategory` to the
new `FILES` field type, creating proper `FileEntity` records in
`core.file` via `fileStorageService.writeFile()`
- Restore `customDomain` in the user query fragment


<img width="825" height="754" alt="Screenshot 2026-02-15 at 15 44 27"
src="https://github.com/user-attachments/assets/9596a3dd-8d3a-43c0-925a-0adef9ee68a8"
/>
<img width="736" height="731" alt="Screenshot 2026-02-15 at 15 44 13"
src="https://github.com/user-attachments/assets/cd1a66c5-731d-43e6-bbc3-703cbeda1652"
/>
<img width="722" height="757" alt="Screenshot 2026-02-15 at 15 44 03"
src="https://github.com/user-attachments/assets/b5210546-6a40-4940-8e4f-874818a614fb"
/>
<img width="907" height="757" alt="Screenshot 2026-02-15 at 15 43 52"
src="https://github.com/user-attachments/assets/ead5b9a8-1989-4d68-9640-583da6233711"
/>
<img width="1002" height="731" alt="Screenshot 2026-02-15 at 15 43 38"
src="https://github.com/user-attachments/assets/38accb8c-f5d5-4bfc-b245-06389849810b"
/>




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches migration/upgrade commands that write to core metadata tables
and adjust field/view definitions, plus changes dev seeding to create
`core.file` records; mistakes could affect UI visibility or seed
integrity across workspaces.
> 
> **Overview**
> Adds a new `upgrade:1-18:backfill-standard-views-and-field-metadata`
command that, per workspace, marks specific fields as `isSystem`,
normalizes morph-relation field `label`/`icon` to
`Target`/`IconArrowUpRight`, and backfills missing standard
`view`/`viewField` rows for `attachment`, `noteTarget`, `taskTarget`,
`timelineActivity`, and `workspaceMember`, followed by cache
invalidation + metadata version bump.
> 
> Refactors morph-relation deduplication to pick a single survivor per
`morphId` using a new `pickMorphGroupSurvivor` rule (prefer active +
non-system, then smallest id), with new unit tests.
> 
> Updates standard metadata generators and snapshots to reflect the new
system flags and default view fields, and rewrites attachment dev
seeding to populate the new `file` (FILES field) JSON and create
corresponding `core.file` entries via `FileStorageService.writeFile`
with workspace-scoped file IDs.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
b1939bbf6f8cce294f9b4cdec06b19778daa205e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2026-02-16 13:34:56 +01:00
committed by GitHub
parent b80146c890
commit c775d65952
29 changed files with 3153 additions and 1308 deletions
@@ -0,0 +1,103 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { filterMorphRelationDuplicateFields } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
const makeMorphField = (
overrides: Partial<FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>> & {
id: string;
morphId: string;
},
): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION> =>
({
type: FieldMetadataType.MORPH_RELATION,
isActive: true,
isSystem: false,
...overrides,
}) as FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>;
const makeTextField = (id: string): FlatFieldMetadata<FieldMetadataType.TEXT> =>
({
id,
type: FieldMetadataType.TEXT,
}) as FlatFieldMetadata<FieldMetadataType.TEXT>;
describe('filterMorphRelationDuplicateFields', () => {
it('should return all fields when there are no morph fields', () => {
const fields = [makeTextField('t1'), makeTextField('t2')];
expect(filterMorphRelationDuplicateFields(fields)).toEqual(fields);
});
it('should return all fields when morph fields have distinct morphIds', () => {
const morph1 = makeMorphField({ id: 'a', morphId: 'morph-1' });
const morph2 = makeMorphField({ id: 'b', morphId: 'morph-2' });
const text = makeTextField('t1');
const result = filterMorphRelationDuplicateFields([morph1, text, morph2]);
expect(result).toHaveLength(3);
expect(result).toContain(text);
expect(result).toContain(morph1);
expect(result).toContain(morph2);
});
it('should deduplicate morph fields sharing the same morphId', () => {
const standard = makeMorphField({
id: 'b',
morphId: 'morph-1',
isSystem: false,
});
const system = makeMorphField({
id: 'a',
morphId: 'morph-1',
isSystem: true,
});
const text = makeTextField('t1');
const result = filterMorphRelationDuplicateFields([system, text, standard]);
expect(result).toHaveLength(2);
expect(result).toContain(text);
expect(result).toContain(standard);
expect(result).not.toContain(system);
});
it('should handle multiple morph groups independently', () => {
const group1Best = makeMorphField({
id: 'a',
morphId: 'morph-1',
isSystem: false,
});
const group1Dup = makeMorphField({
id: 'b',
morphId: 'morph-1',
isSystem: true,
});
const group2Best = makeMorphField({
id: 'c',
morphId: 'morph-2',
isSystem: false,
});
const group2Dup = makeMorphField({
id: 'd',
morphId: 'morph-2',
isSystem: true,
});
const result = filterMorphRelationDuplicateFields([
group1Dup,
group2Dup,
group1Best,
group2Best,
]);
expect(result).toHaveLength(2);
expect(result).toContain(group1Best);
expect(result).toContain(group2Best);
});
it('should return empty array for empty input', () => {
expect(filterMorphRelationDuplicateFields([])).toEqual([]);
});
});
@@ -0,0 +1,85 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { pickMorphGroupSurvivor } from 'src/engine/dataloaders/utils/pick-morph-group-survivor.util';
const makeMorphField = (
overrides: Partial<FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>> & {
id: string;
},
): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION> =>
({
type: FieldMetadataType.MORPH_RELATION,
isActive: true,
isSystem: false,
morphId: 'morph-1',
...overrides,
}) as FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>;
describe('pickMorphGroupSurvivor', () => {
it('should return the only field when group has one element', () => {
const field = makeMorphField({ id: 'a' });
expect(pickMorphGroupSurvivor([field])).toBe(field);
});
it('should prefer active non-system over active system', () => {
const standard = makeMorphField({
id: 'b',
isActive: true,
isSystem: false,
});
const system = makeMorphField({
id: 'a',
isActive: true,
isSystem: true,
});
expect(pickMorphGroupSurvivor([system, standard])).toBe(standard);
});
it('should prefer active over inactive', () => {
const active = makeMorphField({
id: 'b',
isActive: true,
isSystem: true,
});
const inactive = makeMorphField({
id: 'a',
isActive: false,
isSystem: false,
});
expect(pickMorphGroupSurvivor([inactive, active])).toBe(active);
});
it('should break ties by smallest id', () => {
const fieldA = makeMorphField({
id: 'aaa',
isActive: true,
isSystem: false,
});
const fieldB = makeMorphField({
id: 'bbb',
isActive: true,
isSystem: false,
});
expect(pickMorphGroupSurvivor([fieldB, fieldA])).toBe(fieldA);
});
it('should prefer active+non-system (score 3) over inactive+non-system (score 1)', () => {
const best = makeMorphField({
id: 'z',
isActive: true,
isSystem: false,
});
const worse = makeMorphField({
id: 'a',
isActive: false,
isSystem: false,
});
expect(pickMorphGroupSurvivor([worse, best])).toBe(best);
});
});