Files v2 - Add new FILES field type (#17236)

In this PR : 
- New field type FieldMetadataType.FILES added (as jsonb in DB)
- Backend: GraphQL types, validation, REST API schema, data processor
handling
- Frontend: field configuration in settings
- Feature flag IS_FILES_FIELD_ENABLED to gate the feature
- Integration tests for create/filter validation
- Dev seed: Feature flag enabled + FILES field on survey result object


To do in next PRs : 
- Backend : 
-- new workspaceFile controller (for download) & new workspaceFile
upload resolver (for upload)
-- pre-hook/post-hook to ensure fileId existence + listener to ensure
cleaning
- Frontend : FILES field display/edit in table view, record, ...
This commit is contained in:
Etienne
2026-01-20 18:03:49 +01:00
committed by GitHub
parent 41329a0ea9
commit 30c13fc947
72 changed files with 2278 additions and 92 deletions
@@ -3,6 +3,7 @@ import {
type FieldBooleanValue,
type FieldDateTimeValue,
type FieldDateValue,
type FieldFilesValue,
type FieldJsonValue,
type FieldMultiSelectValue,
type FieldNumberValue,
@@ -14,11 +15,12 @@ import {
import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue';
import { type SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType';
import { type SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType';
import { type FieldRatingValue } from 'twenty-shared/types';
import { FILE_CATEGORIES, type FieldRatingValue } from 'twenty-shared/types';
import {
IllustrationIconArray,
IllustrationIconCalendarEvent,
IllustrationIconCalendarTime,
IllustrationIconFile,
IllustrationIconJson,
IllustrationIconNumbers,
IllustrationIconOneToMany,
@@ -139,4 +141,31 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
category: 'Advanced',
exampleValues: [['value1', 'value2'], ['value3'], []],
} as const satisfies SettingsFieldTypeConfig<FieldArrayValue>,
[FieldMetadataType.FILES]: {
label: 'Files',
Icon: IllustrationIconFile,
category: 'Advanced',
exampleValues: [
[
{
fileId: 'file-1',
label: 'Document.pdf',
fileCategory: FILE_CATEGORIES.TEXT_DOCUMENT,
},
{
fileId: 'file-2',
label: 'Image.png',
fileCategory: FILE_CATEGORIES.IMAGE,
},
],
[
{
fileId: 'file-3',
label: 'Report.xlsx',
fileCategory: FILE_CATEGORIES.SPREADSHEET,
},
],
[],
],
} as const satisfies SettingsFieldTypeConfig<FieldFilesValue>,
};
@@ -53,6 +53,10 @@ export const SettingsDataModelFieldMaxValuesForm = ({
title = t`Maximum values`;
description = t`Limit how many values can be added to this field`;
break;
case FieldMetadataType.FILES:
title = t`Maximum files`;
description = t`Limit how many files can be attached to this field`;
break;
default:
return null;
}
@@ -117,6 +117,10 @@ const arrayFieldFormSchema = z
.merge(mergeSettingsSchemas(settingsDataModelFieldMaxValuesSchema))
.extend(isUniqueFieldFormSchema.shape);
const filesFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.FILES) })
.merge(mergeSettingsSchemas(settingsDataModelFieldMaxValuesSchema));
const otherFieldsFormSchema = z
.object({
type: z.enum(
@@ -136,6 +140,7 @@ const otherFieldsFormSchema = z
FieldMetadataType.EMAILS,
FieldMetadataType.LINKS,
FieldMetadataType.ARRAY,
FieldMetadataType.FILES,
]),
) as [FieldMetadataType, ...FieldMetadataType[]],
),
@@ -159,6 +164,7 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
emailsFieldFormSchema,
linksFieldFormSchema,
arrayFieldFormSchema,
filesFieldFormSchema,
otherFieldsFormSchema,
],
);
@@ -178,6 +184,7 @@ const previewableTypes = [
FieldMetadataType.DATE,
FieldMetadataType.DATE_TIME,
FieldMetadataType.EMAILS,
FieldMetadataType.FILES,
FieldMetadataType.FULL_NAME,
FieldMetadataType.LINKS,
FieldMetadataType.MULTI_SELECT,
@@ -325,6 +332,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
FieldMetadataType.EMAILS,
FieldMetadataType.LINKS,
FieldMetadataType.ARRAY,
FieldMetadataType.FILES,
].includes(fieldType) && (
<>
<SettingsDataModelFieldMaxValuesForm
@@ -7,9 +7,11 @@ export const canBeUnique = (
field: Pick<FieldMetadataItem, 'type' | 'isCustom'>,
) => {
if (
[FieldMetadataType.MORPH_RELATION, FieldMetadataType.RELATION].includes(
field.type,
) ||
[
FieldMetadataType.MORPH_RELATION,
FieldMetadataType.RELATION,
FieldMetadataType.FILES,
].includes(field.type) ||
(isCompositeFieldType(field.type) &&
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type].subFields.every(
(subField) => !subField.isIncludedInUniqueConstraint,