chore(server): remove the five dead FileFolder values and their legacy serving pipeline (#22516)
Follow-up cleanup after #22510: shrink `FileFolder` and `fileFolderConfigs` to only folders that actually exist, so per-folder policy entries are real decisions. ## What **Remove the five dead enum values** — `ProfilePicture`, `WorkspaceLogo`, `Attachment`, `PersonPicture`, `File`. They were already marked replaced/removed in the enum, have no production write path, and `FileByIdGuard`'s `SUPPORTED_FILE_FOLDERS` allowlist already rejects them at the serving endpoint. **Delete the legacy path-based serving pipeline that existed only for them** — verified wired to no route: - `FilePathGuard` — registered as a provider in `FileModule` but applied to no controller - `extractFileInfoFromRequest` (parsed the old `/files/profile-picture/original/TOKEN/file.jpg` format) — only consumer was `FilePathGuard` - `checkFileFolder` — only consumer was `extractFileInfoFromRequest` - `settings.storage.imageCropSizes` — keyed exclusively by the three dead picture folders, zero consumers - the crop-size helpers in `utils/image.ts` (`getCropSize`, `ShortCropSize`, `CropSize`) — zero consumers outside the file; `getImageBufferFromUrl` is kept - `AllowedFolders` type — last consumer was `checkFileFolder` **Test fixtures** referencing dead folders were moved to living ones; the specs of deleted utils are deleted with them. **Generated files** (`twenty-front/src/generated-metadata/graphql.ts`, `twenty-client-sdk` schema) hand-updated to match the shrunk GraphQL enum. ## Legacy data safety Workspaces may still hold `File` rows whose `path` starts with a dead prefix (e.g. `attachment/…`). These stay inert, exactly as today: - Serving: `FileByIdGuard` rejects non-supported folders before any config lookup, and file lookups filter by `path LIKE '<current-folder>/%'`, so dead-prefix rows are unreachable. - Every consumer that feeds stored paths into `removeFileFolderFromFileEntityPath` (which throws on unknown prefixes) is upstream-guarded by a current-folder filter or allowlist — audited all seven call sites. - Stored legacy member `avatarUrl` strings are parsed with `extractFileIdFromUrl(url, FileFolder.CorePicture)` and already fall back to `''` for old formats; unchanged. ## GraphQL note `FileFolder` is exposed as a GraphQL enum (input of the dev-only `uploadApplicationFile` mutation, which only accepts application-code folders). Clients sending a removed value were already rejected at the resolver allowlist; they now fail GraphQL enum validation instead. No supported client sends them — the frontend only uses `CorePicture`. Net: **+10 / −301** across 17 files. https://claude.ai/code/session_01AKwhTxYFDhWhCZ4b7sf35W --- _Generated by [Claude Code](https://claude.ai/code/session_01AKwhTxYFDhWhCZ4b7sf35W)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22516?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -4633,12 +4633,7 @@ input CreateAppTokenInput {
|
||||
}
|
||||
|
||||
enum FileFolder {
|
||||
ProfilePicture
|
||||
WorkspaceLogo
|
||||
Attachment
|
||||
PersonPicture
|
||||
CorePicture
|
||||
File
|
||||
AgentChat
|
||||
BuiltLogicFunction
|
||||
BuiltFrontComponent
|
||||
|
||||
@@ -2997,7 +2997,7 @@ export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
|
||||
|
||||
export type AnalyticsType = 'PAGEVIEW' | 'TRACK'
|
||||
|
||||
export type FileFolder = 'ProfilePicture' | 'WorkspaceLogo' | 'Attachment' | 'PersonPicture' | 'CorePicture' | 'File' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient' | 'Dpa'
|
||||
export type FileFolder = 'CorePicture' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient' | 'Dpa'
|
||||
|
||||
export interface Subscription {
|
||||
onEventSubscription?: EventSubscription
|
||||
@@ -9306,12 +9306,7 @@ export const enumAnalyticsType = {
|
||||
}
|
||||
|
||||
export const enumFileFolder = {
|
||||
ProfilePicture: 'ProfilePicture' as const,
|
||||
WorkspaceLogo: 'WorkspaceLogo' as const,
|
||||
Attachment: 'Attachment' as const,
|
||||
PersonPicture: 'PersonPicture' as const,
|
||||
CorePicture: 'CorePicture' as const,
|
||||
File: 'File' as const,
|
||||
AgentChat: 'AgentChat' as const,
|
||||
BuiltLogicFunction: 'BuiltLogicFunction' as const,
|
||||
BuiltFrontComponent: 'BuiltFrontComponent' as const,
|
||||
|
||||
@@ -1911,22 +1911,17 @@ export type FileAttachmentInput = {
|
||||
export enum FileFolder {
|
||||
AgentChat = 'AgentChat',
|
||||
AppTarball = 'AppTarball',
|
||||
Attachment = 'Attachment',
|
||||
BuiltFrontComponent = 'BuiltFrontComponent',
|
||||
BuiltLogicFunction = 'BuiltLogicFunction',
|
||||
CorePicture = 'CorePicture',
|
||||
Dependencies = 'Dependencies',
|
||||
Dpa = 'Dpa',
|
||||
EmailAttachment = 'EmailAttachment',
|
||||
File = 'File',
|
||||
FilesField = 'FilesField',
|
||||
GeneratedSdkClient = 'GeneratedSdkClient',
|
||||
PersonPicture = 'PersonPicture',
|
||||
ProfilePicture = 'ProfilePicture',
|
||||
PublicAsset = 'PublicAsset',
|
||||
Source = 'Source',
|
||||
Workflow = 'Workflow',
|
||||
WorkspaceLogo = 'WorkspaceLogo'
|
||||
Workflow = 'Workflow'
|
||||
}
|
||||
|
||||
export type FileWithSignedUrl = {
|
||||
|
||||
@@ -2,11 +2,6 @@ import { type Settings } from './interfaces/settings.interface';
|
||||
|
||||
export const settings: Settings = {
|
||||
storage: {
|
||||
imageCropSizes: {
|
||||
'profile-picture': ['original'],
|
||||
'workspace-logo': ['original'],
|
||||
'person-picture': ['original'],
|
||||
},
|
||||
maxFileSize: '10MB',
|
||||
},
|
||||
minLengthOfStringForDuplicateCheck: 3,
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { type ShortCropSize } from 'src/utils/image';
|
||||
|
||||
type ValueOfFileFolder = `${FileFolder}`;
|
||||
|
||||
export interface Settings {
|
||||
storage: {
|
||||
imageCropSizes: {
|
||||
[key in ValueOfFileFolder]?: ShortCropSize[];
|
||||
};
|
||||
maxFileSize: `${number}MB`;
|
||||
};
|
||||
minLengthOfStringForDuplicateCheck: number;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileAiChatModule } from 'src/engine/core-modules/file/file-ai-chat/file-ai-chat.module';
|
||||
import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard';
|
||||
import { FileDeletionJob } from 'src/engine/core-modules/file/jobs/file-deletion.job';
|
||||
import { FileWorkspaceFolderDeletionJob } from 'src/engine/core-modules/file/jobs/file-workspace-folder-deletion.job';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
@@ -38,7 +37,6 @@ import { FileService } from './services/file.service';
|
||||
],
|
||||
providers: [
|
||||
FileService,
|
||||
FilePathGuard,
|
||||
FileByIdGuard,
|
||||
FileWorkspaceFolderDeletionJob,
|
||||
FileDeletionJob,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
type CanActivate,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { FileTokenJwtPayloadLegacy } from 'src/engine/core-modules/auth/types/file-token-jwt-payload-legacy.type';
|
||||
import { extractFileInfoFromRequest } from 'src/engine/core-modules/file/utils/extract-file-info-from-request.utils';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
|
||||
@Injectable()
|
||||
export class FilePathGuard implements CanActivate {
|
||||
constructor(private readonly jwtWrapperService: JwtWrapperService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const { filename, fileSignature, ignoreExpirationToken } =
|
||||
extractFileInfoFromRequest(request);
|
||||
|
||||
if (!fileSignature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtWrapperService.verifyJwtToken(
|
||||
fileSignature,
|
||||
ignoreExpirationToken ? { ignoreExpiration: true } : {},
|
||||
);
|
||||
|
||||
if (
|
||||
!payload.workspaceId ||
|
||||
!payload.filename ||
|
||||
filename !== payload.filename
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const decodedPayload =
|
||||
this.jwtWrapperService.decode<FileTokenJwtPayloadLegacy>(fileSignature, {
|
||||
json: true,
|
||||
});
|
||||
|
||||
request.workspaceId = decodedPayload.workspaceId;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
-23
@@ -1,7 +1,6 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { type KebabCase } from 'type-fest';
|
||||
|
||||
registerEnumType(FileFolder, {
|
||||
name: 'FileFolder',
|
||||
@@ -15,30 +14,10 @@ export type FileFolderConfig = {
|
||||
export const IMMUTABLE_FILE_CACHE_CONTROL = 'private, max-age=86400, immutable';
|
||||
|
||||
export const fileFolderConfigs: Record<FileFolder, FileFolderConfig> = {
|
||||
[FileFolder.ProfilePicture]: {
|
||||
ignoreExpirationToken: true,
|
||||
immutable: false,
|
||||
},
|
||||
[FileFolder.WorkspaceLogo]: {
|
||||
ignoreExpirationToken: true,
|
||||
immutable: false,
|
||||
},
|
||||
[FileFolder.Attachment]: {
|
||||
ignoreExpirationToken: false,
|
||||
immutable: false,
|
||||
},
|
||||
[FileFolder.PersonPicture]: {
|
||||
ignoreExpirationToken: false,
|
||||
immutable: false,
|
||||
},
|
||||
[FileFolder.CorePicture]: {
|
||||
ignoreExpirationToken: true,
|
||||
immutable: true,
|
||||
},
|
||||
[FileFolder.File]: {
|
||||
ignoreExpirationToken: false,
|
||||
immutable: false,
|
||||
},
|
||||
[FileFolder.AgentChat]: {
|
||||
ignoreExpirationToken: false,
|
||||
immutable: true,
|
||||
@@ -88,5 +67,3 @@ export const fileFolderConfigs: Record<FileFolder, FileFolderConfig> = {
|
||||
immutable: true,
|
||||
},
|
||||
};
|
||||
|
||||
export type AllowedFolders = KebabCase<keyof typeof FileFolder>;
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { checkFileFolder } from 'src/engine/core-modules/file/utils/check-file-folder.utils';
|
||||
|
||||
describe('checkFileFolder', () => {
|
||||
it('should return the root folder when it is allowed', () => {
|
||||
expect(checkFileFolder(`${FileFolder.Attachment}/file.txt`)).toBe(
|
||||
FileFolder.Attachment,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for disallowed folders', () => {
|
||||
expect(() => checkFileFolder('invalid-folder/file.txt')).toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should sanitize null characters in file path', () => {
|
||||
expect(() => checkFileFolder('\0invalid-folder/file.txt')).toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle edge cases like empty file path', () => {
|
||||
expect(() => checkFileFolder('')).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should handle cases where filePath has no folder', () => {
|
||||
expect(() => checkFileFolder('file.txt')).toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -4,10 +4,10 @@ import { checkFilename } from 'src/engine/core-modules/file/utils/check-file-nam
|
||||
|
||||
describe('checkFilename', () => {
|
||||
it('should return sanitized filename', () => {
|
||||
const filename = `${FileFolder.Attachment}\0.png`;
|
||||
const filename = `${FileFolder.FilesField}\0.png`;
|
||||
const sanitizedFilename = checkFilename(filename);
|
||||
|
||||
expect(sanitizedFilename).toBe(`${FileFolder.Attachment}.png`);
|
||||
expect(sanitizedFilename).toBe(`${FileFolder.FilesField}.png`);
|
||||
});
|
||||
|
||||
it('should throw an error for invalid filename', () => {
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { checkFileFolder } from 'src/engine/core-modules/file/utils/check-file-folder.utils';
|
||||
import { checkFilename } from 'src/engine/core-modules/file/utils/check-file-name.utils';
|
||||
import { extractFileInfoFromRequest } from 'src/engine/core-modules/file/utils/extract-file-info-from-request.utils';
|
||||
|
||||
jest.mock('src/engine/core-modules/file/utils/check-file-name.utils', () => ({
|
||||
checkFilename: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('src/engine/core-modules/file/utils/check-file-folder.utils', () => ({
|
||||
checkFileFolder: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/file/interfaces/file-folder.interface',
|
||||
() => ({
|
||||
fileFolderConfigs: {
|
||||
attachment: { ignoreExpirationToken: false },
|
||||
'profile-picture': { ignoreExpirationToken: true },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
describe('extractFileInfoFromRequest', () => {
|
||||
it('should extract all file info correctly from 3-segment path', () => {
|
||||
const mockRequest = {
|
||||
path: '/files/attachment/filesig123/myfile.txt',
|
||||
} as unknown as Request;
|
||||
|
||||
(checkFilename as jest.Mock).mockReturnValue('validated-file.txt');
|
||||
(checkFileFolder as jest.Mock).mockReturnValue('attachment');
|
||||
|
||||
const result = extractFileInfoFromRequest(mockRequest);
|
||||
|
||||
expect(checkFilename).toHaveBeenCalledWith('myfile.txt');
|
||||
expect(checkFileFolder).toHaveBeenCalledWith('attachment');
|
||||
|
||||
expect(result).toEqual({
|
||||
filename: 'validated-file.txt',
|
||||
fileSignature: 'filesig123',
|
||||
rawFolder: 'attachment',
|
||||
fileFolder: 'attachment',
|
||||
ignoreExpirationToken: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract all file info correctly from 4-segment path with size', () => {
|
||||
const mockRequest = {
|
||||
path: '/files/profile-picture/original/filesig456/avatar.jpg',
|
||||
} as unknown as Request;
|
||||
|
||||
(checkFilename as jest.Mock).mockReturnValue('validated-avatar.jpg');
|
||||
(checkFileFolder as jest.Mock).mockReturnValue('profile-picture');
|
||||
|
||||
const result = extractFileInfoFromRequest(mockRequest);
|
||||
|
||||
expect(checkFilename).toHaveBeenCalledWith('avatar.jpg');
|
||||
expect(checkFileFolder).toHaveBeenCalledWith('profile-picture/original');
|
||||
|
||||
expect(result).toEqual({
|
||||
filename: 'validated-avatar.jpg',
|
||||
fileSignature: 'filesig456',
|
||||
rawFolder: 'profile-picture/original',
|
||||
fileFolder: 'profile-picture',
|
||||
ignoreExpirationToken: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
+4
-6
@@ -7,14 +7,14 @@ import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file
|
||||
describe('removeFileFolderFromFileEntityPath', () => {
|
||||
it('should remove file folder prefix from path', () => {
|
||||
expect(
|
||||
removeFileFolderFromFileEntityPath(`${FileFolder.Attachment}/file.txt`),
|
||||
removeFileFolderFromFileEntityPath(`${FileFolder.FilesField}/file.txt`),
|
||||
).toBe('file.txt');
|
||||
});
|
||||
|
||||
it('should handle nested paths correctly', () => {
|
||||
expect(
|
||||
removeFileFolderFromFileEntityPath(
|
||||
`${FileFolder.Attachment}/subfolder/file.txt`,
|
||||
`${FileFolder.FilesField}/subfolder/file.txt`,
|
||||
),
|
||||
).toBe('subfolder/file.txt');
|
||||
});
|
||||
@@ -22,14 +22,12 @@ describe('removeFileFolderFromFileEntityPath', () => {
|
||||
it('should work with different valid file folders', () => {
|
||||
expect(
|
||||
removeFileFolderFromFileEntityPath(
|
||||
`${FileFolder.ProfilePicture}/avatar.png`,
|
||||
`${FileFolder.CorePicture}/avatar.png`,
|
||||
),
|
||||
).toBe('avatar.png');
|
||||
|
||||
expect(
|
||||
removeFileFolderFromFileEntityPath(
|
||||
`${FileFolder.WorkspaceLogo}/logo.svg`,
|
||||
),
|
||||
removeFileFolderFromFileEntityPath(`${FileFolder.Workflow}/logo.svg`),
|
||||
).toBe('logo.svg');
|
||||
|
||||
expect(
|
||||
|
||||
+2
-4
@@ -108,12 +108,10 @@ describe('setFileResponseHeaders', () => {
|
||||
);
|
||||
|
||||
it.each([
|
||||
FileFolder.ProfilePicture,
|
||||
FileFolder.WorkspaceLogo,
|
||||
FileFolder.PersonPicture,
|
||||
FileFolder.Attachment,
|
||||
FileFolder.PublicAsset,
|
||||
FileFolder.AppTarball,
|
||||
FileFolder.Source,
|
||||
FileFolder.BuiltFrontComponent,
|
||||
])('should not set Cache-Control for mutable folder %s', (fileFolder) => {
|
||||
const res = createMockResponse();
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { type AllowedFolders } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { kebabCase } from 'src/utils/kebab-case';
|
||||
|
||||
export const checkFileFolder = (filePath: string): FileFolder => {
|
||||
const allowedFolders = Object.values(FileFolder).map((value) =>
|
||||
kebabCase(value),
|
||||
);
|
||||
|
||||
const sanitizedFilePath = filePath.replace(/\0/g, '');
|
||||
const [rootFolder] = sanitizedFilePath.split('/');
|
||||
|
||||
if (!allowedFolders.includes(rootFolder as AllowedFolders)) {
|
||||
throw new BadRequestException(`Folder ${rootFolder} is not allowed`);
|
||||
}
|
||||
|
||||
return rootFolder as FileFolder;
|
||||
};
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { fileFolderConfigs } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { checkFileFolder } from 'src/engine/core-modules/file/utils/check-file-folder.utils';
|
||||
import { checkFilename } from 'src/engine/core-modules/file/utils/check-file-name.utils';
|
||||
|
||||
export const extractFileInfoFromRequest = (request: Request) => {
|
||||
// Ex: /files/profile-picture/original/TOKEN/file.jpg
|
||||
const pathSegments = request.path.split('/').filter((segment) => segment);
|
||||
|
||||
const segments = pathSegments.slice(1);
|
||||
|
||||
const filename = checkFilename(segments[segments.length - 1]);
|
||||
const fileSignature = segments[segments.length - 2];
|
||||
const folderSegments = segments.slice(0, segments.length - 2);
|
||||
const rawFolder = folderSegments.join('/');
|
||||
|
||||
const fileFolder = checkFileFolder(rawFolder);
|
||||
|
||||
const ignoreExpirationToken =
|
||||
fileFolderConfigs[fileFolder].ignoreExpirationToken;
|
||||
|
||||
return {
|
||||
filename,
|
||||
fileSignature,
|
||||
rawFolder,
|
||||
fileFolder,
|
||||
ignoreExpirationToken,
|
||||
};
|
||||
};
|
||||
@@ -1,28 +1,6 @@
|
||||
import { type AxiosInstance } from 'axios';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
const cropRegex = /([w|h])([0-9]+)/;
|
||||
|
||||
export type ShortCropSize = `${'w' | 'h'}${number}` | 'original';
|
||||
|
||||
export interface CropSize {
|
||||
type: 'width' | 'height';
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const getCropSize = (value: ShortCropSize): CropSize | null => {
|
||||
const match = value.match(cropRegex);
|
||||
|
||||
if (value === 'original' || match === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: match[1] === 'w' ? 'width' : 'height',
|
||||
value: +match[2],
|
||||
};
|
||||
};
|
||||
|
||||
export const getImageBufferFromUrl = async (
|
||||
url: string,
|
||||
axiosInstance: AxiosInstance,
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
export enum FileFolder {
|
||||
ProfilePicture = 'profile-picture', // replaced by core-picture
|
||||
WorkspaceLogo = 'workspace-logo', // replaced by core-picture
|
||||
Attachment = 'attachment', // replaced by files-field
|
||||
PersonPicture = 'person-picture', // replaced by files-field
|
||||
CorePicture = 'core-picture',
|
||||
File = 'file', // removed
|
||||
AgentChat = 'agent-chat',
|
||||
BuiltLogicFunction = 'built-logic-function',
|
||||
BuiltFrontComponent = 'built-front-component',
|
||||
|
||||
Reference in New Issue
Block a user