Files v2 - Use Files field in attachment (#17707)

- Add FILES field on attachment
- Adapt Attachment logic in front to use new resolver/controller
- Update files-field logic to infer applicationId from fieldMetadataId +
ask for fieldMetadataId in upload resolver
- Design update


To do in next PR : 
- Adapt activity files logic
This commit is contained in:
Etienne
2026-02-05 17:42:19 +01:00
committed by GitHub
parent e382641ac3
commit 77d15356e1
38 changed files with 1905 additions and 1533 deletions
@@ -99,6 +99,7 @@ export const useOpenFieldInputEditMode = () => {
if (isDefined(objectMetadataItem)) {
openFilesFieldInput({
fieldName: fieldDefinition.metadata.fieldName,
fieldMetadataId: fieldDefinition.fieldMetadataId,
recordId,
prefix,
updateRecord: (updateInput) => {
@@ -1,14 +1,30 @@
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useFilesFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useFilesFieldDisplay';
import { filesFieldUploadState } from '@/object-record/record-field/ui/states/filesFieldUploadState';
import { FilesDisplay } from '@/ui/field/display/components/FilesDisplay';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
export const FilesFieldDisplay = () => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const { fieldValue, disableChipClick } = useFilesFieldDisplay();
if (!Array.isArray(fieldValue)) {
return <></>;
}
const uploadState = useRecoilValue(
filesFieldUploadState({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
}),
);
const isUploadWindowOpen = uploadState === 'UPLOAD_WINDOW_OPEN';
const isFileUploading = uploadState === 'UPLOADING_FILE';
return (
<FilesDisplay value={fieldValue} forceDisableClick={disableChipClick} />
<FilesDisplay
value={fieldValue}
forceDisableClick={disableChipClick}
isUploadWindowOpen={isUploadWindowOpen}
isFileUploading={isFileUploading}
/>
);
};
@@ -15,10 +15,10 @@ export const useUploadFilesFieldFile = () => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const { t } = useLingui();
const uploadFile = async (file: File) => {
const uploadFile = async (file: File, fieldMetadataId: string) => {
try {
const result = await uploadFilesFieldFile({
variables: { file },
variables: { file, fieldMetadataId },
});
const uploadedFile = result?.data?.uploadFilesFieldFile;
@@ -61,9 +61,13 @@ export const FilesFieldInput = () => {
const nextValue = parseFilesArrayToFilesValue(updatedFiles);
if (isDefined(nextValue)) {
setDraftValue(nextValue);
if (nextValue.length === 0) {
onEnter?.({ newValue: nextValue });
}
}
},
[parseFilesArrayToFilesValue, setDraftValue],
[parseFilesArrayToFilesValue, setDraftValue, onEnter],
);
const handleUploadClick = useCallback(() => {
@@ -89,6 +93,7 @@ export const FilesFieldInput = () => {
try {
const uploadedFiles = await uploadMultipleFiles(
selectedFiles,
fieldDefinition.fieldMetadataId,
uploadFile,
);
@@ -113,6 +118,7 @@ export const FilesFieldInput = () => {
handleChange,
onEnter,
parseFilesArrayToFilesValue,
fieldDefinition,
]);
const setIsFieldInError = useSetRecoilComponentState(
@@ -1,6 +1,7 @@
import { useFileUpload } from '@/file-upload/hooks/useFileUpload';
import { useUploadFilesFieldFile } from '@/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile';
import { uploadMultipleFiles } from '@/object-record/record-field/ui/meta-types/utils/uploadMultipleFiles';
import { filesFieldUploadState } from '@/object-record/record-field/ui/states/filesFieldUploadState';
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
@@ -36,6 +37,7 @@ export const useOpenFilesFieldInput = () => {
({ snapshot, set }) =>
async ({
fieldName,
fieldMetadataId,
recordId,
prefix,
updateRecord,
@@ -43,6 +45,7 @@ export const useOpenFilesFieldInput = () => {
fieldDefinition,
}: {
fieldName: string;
fieldMetadataId: string;
recordId: string;
prefix?: string;
updateRecord: (updateInput: Record<string, unknown>) => void;
@@ -92,6 +95,11 @@ export const useOpenFilesFieldInput = () => {
const currentFileCount = isDefined(fieldValue) ? fieldValue.length : 0;
set(
filesFieldUploadState({ recordId, fieldName }),
'UPLOAD_WINDOW_OPEN',
);
openFileUpload({
multiple: true,
onUpload: async (selectedFiles: File[]) => {
@@ -100,6 +108,8 @@ export const useOpenFilesFieldInput = () => {
message: t`Cannot upload more than ${maxNumberOfValues} files`,
});
set(filesFieldUploadState({ recordId, fieldName }), null);
if (isTableContext && isDefined(recordTableId)) {
set(
recordTableCellEditModePositionComponentState.atomFamily({
@@ -117,9 +127,15 @@ export const useOpenFilesFieldInput = () => {
return;
}
set(
filesFieldUploadState({ recordId, fieldName }),
'UPLOADING_FILE',
);
try {
const uploadedFiles = await uploadMultipleFiles(
selectedFiles,
fieldMetadataId,
uploadFile,
);
@@ -129,6 +145,8 @@ export const useOpenFilesFieldInput = () => {
});
}
} finally {
set(filesFieldUploadState({ recordId, fieldName }), null);
if (isTableContext && isDefined(recordTableId)) {
set(
recordTableCellEditModePositionComponentState.atomFamily({
@@ -146,6 +164,8 @@ export const useOpenFilesFieldInput = () => {
}
},
onCancel: () => {
set(filesFieldUploadState({ recordId, fieldName }), null);
if (isTableContext && isDefined(recordTableId)) {
set(
recordTableCellEditModePositionComponentState.atomFamily({
@@ -3,12 +3,16 @@ import { isDefined } from 'twenty-shared/utils';
export const uploadMultipleFiles = async (
files: File[],
uploadFile: (file: File) => Promise<FieldFilesValue | undefined>,
fieldMetadataId: string,
uploadFile: (
file: File,
fieldMetadataId: string,
) => Promise<FieldFilesValue | undefined>,
): Promise<FieldFilesValue[]> => {
const uploadedFiles: FieldFilesValue[] = [];
for (const file of files) {
const uploadedFile = await uploadFile(file);
const uploadedFile = await uploadFile(file, fieldMetadataId);
if (isDefined(uploadedFile)) {
uploadedFiles.push(uploadedFile);
}
@@ -0,0 +1,16 @@
import { atomFamily } from 'recoil';
type FilesFieldUploadStateKey = {
recordId: string;
fieldName: string;
};
type FilesFieldUploadState = 'UPLOAD_WINDOW_OPEN' | 'UPLOADING_FILE' | null;
export const filesFieldUploadState = atomFamily<
FilesFieldUploadState,
FilesFieldUploadStateKey
>({
key: 'filesFieldUploadState',
default: null,
});