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:
@@ -1477,6 +1477,7 @@ export enum FeatureFlagKey {
|
||||
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
|
||||
@@ -3114,6 +3115,7 @@ export type MutationUploadFileArgs = {
|
||||
|
||||
|
||||
export type MutationUploadFilesFieldFileArgs = {
|
||||
fieldMetadataId: Scalars['String'];
|
||||
file: Scalars['Upload'];
|
||||
};
|
||||
|
||||
@@ -5979,6 +5981,7 @@ export type DeleteFileMutation = { __typename?: 'Mutation', deleteFile: { __type
|
||||
|
||||
export type UploadFilesFieldFileMutationVariables = Exact<{
|
||||
file: Scalars['Upload'];
|
||||
fieldMetadataId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -10326,8 +10329,8 @@ export type DeleteFileMutationHookResult = ReturnType<typeof useDeleteFileMutati
|
||||
export type DeleteFileMutationResult = Apollo.MutationResult<DeleteFileMutation>;
|
||||
export type DeleteFileMutationOptions = Apollo.BaseMutationOptions<DeleteFileMutation, DeleteFileMutationVariables>;
|
||||
export const UploadFilesFieldFileDocument = gql`
|
||||
mutation UploadFilesFieldFile($file: Upload!) {
|
||||
uploadFilesFieldFile(file: $file) {
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
path
|
||||
size
|
||||
@@ -10351,6 +10354,7 @@ export type UploadFilesFieldFileMutationFn = Apollo.MutationFunction<UploadFiles
|
||||
* const [uploadFilesFieldFileMutation, { data, loading, error }] = useUploadFilesFieldFileMutation({
|
||||
* variables: {
|
||||
* file: // value for 'file'
|
||||
* fieldMetadataId: // value for 'fieldMetadataId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -1449,6 +1449,7 @@ export enum FeatureFlagKey {
|
||||
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
|
||||
@@ -3019,6 +3020,7 @@ export type MutationUploadFileArgs = {
|
||||
|
||||
|
||||
export type MutationUploadFilesFieldFileArgs = {
|
||||
fieldMetadataId: Scalars['String'];
|
||||
file: Scalars['Upload'];
|
||||
};
|
||||
|
||||
|
||||
@@ -10,15 +10,18 @@ import { downloadFile } from '@/activities/files/utils/downloadFile';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActivityList } from '@/activities/components/ActivityList';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
import { IconDownload, IconX } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { AttachmentRow } from './AttachmentRow';
|
||||
|
||||
const DocumentViewer = lazy(() =>
|
||||
@@ -134,6 +137,10 @@ export const AttachmentList = ({
|
||||
isAttachmentPreviewEnabledState,
|
||||
);
|
||||
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const hasDownloadPermission = useHasPermissionFlag(
|
||||
PermissionFlagType.DOWNLOAD_FILE,
|
||||
);
|
||||
@@ -144,6 +151,16 @@ export const AttachmentList = ({
|
||||
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
||||
const getAttachmentUrl = (attachment: Attachment) => {
|
||||
const fileUrl = isFilesFieldMigrated
|
||||
? attachment.file?.[0]?.url || attachment.fullPath
|
||||
: attachment.fullPath;
|
||||
|
||||
assertIsDefinedOrThrow(fileUrl, new Error(t`File URL is not defined`));
|
||||
|
||||
return fileUrl;
|
||||
};
|
||||
|
||||
const onUploadFile = async (file: File) => {
|
||||
await uploadAttachmentFile(file, targetableObject);
|
||||
};
|
||||
@@ -167,7 +184,10 @@ export const AttachmentList = ({
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!previewedAttachment) return;
|
||||
downloadFile(previewedAttachment.fullPath, previewedAttachment.name);
|
||||
downloadFile(
|
||||
getAttachmentUrl(previewedAttachment),
|
||||
previewedAttachment.name,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -248,7 +268,7 @@ export const AttachmentList = ({
|
||||
>
|
||||
<DocumentViewer
|
||||
documentName={previewedAttachment.name}
|
||||
documentUrl={previewedAttachment.fullPath}
|
||||
documentUrl={getAttachmentUrl(previewedAttachment)}
|
||||
/>
|
||||
</Suspense>
|
||||
</StyledModalContent>
|
||||
|
||||
@@ -9,17 +9,21 @@ import {
|
||||
FieldContext,
|
||||
type GenericFieldContextType,
|
||||
} from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { getFileCategoryFromExtension } from '@/object-record/record-field/ui/utils/getFileCategoryFromExtension';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FileIcon } from '@/file/components/FileIcon';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { isNavigationModifierPressed } from 'twenty-ui/utilities';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
|
||||
|
||||
@@ -82,6 +86,10 @@ export const AttachmentRow = ({
|
||||
const theme = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const hasDownloadPermission = useHasPermissionFlag(
|
||||
PermissionFlagType.DOWNLOAD_FILE,
|
||||
);
|
||||
@@ -92,6 +100,16 @@ export const AttachmentRow = ({
|
||||
const [attachmentFileName, setAttachmentFileName] =
|
||||
useState(originalFileName);
|
||||
|
||||
const fileCategory = isFilesFieldMigrated
|
||||
? getFileCategoryFromExtension(attachment.file?.[0]?.extension)
|
||||
: attachment.fileCategory;
|
||||
|
||||
const fileUrl = isFilesFieldMigrated
|
||||
? attachment.file?.[0]?.url
|
||||
: attachment.fullPath;
|
||||
|
||||
assertIsDefinedOrThrow(fileUrl, new Error(t`File URL is not defined`));
|
||||
|
||||
const { destroyOneRecord: destroyOneAttachment } = useDestroyOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
});
|
||||
@@ -114,7 +132,19 @@ export const AttachmentRow = ({
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
idToUpdate: attachment.id,
|
||||
updateOneRecordInput: { name: newFileName },
|
||||
updateOneRecordInput: {
|
||||
name: newFileName,
|
||||
...(isFilesFieldMigrated && isDefined(attachment.file?.[0]?.fileId)
|
||||
? {
|
||||
file: [
|
||||
{
|
||||
fileId: attachment.file?.[0]?.fileId,
|
||||
label: newFileName,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,10 +163,7 @@ export const AttachmentRow = ({
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
downloadFile(
|
||||
attachment.fullPath,
|
||||
`${attachmentFileName}${attachmentFileExtension}`,
|
||||
);
|
||||
downloadFile(fileUrl, `${attachmentFileName}${attachmentFileExtension}`);
|
||||
};
|
||||
|
||||
const handleOpenDocument = (e: React.MouseEvent) => {
|
||||
@@ -162,7 +189,7 @@ export const AttachmentRow = ({
|
||||
>
|
||||
<ActivityRow disabled>
|
||||
<StyledLeftContent>
|
||||
<FileIcon fileCategory={attachment.fileCategory} />
|
||||
<FileIcon fileCategory={fileCategory} />
|
||||
{isEditing ? (
|
||||
<SettingsTextInput
|
||||
instanceId={`attachment-${attachment.id}-name`}
|
||||
@@ -176,7 +203,7 @@ export const AttachmentRow = ({
|
||||
<StyledLinkContainer>
|
||||
<StyledLink
|
||||
onClick={handleOpenDocument}
|
||||
href={attachment.fullPath}
|
||||
href={fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
+63
-13
@@ -3,22 +3,39 @@ import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FileFolder,
|
||||
useUploadFileMutation,
|
||||
useUploadFilesFieldFileMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { FeatureFlagKey, FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
export const useUploadAttachmentFile = () => {
|
||||
const coreClient = useApolloCoreClient();
|
||||
const [uploadFile] = useUploadFileMutation({ client: coreClient });
|
||||
const [uploadFilesFieldFile] = useUploadFilesFieldFileMutation({
|
||||
client: coreClient,
|
||||
});
|
||||
const isAttachmentMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
);
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const { objectMetadataItem: attachmentMetadata } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
});
|
||||
|
||||
const filesFieldMetadataId = attachmentMetadata.fields.find(
|
||||
(field) => field.type === FieldMetadataType.FILES && field.name === 'file',
|
||||
)?.id;
|
||||
|
||||
const { createOneRecord: createOneAttachment } =
|
||||
useCreateOneRecord<Attachment>({
|
||||
@@ -30,21 +47,44 @@ export const useUploadAttachmentFile = () => {
|
||||
file: File,
|
||||
targetableObject: ActivityTargetableObject,
|
||||
) => {
|
||||
const result = await uploadFile({
|
||||
variables: {
|
||||
file,
|
||||
fileFolder: FileFolder.Attachment,
|
||||
},
|
||||
});
|
||||
let attachmentPath: string;
|
||||
let fileId: string | undefined;
|
||||
|
||||
const signedFile = result?.data?.uploadFile;
|
||||
if (isFilesFieldMigrated) {
|
||||
assertIsDefinedOrThrow(
|
||||
filesFieldMetadataId,
|
||||
new Error(t`File field not found for attachment object`),
|
||||
);
|
||||
|
||||
if (!isDefined(signedFile)) {
|
||||
throw new Error("Couldn't upload the attachment.");
|
||||
const result = await uploadFilesFieldFile({
|
||||
variables: { file, fieldMetadataId: filesFieldMetadataId },
|
||||
});
|
||||
|
||||
const uploadedFile = result?.data?.uploadFilesFieldFile;
|
||||
|
||||
if (!isDefined(uploadedFile)) {
|
||||
throw new Error("Couldn't upload the attachment.");
|
||||
}
|
||||
|
||||
attachmentPath = uploadedFile.path;
|
||||
fileId = uploadedFile.id;
|
||||
} else {
|
||||
const result = await uploadFile({
|
||||
variables: {
|
||||
file,
|
||||
fileFolder: FileFolder.Attachment,
|
||||
},
|
||||
});
|
||||
|
||||
const signedFile = result?.data?.uploadFile;
|
||||
|
||||
if (!isDefined(signedFile)) {
|
||||
throw new Error("Couldn't upload the attachment.");
|
||||
}
|
||||
|
||||
attachmentPath = signedFile.path;
|
||||
}
|
||||
|
||||
const { path: attachmentPath } = signedFile;
|
||||
|
||||
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
isMorphRelation: isAttachmentMigrated,
|
||||
@@ -55,6 +95,16 @@ export const useUploadAttachmentFile = () => {
|
||||
fullPath: attachmentPath,
|
||||
fileCategory: getFileType(file.name),
|
||||
[targetableObjectFieldIdName]: targetableObject.id,
|
||||
...(isFilesFieldMigrated && isDefined(fileId)
|
||||
? {
|
||||
file: [
|
||||
{
|
||||
fileId,
|
||||
label: file.name,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} as Partial<Attachment>;
|
||||
|
||||
const createdAttachment = await createOneAttachment(attachmentToCreate);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
import { type AttachmentFileCategory } from './AttachmentFileCategory';
|
||||
|
||||
export type { AttachmentFileCategory };
|
||||
@@ -7,6 +9,7 @@ export type Attachment = {
|
||||
name: string;
|
||||
fullPath: string;
|
||||
fileCategory: AttachmentFileCategory;
|
||||
file?: FieldFilesValue[] | null;
|
||||
companyId?: string | null;
|
||||
personId?: string | null;
|
||||
taskId?: string | null;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPLOAD_FILES_FIELD_FILE = gql`
|
||||
mutation UploadFilesFieldFile($file: Upload!) {
|
||||
uploadFilesFieldFile(file: $file) {
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
path
|
||||
size
|
||||
|
||||
+1
@@ -99,6 +99,7 @@ export const useOpenFieldInputEditMode = () => {
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
openFilesFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
||||
recordId,
|
||||
prefix,
|
||||
updateRecord: (updateInput) => {
|
||||
|
||||
+20
-4
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+7
-1
@@ -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(
|
||||
|
||||
+20
@@ -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({
|
||||
|
||||
+6
-2
@@ -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);
|
||||
}
|
||||
|
||||
+16
@@ -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,
|
||||
});
|
||||
+1
-1
@@ -144,7 +144,7 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
|
||||
[FieldMetadataType.FILES]: {
|
||||
label: 'Files',
|
||||
Icon: IllustrationIconFile,
|
||||
category: 'Advanced',
|
||||
category: 'Basic',
|
||||
exampleValues: [
|
||||
[
|
||||
{
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FileIcon } from '@/file/components/FileIcon';
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { getFileCategoryFromExtension } from '@/object-record/record-field/ui/utils/getFileCategoryFromExtension';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ChipVariant, LinkChip } from 'twenty-ui/components';
|
||||
import { Chip, ChipVariant } from 'twenty-ui/components';
|
||||
|
||||
const MAX_WIDTH = 120;
|
||||
|
||||
const StyledClickableContainer = styled.div<{ clickable: boolean }>`
|
||||
cursor: ${({ clickable }) => (clickable ? 'pointer' : 'inherit')};
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
type FileChipProps = {
|
||||
file: FieldFilesValue;
|
||||
onClick: (file: FieldFilesValue) => void;
|
||||
@@ -17,8 +24,10 @@ export const FileChip = ({
|
||||
onClick,
|
||||
forceDisableClick,
|
||||
}: FileChipProps) => {
|
||||
const handleClick = (event: React.MouseEvent): void => {
|
||||
if (isDefined(forceDisableClick)) {
|
||||
const isClickable = forceDisableClick !== true;
|
||||
|
||||
const handleMouseDown = (event: React.MouseEvent): void => {
|
||||
if (!isClickable) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
@@ -36,14 +45,17 @@ export const FileChip = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<LinkChip
|
||||
to="#"
|
||||
label={file.label}
|
||||
maxWidth={MAX_WIDTH}
|
||||
leftComponent={fileIcon}
|
||||
variant={ChipVariant.Highlighted}
|
||||
onClick={forceDisableClick ? undefined : handleClick}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
<StyledClickableContainer
|
||||
clickable={isClickable}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<Chip
|
||||
label={file.label ?? ''}
|
||||
maxWidth={MAX_WIDTH}
|
||||
leftComponent={fileIcon}
|
||||
variant={ChipVariant.Highlighted}
|
||||
clickable={isClickable}
|
||||
/>
|
||||
</StyledClickableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { downloadFile } from '@/activities/files/utils/downloadFile';
|
||||
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { FileChip } from '@/ui/field/display/components/FileChip';
|
||||
import { UploadFileChip } from '@/ui/field/display/components/UploadFileChip';
|
||||
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
@@ -10,11 +11,15 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
type FilesDisplayProps = {
|
||||
value?: FieldFilesValue[];
|
||||
forceDisableClick?: boolean;
|
||||
isUploadWindowOpen?: boolean;
|
||||
isFileUploading?: boolean;
|
||||
};
|
||||
|
||||
export const FilesDisplay = ({
|
||||
value,
|
||||
forceDisableClick,
|
||||
isUploadWindowOpen = false,
|
||||
isFileUploading = false,
|
||||
}: FilesDisplayProps) => {
|
||||
const setFilePreview = useSetRecoilState(filePreviewState);
|
||||
const isAttachmentPreviewEnabled = useRecoilValue(
|
||||
@@ -32,6 +37,12 @@ export const FilesDisplay = ({
|
||||
};
|
||||
|
||||
if (!isDefined(value) || value.length === 0) {
|
||||
if (isFileUploading) {
|
||||
return <UploadFileChip isLoading={true} />;
|
||||
}
|
||||
if (isUploadWindowOpen) {
|
||||
return <UploadFileChip isLoading={false} />;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconArrowUp } from 'twenty-ui/display';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(5)};
|
||||
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledIconBox = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.font.color.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
`;
|
||||
|
||||
const StyledStaticLoader = styled.div`
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.font.color.tertiary};
|
||||
border-radius: 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
height: 12px;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
type UploadFileChipProps = {
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export const UploadFileChip = ({ isLoading = true }: UploadFileChipProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledIconBox>
|
||||
<IconArrowUp size={theme.icon.size.sm} />
|
||||
</StyledIconBox>
|
||||
{isLoading ? <Loader /> : <StyledStaticLoader />}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+1
@@ -14,6 +14,7 @@ export enum FeatureFlagKey {
|
||||
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED',
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
|
||||
+7
-1
@@ -12,13 +12,19 @@ import { FilesFieldDeletionListener } from 'src/engine/core-modules/file/files-f
|
||||
import { FilesFieldResolver } from 'src/engine/core-modules/file/files-field/resolvers/files-field.resolver';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule,
|
||||
TypeOrmModule.forFeature([FileEntity, WorkspaceEntity, ApplicationEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
FileEntity,
|
||||
WorkspaceEntity,
|
||||
ApplicationEntity,
|
||||
FieldMetadataEntity,
|
||||
]),
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
|
||||
+15
-4
@@ -20,6 +20,7 @@ import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file
|
||||
import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
import {
|
||||
FilesFieldException,
|
||||
@@ -32,6 +33,8 @@ export class FilesFieldService {
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(FileEntity)
|
||||
private readonly fileRepository: Repository<FileEntity>,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
@@ -43,13 +46,13 @@ export class FilesFieldService {
|
||||
filename,
|
||||
declaredMimeType,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
file: Buffer;
|
||||
filename: string;
|
||||
declaredMimeType: string | undefined;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
fieldMetadataId: string;
|
||||
}): Promise<FileEntity> {
|
||||
const { mimeType, ext } = await extractFileInfo({
|
||||
file,
|
||||
@@ -62,16 +65,24 @@ export class FilesFieldService {
|
||||
const fileId = v4();
|
||||
const name = `${fileId}${ext ? `.${ext}` : ''}`;
|
||||
|
||||
const fieldMetadata = await this.fieldMetadataRepository.findOneOrFail({
|
||||
select: ['applicationId', 'universalIdentifier'],
|
||||
where: {
|
||||
id: fieldMetadataId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const application = await this.applicationRepository.findOneOrFail({
|
||||
where: {
|
||||
id: applicationId,
|
||||
id: fieldMetadata.applicationId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return await this.fileStorageService.writeFile_v2({
|
||||
sourceFile: sanitizedFile,
|
||||
resourcePath: name,
|
||||
resourcePath: `${fieldMetadata.universalIdentifier}/${name}`,
|
||||
mimeType,
|
||||
fileFolder: FileFolder.FilesField,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
|
||||
+8
-2
@@ -27,9 +27,15 @@ export class FilesFieldResolver {
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
|
||||
async uploadFilesFieldFile(
|
||||
@AuthWorkspace()
|
||||
{ id: workspaceId, workspaceCustomApplicationId }: WorkspaceEntity,
|
||||
{ id: workspaceId }: WorkspaceEntity,
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
{ createReadStream, filename, mimetype }: FileUpload,
|
||||
@Args({
|
||||
name: 'fieldMetadataId',
|
||||
type: () => String,
|
||||
nullable: false,
|
||||
})
|
||||
fieldMetadataId: string,
|
||||
): Promise<FileDTO> {
|
||||
const stream = createReadStream();
|
||||
const buffer = await streamToBuffer(stream);
|
||||
@@ -39,7 +45,7 @@ export class FilesFieldResolver {
|
||||
filename,
|
||||
declaredMimeType: mimetype,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
fieldMetadataId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -243,6 +243,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED: false,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false,
|
||||
IS_MARKETPLACE_ENABLED: false,
|
||||
IS_FILES_FIELD_MIGRATED: false,
|
||||
},
|
||||
userWorkspaceRoleMap: {},
|
||||
eventEmitterService: {
|
||||
|
||||
+1
-6
@@ -1223,7 +1223,6 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
|
||||
let filesFieldDiffByEntityIndex = null;
|
||||
let filesFieldFileIds = null;
|
||||
let fileIdToApplicationId = new Map<string, string>();
|
||||
|
||||
filesFieldDiffByEntityIndex =
|
||||
filesFieldSync.computeFilesFieldDiffBeforeUpsert(
|
||||
@@ -1241,7 +1240,6 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
});
|
||||
|
||||
filesFieldFileIds = result.fileIds;
|
||||
fileIdToApplicationId = result.fileIdToApplicationId;
|
||||
|
||||
entityWithConnectedRelations.splice(
|
||||
0,
|
||||
@@ -1286,10 +1284,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
.finally(() => queryRunnerForEntityPersistExecutor.release());
|
||||
|
||||
if (isDefined(filesFieldFileIds)) {
|
||||
await filesFieldSync.updateFileEntityRecords(
|
||||
filesFieldFileIds,
|
||||
fileIdToApplicationId,
|
||||
);
|
||||
await filesFieldSync.updateFileEntityRecords(filesFieldFileIds);
|
||||
}
|
||||
|
||||
const resultArray = Array.isArray(result) ? result : [result];
|
||||
|
||||
+71
-57
@@ -277,6 +277,28 @@ export class FilesFieldSync {
|
||||
}
|
||||
}
|
||||
|
||||
private validateFileFieldUniversalIdentifier(
|
||||
fileId: string,
|
||||
fileEntity: FileEntity,
|
||||
fileIdToFieldUniversalIdentifier: Map<string, string>,
|
||||
): void {
|
||||
const expectedUniversalIdentifier =
|
||||
fileIdToFieldUniversalIdentifier.get(fileId);
|
||||
|
||||
if (
|
||||
isDefined(expectedUniversalIdentifier) &&
|
||||
!fileEntity.path.includes(expectedUniversalIdentifier)
|
||||
) {
|
||||
throw new TwentyORMException(
|
||||
`File ${fileId} was not uploaded for this field`,
|
||||
TwentyORMExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: msg`File ${fileId} was not uploaded for this field. Please re-upload the file.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private validateAndComputeFilesFieldDiff(
|
||||
entity: Record<string, unknown>,
|
||||
filesField: FlatFieldMetadata,
|
||||
@@ -358,7 +380,6 @@ export class FilesFieldSync {
|
||||
toUpdate: Set<string>;
|
||||
toRemove: Set<string>;
|
||||
};
|
||||
fileIdToApplicationId: Map<string, string>;
|
||||
}> {
|
||||
if (Object.keys(filesFieldDiffByEntityIndex).length === 0) {
|
||||
return {
|
||||
@@ -368,7 +389,6 @@ export class FilesFieldSync {
|
||||
toUpdate: new Set<string>(),
|
||||
toRemove: new Set<string>(),
|
||||
},
|
||||
fileIdToApplicationId: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -377,12 +397,11 @@ export class FilesFieldSync {
|
||||
this.internalContext,
|
||||
);
|
||||
|
||||
const { toAdd, toUpdate, toRemove, fileIdToApplicationId } =
|
||||
await this.validateAndEnrichFileDiffs(
|
||||
filesFieldDiffByEntityIndex,
|
||||
workspaceId,
|
||||
objectMetadata.id,
|
||||
);
|
||||
const { toAdd, toUpdate, toRemove } = await this.validateAndEnrichFileDiffs(
|
||||
filesFieldDiffByEntityIndex,
|
||||
workspaceId,
|
||||
objectMetadata.id,
|
||||
);
|
||||
|
||||
const updatedEntities = this.updateEntitiesWithEnrichedFilesFieldValues(
|
||||
entities,
|
||||
@@ -392,7 +411,6 @@ export class FilesFieldSync {
|
||||
return {
|
||||
entities: updatedEntities,
|
||||
fileIds: { toAdd, toUpdate, toRemove },
|
||||
fileIdToApplicationId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -404,7 +422,6 @@ export class FilesFieldSync {
|
||||
toAdd: Set<string>;
|
||||
toUpdate: Set<string>;
|
||||
toRemove: Set<string>;
|
||||
fileIdToApplicationId: Map<string, string>;
|
||||
}> {
|
||||
const allFileIds = {
|
||||
toAdd: new Set<string>(),
|
||||
@@ -412,28 +429,39 @@ export class FilesFieldSync {
|
||||
toRemove: new Set<string>(),
|
||||
};
|
||||
|
||||
const fileIdToApplicationId = new Map<string, string>();
|
||||
const allFileIdsToFetch = new Set<string>();
|
||||
|
||||
const filesFields = this.getFilesFields(objectMetadataId);
|
||||
const fieldNameToApplicationId = new Map(
|
||||
filesFields.map((field) => [field.name, field.applicationId]),
|
||||
const fieldNameToUniversalIdentifier = new Map(
|
||||
filesFields.map((field) => [field.name, field.universalIdentifier]),
|
||||
);
|
||||
|
||||
const fileIdToFieldUniversalIdentifier = new Map<string, string>();
|
||||
|
||||
for (const entityDiffs of Object.values(filesFieldDiffByEntityIndex)) {
|
||||
for (const [fieldName, diff] of Object.entries(entityDiffs)) {
|
||||
const fieldApplicationId = fieldNameToApplicationId.get(fieldName);
|
||||
const fieldUniversalIdentifier =
|
||||
fieldNameToUniversalIdentifier.get(fieldName);
|
||||
|
||||
diff.toAdd.forEach((file) => {
|
||||
allFileIds.toAdd.add(file.fileId);
|
||||
allFileIdsToFetch.add(file.fileId);
|
||||
if (fieldApplicationId) {
|
||||
fileIdToApplicationId.set(file.fileId, fieldApplicationId);
|
||||
if (isDefined(fieldUniversalIdentifier)) {
|
||||
fileIdToFieldUniversalIdentifier.set(
|
||||
file.fileId,
|
||||
fieldUniversalIdentifier,
|
||||
);
|
||||
}
|
||||
});
|
||||
diff.toUpdate.forEach((file) => {
|
||||
allFileIds.toUpdate.add(file.fileId);
|
||||
allFileIdsToFetch.add(file.fileId);
|
||||
if (isDefined(fieldUniversalIdentifier)) {
|
||||
fileIdToFieldUniversalIdentifier.set(
|
||||
file.fileId,
|
||||
fieldUniversalIdentifier,
|
||||
);
|
||||
}
|
||||
});
|
||||
diff.toRemove.forEach((file) => {
|
||||
allFileIds.toRemove.add(file.fileId);
|
||||
@@ -442,7 +470,7 @@ export class FilesFieldSync {
|
||||
}
|
||||
|
||||
if (allFileIdsToFetch.size === 0 && allFileIds.toRemove.size === 0) {
|
||||
return { ...allFileIds, fileIdToApplicationId };
|
||||
return allFileIds;
|
||||
}
|
||||
|
||||
const existingFiles = await this.fileRepository.find({
|
||||
@@ -469,6 +497,12 @@ export class FilesFieldSync {
|
||||
);
|
||||
}
|
||||
|
||||
this.validateFileFieldUniversalIdentifier(
|
||||
file.fileId,
|
||||
fileEntity,
|
||||
fileIdToFieldUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (!fileEntity.settings?.isTemporaryFile) {
|
||||
const fileId = file.fileId;
|
||||
|
||||
@@ -494,6 +528,12 @@ export class FilesFieldSync {
|
||||
);
|
||||
}
|
||||
|
||||
this.validateFileFieldUniversalIdentifier(
|
||||
file.fileId,
|
||||
fileEntity,
|
||||
fileIdToFieldUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (fileEntity.settings?.isTemporaryFile) {
|
||||
throw new TwentyORMException(
|
||||
`File ${file.fileId} to update should not be a temporary file`,
|
||||
@@ -507,50 +547,24 @@ export class FilesFieldSync {
|
||||
}
|
||||
}
|
||||
|
||||
return { ...allFileIds, fileIdToApplicationId };
|
||||
return allFileIds;
|
||||
}
|
||||
|
||||
async updateFileEntityRecords(
|
||||
fileIds: {
|
||||
toAdd: Set<string>;
|
||||
toUpdate: Set<string>;
|
||||
toRemove: Set<string>;
|
||||
},
|
||||
fileIdToApplicationId: Map<string, string>,
|
||||
): Promise<void> {
|
||||
async updateFileEntityRecords(fileIds: {
|
||||
toAdd: Set<string>;
|
||||
toUpdate: Set<string>;
|
||||
toRemove: Set<string>;
|
||||
}): Promise<void> {
|
||||
if (fileIds.toAdd.size > 0) {
|
||||
const fileIdsByApplicationId = Array.from(fileIds.toAdd).reduce(
|
||||
(acc, fileId) => {
|
||||
const applicationId = fileIdToApplicationId.get(fileId);
|
||||
|
||||
if (!applicationId) {
|
||||
throw new TwentyORMException(
|
||||
`Application ID not found for file ${fileId}`,
|
||||
TwentyORMExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
acc[applicationId] = [...(acc[applicationId] || []), fileId];
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string[]>,
|
||||
);
|
||||
|
||||
for (const [applicationId, fileIds] of Object.entries(
|
||||
fileIdsByApplicationId,
|
||||
)) {
|
||||
await this.fileRepository.update(
|
||||
{ id: In(fileIds) },
|
||||
{
|
||||
settings: {
|
||||
isTemporaryFile: false,
|
||||
toDelete: false,
|
||||
},
|
||||
applicationId,
|
||||
await this.fileRepository.update(
|
||||
{ id: In([...fileIds.toAdd]) },
|
||||
{
|
||||
settings: {
|
||||
isTemporaryFile: false,
|
||||
toDelete: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (fileIds.toRemove.size > 0) {
|
||||
|
||||
+1
-6
@@ -169,7 +169,6 @@ export class WorkspaceInsertQueryBuilder<
|
||||
);
|
||||
|
||||
let filesFieldFileIds = null;
|
||||
let fileIdToApplicationId = new Map<string, string>();
|
||||
|
||||
const entities = Array.isArray(this.expressionMap.valuesSet)
|
||||
? this.expressionMap.valuesSet
|
||||
@@ -191,7 +190,6 @@ export class WorkspaceInsertQueryBuilder<
|
||||
});
|
||||
|
||||
filesFieldFileIds = result.fileIds;
|
||||
fileIdToApplicationId = result.fileIdToApplicationId;
|
||||
|
||||
this.expressionMap.valuesSet = Array.isArray(
|
||||
this.expressionMap.valuesSet,
|
||||
@@ -227,10 +225,7 @@ export class WorkspaceInsertQueryBuilder<
|
||||
const result = await super.execute();
|
||||
|
||||
if (isDefined(filesFieldFileIds)) {
|
||||
await this.filesFieldSync.updateFileEntityRecords(
|
||||
filesFieldFileIds,
|
||||
fileIdToApplicationId,
|
||||
);
|
||||
await this.filesFieldSync.updateFileEntityRecords(filesFieldFileIds);
|
||||
}
|
||||
const eventSelectQueryBuilder = (
|
||||
this.connection.manager as WorkspaceEntityManager
|
||||
|
||||
+3
-13
@@ -173,7 +173,6 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
|
||||
let filesFieldDiffByEntityIndex = null;
|
||||
let filesFieldFileIds = null;
|
||||
let fileIdToApplicationId = new Map<string, string>();
|
||||
|
||||
const updatePayload = Array.isArray(this.expressionMap.valuesSet)
|
||||
? (this.expressionMap.valuesSet[0] ?? {})
|
||||
@@ -197,7 +196,6 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
});
|
||||
|
||||
filesFieldFileIds = result.fileIds;
|
||||
fileIdToApplicationId = result.fileIdToApplicationId;
|
||||
|
||||
this.expressionMap.valuesSet = result.entities[0];
|
||||
}
|
||||
@@ -236,10 +234,7 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
const result = await super.execute();
|
||||
|
||||
if (isDefined(filesFieldFileIds)) {
|
||||
await this.filesFieldSync.updateFileEntityRecords(
|
||||
filesFieldFileIds,
|
||||
fileIdToApplicationId,
|
||||
);
|
||||
await this.filesFieldSync.updateFileEntityRecords(filesFieldFileIds);
|
||||
}
|
||||
|
||||
const after = await eventSelectQueryBuilder.getMany();
|
||||
@@ -374,7 +369,6 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
|
||||
let filesFieldDiffByEntityIndex = null;
|
||||
let filesFieldFileIds = null;
|
||||
let fileIdToApplicationId = null;
|
||||
|
||||
const entities = this.manyInputs.map((input) => input.partialEntity);
|
||||
|
||||
@@ -394,7 +388,6 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
});
|
||||
|
||||
filesFieldFileIds = result.fileIds;
|
||||
fileIdToApplicationId = result.fileIdToApplicationId;
|
||||
|
||||
this.manyInputs = result.entities.map((updatedEntity, index) => ({
|
||||
criteria: this.manyInputs[index].criteria,
|
||||
@@ -451,11 +444,8 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
if (isDefined(filesFieldFileIds) && isDefined(fileIdToApplicationId)) {
|
||||
await this.filesFieldSync.updateFileEntityRecords(
|
||||
filesFieldFileIds,
|
||||
fileIdToApplicationId,
|
||||
);
|
||||
if (isDefined(filesFieldFileIds)) {
|
||||
await this.filesFieldSync.updateFileEntityRecords(filesFieldFileIds);
|
||||
}
|
||||
|
||||
const afterRecords = await eventSelectQueryBuilder.getMany();
|
||||
|
||||
+1287
-1284
File diff suppressed because it is too large
Load Diff
+23
-1
@@ -1,10 +1,10 @@
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
DateDisplayFormat,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
@@ -126,6 +126,27 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
file: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'file',
|
||||
type: FieldMetadataType.FILES,
|
||||
label: 'File',
|
||||
description: 'Attachment file',
|
||||
icon: 'IconFileUpload',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
settings: {
|
||||
maxNumberOfValues: 1,
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
//deprecated
|
||||
fullPath: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
@@ -143,6 +164,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
//deprecated
|
||||
fileCategory: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export const ATTACHMENT_STANDARD_FIELD_IDS = {
|
||||
name: '20202020-87a5-48f8-bbf7-ade388825a57',
|
||||
fullPath: '20202020-0d19-453d-8e8d-fbcda8ca3747',
|
||||
type: '20202020-a417-49b8-a40b-f6a7874caa0d',
|
||||
file: '20202020-15db-460e-8166-c7b5d87ad4be',
|
||||
fileCategory: '20202020-8c3f-4d9e-9a1b-2e5f7a8c9d0e',
|
||||
createdBy: '395be3bd-a5c9-463d-aafe-9bc3bbec3f15',
|
||||
updatedBy: '376239d1-3e65-4cb6-b5d8-e0917d43cc93',
|
||||
|
||||
+5
@@ -1,5 +1,6 @@
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import { type FileOutput } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.type';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { type CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
|
||||
@@ -13,10 +14,14 @@ import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standa
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
/** @deprecated Use `file[0].label` field instead */
|
||||
name: string | null;
|
||||
file: FileOutput[] | null;
|
||||
/** @deprecated Use `file[0].fileId` field instead */
|
||||
fullPath: string | null;
|
||||
/** @deprecated Use `fileCategory` field instead */
|
||||
type: string | null;
|
||||
/** @deprecated Use `file[0].extension` field instead */
|
||||
fileCategory: string;
|
||||
createdBy: ActorMetadata;
|
||||
updatedBy: ActorMetadata;
|
||||
|
||||
+34
-39
@@ -2,23 +2,13 @@ import gql from 'graphql-tag';
|
||||
import request from 'supertest';
|
||||
import { makeGraphqlAPIRequestWithFileUpload } from 'test/integration/graphql/utils/make-graphql-api-request-with-file-upload.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { uploadFilesFieldFileMutation } from 'test/integration/graphql/utils/upload-files-field-file-mutation.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const uploadWorkspaceFieldFileMutation = gql`
|
||||
mutation UploadFilesFieldFile($file: Upload!) {
|
||||
uploadFilesFieldFile(file: $file) {
|
||||
id
|
||||
path
|
||||
size
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteFileMutation = gql`
|
||||
mutation DeleteFile($fileId: UUID!) {
|
||||
deleteFile(fileId: $fileId) {
|
||||
@@ -59,33 +49,6 @@ type UploadedFile = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
const uploadFile = async (
|
||||
filename: string,
|
||||
content: string,
|
||||
contentType: string,
|
||||
): Promise<UploadedFile> => {
|
||||
const response = await makeGraphqlAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadWorkspaceFieldFileMutation,
|
||||
variables: { file: null },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
buffer: Buffer.from(content),
|
||||
filename,
|
||||
contentType,
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
return {
|
||||
id: response.body.data.uploadFilesFieldFile.id,
|
||||
contentType,
|
||||
content,
|
||||
};
|
||||
};
|
||||
|
||||
const deleteFile = async (fileId: string): Promise<void> => {
|
||||
await makeGraphqlAPIRequest({
|
||||
query: deleteFileMutation,
|
||||
@@ -95,8 +58,36 @@ const deleteFile = async (fileId: string): Promise<void> => {
|
||||
|
||||
describe('files-field.controller - GET /files-field/:id', () => {
|
||||
let createdObjectMetadataId = '';
|
||||
let createdFieldMetadataId = '';
|
||||
let uploadedFiles: UploadedFile[] = [];
|
||||
|
||||
const uploadFile = async (
|
||||
filename: string,
|
||||
content: string,
|
||||
contentType: string,
|
||||
): Promise<UploadedFile> => {
|
||||
const response = await makeGraphqlAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadFilesFieldFileMutation,
|
||||
variables: { file: null, fieldMetadataId: createdFieldMetadataId },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
buffer: Buffer.from(content),
|
||||
filename,
|
||||
contentType,
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
return {
|
||||
id: response.body.data.uploadFilesFieldFile.id,
|
||||
contentType,
|
||||
content,
|
||||
};
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
@@ -116,7 +107,9 @@ describe('files-field.controller - GET /files-field/:id', () => {
|
||||
|
||||
createdObjectMetadataId = objectMetadataId;
|
||||
|
||||
await createOneFieldMetadata({
|
||||
const {
|
||||
data: { createOneField: createdFieldMetadata },
|
||||
} = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'filesField',
|
||||
label: 'Files Field',
|
||||
@@ -131,6 +124,8 @@ describe('files-field.controller - GET /files-field/:id', () => {
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
createdFieldMetadataId = createdFieldMetadata.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+35
-38
@@ -1,23 +1,13 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { makeGraphqlAPIRequestWithFileUpload } from 'test/integration/graphql/utils/make-graphql-api-request-with-file-upload.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { uploadFilesFieldFileMutation } from 'test/integration/graphql/utils/upload-files-field-file-mutation.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const uploadWorkspaceFieldFileMutation = gql`
|
||||
mutation UploadFilesFieldFile($file: Upload!) {
|
||||
uploadFilesFieldFile(file: $file) {
|
||||
id
|
||||
path
|
||||
size
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteFileMutation = gql`
|
||||
mutation DeleteFile($fileId: UUID!) {
|
||||
deleteFile(fileId: $fileId) {
|
||||
@@ -75,32 +65,6 @@ type UploadedFile = {
|
||||
contentType: string;
|
||||
};
|
||||
|
||||
const uploadFile = async (
|
||||
filename: string,
|
||||
content: string,
|
||||
contentType: string,
|
||||
): Promise<UploadedFile> => {
|
||||
const response = await makeGraphqlAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadWorkspaceFieldFileMutation,
|
||||
variables: { file: null },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
buffer: Buffer.from(content),
|
||||
filename,
|
||||
contentType,
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
return {
|
||||
id: response.body.data.uploadFilesFieldFile.id,
|
||||
contentType,
|
||||
};
|
||||
};
|
||||
|
||||
const deleteFile = async (fileId: string): Promise<void> => {
|
||||
await makeGraphqlAPIRequest({
|
||||
query: deleteFileMutation,
|
||||
@@ -110,8 +74,35 @@ const deleteFile = async (fileId: string): Promise<void> => {
|
||||
|
||||
describe('fileFieldSync - FILES field <> files sync', () => {
|
||||
let createdObjectMetadataId = '';
|
||||
let createdFieldMetadataId = '';
|
||||
let uploadedFiles: UploadedFile[] = [];
|
||||
|
||||
const uploadFile = async (
|
||||
filename: string,
|
||||
content: string,
|
||||
contentType: string,
|
||||
): Promise<UploadedFile> => {
|
||||
const response = await makeGraphqlAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadFilesFieldFileMutation,
|
||||
variables: { file: null, fieldMetadataId: createdFieldMetadataId },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
buffer: Buffer.from(content),
|
||||
filename,
|
||||
contentType,
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
return {
|
||||
id: response.body.data.uploadFilesFieldFile.id,
|
||||
contentType,
|
||||
};
|
||||
};
|
||||
|
||||
const checkFileExistsInDB = async (fileId: string): Promise<boolean> => {
|
||||
const result = await global.testDataSource.query(
|
||||
'SELECT id FROM core."file" WHERE id = $1 AND "deletedAt" IS NULL',
|
||||
@@ -168,7 +159,11 @@ describe('fileFieldSync - FILES field <> files sync', () => {
|
||||
|
||||
createdObjectMetadataId = objectMetadataId;
|
||||
|
||||
await createOneFieldMetadata({
|
||||
const {
|
||||
data: {
|
||||
createOneField: { id: fieldMetadataId },
|
||||
},
|
||||
} = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'filesField',
|
||||
label: 'Files Field',
|
||||
@@ -183,6 +178,8 @@ describe('fileFieldSync - FILES field <> files sync', () => {
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
createdFieldMetadataId = fieldMetadataId;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+82
-14
@@ -1,18 +1,12 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { makeGraphqlAPIRequestWithFileUpload } from 'test/integration/graphql/utils/make-graphql-api-request-with-file-upload.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
const uploadFilesFieldFileMutation = gql`
|
||||
mutation uploadFilesFieldFile($file: Upload!) {
|
||||
uploadFilesFieldFile(file: $file) {
|
||||
id
|
||||
path
|
||||
size
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
import { uploadFilesFieldFileMutation } from 'test/integration/graphql/utils/upload-files-field-file-mutation.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { FieldMetadataType, FileFolder } from 'twenty-shared/types';
|
||||
|
||||
const deleteFileMutation = gql`
|
||||
mutation DeleteFile($fileId: UUID!) {
|
||||
@@ -23,10 +17,61 @@ const deleteFileMutation = gql`
|
||||
`;
|
||||
|
||||
describe('uploadFilesFieldFile', () => {
|
||||
let createdObjectMetadataId: string;
|
||||
let createdFieldMetadataId: string;
|
||||
let uploadedFileId: string | null = null;
|
||||
|
||||
beforeAll(() => {
|
||||
const getFieldMetadataUniversalIdentifier = async (
|
||||
fieldMetadataId: string,
|
||||
): Promise<string> => {
|
||||
const result = await global.testDataSource.query(
|
||||
'SELECT "universalIdentifier" FROM core."fieldMetadata" WHERE id = $1',
|
||||
[fieldMetadataId],
|
||||
);
|
||||
|
||||
return result[0].universalIdentifier;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'uploadTestObject',
|
||||
namePlural: 'uploadTestObjects',
|
||||
labelSingular: 'Upload Test Object',
|
||||
labelPlural: 'Upload Test Objects',
|
||||
icon: 'IconFile',
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = objectMetadataId;
|
||||
|
||||
const {
|
||||
data: {
|
||||
createOneField: { id: fieldMetadataId },
|
||||
},
|
||||
} = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'filesField',
|
||||
label: 'Files Field',
|
||||
type: FieldMetadataType.FILES,
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
settings: { maxNumberOfValues: 5 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
createdFieldMetadataId = fieldMetadataId;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -36,6 +81,20 @@ describe('uploadFilesFieldFile', () => {
|
||||
variables: { fileId: uploadedFileId },
|
||||
});
|
||||
}
|
||||
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
@@ -47,7 +106,10 @@ describe('uploadFilesFieldFile', () => {
|
||||
const response = await makeGraphqlAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadFilesFieldFileMutation,
|
||||
variables: { file: null },
|
||||
variables: {
|
||||
file: null,
|
||||
fieldMetadataId: createdFieldMetadataId,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
@@ -69,6 +131,12 @@ describe('uploadFilesFieldFile', () => {
|
||||
expect(fileResult.path).toBeDefined();
|
||||
expect(typeof fileResult.path).toBe('string');
|
||||
expect(fileResult.path).toContain(FileFolder.FilesField);
|
||||
|
||||
const fieldUniversalIdentifier = await getFieldMetadataUniversalIdentifier(
|
||||
createdFieldMetadataId,
|
||||
);
|
||||
|
||||
expect(fileResult.path).toContain(fieldUniversalIdentifier);
|
||||
expect(fileResult.size).toBe(testFileContent.length);
|
||||
expect(fileResult.createdAt).toBeDefined();
|
||||
|
||||
|
||||
+15
-15
@@ -1,11 +1,11 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { getFieldMetadataCreationInputs } from 'test/integration/graphql/suites/inputs-validation/utils/get-field-metadata-creation-inputs.util';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithFileUpload } from 'test/integration/graphql/utils/make-graphql-api-request-with-file-upload.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { uploadFilesFieldFileMutation } from 'test/integration/graphql/utils/upload-files-field-file-mutation.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -28,17 +28,6 @@ export const TEST_UUID_FIELD_VALUE = '20202020-b21e-4ec2-873b-de4264d89025';
|
||||
export const TEST_TARGET_OBJECT_RECORD_ID_FIELD_VALUE =
|
||||
'20202020-b21e-4ec2-873b-de4264d89021';
|
||||
|
||||
const uploadFilesFieldFileMutation = gql`
|
||||
mutation UploadFilesFieldFile($file: Upload!) {
|
||||
uploadFilesFieldFile(file: $file) {
|
||||
id
|
||||
path
|
||||
size
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const joinColumnNameForManyToOneMorphRelationField1 =
|
||||
computeMorphRelationFieldName({
|
||||
fieldName: 'manyToOneMorphRelationField',
|
||||
@@ -91,10 +80,17 @@ export const setupTestObjectsWithAllFieldTypes = async (
|
||||
targetObjectMetadata2Id,
|
||||
);
|
||||
|
||||
let filesFieldMetadataId: string | undefined;
|
||||
|
||||
for (const input of fieldMetadataCreationInputs) {
|
||||
await createOneFieldMetadata({
|
||||
const result = await createOneFieldMetadata({
|
||||
input,
|
||||
gqlFields: 'id name type',
|
||||
});
|
||||
|
||||
if (input.type === FieldMetadataType.FILES) {
|
||||
filesFieldMetadataId = result.data.createOneField.id;
|
||||
}
|
||||
}
|
||||
|
||||
await makeGraphqlAPIRequest(
|
||||
@@ -115,6 +111,10 @@ export const setupTestObjectsWithAllFieldTypes = async (
|
||||
if (withFilesField) {
|
||||
jest.useRealTimers();
|
||||
|
||||
if (!filesFieldMetadataId) {
|
||||
throw new Error('FILES field metadata was not created');
|
||||
}
|
||||
|
||||
const testFileContent = 'Test document content';
|
||||
const testFileName = 'Document.pdf';
|
||||
const testMimeType = 'application/pdf';
|
||||
@@ -122,7 +122,7 @@ export const setupTestObjectsWithAllFieldTypes = async (
|
||||
const uploadResponse = await makeGraphqlAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadFilesFieldFileMutation,
|
||||
variables: { file: null },
|
||||
variables: { file: null, fieldMetadataId: filesFieldMetadataId },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const uploadFilesFieldFileMutation = gql`
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
path
|
||||
size
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -19,7 +19,10 @@ export const STANDARD_OBJECTS = {
|
||||
universalIdentifier: '20202020-a01d-4004-9d04-4f8fb16eae5d',
|
||||
},
|
||||
name: { universalIdentifier: '20202020-87a5-48f8-bbf7-ade388825a57' },
|
||||
file: { universalIdentifier: '20202020-15db-460e-8166-c7b5d87ad4be' },
|
||||
//deprecated
|
||||
fullPath: { universalIdentifier: '20202020-0d19-453d-8e8d-fbcda8ca3747' },
|
||||
//deprecated
|
||||
fileCategory: {
|
||||
universalIdentifier: '20202020-8c3f-4d9e-9a1b-2e5f7a8c9d0e',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 4.5C5.5 3.94772 5.94772 3.5 6.5 3.5H13.5L18.5 8.5V19.5C18.5 20.0523 18.0523 20.5 17.5 20.5H6.5C5.94772 20.5 5.5 20.0523 5.5 19.5V4.5Z" fill="currentFill"/>
|
||||
<path d="M13.5 3.5V8.5H18.5M5.5 4.5C5.5 3.94772 5.94772 3.5 6.5 3.5H13.5L18.5 8.5V19.5C18.5 20.0523 18.0523 20.5 17.5 20.5H6.5C5.94772 20.5 5.5 20.0523 5.5 19.5V4.5Z" stroke="currentColor" stroke-width="1.49625" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.2892 3.45947L13.5508 7.20033C13.5681 7.44837 13.6833 7.67935 13.8709 7.84247C14.0586 8.0056 14.3034 8.0875 14.5514 8.07015L18.2923 7.80856" fill="#F0F4FF"/>
|
||||
<path d="M17.272 20.0972L7.91983 20.7511C7.42376 20.7858 6.93422 20.622 6.55892 20.2958C6.18362 19.9695 5.95329 19.5076 5.9186 19.0115L5.00305 5.91847C4.96836 5.4224 5.13215 4.93287 5.4584 4.55756C5.78464 4.18226 6.24662 3.95193 6.74269 3.91724L13.2892 3.45947L18.2923 7.80856L19.0116 18.0959C19.0463 18.592 18.8825 19.0815 18.5563 19.4568C18.23 19.8322 17.7681 20.0625 17.272 20.0972Z" fill="#F0F4FF"/>
|
||||
<path d="M13.2892 3.45947L13.5508 7.20033C13.5681 7.44837 13.6833 7.67935 13.8709 7.84247C14.0586 8.0056 14.3034 8.0875 14.5514 8.07015L18.2923 7.80856M13.2892 3.45947L6.74269 3.91724C6.24662 3.95193 5.78464 4.18226 5.4584 4.55756C5.13215 4.93287 4.96836 5.4224 5.00305 5.91847L5.9186 19.0115C5.95329 19.5076 6.18362 19.9695 6.55892 20.2958C6.93422 20.622 7.42376 20.7858 7.91983 20.7511L17.272 20.0972C17.7681 20.0625 18.23 19.8322 18.5563 19.4568C18.8825 19.0815 19.0463 18.592 19.0116 18.0959L18.2923 7.80856M13.2892 3.45947L18.2923 7.80856" stroke="#8DA4EF" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 534 B After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user