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:
+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',
|
||||
|
||||
Reference in New Issue
Block a user