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);
});
});
@@ -2,52 +2,42 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { pickMorphGroupSurvivor } from 'src/engine/dataloaders/utils/pick-morph-group-survivor.util';
export const filterMorphRelationDuplicateFields = (
flatFieldMetadatas: FlatFieldMetadata[],
): FlatFieldMetadata[] => {
const initialAccumulator: {
morphFlatFieldMetadatas: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[];
otherFlatFieldMetadatas: FlatFieldMetadata[];
} = {
morphFlatFieldMetadatas: [],
otherFlatFieldMetadatas: [],
};
const { morphFlatFieldMetadatas, otherFlatFieldMetadatas } =
flatFieldMetadatas.reduce((acc, flatFieldMetadata) => {
if (
isFlatFieldMetadataOfType(
flatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
return {
...acc,
morphFlatFieldMetadatas: [
...acc.morphFlatFieldMetadatas,
flatFieldMetadata,
],
};
}
const otherFlatFieldMetadatas: FlatFieldMetadata[] = [];
const morphGroupsByMorphId = new Map<
string,
FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[]
>();
return {
...acc,
otherFlatFieldMetadatas: [
...acc.otherFlatFieldMetadatas,
flatFieldMetadata,
],
};
}, initialAccumulator);
for (const flatFieldMetadata of flatFieldMetadatas) {
if (
isFlatFieldMetadataOfType(
flatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
const existing =
morphGroupsByMorphId.get(flatFieldMetadata.morphId) ?? [];
const filteredMorphFlatFieldMetadatas = morphFlatFieldMetadatas.filter(
(currentField) =>
!morphFlatFieldMetadatas.some(
(otherField) =>
currentField.id !== otherField.id &&
otherField.morphId === currentField.morphId &&
otherField.id < currentField.id,
),
);
morphGroupsByMorphId.set(flatFieldMetadata.morphId, [
...existing,
flatFieldMetadata,
]);
} else {
otherFlatFieldMetadatas.push(flatFieldMetadata);
}
}
const filteredMorphFlatFieldMetadatas: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[] =
[];
for (const group of morphGroupsByMorphId.values()) {
filteredMorphFlatFieldMetadatas.push(pickMorphGroupSurvivor(group));
}
return [...otherFlatFieldMetadatas, ...filteredMorphFlatFieldMetadatas];
};
@@ -0,0 +1,19 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
// Prefers active non-system fields (standard targets) over system ones
// (auto-created for custom objects). Smallest id breaks ties.
const scoreMorphField = (
field: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>,
): number => (field.isActive ? 2 : 0) + (field.isSystem ? 0 : 1);
export const pickMorphGroupSurvivor = (
group: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[],
): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION> => {
return group.reduce((best, current) => {
const diff = scoreMorphField(current) - scoreMorphField(best);
return diff > 0 || (diff === 0 && current.id < best.id) ? current : best;
});
};