File - Migrate avatarUrl > avatarFile on person (data migration + logic) + Attachment data migration (#17752)
- Migration command
- Check IS_FILES_FIELD_MIGRATED:false
- Check or create avatarFile field
- Fetch all people with avatarUrl
- Move (Copy/move) file in storage
- Create core.file record
- Update person record
- bonus : attachment migration : fullPath > file (same logic)
- BE logic
- Add avatarFile field on person
- FE logic
- Adapt logic to upload on/display avatarFile data
The whole imageIdentifier logic will be done later
This commit is contained in:
+5
-2
@@ -12,11 +12,14 @@ export const buildIdentifierGqlFields = (
|
||||
| 'imageIdentifierFieldMetadataId'
|
||||
| 'nameSingular'
|
||||
>,
|
||||
isFilesFieldMigrated?: boolean,
|
||||
): RecordGqlFields => {
|
||||
const labelIdentifierField =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadata);
|
||||
const imageIdentifierField =
|
||||
getImageIdentifierFieldMetadataItem(objectMetadata);
|
||||
const imageIdentifierField = getImageIdentifierFieldMetadataItem(
|
||||
objectMetadata,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
|
||||
return {
|
||||
id: true,
|
||||
|
||||
+7
-1
@@ -27,6 +27,7 @@ export type GenerateDepthRecordGqlFieldsFromFields = {
|
||||
>[];
|
||||
depth: 0 | 1;
|
||||
shouldOnlyLoadRelationIdentifiers?: boolean;
|
||||
isFilesFieldMigrated?: boolean;
|
||||
};
|
||||
|
||||
export const generateDepthRecordGqlFieldsFromFields = ({
|
||||
@@ -34,6 +35,7 @@ export const generateDepthRecordGqlFieldsFromFields = ({
|
||||
fields,
|
||||
depth,
|
||||
shouldOnlyLoadRelationIdentifiers = true,
|
||||
isFilesFieldMigrated,
|
||||
}: GenerateDepthRecordGqlFieldsFromFields) => {
|
||||
const generatedRecordGqlFields: RecordGqlFields = fields.reduce(
|
||||
(recordGqlFields, fieldMetadata) => {
|
||||
@@ -83,6 +85,7 @@ export const generateDepthRecordGqlFieldsFromFields = ({
|
||||
const junctionGqlFields = generateJunctionRelationGqlFields({
|
||||
fieldMetadataItem: fieldMetadata,
|
||||
objectMetadataItems,
|
||||
isFilesFieldMigrated,
|
||||
});
|
||||
|
||||
if (isDefined(junctionGqlFields) && depth === 1) {
|
||||
@@ -97,7 +100,10 @@ export const generateDepthRecordGqlFieldsFromFields = ({
|
||||
getLabelIdentifierFieldMetadataItem(targetObjectMetadataItem);
|
||||
|
||||
const imageIdentifierFieldMetadataItem =
|
||||
getImageIdentifierFieldMetadataItem(targetObjectMetadataItem);
|
||||
getImageIdentifierFieldMetadataItem(
|
||||
targetObjectMetadataItem,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
|
||||
const relationIdentifierSubGqlFields = {
|
||||
id: true,
|
||||
|
||||
+29
-6
@@ -16,11 +16,13 @@ type JunctionFieldMetadataItem = Pick<
|
||||
type GenerateJunctionRelationGqlFieldsArgs = {
|
||||
fieldMetadataItem: JunctionFieldMetadataItem;
|
||||
objectMetadataItems: JunctionObjectMetadataItem[];
|
||||
isFilesFieldMigrated?: boolean;
|
||||
};
|
||||
|
||||
const buildRegularTargetFieldGqlFields = (
|
||||
targetField: JunctionFieldMetadataItem,
|
||||
objectMetadataItems: JunctionObjectMetadataItem[],
|
||||
isFilesFieldMigrated?: boolean,
|
||||
): RecordGqlFields => {
|
||||
const targetObjectMetadata = objectMetadataItems.find(
|
||||
(item) => item.id === targetField.relation?.targetObjectMetadata.id,
|
||||
@@ -31,13 +33,17 @@ const buildRegularTargetFieldGqlFields = (
|
||||
}
|
||||
|
||||
return {
|
||||
[targetField.name]: buildIdentifierGqlFields(targetObjectMetadata),
|
||||
[targetField.name]: buildIdentifierGqlFields(
|
||||
targetObjectMetadata,
|
||||
isFilesFieldMigrated,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const buildMorphTargetFieldGqlFields = (
|
||||
targetField: JunctionFieldMetadataItem,
|
||||
objectMetadataItems: JunctionObjectMetadataItem[],
|
||||
isFilesFieldMigrated?: boolean,
|
||||
): RecordGqlFields => {
|
||||
const morphRelations = targetField.morphRelations;
|
||||
|
||||
@@ -63,7 +69,10 @@ const buildMorphTargetFieldGqlFields = (
|
||||
targetObjectMetadataNamePlural: targetObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
result[computedFieldName] = buildIdentifierGqlFields(targetObjectMetadata);
|
||||
result[computedFieldName] = buildIdentifierGqlFields(
|
||||
targetObjectMetadata,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -72,17 +81,27 @@ const buildMorphTargetFieldGqlFields = (
|
||||
const buildTargetFieldGqlFields = (
|
||||
targetField: JunctionFieldMetadataItem,
|
||||
objectMetadataItems: JunctionObjectMetadataItem[],
|
||||
isFilesFieldMigrated?: boolean,
|
||||
): RecordGqlFields => {
|
||||
if (targetField.type === FieldMetadataType.MORPH_RELATION) {
|
||||
return buildMorphTargetFieldGqlFields(targetField, objectMetadataItems);
|
||||
return buildMorphTargetFieldGqlFields(
|
||||
targetField,
|
||||
objectMetadataItems,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
}
|
||||
return buildRegularTargetFieldGqlFields(targetField, objectMetadataItems);
|
||||
return buildRegularTargetFieldGqlFields(
|
||||
targetField,
|
||||
objectMetadataItems,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
};
|
||||
|
||||
// Generates GraphQL fields for a junction relation, including the nested target objects
|
||||
export const generateJunctionRelationGqlFields = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItems,
|
||||
isFilesFieldMigrated,
|
||||
}: GenerateJunctionRelationGqlFieldsArgs): RecordGqlFields | null => {
|
||||
const junctionConfig = getJunctionConfig({
|
||||
settings: fieldMetadataItem.settings,
|
||||
@@ -100,13 +119,17 @@ export const generateJunctionRelationGqlFields = ({
|
||||
const junctionTargetFields = targetFields.reduce<RecordGqlFields>(
|
||||
(acc, targetField) => ({
|
||||
...acc,
|
||||
...buildTargetFieldGqlFields(targetField, objectMetadataItems),
|
||||
...buildTargetFieldGqlFields(
|
||||
targetField,
|
||||
objectMetadataItems,
|
||||
isFilesFieldMigrated,
|
||||
),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
...buildIdentifierGqlFields(junctionObjectMetadata),
|
||||
...buildIdentifierGqlFields(junctionObjectMetadata, isFilesFieldMigrated),
|
||||
...junctionTargetFields,
|
||||
};
|
||||
};
|
||||
|
||||
-1
@@ -56,7 +56,6 @@ export const RecordFieldList = ({
|
||||
|
||||
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
});
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
|
||||
+11
-2
@@ -9,7 +9,9 @@ import { generateDepthRecordGqlFieldsFromFields } from '@/object-record/graphql/
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
type UseRecordsFieldVisibleGqlFields = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
@@ -29,6 +31,10 @@ export const useRecordsFieldVisibleGqlFields = ({
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const allDepthOneGqlFields = generateDepthRecordGqlFieldsFromFields({
|
||||
objectMetadataItems,
|
||||
fields: visibleRecordFields.map(
|
||||
@@ -36,12 +42,15 @@ export const useRecordsFieldVisibleGqlFields = ({
|
||||
fieldMetadataItemByFieldMetadataItemId[field.fieldMetadataItemId],
|
||||
),
|
||||
depth: 1,
|
||||
isFilesFieldMigrated,
|
||||
});
|
||||
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
const imageIdentifierFieldMetadataItem =
|
||||
getImageIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
const imageIdentifierFieldMetadataItem = getImageIdentifierFieldMetadataItem(
|
||||
objectMetadataItem,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
|
||||
const hasPosition = hasObjectMetadataItemPositionField(objectMetadataItem);
|
||||
|
||||
|
||||
-1
@@ -64,7 +64,6 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
|
||||
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
});
|
||||
|
||||
const isLabelIdentifierReadOnly = useIsRecordFieldReadOnly({
|
||||
|
||||
+13
-6
@@ -1,8 +1,10 @@
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { usePersonAvatarUpload } from '@/object-record/record-show/hooks/usePersonAvatarUpload';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
@@ -11,10 +13,11 @@ import { RecordTitleCell } from '@/object-record/record-title-cell/components/Re
|
||||
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
type SummaryCardProps = {
|
||||
objectNameSingular: string;
|
||||
@@ -41,12 +44,15 @@ export const SummaryCard = ({
|
||||
const allowRequestsToTwentyIcons = useRecoilValue(
|
||||
allowRequestsToTwentyIconsState,
|
||||
);
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const { onUploadPicture, useUpdateOneObjectRecordMutation } =
|
||||
useRecordShowContainerActions({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
});
|
||||
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { onUploadPicture } = usePersonAvatarUpload(objectRecordId);
|
||||
|
||||
const isMobile = useIsMobile() || isInRightDrawer;
|
||||
|
||||
@@ -54,6 +60,7 @@ export const SummaryCard = ({
|
||||
recordStoreIdentifierFamilySelector({
|
||||
recordId: objectRecordId,
|
||||
allowRequestsToTwentyIcons,
|
||||
isFilesFieldMigrated,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FileFolder,
|
||||
useUploadFilesFieldFileMutation,
|
||||
useUploadImageMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey, FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
export const usePersonAvatarUpload = (personRecordId: string) => {
|
||||
const coreClient = useApolloCoreClient();
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const [uploadFilesFieldFile] = useUploadFilesFieldFileMutation({
|
||||
client: coreClient,
|
||||
});
|
||||
const { updateOneRecord } = useUpdateOneRecord();
|
||||
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const { objectMetadataItem: personMetadata } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
});
|
||||
|
||||
const avatarFileFieldMetadataId = personMetadata.fields.find(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.FILES && field.name === 'avatarFile',
|
||||
)?.id;
|
||||
|
||||
const onUploadPicture = async (file: File) => {
|
||||
if (isFilesFieldMigrated) {
|
||||
assertIsDefinedOrThrow(
|
||||
avatarFileFieldMetadataId,
|
||||
new Error(t`Avatar file field not found for person object`),
|
||||
);
|
||||
|
||||
const result = await uploadFilesFieldFile({
|
||||
variables: { file, fieldMetadataId: avatarFileFieldMetadataId },
|
||||
});
|
||||
|
||||
const uploadedFile = result?.data?.uploadFilesFieldFile;
|
||||
|
||||
if (!isDefined(uploadedFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
idToUpdate: personRecordId,
|
||||
updateOneRecordInput: {
|
||||
avatarFile: [
|
||||
{
|
||||
fileId: uploadedFile.id,
|
||||
label: file.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const result = await uploadImage({
|
||||
variables: {
|
||||
file,
|
||||
fileFolder: FileFolder.PersonPicture,
|
||||
},
|
||||
});
|
||||
|
||||
const avatarSignedFile = result?.data?.uploadImage;
|
||||
|
||||
if (!avatarSignedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
idToUpdate: personRecordId,
|
||||
updateOneRecordInput: {
|
||||
avatarUrl: avatarSignedFile.path,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { onUploadPicture };
|
||||
};
|
||||
-35
@@ -3,21 +3,14 @@ import {
|
||||
type RecordUpdateHook,
|
||||
type RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import {
|
||||
FileFolder,
|
||||
useUploadImageMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
interface UseRecordShowContainerActionsProps {
|
||||
objectNameSingular: string;
|
||||
objectRecordId: string;
|
||||
}
|
||||
|
||||
export const useRecordShowContainerActions = ({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
}: UseRecordShowContainerActionsProps) => {
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const { updateOneRecord } = useUpdateOneRecord();
|
||||
|
||||
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
||||
@@ -32,35 +25,7 @@ export const useRecordShowContainerActions = ({
|
||||
return [updateEntity, { loading: false }];
|
||||
};
|
||||
|
||||
const onUploadPicture = async (file: File) => {
|
||||
if (objectNameSingular !== 'person') {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await uploadImage({
|
||||
variables: {
|
||||
file,
|
||||
fileFolder: FileFolder.PersonPicture,
|
||||
},
|
||||
});
|
||||
|
||||
const avatarSignedFile = result?.data?.uploadImage;
|
||||
|
||||
if (!avatarSignedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
objectNameSingular,
|
||||
idToUpdate: objectRecordId,
|
||||
updateOneRecordInput: {
|
||||
avatarUrl: avatarSignedFile.path,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onUploadPicture,
|
||||
useUpdateOneObjectRecordMutation,
|
||||
};
|
||||
};
|
||||
|
||||
+3
@@ -11,9 +11,11 @@ export const recordStoreIdentifierFamilySelector = selectorFamily({
|
||||
({
|
||||
recordId,
|
||||
allowRequestsToTwentyIcons,
|
||||
isFilesFieldMigrated,
|
||||
}: {
|
||||
recordId: string;
|
||||
allowRequestsToTwentyIcons: boolean;
|
||||
isFilesFieldMigrated?: boolean;
|
||||
}) =>
|
||||
({ get }) => {
|
||||
const recordFromStore = get(recordStoreFamilyState(recordId));
|
||||
@@ -35,6 +37,7 @@ export const recordStoreIdentifierFamilySelector = selectorFamily({
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
record: recordFromStore,
|
||||
allowRequestsToTwentyIcons,
|
||||
isFilesFieldMigrated,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
export const getRecordChipGenerators = (
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
allowRequestsToTwentyIcons?: boolean,
|
||||
isFilesFieldMigrated?: boolean,
|
||||
) => {
|
||||
const chipGeneratorPerObjectPerField: ChipGeneratorPerObjectNameSingularPerFieldName =
|
||||
{};
|
||||
@@ -94,6 +95,7 @@ export const getRecordChipGenerators = (
|
||||
record,
|
||||
imageIdentifierFieldMetadataToUse,
|
||||
allowRequestsToTwentyIcons,
|
||||
isFilesFieldMigrated,
|
||||
),
|
||||
avatarType,
|
||||
isLabelIdentifier,
|
||||
|
||||
Reference in New Issue
Block a user