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
@@ -5,12 +5,16 @@ import { useRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { UPLOAD_WORKSPACE_MEMBER_PROFILE_PICTURE } from '@/settings/members/graphql/mutations/uploadWorkspaceMemberProfilePicture';
import { useCanEditProfileField } from '@/settings/profile/hooks/useCanEditProfileField';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ImageInput } from '@/ui/input/components/ImageInput';
import { useMutation } from '@apollo/client';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { buildSignedPath, isDefined } from 'twenty-shared/utils';
import {
FeatureFlagKey,
useUploadWorkspaceMemberProfilePictureLegacyMutation,
useUploadWorkspaceMemberProfilePictureMutation,
} from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type WorkspaceMemberPictureUploaderProps = {
@@ -26,6 +30,9 @@ export const WorkspaceMemberPictureUploader = ({
onAvatarUpdated,
disabled = false,
}: WorkspaceMemberPictureUploaderProps) => {
const isCorePictureMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
);
const { enqueueErrorSnackBar } = useSnackBar();
const [isUploading, setIsUploading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
@@ -36,7 +43,9 @@ export const WorkspaceMemberPictureUploader = ({
currentWorkspaceMemberState,
);
const [uploadPicture] = useMutation(UPLOAD_WORKSPACE_MEMBER_PROFILE_PICTURE);
const [uploadPicture] = useUploadWorkspaceMemberProfilePictureMutation();
const [uploadPictureLegacy] =
useUploadWorkspaceMemberProfilePictureLegacyMutation();
const { updateOneRecord } = useUpdateOneRecord();
@@ -56,29 +65,54 @@ export const WorkspaceMemberPictureUploader = ({
setIsUploading(true);
setErrorMessage(null);
let newAvatarUrl: string | null = null;
try {
const { data } = await uploadPicture({
variables: { file },
context: {
fetchOptions: {
signal: controller.signal,
if (!isCorePictureMigrated) {
const { data } = await uploadPictureLegacy({
variables: { file },
context: {
fetchOptions: {
signal: controller.signal,
},
},
},
});
});
const signedFile = data?.uploadWorkspaceMemberProfilePicture;
if (!isDefined(signedFile)) {
throw new Error('Avatar upload failed');
const signedFile = data?.uploadWorkspaceMemberProfilePictureLegacy;
if (!isDefined(signedFile)) {
throw new Error('Avatar upload failed');
}
await updateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
idToUpdate: workspaceMemberId,
updateOneRecordInput: { avatarUrl: signedFile.path },
});
newAvatarUrl = buildSignedPath(signedFile);
} else {
const { data } = await uploadPicture({
variables: { file },
context: {
fetchOptions: {
signal: controller.signal,
},
},
});
const signedFile = data?.uploadWorkspaceMemberProfilePicture;
if (!isDefined(signedFile)) {
throw new Error('Avatar upload failed');
}
await updateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
idToUpdate: workspaceMemberId,
updateOneRecordInput: { avatarUrl: signedFile.url },
});
newAvatarUrl = signedFile.url;
}
await updateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
idToUpdate: workspaceMemberId,
updateOneRecordInput: { avatarUrl: signedFile.path },
});
const newAvatarUrl = buildSignedPath(signedFile);
if (isEditingSelf && isDefined(currentWorkspaceMember)) {
setCurrentWorkspaceMember({
...currentWorkspaceMember,