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
@@ -1482,6 +1482,7 @@ export enum FeatureFlagKey {
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED',
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_CORE_PICTURE_MIGRATED = 'IS_CORE_PICTURE_MIGRATED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
@@ -1492,6 +1493,7 @@ export enum FeatureFlagKey {
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED',
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
IS_OTHER_FILE_MIGRATED = 'IS_OTHER_FILE_MIGRATED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
@@ -1630,6 +1632,7 @@ export enum FileFolder {
Attachment = 'Attachment',
BuiltFrontComponent = 'BuiltFrontComponent',
BuiltLogicFunction = 'BuiltLogicFunction',
CorePicture = 'CorePicture',
Dependencies = 'Dependencies',
File = 'File',
FilesField = 'FilesField',
@@ -1637,16 +1640,12 @@ export enum FileFolder {
ProfilePicture = 'ProfilePicture',
PublicAsset = 'PublicAsset',
Source = 'Source',
Workflow = 'Workflow',
WorkspaceLogo = 'WorkspaceLogo'
}
export type FilesConfiguration = {
__typename?: 'FilesConfiguration';
configurationType: WidgetConfigurationType;
};
export type FilesFieldFile = {
__typename?: 'FilesFieldFile';
export type FileWithSignedUrl = {
__typename?: 'FileWithSignedUrl';
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
path: Scalars['String'];
@@ -1654,6 +1653,11 @@ export type FilesFieldFile = {
url: Scalars['String'];
};
export type FilesConfiguration = {
__typename?: 'FilesConfiguration';
configurationType: WidgetConfigurationType;
};
export type FindAvailableSsoidpOutput = {
__typename?: 'FindAvailableSSOIDPOutput';
id: Scalars['UUID'];
@@ -2168,6 +2172,7 @@ export type Mutation = {
createCoreViewSort: CoreViewSort;
createDatabaseConfigVariable: Scalars['Boolean'];
createEmailingDomain: EmailingDomain;
/** @deprecated Use specific file service instead */
createFile: File;
createFrontComponent: FrontComponent;
createManyCoreViewFieldGroups: Array<CoreViewFieldGroup>;
@@ -2203,6 +2208,7 @@ export type Mutation = {
deleteCurrentWorkspace: Workspace;
deleteDatabaseConfigVariable: Scalars['Boolean'];
deleteEmailingDomain: Scalars['Boolean'];
/** @deprecated */
deleteFile: File;
deleteFrontComponent: FrontComponent;
deleteJobs: DeleteJobsResponse;
@@ -2307,10 +2313,13 @@ export type Mutation = {
uploadApplicationFile: File;
/** @deprecated Use uploadFilesFieldFile instead */
uploadFile: SignedFile;
uploadFilesFieldFile: FilesFieldFile;
uploadFilesFieldFile: FileWithSignedUrl;
uploadImage: SignedFile;
uploadWorkspaceLogo: SignedFile;
uploadWorkspaceMemberProfilePicture: SignedFile;
uploadWorkflowFile: FileWithSignedUrl;
uploadWorkspaceLogo: FileWithSignedUrl;
uploadWorkspaceLogoLegacy: SignedFile;
uploadWorkspaceMemberProfilePicture: FileWithSignedUrl;
uploadWorkspaceMemberProfilePictureLegacy: SignedFile;
upsertFieldPermissions: Array<FieldPermission>;
upsertObjectPermissions: Array<ObjectPermission>;
upsertPermissionFlags: Array<PermissionFlag>;
@@ -3130,16 +3139,31 @@ export type MutationUploadImageArgs = {
};
export type MutationUploadWorkflowFileArgs = {
file: Scalars['Upload'];
};
export type MutationUploadWorkspaceLogoArgs = {
file: Scalars['Upload'];
};
export type MutationUploadWorkspaceLogoLegacyArgs = {
file: Scalars['Upload'];
};
export type MutationUploadWorkspaceMemberProfilePictureArgs = {
file: Scalars['Upload'];
};
export type MutationUploadWorkspaceMemberProfilePictureLegacyArgs = {
file: Scalars['Upload'];
};
export type MutationUpsertFieldPermissionsArgs = {
upsertFieldPermissionsInput: UpsertFieldPermissionsInput;
};
@@ -5080,6 +5104,7 @@ export type Workspace = {
isPublicInviteLinkEnabled: Scalars['Boolean'];
isTwoFactorAuthenticationEnforced: Scalars['Boolean'];
logo?: Maybe<Scalars['String']>;
logoFileId?: Maybe<Scalars['UUID']>;
metadataVersion: Scalars['Float'];
routerModel: Scalars['String'];
smartModel: Scalars['String'];
@@ -5734,7 +5759,14 @@ export type UploadFilesFieldFileMutationVariables = Exact<{
}>;
export type UploadFilesFieldFileMutation = { __typename?: 'Mutation', uploadFilesFieldFile: { __typename?: 'FilesFieldFile', id: string, path: string, size: number, createdAt: string, url: string } };
export type UploadFilesFieldFileMutation = { __typename?: 'Mutation', uploadFilesFieldFile: { __typename?: 'FileWithSignedUrl', id: string, path: string, size: number, createdAt: string, url: string } };
export type UploadWorkflowFileMutationVariables = Exact<{
file: Scalars['Upload'];
}>;
export type UploadWorkflowFileMutation = { __typename?: 'Mutation', uploadWorkflowFile: { __typename?: 'FileWithSignedUrl', id: string, path: string, size: number, createdAt: string, url: string } };
export type FindManyFrontComponentsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -6221,7 +6253,14 @@ export type UploadWorkspaceMemberProfilePictureMutationVariables = Exact<{
}>;
export type UploadWorkspaceMemberProfilePictureMutation = { __typename?: 'Mutation', uploadWorkspaceMemberProfilePicture: { __typename?: 'SignedFile', path: string, token: string } };
export type UploadWorkspaceMemberProfilePictureMutation = { __typename?: 'Mutation', uploadWorkspaceMemberProfilePicture: { __typename?: 'FileWithSignedUrl', url: string } };
export type UploadWorkspaceMemberProfilePictureLegacyMutationVariables = Exact<{
file: Scalars['Upload'];
}>;
export type UploadWorkspaceMemberProfilePictureLegacyMutation = { __typename?: 'Mutation', uploadWorkspaceMemberProfilePictureLegacy: { __typename?: 'SignedFile', path: string, token: string } };
export type UpdateUserEmailMutationVariables = Exact<{
newEmail: Scalars['String'];
@@ -6740,7 +6779,14 @@ export type UploadWorkspaceLogoMutationVariables = Exact<{
}>;
export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorkspaceLogo: { __typename?: 'SignedFile', path: string, token: string } };
export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorkspaceLogo: { __typename?: 'FileWithSignedUrl', url: string } };
export type UploadWorkspaceLogoLegacyMutationVariables = Exact<{
file: Scalars['Upload'];
}>;
export type UploadWorkspaceLogoLegacyMutation = { __typename?: 'Mutation', uploadWorkspaceLogoLegacy: { __typename?: 'SignedFile', path: string, token: string } };
export type CheckCustomDomainValidRecordsMutationVariables = Exact<{ [key: string]: never; }>;
@@ -10440,6 +10486,43 @@ export function useUploadFilesFieldFileMutation(baseOptions?: Apollo.MutationHoo
export type UploadFilesFieldFileMutationHookResult = ReturnType<typeof useUploadFilesFieldFileMutation>;
export type UploadFilesFieldFileMutationResult = Apollo.MutationResult<UploadFilesFieldFileMutation>;
export type UploadFilesFieldFileMutationOptions = Apollo.BaseMutationOptions<UploadFilesFieldFileMutation, UploadFilesFieldFileMutationVariables>;
export const UploadWorkflowFileDocument = gql`
mutation UploadWorkflowFile($file: Upload!) {
uploadWorkflowFile(file: $file) {
id
path
size
createdAt
url
}
}
`;
export type UploadWorkflowFileMutationFn = Apollo.MutationFunction<UploadWorkflowFileMutation, UploadWorkflowFileMutationVariables>;
/**
* __useUploadWorkflowFileMutation__
*
* To run a mutation, you first call `useUploadWorkflowFileMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUploadWorkflowFileMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [uploadWorkflowFileMutation, { data, loading, error }] = useUploadWorkflowFileMutation({
* variables: {
* file: // value for 'file'
* },
* });
*/
export function useUploadWorkflowFileMutation(baseOptions?: Apollo.MutationHookOptions<UploadWorkflowFileMutation, UploadWorkflowFileMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UploadWorkflowFileMutation, UploadWorkflowFileMutationVariables>(UploadWorkflowFileDocument, options);
}
export type UploadWorkflowFileMutationHookResult = ReturnType<typeof useUploadWorkflowFileMutation>;
export type UploadWorkflowFileMutationResult = Apollo.MutationResult<UploadWorkflowFileMutation>;
export type UploadWorkflowFileMutationOptions = Apollo.BaseMutationOptions<UploadWorkflowFileMutation, UploadWorkflowFileMutationVariables>;
export const FindManyFrontComponentsDocument = gql`
query FindManyFrontComponents {
frontComponents {
@@ -13095,8 +13178,7 @@ export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptio
export const UploadWorkspaceMemberProfilePictureDocument = gql`
mutation UploadWorkspaceMemberProfilePicture($file: Upload!) {
uploadWorkspaceMemberProfilePicture(file: $file) {
path
token
url
}
}
`;
@@ -13126,6 +13208,40 @@ export function useUploadWorkspaceMemberProfilePictureMutation(baseOptions?: Apo
export type UploadWorkspaceMemberProfilePictureMutationHookResult = ReturnType<typeof useUploadWorkspaceMemberProfilePictureMutation>;
export type UploadWorkspaceMemberProfilePictureMutationResult = Apollo.MutationResult<UploadWorkspaceMemberProfilePictureMutation>;
export type UploadWorkspaceMemberProfilePictureMutationOptions = Apollo.BaseMutationOptions<UploadWorkspaceMemberProfilePictureMutation, UploadWorkspaceMemberProfilePictureMutationVariables>;
export const UploadWorkspaceMemberProfilePictureLegacyDocument = gql`
mutation UploadWorkspaceMemberProfilePictureLegacy($file: Upload!) {
uploadWorkspaceMemberProfilePictureLegacy(file: $file) {
path
token
}
}
`;
export type UploadWorkspaceMemberProfilePictureLegacyMutationFn = Apollo.MutationFunction<UploadWorkspaceMemberProfilePictureLegacyMutation, UploadWorkspaceMemberProfilePictureLegacyMutationVariables>;
/**
* __useUploadWorkspaceMemberProfilePictureLegacyMutation__
*
* To run a mutation, you first call `useUploadWorkspaceMemberProfilePictureLegacyMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUploadWorkspaceMemberProfilePictureLegacyMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [uploadWorkspaceMemberProfilePictureLegacyMutation, { data, loading, error }] = useUploadWorkspaceMemberProfilePictureLegacyMutation({
* variables: {
* file: // value for 'file'
* },
* });
*/
export function useUploadWorkspaceMemberProfilePictureLegacyMutation(baseOptions?: Apollo.MutationHookOptions<UploadWorkspaceMemberProfilePictureLegacyMutation, UploadWorkspaceMemberProfilePictureLegacyMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UploadWorkspaceMemberProfilePictureLegacyMutation, UploadWorkspaceMemberProfilePictureLegacyMutationVariables>(UploadWorkspaceMemberProfilePictureLegacyDocument, options);
}
export type UploadWorkspaceMemberProfilePictureLegacyMutationHookResult = ReturnType<typeof useUploadWorkspaceMemberProfilePictureLegacyMutation>;
export type UploadWorkspaceMemberProfilePictureLegacyMutationResult = Apollo.MutationResult<UploadWorkspaceMemberProfilePictureLegacyMutation>;
export type UploadWorkspaceMemberProfilePictureLegacyMutationOptions = Apollo.BaseMutationOptions<UploadWorkspaceMemberProfilePictureLegacyMutation, UploadWorkspaceMemberProfilePictureLegacyMutationVariables>;
export const UpdateUserEmailDocument = gql`
mutation UpdateUserEmail($newEmail: String!, $verifyEmailRedirectPath: String) {
updateUserEmail(
@@ -15527,8 +15643,7 @@ export type UpdateWorkspaceMutationOptions = Apollo.BaseMutationOptions<UpdateWo
export const UploadWorkspaceLogoDocument = gql`
mutation UploadWorkspaceLogo($file: Upload!) {
uploadWorkspaceLogo(file: $file) {
path
token
url
}
}
`;
@@ -15558,6 +15673,40 @@ export function useUploadWorkspaceLogoMutation(baseOptions?: Apollo.MutationHook
export type UploadWorkspaceLogoMutationHookResult = ReturnType<typeof useUploadWorkspaceLogoMutation>;
export type UploadWorkspaceLogoMutationResult = Apollo.MutationResult<UploadWorkspaceLogoMutation>;
export type UploadWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>;
export const UploadWorkspaceLogoLegacyDocument = gql`
mutation UploadWorkspaceLogoLegacy($file: Upload!) {
uploadWorkspaceLogoLegacy(file: $file) {
path
token
}
}
`;
export type UploadWorkspaceLogoLegacyMutationFn = Apollo.MutationFunction<UploadWorkspaceLogoLegacyMutation, UploadWorkspaceLogoLegacyMutationVariables>;
/**
* __useUploadWorkspaceLogoLegacyMutation__
*
* To run a mutation, you first call `useUploadWorkspaceLogoLegacyMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUploadWorkspaceLogoLegacyMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [uploadWorkspaceLogoLegacyMutation, { data, loading, error }] = useUploadWorkspaceLogoLegacyMutation({
* variables: {
* file: // value for 'file'
* },
* });
*/
export function useUploadWorkspaceLogoLegacyMutation(baseOptions?: Apollo.MutationHookOptions<UploadWorkspaceLogoLegacyMutation, UploadWorkspaceLogoLegacyMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UploadWorkspaceLogoLegacyMutation, UploadWorkspaceLogoLegacyMutationVariables>(UploadWorkspaceLogoLegacyDocument, options);
}
export type UploadWorkspaceLogoLegacyMutationHookResult = ReturnType<typeof useUploadWorkspaceLogoLegacyMutation>;
export type UploadWorkspaceLogoLegacyMutationResult = Apollo.MutationResult<UploadWorkspaceLogoLegacyMutation>;
export type UploadWorkspaceLogoLegacyMutationOptions = Apollo.BaseMutationOptions<UploadWorkspaceLogoLegacyMutation, UploadWorkspaceLogoLegacyMutationVariables>;
export const CheckCustomDomainValidRecordsDocument = gql`
mutation CheckCustomDomainValidRecords {
checkCustomDomainValidRecords {
@@ -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`,
@@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const UPLOAD_WORKFLOW_FILE = gql`
mutation UploadWorkflowFile($file: Upload!) {
uploadWorkflowFile(file: $file) {
id
path
size
createdAt
url
}
}
`;
@@ -3,8 +3,7 @@ import { gql } from '@apollo/client';
export const UPLOAD_WORKSPACE_MEMBER_PROFILE_PICTURE = gql`
mutation UploadWorkspaceMemberProfilePicture($file: Upload!) {
uploadWorkspaceMemberProfilePicture(file: $file) {
path
token
url
}
}
`;
@@ -0,0 +1,10 @@
import { gql } from '@apollo/client';
export const UPLOAD_WORKSPACE_MEMBER_PROFILE_PICTURE_LEGACY = gql`
mutation UploadWorkspaceMemberProfilePictureLegacy($file: Upload!) {
uploadWorkspaceMemberProfilePictureLegacy(file: $file) {
path
token
}
}
`;
@@ -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,
@@ -2,14 +2,21 @@ import { useRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { ImageInput } from '@/ui/input/components/ImageInput';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { buildSignedPath } from 'twenty-shared/utils';
import {
FeatureFlagKey,
useUpdateWorkspaceMutation,
useUploadWorkspaceLogoLegacyMutation,
useUploadWorkspaceLogoMutation,
} from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { buildSignedPath } from 'twenty-shared/utils';
export const WorkspaceLogoUploader = () => {
const isCorePictureMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
);
const [uploadLogoLegacy] = useUploadWorkspaceLogoLegacyMutation();
const [uploadLogo] = useUploadWorkspaceLogoMutation();
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
@@ -23,17 +30,32 @@ export const WorkspaceLogoUploader = () => {
if (!currentWorkspace?.id) {
throw new Error('Workspace id not found');
}
await uploadLogo({
variables: {
file,
},
onCompleted: (data) => {
setCurrentWorkspace({
...currentWorkspace,
logo: buildSignedPath(data.uploadWorkspaceLogo),
});
},
});
if (isCorePictureMigrated) {
await uploadLogo({
variables: {
file,
},
onCompleted: (data) => {
setCurrentWorkspace({
...currentWorkspace,
logo: data.uploadWorkspaceLogo.url,
});
},
});
} else {
await uploadLogoLegacy({
variables: {
file,
},
onCompleted: (data) => {
setCurrentWorkspace({
...currentWorkspace,
logo: buildSignedPath(data.uploadWorkspaceLogoLegacy),
});
},
});
}
};
const onRemove = async () => {
@@ -0,0 +1,12 @@
import {
type EmailRecipients,
type WorkflowAttachment,
} from 'twenty-shared/workflow';
export type EmailFormData = {
connectedAccountId: string;
recipients: Required<EmailRecipients>;
subject: string;
body: string;
files: WorkflowAttachment[];
};
@@ -120,7 +120,6 @@ export type WorkflowVersionStatus =
| 'DEACTIVATED'
| 'ARCHIVED';
// Keep existing types that are not covered by schemas
export type WorkflowVersion = {
id: string;
name: string;
@@ -2,8 +2,8 @@ import {
type WorkflowStep,
type WorkflowTrigger,
} from '@/workflow/types/Workflow';
import { FieldMetadataType } from 'twenty-shared/types';
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
import { FieldMetadataType } from 'twenty-shared/types';
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
@@ -1,12 +1,12 @@
import { type WorkflowRunFlow } from '@/workflow/types/Workflow';
import { type WorkflowRunStepContext } from '@/workflow/workflow-steps/types/WorkflowRunStepContext';
import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps';
import { getWorkflowRunAllStepInfoHistory } from '@/workflow/workflow-steps/utils/getWorkflowRunAllStepInfoHistory';
import { isDefined } from 'twenty-shared/utils';
import {
TRIGGER_STEP_ID,
type WorkflowRunStepInfos,
} from 'twenty-shared/workflow';
import { type WorkflowRunStepContext } from '@/workflow/workflow-steps/types/WorkflowRunStepContext';
import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps';
import { getWorkflowRunAllStepInfoHistory } from '@/workflow/workflow-steps/utils/getWorkflowRunAllStepInfoHistory';
import { isDefined } from 'twenty-shared/utils';
export const getWorkflowRunStepContext = ({
stepId,
@@ -22,7 +22,6 @@ import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithC
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { type WorkflowEmailAction } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowEmailAction';
import { useEmailForm } from '@/workflow/workflow-steps/workflow-actions/hooks/useEmailForm';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
@@ -31,6 +30,7 @@ import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { ConnectedAccountProvider, SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type WorkflowEmailAction } from '@/workflow/types/WorkflowEmailAction';
import { Callout, IconPlus } from 'twenty-ui/display';
import { Button, type SelectOption } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
@@ -1,4 +1,3 @@
import { t } from '@lingui/core/macro';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
@@ -14,6 +13,7 @@ import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-ac
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
@@ -1,10 +0,0 @@
import { type WorkflowAttachmentType } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowAttachmentType';
import { type EmailRecipients } from 'twenty-shared/workflow';
export type EmailFormData = {
connectedAccountId: string;
recipients: Required<EmailRecipients>;
subject: string;
body: string;
files: WorkflowAttachmentType[];
};
@@ -1,7 +0,0 @@
export type WorkflowAttachmentType = {
id: string;
name: string;
size: number;
type: string;
createdAt: string;
};
@@ -1,6 +1,6 @@
import { type EmailFormData } from '@/workflow/workflow-steps/workflow-actions/email-action/types/EmailFormData';
import { type WorkflowEmailAction } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowEmailAction';
import { useState } from 'react';
import { type EmailFormData } from '@/workflow/types/EmailFormData';
import { type WorkflowEmailAction } from '@/workflow/types/WorkflowEmailAction';
import { type JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
@@ -1,8 +1,8 @@
import { type WorkflowHttpRequestAction } from '@/workflow/types/Workflow';
import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow';
import { parseAndValidateVariableFriendlyStringifiedJson } from '@/workflow/utils/parseAndValidateVariableFriendlyStringifiedJson';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow';
import { convertOutputSchemaToJson } from '@/workflow/workflow-steps/workflow-actions/http-request-action/utils/convertOutputSchemaToJson';
import { getHttpRequestOutputSchema } from '@/workflow/workflow-steps/workflow-actions/http-request-action/utils/getHttpRequestOutputSchema';
@@ -3,8 +3,7 @@ import { gql } from '@apollo/client';
export const UPLOAD_WORKSPACE_LOGO = gql`
mutation UploadWorkspaceLogo($file: Upload!) {
uploadWorkspaceLogo(file: $file) {
path
token
url
}
}
`;
@@ -0,0 +1,10 @@
import { gql } from '@apollo/client';
export const UPLOAD_WORKSPACE_LOGO_LEGACY = gql`
mutation UploadWorkspaceLogoLegacy($file: Upload!) {
uploadWorkspaceLogoLegacy(file: $file) {
path
token
}
}
`;