Fix author attachment field (#15065)
# Migrate Attachment Author to CreatedBy Field **Twill Task**: https://twill.ai/twentyhq/ENG/tasks/7 ## Summary This PR implements a migration to transition the `Attachment` object from using an `author` relation field to using the standard `createdBy` field, addressing issue https://github.com/twentyhq/core-team-issues/issues/1594. ## Changes - **Added migration command** (`1-8-migrate-attachment-author-to-created-by.command.ts`): - Migrates existing attachment data to use `createdBy` instead of `author` - Ensures data integrity during the transition to the standard field pattern - **Updated Attachment workspace entity**: - Added `createdBy` relation field to the `Attachment` standard object - Registered new field ID in `standard-field-ids.ts` constants - **Integrated migration into upgrade pipeline**: - Added migration module for version 1.8 - Registered in the main upgrade version command module This change aligns the `Attachment` object with Twenty's standard field conventions by using the built-in `createdBy` field instead of a custom `author` field. --- Fixes https://github.com/twentyhq/core-team-issues/issues/1594 --------- Co-authored-by: Twill <agent@twill.ai> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -31,7 +31,7 @@ npx nx storybook:build twenty-front # Build Storybook
|
||||
npx nx storybook:serve-and-test:static twenty-front # Run Storybook tests
|
||||
|
||||
|
||||
When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
@@ -68,7 +68,7 @@ npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
|
||||
# Sync metadata
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f
|
||||
npx nx run twenty-server:command workspace:sync-metadata
|
||||
```
|
||||
|
||||
### GraphQL
|
||||
@@ -149,4 +149,4 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
- `tsconfig.base.json` - Base TypeScript configuration
|
||||
- `package.json` - Root package with workspace definitions
|
||||
- `.cursor/rules/` - Development guidelines and best practices
|
||||
- `.cursor/rules/` - Development guidelines and best practices
|
||||
|
||||
@@ -4,11 +4,11 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type ChangeEvent, useRef } from 'react';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
|
||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { FileIcon } from '@/file/components/FileIcon';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { type AttachmentType } from '../../files/types/Attachment';
|
||||
import { getFileType } from '../../files/utils/getFileType';
|
||||
|
||||
const StyledFileInput = styled.input`
|
||||
display: none;
|
||||
@@ -46,8 +46,8 @@ export const FileBlock = createReactBlockSpec(
|
||||
name: {
|
||||
default: '' as string,
|
||||
},
|
||||
fileType: {
|
||||
default: 'Other' as AttachmentType,
|
||||
fileCategory: {
|
||||
default: 'OTHER' as AttachmentFileCategory,
|
||||
},
|
||||
},
|
||||
content: 'none',
|
||||
@@ -72,7 +72,7 @@ export const FileBlock = createReactBlockSpec(
|
||||
...block.props,
|
||||
...{
|
||||
url: fileUrl,
|
||||
fileType: getFileType(file.name),
|
||||
fileCategory: getFileType(file.name),
|
||||
name: file.name,
|
||||
},
|
||||
},
|
||||
@@ -89,7 +89,9 @@ export const FileBlock = createReactBlockSpec(
|
||||
if (isNonEmptyString(block.props.url)) {
|
||||
return (
|
||||
<StyledFileLine>
|
||||
<FileIcon fileType={block.props.fileType as AttachmentType} />
|
||||
<FileIcon
|
||||
fileCategory={block.props.fileCategory as AttachmentFileCategory}
|
||||
/>
|
||||
<StyledLink href={block.props.url} target="__blank">
|
||||
{block.props.name}
|
||||
</StyledLink>
|
||||
|
||||
@@ -162,7 +162,7 @@ export const AttachmentRow = ({
|
||||
>
|
||||
<ActivityRow disabled>
|
||||
<StyledLeftContent>
|
||||
<FileIcon fileType={attachment.type} />
|
||||
<FileIcon fileCategory={attachment.fileCategory} />
|
||||
{isEditing ? (
|
||||
<SettingsTextInput
|
||||
instanceId={`attachment-${attachment.id}-name`}
|
||||
|
||||
+1
-8
@@ -1,10 +1,7 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { type Attachment } from '@/activities/files/types/Attachment';
|
||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
@@ -15,7 +12,6 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUploadAttachmentFile = () => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const coreClient = useApolloCoreClient();
|
||||
const [uploadFile] = useUploadFileMutation({ client: coreClient });
|
||||
|
||||
@@ -49,13 +45,10 @@ export const useUploadAttachmentFile = () => {
|
||||
});
|
||||
|
||||
const attachmentToCreate = {
|
||||
authorId: currentWorkspaceMember?.id,
|
||||
name: file.name,
|
||||
fullPath: attachmentPath,
|
||||
type: getFileType(file.name),
|
||||
fileCategory: getFileType(file.name),
|
||||
[targetableObjectFieldIdName]: targetableObject.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as Partial<Attachment>;
|
||||
|
||||
const createdAttachment = await createOneAttachment(attachmentToCreate);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { type AttachmentFileCategory } from './AttachmentFileCategory';
|
||||
|
||||
export type { AttachmentFileCategory };
|
||||
|
||||
export type Attachment = {
|
||||
id: string;
|
||||
name: string;
|
||||
fullPath: string;
|
||||
type: AttachmentType;
|
||||
fileCategory: AttachmentFileCategory;
|
||||
companyId: string;
|
||||
personId: string;
|
||||
authorId: string;
|
||||
// Deprecated: Use createdBy instead
|
||||
authorId?: string;
|
||||
createdBy?: {
|
||||
source: string;
|
||||
workspaceMemberId: string | null;
|
||||
name: string;
|
||||
};
|
||||
createdAt: string;
|
||||
__typename: string;
|
||||
};
|
||||
|
||||
export type AttachmentType =
|
||||
| 'Archive'
|
||||
| 'Audio'
|
||||
| 'Image'
|
||||
| 'Presentation'
|
||||
| 'Spreadsheet'
|
||||
| 'TextDocument'
|
||||
| 'Video'
|
||||
| 'Other';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export type AttachmentFileCategory =
|
||||
| 'ARCHIVE'
|
||||
| 'AUDIO'
|
||||
| 'IMAGE'
|
||||
| 'PRESENTATION'
|
||||
| 'SPREADSHEET'
|
||||
| 'TEXT_DOCUMENT'
|
||||
| 'VIDEO'
|
||||
| 'OTHER';
|
||||
+8
-8
@@ -1,13 +1,13 @@
|
||||
import { getFileType } from '../getFileType';
|
||||
|
||||
describe('getFileType', () => {
|
||||
it('should return the correct file type for a given file name', () => {
|
||||
expect(getFileType('test.doc')).toBe('TextDocument');
|
||||
expect(getFileType('test.xls')).toBe('Spreadsheet');
|
||||
expect(getFileType('test.ppt')).toBe('Presentation');
|
||||
expect(getFileType('test.png')).toBe('Image');
|
||||
expect(getFileType('test.mp4')).toBe('Video');
|
||||
expect(getFileType('test.mp3')).toBe('Audio');
|
||||
expect(getFileType('test.zip')).toBe('Archive');
|
||||
it('should return the correct file category for a given file name', () => {
|
||||
expect(getFileType('test.doc')).toBe('TEXT_DOCUMENT');
|
||||
expect(getFileType('test.xls')).toBe('SPREADSHEET');
|
||||
expect(getFileType('test.ppt')).toBe('PRESENTATION');
|
||||
expect(getFileType('test.png')).toBe('IMAGE');
|
||||
expect(getFileType('test.mp4')).toBe('VIDEO');
|
||||
expect(getFileType('test.mp3')).toBe('AUDIO');
|
||||
expect(getFileType('test.zip')).toBe('ARCHIVE');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,67 +1,69 @@
|
||||
import { type AttachmentType } from '@/activities/files/types/Attachment';
|
||||
import { type AttachmentFileCategory } from '@/activities/files/types/Attachment';
|
||||
|
||||
const FileExtensionMapping: { [key: string]: AttachmentType } = {
|
||||
doc: 'TextDocument',
|
||||
docm: 'TextDocument',
|
||||
docx: 'TextDocument',
|
||||
dot: 'TextDocument',
|
||||
dotx: 'TextDocument',
|
||||
odt: 'TextDocument',
|
||||
pdf: 'TextDocument',
|
||||
txt: 'TextDocument',
|
||||
rtf: 'TextDocument',
|
||||
ps: 'TextDocument',
|
||||
tex: 'TextDocument',
|
||||
pages: 'TextDocument',
|
||||
xls: 'Spreadsheet',
|
||||
xlsb: 'Spreadsheet',
|
||||
xlsm: 'Spreadsheet',
|
||||
xlsx: 'Spreadsheet',
|
||||
xltx: 'Spreadsheet',
|
||||
csv: 'Spreadsheet',
|
||||
tsv: 'Spreadsheet',
|
||||
ods: 'Spreadsheet',
|
||||
numbers: 'Spreadsheet',
|
||||
ppt: 'Presentation',
|
||||
pptx: 'Presentation',
|
||||
potx: 'Presentation',
|
||||
odp: 'Presentation',
|
||||
html: 'Presentation',
|
||||
key: 'Presentation',
|
||||
kth: 'Presentation',
|
||||
png: 'Image',
|
||||
jpg: 'Image',
|
||||
jpeg: 'Image',
|
||||
svg: 'Image',
|
||||
gif: 'Image',
|
||||
webp: 'Image',
|
||||
heif: 'Image',
|
||||
tif: 'Image',
|
||||
tiff: 'Image',
|
||||
bmp: 'Image',
|
||||
ico: 'Image',
|
||||
mp4: 'Video',
|
||||
avi: 'Video',
|
||||
mov: 'Video',
|
||||
wmv: 'Video',
|
||||
mpg: 'Video',
|
||||
mpeg: 'Video',
|
||||
mp3: 'Audio',
|
||||
wav: 'Audio',
|
||||
ogg: 'Audio',
|
||||
wma: 'Audio',
|
||||
zip: 'Archive',
|
||||
tar: 'Archive',
|
||||
iso: 'Archive',
|
||||
gz: 'Archive',
|
||||
rar: 'Archive',
|
||||
'7z': 'Archive',
|
||||
const FileExtensionMapping: {
|
||||
[key: string]: AttachmentFileCategory;
|
||||
} = {
|
||||
doc: 'TEXT_DOCUMENT',
|
||||
docm: 'TEXT_DOCUMENT',
|
||||
docx: 'TEXT_DOCUMENT',
|
||||
dot: 'TEXT_DOCUMENT',
|
||||
dotx: 'TEXT_DOCUMENT',
|
||||
odt: 'TEXT_DOCUMENT',
|
||||
pdf: 'TEXT_DOCUMENT',
|
||||
txt: 'TEXT_DOCUMENT',
|
||||
rtf: 'TEXT_DOCUMENT',
|
||||
ps: 'TEXT_DOCUMENT',
|
||||
tex: 'TEXT_DOCUMENT',
|
||||
pages: 'TEXT_DOCUMENT',
|
||||
xls: 'SPREADSHEET',
|
||||
xlsb: 'SPREADSHEET',
|
||||
xlsm: 'SPREADSHEET',
|
||||
xlsx: 'SPREADSHEET',
|
||||
xltx: 'SPREADSHEET',
|
||||
csv: 'SPREADSHEET',
|
||||
tsv: 'SPREADSHEET',
|
||||
ods: 'SPREADSHEET',
|
||||
numbers: 'SPREADSHEET',
|
||||
ppt: 'PRESENTATION',
|
||||
pptx: 'PRESENTATION',
|
||||
potx: 'PRESENTATION',
|
||||
odp: 'PRESENTATION',
|
||||
html: 'PRESENTATION',
|
||||
key: 'PRESENTATION',
|
||||
kth: 'PRESENTATION',
|
||||
png: 'IMAGE',
|
||||
jpg: 'IMAGE',
|
||||
jpeg: 'IMAGE',
|
||||
svg: 'IMAGE',
|
||||
gif: 'IMAGE',
|
||||
webp: 'IMAGE',
|
||||
heif: 'IMAGE',
|
||||
tif: 'IMAGE',
|
||||
tiff: 'IMAGE',
|
||||
bmp: 'IMAGE',
|
||||
ico: 'IMAGE',
|
||||
mp4: 'VIDEO',
|
||||
avi: 'VIDEO',
|
||||
mov: 'VIDEO',
|
||||
wmv: 'VIDEO',
|
||||
mpg: 'VIDEO',
|
||||
mpeg: 'VIDEO',
|
||||
mp3: 'AUDIO',
|
||||
wav: 'AUDIO',
|
||||
ogg: 'AUDIO',
|
||||
wma: 'AUDIO',
|
||||
zip: 'ARCHIVE',
|
||||
tar: 'ARCHIVE',
|
||||
iso: 'ARCHIVE',
|
||||
gz: 'ARCHIVE',
|
||||
rar: 'ARCHIVE',
|
||||
'7z': 'ARCHIVE',
|
||||
};
|
||||
|
||||
export const getFileType = (fileName: string): AttachmentType => {
|
||||
export const getFileType = (fileName: string): AttachmentFileCategory => {
|
||||
const fileExtension = fileName.split('.').at(-1);
|
||||
if (!fileExtension) {
|
||||
return 'Other';
|
||||
return 'OTHER';
|
||||
}
|
||||
return FileExtensionMapping[fileExtension.toLowerCase()] ?? 'Other';
|
||||
return FileExtensionMapping[fileExtension.toLowerCase()] ?? 'OTHER';
|
||||
};
|
||||
|
||||
+6
@@ -34,7 +34,13 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
|
||||
name: true,
|
||||
__typename: true,
|
||||
},
|
||||
// Deprecated: Use createdBy instead
|
||||
authorId: true,
|
||||
createdBy: {
|
||||
source: true,
|
||||
workspaceMemberId: true,
|
||||
name: true,
|
||||
},
|
||||
assigneeId: true,
|
||||
assignee: {
|
||||
id: true,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
|
||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { IconMapping, useFileTypeColors } from '@/file/utils/fileIconMappings';
|
||||
import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
|
||||
import { IconMapping } from '@/file/utils/fileIconMappings';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { type FileUIPart } from 'ai';
|
||||
import { AvatarChip, Chip, ChipVariant } from 'twenty-ui/components';
|
||||
import { IconX } from 'twenty-ui/display';
|
||||
import { type IconComponent, IconX } from 'twenty-ui/display';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
|
||||
export const AgentChatFilePreview = ({
|
||||
@@ -16,11 +18,17 @@ export const AgentChatFilePreview = ({
|
||||
isUploading?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const iconColors = useFileTypeColors();
|
||||
const iconColors: Record<AttachmentFileCategory, string> =
|
||||
useFileCategoryColors();
|
||||
|
||||
const fileName =
|
||||
file instanceof File ? file.name : (file.filename ?? 'Unknown file');
|
||||
|
||||
const fileCategory: AttachmentFileCategory = getFileType(fileName);
|
||||
|
||||
const FileCategoryIcon: IconComponent = IconMapping[fileCategory];
|
||||
const iconBackgroundColor: string = iconColors[fileCategory];
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={fileName}
|
||||
@@ -30,8 +38,8 @@ export const AgentChatFilePreview = ({
|
||||
<Loader color="yellow" />
|
||||
) : (
|
||||
<AvatarChip
|
||||
Icon={IconMapping[getFileType(fileName)]}
|
||||
IconBackgroundColor={iconColors[getFileType(fileName)]}
|
||||
Icon={FileCategoryIcon}
|
||||
IconBackgroundColor={iconBackgroundColor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type AttachmentType } from '@/activities/files/types/Attachment';
|
||||
import { IconMapping, useFileTypeColors } from '@/file/utils/fileIconMappings';
|
||||
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
|
||||
import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
|
||||
import { IconMapping } from '@/file/utils/fileIconMappings';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
@@ -14,14 +15,18 @@ const StyledIconContainer = styled.div<{ background: string }>`
|
||||
padding: ${({ theme }) => theme.spacing(1.25)};
|
||||
`;
|
||||
|
||||
export const FileIcon = ({ fileType }: { fileType: AttachmentType }) => {
|
||||
export const FileIcon = ({
|
||||
fileCategory,
|
||||
}: {
|
||||
fileCategory: AttachmentFileCategory;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const iconColors = useFileTypeColors();
|
||||
const iconColors = useFileCategoryColors();
|
||||
|
||||
const Icon = IconMapping[fileType];
|
||||
const Icon = IconMapping[fileCategory];
|
||||
|
||||
return (
|
||||
<StyledIconContainer background={iconColors[fileType]}>
|
||||
<StyledIconContainer background={iconColors[fileCategory]}>
|
||||
{Icon && <Icon size={theme.icon.size.sm} />}
|
||||
</StyledIconContainer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ThemeColor } from 'twenty-ui/theme';
|
||||
|
||||
export const useFileCategoryColors = (): Record<
|
||||
AttachmentFileCategory,
|
||||
string
|
||||
> => {
|
||||
const theme = useTheme();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
});
|
||||
|
||||
const fileCategoryField = objectMetadataItem.fields.find(
|
||||
(field) => field.name === 'fileCategory',
|
||||
);
|
||||
|
||||
const colorMap: Record<AttachmentFileCategory, string> = {
|
||||
ARCHIVE: theme.color.gray,
|
||||
AUDIO: theme.color.pink,
|
||||
IMAGE: theme.color.yellow,
|
||||
PRESENTATION: theme.color.orange,
|
||||
SPREADSHEET: theme.color.turquoise,
|
||||
TEXT_DOCUMENT: theme.color.blue,
|
||||
VIDEO: theme.color.purple,
|
||||
OTHER: theme.color.gray,
|
||||
};
|
||||
|
||||
if (isDefined(fileCategoryField?.options)) {
|
||||
fileCategoryField.options.forEach((option) => {
|
||||
const category = option.value as AttachmentFileCategory;
|
||||
const color = option.color as ThemeColor;
|
||||
if (
|
||||
isDefined(category) &&
|
||||
isDefined(color) &&
|
||||
isDefined(theme.color[color])
|
||||
) {
|
||||
colorMap[category] = theme.color[color];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return colorMap;
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type AttachmentType } from '@/activities/files/types/Attachment';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
|
||||
import {
|
||||
type IconComponent,
|
||||
IconFile,
|
||||
@@ -12,29 +11,15 @@ import {
|
||||
IconVideo,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
export const IconMapping: { [key in AttachmentType]: IconComponent } = {
|
||||
Archive: IconFileZip,
|
||||
Audio: IconHeadphones,
|
||||
Image: IconPhoto,
|
||||
Presentation: IconPresentation,
|
||||
Spreadsheet: IconTable,
|
||||
TextDocument: IconFileText,
|
||||
Video: IconVideo,
|
||||
Other: IconFile,
|
||||
};
|
||||
|
||||
const getIconColors = (theme: any): { [key in AttachmentType]: string } => ({
|
||||
Archive: theme.color.gray,
|
||||
Audio: theme.color.pink,
|
||||
Image: theme.color.yellow,
|
||||
Presentation: theme.color.orange,
|
||||
Spreadsheet: theme.color.turquoise,
|
||||
TextDocument: theme.color.blue,
|
||||
Video: theme.color.purple,
|
||||
Other: theme.color.gray,
|
||||
});
|
||||
|
||||
export const useFileTypeColors = () => {
|
||||
const theme = useTheme();
|
||||
return getIconColors(theme);
|
||||
export const IconMapping: {
|
||||
[key in AttachmentFileCategory]: IconComponent;
|
||||
} = {
|
||||
ARCHIVE: IconFileZip,
|
||||
AUDIO: IconHeadphones,
|
||||
IMAGE: IconPhoto,
|
||||
PRESENTATION: IconPresentation,
|
||||
SPREADSHEET: IconTable,
|
||||
TEXT_DOCUMENT: IconFileText,
|
||||
VIDEO: IconVideo,
|
||||
OTHER: IconFile,
|
||||
};
|
||||
|
||||
+5
@@ -160,6 +160,11 @@ describe('UpgradeCommandRunner', () => {
|
||||
});
|
||||
|
||||
upgradeCommandRunner = module.get(commandRunner);
|
||||
|
||||
jest.spyOn(upgradeCommandRunner['logger'], 'log').mockImplementation();
|
||||
jest.spyOn(upgradeCommandRunner['logger'], 'error').mockImplementation();
|
||||
jest.spyOn(upgradeCommandRunner['logger'], 'warn').mockImplementation();
|
||||
|
||||
runBeforeSyncMetadataSpy = jest.spyOn(
|
||||
upgradeCommandRunner,
|
||||
'runBeforeSyncMetadata',
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-10:migrate-attachment-author-to-created-by',
|
||||
description:
|
||||
'Migrate attachment author field data to createdBy composite field',
|
||||
})
|
||||
export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Migrating attachment author to createdBy for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const attachmentRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'attachment',
|
||||
);
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const attachments = await attachmentRepository.find({
|
||||
where: {
|
||||
authorId: Not(IsNull()),
|
||||
createdBy: {
|
||||
workspaceMemberId: IsNull(),
|
||||
},
|
||||
},
|
||||
select: ['id', 'authorId'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
let migratedCount = 0;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const { id, authorId } = attachment;
|
||||
|
||||
if (!isDefined(authorId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: { id: authorId },
|
||||
});
|
||||
|
||||
if (!isDefined(workspaceMember)) {
|
||||
this.logger.warn(
|
||||
`Workspace member ${authorId} not found for attachment ${id}, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstName = workspaceMember.name?.firstName || '';
|
||||
const lastName = workspaceMember.name?.lastName || '';
|
||||
const displayName =
|
||||
firstName || lastName ? `${firstName} ${lastName}`.trim() : 'Unknown';
|
||||
|
||||
await attachmentRepository.update(
|
||||
{ id },
|
||||
{
|
||||
createdBy: {
|
||||
source: FieldActorSource.MANUAL,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
name: displayName,
|
||||
context: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
migratedCount++;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
|
||||
const TYPE_TO_FILE_CATEGORY_MAPPING: Record<string, string> = {
|
||||
Archive: 'ARCHIVE',
|
||||
Audio: 'AUDIO',
|
||||
Image: 'IMAGE',
|
||||
Presentation: 'PRESENTATION',
|
||||
Spreadsheet: 'SPREADSHEET',
|
||||
TextDocument: 'TEXT_DOCUMENT',
|
||||
Video: 'VIDEO',
|
||||
Other: 'OTHER',
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-10:migrate-attachment-type-to-file-category',
|
||||
description:
|
||||
'Migrate attachment type field data to fileCategory SELECT field',
|
||||
})
|
||||
export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Migrating attachment type to fileCategory for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const attachmentRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'attachment',
|
||||
);
|
||||
|
||||
const attachments = await attachmentRepository.find({
|
||||
select: ['id', 'type'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
let migratedCount = 0;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const { id, type } = attachment;
|
||||
|
||||
const fileCategory =
|
||||
TYPE_TO_FILE_CATEGORY_MAPPING[type] ||
|
||||
TYPE_TO_FILE_CATEGORY_MAPPING.Other;
|
||||
|
||||
await attachmentRepository.update(
|
||||
{ id },
|
||||
{
|
||||
fileCategory,
|
||||
},
|
||||
);
|
||||
|
||||
migratedCount++;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
|
||||
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace]),
|
||||
WorkspaceSchemaManagerModule,
|
||||
],
|
||||
providers: [
|
||||
MigrateAttachmentAuthorToCreatedByCommand,
|
||||
MigrateAttachmentTypeToFileCategoryCommand,
|
||||
],
|
||||
exports: [
|
||||
MigrateAttachmentAuthorToCreatedByCommand,
|
||||
MigrateAttachmentTypeToFileCategoryCommand,
|
||||
],
|
||||
})
|
||||
export class V1_10_UpgradeVersionCommandModule {}
|
||||
+3
-3
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
|
||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
||||
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command';
|
||||
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value';
|
||||
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command';
|
||||
@@ -10,12 +11,11 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
+2
@@ -10,6 +10,7 @@ import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
|
||||
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
|
||||
import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module';
|
||||
import { V1_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module';
|
||||
import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module';
|
||||
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
@@ -26,6 +27,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
V1_6_UpgradeVersionCommandModule,
|
||||
V1_7_UpgradeVersionCommandModule,
|
||||
V1_8_UpgradeVersionCommandModule,
|
||||
V1_10_UpgradeVersionCommandModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
],
|
||||
providers: [UpgradeCommand],
|
||||
|
||||
+17
-2
@@ -19,6 +19,8 @@ import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upg
|
||||
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
|
||||
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
|
||||
import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command';
|
||||
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
|
||||
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
|
||||
import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command';
|
||||
import { AddNextStepIdsToWorkflowVersionTriggers } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-next-step-ids-to-workflow-version-triggers.command';
|
||||
import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-version-command/1-2/1-2-remove-workflow-runs-without-state.command';
|
||||
@@ -30,6 +32,7 @@ import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade
|
||||
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
|
||||
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
|
||||
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
|
||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
||||
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command';
|
||||
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value';
|
||||
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command';
|
||||
@@ -37,7 +40,6 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade',
|
||||
@@ -90,11 +92,15 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillWorkflowManualTriggerAvailabilityCommand: BackfillWorkflowManualTriggerAvailabilityCommand,
|
||||
|
||||
// 1.8 Commands
|
||||
protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand,
|
||||
protected readonly migrateWorkflowStepFilterOperandValueCommand: MigrateWorkflowStepFilterOperandValueCommand,
|
||||
protected readonly deduplicateUniqueFieldsCommand: DeduplicateUniqueFieldsCommand,
|
||||
protected readonly regeneratePersonSearchVectorWithPhonesCommand: RegeneratePersonSearchVectorWithPhonesCommand,
|
||||
protected readonly migrateChannelSyncStagesCommand: MigrateChannelSyncStagesCommand,
|
||||
protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand,
|
||||
|
||||
// 1.10 Commands
|
||||
protected readonly migrateAttachmentAuthorToCreatedByCommand: MigrateAttachmentAuthorToCreatedByCommand,
|
||||
protected readonly migrateAttachmentTypeToFileCategoryCommand: MigrateAttachmentTypeToFileCategoryCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -199,6 +205,14 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_1100: VersionCommands = {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [
|
||||
this.migrateAttachmentAuthorToCreatedByCommand,
|
||||
this.migrateAttachmentTypeToFileCategoryCommand,
|
||||
],
|
||||
};
|
||||
|
||||
this.allCommands = {
|
||||
'0.53.0': commands_053,
|
||||
'0.54.0': commands_054,
|
||||
@@ -213,6 +227,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
'1.6.0': commands_160,
|
||||
'1.7.0': commands_170,
|
||||
'1.8.0': commands_180,
|
||||
'1.10.0': commands_1100,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ describe('WorkerHealthIndicator', () => {
|
||||
|
||||
const mockRedisService = {
|
||||
getClient: () => mockRedis,
|
||||
getQueueClient: () => mockRedis,
|
||||
} as unknown as RedisClientService;
|
||||
|
||||
healthIndicatorService = {
|
||||
|
||||
+172
-66
@@ -1,3 +1,4 @@
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { COMPANY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant';
|
||||
import { NOTE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/note-data-seeds.constant';
|
||||
import { OPPORTUNITY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant';
|
||||
@@ -10,10 +11,12 @@ type AttachmentDataSeed = {
|
||||
name: string;
|
||||
fullPath: string;
|
||||
type: string;
|
||||
fileCategory?: string;
|
||||
// Deprecated: Use createdBy instead
|
||||
authorId: string | null;
|
||||
// createdBySource: string;
|
||||
// createdByWorkspaceMemberId: string;
|
||||
// createdByName: string;
|
||||
createdBySource: string;
|
||||
createdByWorkspaceMemberId: string;
|
||||
createdByName: string;
|
||||
personId: string | null;
|
||||
companyId: string | null;
|
||||
noteId: string | null;
|
||||
@@ -26,10 +29,11 @@ export const ATTACHMENT_DATA_SEED_COLUMNS: (keyof AttachmentDataSeed)[] = [
|
||||
'name',
|
||||
'fullPath',
|
||||
'type',
|
||||
'authorId',
|
||||
// 'createdBySource',
|
||||
// 'createdByWorkspaceMemberId',
|
||||
// 'createdByName',
|
||||
'fileCategory',
|
||||
'authorId', // Deprecated: kept for backward compatibility during migration
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
'personId',
|
||||
'companyId',
|
||||
'noteId',
|
||||
@@ -52,69 +56,169 @@ const GENERATE_ATTACHMENT_IDS = (): Record<string, string> => {
|
||||
|
||||
export const ATTACHMENT_DATA_SEED_IDS = GENERATE_ATTACHMENT_IDS();
|
||||
|
||||
// Pool of 5 reusable file templates
|
||||
const FILE_TEMPLATES = [
|
||||
{
|
||||
name: 'Contract Agreement.pdf',
|
||||
fullPath: 'attachment/sample-contract.pdf',
|
||||
type: 'TextDocument',
|
||||
},
|
||||
{
|
||||
name: 'Budget 2024.xlsx',
|
||||
fullPath: 'attachment/budget-2024.xlsx',
|
||||
type: 'Spreadsheet',
|
||||
},
|
||||
{
|
||||
name: 'Product Presentation.pptx',
|
||||
fullPath: 'attachment/presentation.pptx',
|
||||
type: 'Presentation',
|
||||
},
|
||||
{
|
||||
name: 'Screenshot.png',
|
||||
fullPath: 'attachment/screenshot.png',
|
||||
type: 'Image',
|
||||
},
|
||||
{
|
||||
name: 'Archive.zip',
|
||||
fullPath: 'attachment/archive.zip',
|
||||
type: 'Archive',
|
||||
},
|
||||
// Pool of 5 reusable file paths for attachments
|
||||
const FILE_TEMPLATE_PATHS = [
|
||||
'attachment/sample-contract.pdf',
|
||||
'attachment/budget-2024.xlsx',
|
||||
'attachment/presentation.pptx',
|
||||
'attachment/screenshot.png',
|
||||
'attachment/archive.zip',
|
||||
];
|
||||
|
||||
// Additional name variations for more realistic variety
|
||||
const FILE_NAME_VARIATIONS = [
|
||||
// Documents
|
||||
{ name: 'Service Agreement.pdf', type: 'TextDocument', pathIndex: 0 },
|
||||
{ name: 'NDA Document.pdf', type: 'TextDocument', pathIndex: 0 },
|
||||
{ name: 'Project Proposal.pdf', type: 'TextDocument', pathIndex: 0 },
|
||||
{ name: 'Invoice Q1 2024.pdf', type: 'TextDocument', pathIndex: 0 },
|
||||
{ name: 'Meeting Notes.pdf', type: 'TextDocument', pathIndex: 0 },
|
||||
{ name: 'Report Final.pdf', type: 'TextDocument', pathIndex: 0 },
|
||||
{ name: 'Contract Signed.pdf', type: 'TextDocument', pathIndex: 0 },
|
||||
{
|
||||
name: 'Service Agreement.pdf',
|
||||
type: 'TextDocument',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'NDA Document.pdf',
|
||||
type: 'TextDocument',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Project Proposal.pdf',
|
||||
type: 'TextDocument',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Invoice Q1 2024.pdf',
|
||||
type: 'TextDocument',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Meeting Notes.pdf',
|
||||
type: 'TextDocument',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Report Final.pdf',
|
||||
type: 'TextDocument',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'Contract Signed.pdf',
|
||||
type: 'TextDocument',
|
||||
fileCategory: 'TEXT_DOCUMENT',
|
||||
pathIndex: 0,
|
||||
},
|
||||
// Spreadsheets
|
||||
{ name: 'Financial Forecast.xlsx', type: 'Spreadsheet', pathIndex: 1 },
|
||||
{ name: 'Sales Report Q4.xlsx', type: 'Spreadsheet', pathIndex: 1 },
|
||||
{ name: 'Team Roster.xlsx', type: 'Spreadsheet', pathIndex: 1 },
|
||||
{ name: 'Expense Report.xlsx', type: 'Spreadsheet', pathIndex: 1 },
|
||||
{ name: 'Inventory List.xlsx', type: 'Spreadsheet', pathIndex: 1 },
|
||||
{ name: 'Data Export.csv', type: 'Spreadsheet', pathIndex: 1 },
|
||||
{
|
||||
name: 'Financial Forecast.xlsx',
|
||||
type: 'Spreadsheet',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Sales Report Q4.xlsx',
|
||||
type: 'Spreadsheet',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Team Roster.xlsx',
|
||||
type: 'Spreadsheet',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Expense Report.xlsx',
|
||||
type: 'Spreadsheet',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Inventory List.xlsx',
|
||||
type: 'Spreadsheet',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'Data Export.csv',
|
||||
type: 'Spreadsheet',
|
||||
fileCategory: 'SPREADSHEET',
|
||||
pathIndex: 1,
|
||||
},
|
||||
// Presentations
|
||||
{ name: 'Pitch Deck.pptx', type: 'Presentation', pathIndex: 2 },
|
||||
{ name: 'Q4 Results.pptx', type: 'Presentation', pathIndex: 2 },
|
||||
{ name: 'Roadmap 2024.pptx', type: 'Presentation', pathIndex: 2 },
|
||||
{ name: 'Company Overview.pptx', type: 'Presentation', pathIndex: 2 },
|
||||
{ name: 'Training Materials.pptx', type: 'Presentation', pathIndex: 2 },
|
||||
{
|
||||
name: 'Pitch Deck.pptx',
|
||||
type: 'Presentation',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{
|
||||
name: 'Q4 Results.pptx',
|
||||
type: 'Presentation',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{
|
||||
name: 'Roadmap 2024.pptx',
|
||||
type: 'Presentation',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{
|
||||
name: 'Company Overview.pptx',
|
||||
type: 'Presentation',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
{
|
||||
name: 'Training Materials.pptx',
|
||||
type: 'Presentation',
|
||||
fileCategory: 'PRESENTATION',
|
||||
pathIndex: 2,
|
||||
},
|
||||
// Images
|
||||
{ name: 'Company Logo.png', type: 'Image', pathIndex: 3 },
|
||||
{ name: 'Product Photo.jpg', type: 'Image', pathIndex: 3 },
|
||||
{ name: 'Diagram.png', type: 'Image', pathIndex: 3 },
|
||||
{ name: 'Wireframe.png', type: 'Image', pathIndex: 3 },
|
||||
{ name: 'Mockup Design.png', type: 'Image', pathIndex: 3 },
|
||||
{ name: 'Headshot.jpg', type: 'Image', pathIndex: 3 },
|
||||
{
|
||||
name: 'Company Logo.png',
|
||||
type: 'Image',
|
||||
fileCategory: 'IMAGE',
|
||||
pathIndex: 3,
|
||||
},
|
||||
{
|
||||
name: 'Product Photo.jpg',
|
||||
type: 'Image',
|
||||
fileCategory: 'IMAGE',
|
||||
pathIndex: 3,
|
||||
},
|
||||
{ name: 'Diagram.png', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
|
||||
{ name: 'Wireframe.png', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
|
||||
{
|
||||
name: 'Mockup Design.png',
|
||||
type: 'Image',
|
||||
fileCategory: 'IMAGE',
|
||||
pathIndex: 3,
|
||||
},
|
||||
{ name: 'Headshot.jpg', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
|
||||
// Archives
|
||||
{ name: 'Project Files.zip', type: 'Archive', pathIndex: 4 },
|
||||
{ name: 'Backup Data.zip', type: 'Archive', pathIndex: 4 },
|
||||
{ name: 'Source Code.zip', type: 'Archive', pathIndex: 4 },
|
||||
{
|
||||
name: 'Project Files.zip',
|
||||
type: 'Archive',
|
||||
fileCategory: 'ARCHIVE',
|
||||
pathIndex: 4,
|
||||
},
|
||||
{
|
||||
name: 'Backup Data.zip',
|
||||
type: 'Archive',
|
||||
fileCategory: 'ARCHIVE',
|
||||
pathIndex: 4,
|
||||
},
|
||||
{
|
||||
name: 'Source Code.zip',
|
||||
type: 'Archive',
|
||||
fileCategory: 'ARCHIVE',
|
||||
pathIndex: 4,
|
||||
},
|
||||
];
|
||||
|
||||
const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
|
||||
@@ -133,7 +237,7 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
|
||||
// Cycle through file name variations
|
||||
const NAME_VARIATION_INDEX = INDEX % FILE_NAME_VARIATIONS.length;
|
||||
const NAME_VARIATION = FILE_NAME_VARIATIONS[NAME_VARIATION_INDEX];
|
||||
const FILE_TEMPLATE = FILE_TEMPLATES[NAME_VARIATION.pathIndex];
|
||||
const FILE_PATH = FILE_TEMPLATE_PATHS[NAME_VARIATION.pathIndex];
|
||||
|
||||
// Determine which entity this attachment belongs to
|
||||
// Distribution: ~30% person, ~30% company, ~20% note, ~15% task, ~5% opportunity
|
||||
@@ -170,12 +274,14 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
|
||||
ATTACHMENT_SEEDS.push({
|
||||
id: ATTACHMENT_DATA_SEED_IDS[`ID_${INDEX}`],
|
||||
name: NAME_VARIATION.name,
|
||||
fullPath: FILE_TEMPLATE.fullPath,
|
||||
fullPath: FILE_PATH,
|
||||
type: NAME_VARIATION.type,
|
||||
fileCategory: NAME_VARIATION.fileCategory,
|
||||
// Deprecated: Use createdBy fields instead
|
||||
authorId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
// createdBySource: 'MANUAL',
|
||||
//createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
//createdByName: 'Tim A',
|
||||
createdBySource: FieldActorSource.MANUAL,
|
||||
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
createdByName: 'Tim A',
|
||||
personId,
|
||||
companyId,
|
||||
noteId,
|
||||
|
||||
+2
@@ -33,6 +33,8 @@ export const ATTACHMENT_STANDARD_FIELD_IDS = {
|
||||
name: '20202020-87a5-48f8-bbf7-ade388825a57',
|
||||
fullPath: '20202020-0d19-453d-8e8d-fbcda8ca3747',
|
||||
type: '20202020-a417-49b8-a40b-f6a7874caa0d',
|
||||
fileCategory: '20202020-8c3f-4d9e-9a1b-2e5f7a8c9d0e',
|
||||
createdBy: '395be3bd-a5c9-463d-aafe-9bc3bbec3f15',
|
||||
author: '20202020-6501-4ac5-a4ef-b2f8522ef6cd',
|
||||
activity: '20202020-b569-481b-a13f-9b94e47e54fe',
|
||||
task: '20202020-51e5-4621-9cf8-215487951c4b',
|
||||
|
||||
+77
-3
@@ -6,12 +6,14 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||
import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator';
|
||||
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
@@ -57,20 +59,92 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
fullPath: string;
|
||||
|
||||
// Deprecated: Use fileCategory instead
|
||||
@WorkspaceField({
|
||||
standardId: ATTACHMENT_STANDARD_FIELD_IDS.type,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Type`,
|
||||
description: msg`Attachment type`,
|
||||
label: msg`Type (deprecated)`,
|
||||
description: msg`Attachment type (deprecated - use fileCategory)`,
|
||||
icon: 'IconList',
|
||||
})
|
||||
type: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: ATTACHMENT_STANDARD_FIELD_IDS.fileCategory,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: msg`File category`,
|
||||
description: msg`Attachment file category`,
|
||||
icon: 'IconList',
|
||||
options: [
|
||||
{
|
||||
value: 'ARCHIVE',
|
||||
label: 'Archive',
|
||||
position: 0,
|
||||
color: 'gray',
|
||||
},
|
||||
{
|
||||
value: 'AUDIO',
|
||||
label: 'Audio',
|
||||
position: 1,
|
||||
color: 'pink',
|
||||
},
|
||||
{
|
||||
value: 'IMAGE',
|
||||
label: 'Image',
|
||||
position: 2,
|
||||
color: 'yellow',
|
||||
},
|
||||
{
|
||||
value: 'PRESENTATION',
|
||||
label: 'Presentation',
|
||||
position: 3,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
value: 'SPREADSHEET',
|
||||
label: 'Spreadsheet',
|
||||
position: 4,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: 'TEXT_DOCUMENT',
|
||||
label: 'Text Document',
|
||||
position: 5,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: 'VIDEO',
|
||||
label: 'Video',
|
||||
position: 6,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: 'OTHER',
|
||||
label: 'Other',
|
||||
position: 7,
|
||||
color: 'gray',
|
||||
},
|
||||
],
|
||||
defaultValue: "'OTHER'",
|
||||
})
|
||||
fileCategory: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: ATTACHMENT_STANDARD_FIELD_IDS.createdBy,
|
||||
type: FieldMetadataType.ACTOR,
|
||||
label: msg`Created by`,
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
description: msg`The creator of the record`,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
createdBy: ActorMetadata;
|
||||
|
||||
// Deprecated: Use createdBy composite field instead
|
||||
@WorkspaceRelation({
|
||||
standardId: ATTACHMENT_STANDARD_FIELD_IDS.author,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Author`,
|
||||
description: msg`Attachment author`,
|
||||
description: msg`Attachment author (deprecated - use createdBy)`,
|
||||
icon: 'IconCircleUser',
|
||||
inverseSideTarget: () => WorkspaceMemberWorkspaceEntity,
|
||||
inverseSideFieldKey: 'authoredAttachments',
|
||||
|
||||
-12
@@ -15,7 +15,6 @@ import {
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@@ -59,17 +58,6 @@ export class WorkspaceMemberDeleteOnePostQueryHook
|
||||
},
|
||||
);
|
||||
|
||||
const attachmentRepository =
|
||||
await this.twentyORMManager.getRepository<AttachmentWorkspaceEntity>(
|
||||
'attachment',
|
||||
);
|
||||
|
||||
const authorId = targettedWorkspaceMemberId;
|
||||
|
||||
await attachmentRepository.delete({
|
||||
authorId,
|
||||
});
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
'workspaceMember',
|
||||
|
||||
-1
@@ -59,7 +59,6 @@ describe('attachmentsResolver (e2e)', () => {
|
||||
expect(attachments).toHaveProperty('createdAt');
|
||||
expect(attachments).toHaveProperty('updatedAt');
|
||||
expect(attachments).toHaveProperty('deletedAt');
|
||||
expect(attachments).toHaveProperty('authorId');
|
||||
expect(attachments).toHaveProperty('taskId');
|
||||
expect(attachments).toHaveProperty('noteId');
|
||||
expect(attachments).toHaveProperty('personId');
|
||||
|
||||
Reference in New Issue
Block a user