File - Migrate core pictures (workspace and member logo) + workflow attachments (#17924)

- Create a common file-by-id download controller
- Create core picture module with resolver and logic to handle
workspaceLogo and workspaceMemberProfilePicture update
- Create workflow file module (same)
- Data migration
This commit is contained in:
Etienne
2026-02-17 16:42:50 +01:00
committed by GitHub
parent 963f2de864
commit 163c1175cb
85 changed files with 2212 additions and 335 deletions
@@ -1,14 +1,14 @@
import { getFileType } from '@/activities/files/utils/getFileType';
import { IconMapping } from '@/file/utils/fileIconMappings';
import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
import { type WorkflowAttachmentType } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowAttachmentType';
import { IconMapping } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { type WorkflowAttachment } from 'twenty-shared/workflow';
import { AvatarChip } from 'twenty-ui/components';
import { IconX } from 'twenty-ui/display';
type WorkflowAttachmentChipProps = {
file: WorkflowAttachmentType;
file: WorkflowAttachment;
onRemove: () => void;
readonly?: boolean;
};
@@ -1,18 +1,18 @@
import { InputLabel } from '@/ui/input/components/InputLabel';
import { WorkflowAttachmentChip } from '@/advanced-text-editor/components/WorkflowAttachmentChip';
import { useUploadWorkflowFile } from '@/advanced-text-editor/hooks/useUploadWorkflowFile';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { type WorkflowAttachmentType } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowAttachmentType';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { type ChangeEvent, useRef } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type WorkflowAttachment } from 'twenty-shared/workflow';
import { IconUpload } from 'twenty-ui/display';
import { useTheme } from '@emotion/react';
type WorkflowSendEmailAttachmentsProps = {
files: WorkflowAttachmentType[];
onChange: (files: WorkflowAttachmentType[]) => void;
files: WorkflowAttachment[];
onChange: (files: WorkflowAttachment[]) => void;
label?: string;
};
@@ -92,9 +92,7 @@ export const WorkflowSendEmailAttachments = ({
filesToUpload.map((file) => uploadWorkflowFile(file)),
);
const successfulUploads = uploadedFiles.filter(
(file): file is WorkflowAttachmentType => file !== null,
);
const successfulUploads = uploadedFiles.filter(isDefined);
if (successfulUploads.length > 0) {
onChange([...files, ...successfulUploads]);
@@ -132,7 +130,7 @@ export const WorkflowSendEmailAttachments = ({
>
{files.length > 0 ? (
<StyledChipsContainer>
{files.map((file: WorkflowAttachmentType) => (
{files.map((file: WorkflowAttachment) => (
<WorkflowAttachmentChip
key={file.id}
file={file}
@@ -1,31 +1,33 @@
import { MAX_ATTACHMENT_SIZE } from '@/advanced-text-editor/utils/MaxAttachmentSize';
import { formatFileSize } from '@/file/utils/formatFileSize';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useApolloClient } from '@apollo/client';
import { t } from '@lingui/core/macro';
import {
extractFolderPathFilenameAndTypeOrThrow,
isDefined,
} from 'twenty-shared/utils';
import { useCreateFileMutation } from '~/generated-metadata/graphql';
import { type WorkflowAttachment } from 'twenty-shared/workflow';
import {
FeatureFlagKey,
useCreateFileMutation,
useUploadWorkflowFileMutation,
} from '~/generated-metadata/graphql';
import { logError } from '~/utils/logError';
type WorkflowFile = {
id: string;
name: string;
size: number;
type: string;
createdAt: string;
};
export const useUploadWorkflowFile = () => {
const coreClient = useApolloCoreClient();
const [createFile] = useCreateFileMutation({ client: coreClient });
const isOtherFileMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_OTHER_FILE_MIGRATED,
);
const [uploadWorkflowFileMutation] = useUploadWorkflowFileMutation();
const apolloClient = useApolloClient();
const [createFile] = useCreateFileMutation({ client: apolloClient });
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const uploadWorkflowFile = async (
file: File,
): Promise<WorkflowFile | null> => {
): Promise<WorkflowAttachment | null> => {
try {
if (file.size > MAX_ATTACHMENT_SIZE) {
const fileName = file.name;
@@ -36,28 +38,42 @@ export const useUploadWorkflowFile = () => {
return null;
}
const result = await createFile({
variables: { file },
});
let workflowFile: WorkflowAttachment;
if (isOtherFileMigrated) {
const result = await uploadWorkflowFileMutation({
variables: { file },
});
const uploadedFile = result?.data?.uploadWorkflowFile;
if (!isDefined(uploadedFile)) {
throw new Error('File upload failed');
}
workflowFile = {
id: uploadedFile.id,
name: file.name,
size: uploadedFile.size,
type: extractFolderPathFilenameAndTypeOrThrow(uploadedFile.path).type,
createdAt: uploadedFile.createdAt,
};
} else {
const result = await createFile({
variables: { file },
});
const uploadedFile = result?.data?.createFile;
const uploadedFile = result?.data?.createFile;
if (!isDefined(uploadedFile)) {
throw new Error('File upload failed');
if (!isDefined(uploadedFile)) {
throw new Error('File upload failed');
}
workflowFile = {
id: uploadedFile.id,
name: file.name,
size: uploadedFile.size,
type: extractFolderPathFilenameAndTypeOrThrow(uploadedFile.path).type,
createdAt: uploadedFile.createdAt,
};
}
const { type } = extractFolderPathFilenameAndTypeOrThrow(
uploadedFile.path,
);
const workflowFile: WorkflowFile = {
id: uploadedFile.id,
name: file.name,
size: uploadedFile.size,
type: type,
createdAt: uploadedFile.createdAt,
};
const fileName = file.name;
enqueueSuccessSnackBar({
message: t`File "${fileName}" uploaded successfully`,