diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 2c5a2cd1e..903594dea 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -689,6 +689,14 @@ export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null } } }; +export type UploadFileMutationVariables = Exact<{ + file: Scalars['Upload']; + fileFolder?: InputMaybe; +}>; + + +export type UploadFileMutation = { __typename?: 'Mutation', uploadFile: string }; + export type UploadImageMutationVariables = Exact<{ file: Scalars['Upload']; fileFolder?: InputMaybe; @@ -1126,6 +1134,38 @@ export function useGetClientConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOp export type GetClientConfigQueryHookResult = ReturnType; export type GetClientConfigLazyQueryHookResult = ReturnType; export type GetClientConfigQueryResult = Apollo.QueryResult; +export const UploadFileDocument = gql` + mutation uploadFile($file: Upload!, $fileFolder: FileFolder) { + uploadFile(file: $file, fileFolder: $fileFolder) +} + `; +export type UploadFileMutationFn = Apollo.MutationFunction; + +/** + * __useUploadFileMutation__ + * + * To run a mutation, you first call `useUploadFileMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUploadFileMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [uploadFileMutation, { data, loading, error }] = useUploadFileMutation({ + * variables: { + * file: // value for 'file' + * fileFolder: // value for 'fileFolder' + * }, + * }); + */ +export function useUploadFileMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UploadFileDocument, options); + } +export type UploadFileMutationHookResult = ReturnType; +export type UploadFileMutationResult = Apollo.MutationResult; +export type UploadFileMutationOptions = Apollo.BaseMutationOptions; export const UploadImageDocument = gql` mutation uploadImage($file: Upload!, $fileFolder: FileFolder) { uploadImage(file: $file, fileFolder: $fileFolder) diff --git a/front/src/modules/activities/files/components/AttachmentDropdown.tsx b/front/src/modules/activities/files/components/AttachmentDropdown.tsx new file mode 100644 index 000000000..7d81562fc --- /dev/null +++ b/front/src/modules/activities/files/components/AttachmentDropdown.tsx @@ -0,0 +1,64 @@ +import { IconDotsVertical, IconDownload, IconTrash } from '@/ui/display/icon'; +import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; + +type AttachmentDropdownProps = { + onDownload: () => void; + onDelete: () => void; + scopeKey: string; +}; + +export const AttachmentDropdown = ({ + onDownload, + onDelete, + scopeKey, +}: AttachmentDropdownProps) => { + const dropdownScopeId = `${scopeKey}-settings-field-active-action-dropdown`; + + const { closeDropdown } = useDropdown({ dropdownScopeId }); + + const handleDownload = () => { + onDownload(); + closeDropdown(); + }; + + const handleDelete = () => { + onDelete(); + closeDropdown(); + }; + + return ( + + + } + dropdownComponents={ + + + + + + + } + dropdownHotkeyScope={{ + scope: dropdownScopeId, + }} + /> + + ); +}; diff --git a/front/src/modules/activities/files/components/AttachmentIcon.tsx b/front/src/modules/activities/files/components/AttachmentIcon.tsx new file mode 100644 index 000000000..fe8c153b4 --- /dev/null +++ b/front/src/modules/activities/files/components/AttachmentIcon.tsx @@ -0,0 +1,64 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { + Attachment, + AttachmentType, +} from '@/activities/files/types/Attachment'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { + IconFile, + IconFileText, + IconFileZip, + IconHeadphones, + IconPhoto, + IconPresentation, + IconTable, + IconVideo, +} from '@/ui/input/constants/icons'; + +const StyledIconContainer = styled.div<{ background: string }>` + align-items: center; + background: ${({ background }) => background}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + color: ${({ theme }) => theme.grayScale.gray0}; + display: flex; + flex-shrink: 0; + height: 20px; + justify-content: center; + width: 20px; +`; + +const IconMapping: { [key in AttachmentType]: IconComponent } = { + Archive: IconFileZip, + Audio: IconHeadphones, + Image: IconPhoto, + Presentation: IconPresentation, + Spreadsheet: IconTable, + TextDocument: IconFileText, + Video: IconVideo, + Other: IconFile, +}; + +export const AttachmentIcon = ({ attachment }: { attachment: Attachment }) => { + const theme = useTheme(); + + const IconColors: { [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, + }; + + const Icon = IconMapping[attachment.type]; + + return ( + + {Icon && } + + ); +}; diff --git a/front/src/modules/activities/files/components/AttachmentList.tsx b/front/src/modules/activities/files/components/AttachmentList.tsx new file mode 100644 index 000000000..f82827d7d --- /dev/null +++ b/front/src/modules/activities/files/components/AttachmentList.tsx @@ -0,0 +1,76 @@ +import { ReactElement } from 'react'; +import styled from '@emotion/styled'; + +import { Attachment } from '@/activities/files/types/Attachment'; + +import { AttachmentRow } from './AttachmentRow'; + +type AttachmentListProps = { + title: string; + attachments: Attachment[]; + button?: ReactElement | false; +}; + +const StyledContainer = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + flex-direction: column; + justify-content: center; + padding: 8px 24px; +`; + +const StyledTitleBar = styled.h3` + display: flex; + justify-content: space-between; + margin-bottom: ${({ theme }) => theme.spacing(4)}; + margin-top: ${({ theme }) => theme.spacing(4)}; + place-items: center; + width: 100%; +`; + +const StyledTitle = styled.h3` + color: ${({ theme }) => theme.font.color.primary}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; +`; + +const StyledCount = styled.span` + color: ${({ theme }) => theme.font.color.light}; + margin-left: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledAttachmentContainer = styled.div` + align-items: flex-start; + align-self: stretch; + background: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.md}; + disply: flex; + flex-flow: column nowrap; + justify-content: center; + width: 100%; +`; + +export const AttachmentList = ({ + title, + attachments, + button, +}: AttachmentListProps) => ( + <> + {attachments && attachments.length > 0 && ( + + + + {title} {attachments.length} + + {button} + + + {attachments.map((attachment) => ( + + ))} + + + )} + +); diff --git a/front/src/modules/activities/files/components/AttachmentRow.tsx b/front/src/modules/activities/files/components/AttachmentRow.tsx new file mode 100644 index 000000000..4bc3c04c5 --- /dev/null +++ b/front/src/modules/activities/files/components/AttachmentRow.tsx @@ -0,0 +1,104 @@ +import { useMemo } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { AttachmentDropdown } from '@/activities/files/components/AttachmentDropdown'; +import { AttachmentIcon } from '@/activities/files/components/AttachmentIcon'; +import { Attachment } from '@/activities/files/types/Attachment'; +import { downloadFile } from '@/activities/files/utils/downloadFile'; +import { useDeleteOneObjectRecord } from '@/object-record/hooks/useDeleteOneObjectRecord'; +import { IconCalendar } from '@/ui/display/icon'; +import { + FieldContext, + GenericFieldContextType, +} from '@/ui/object/field/contexts/FieldContext'; +import { formatToHumanReadableDate } from '~/utils'; + +const StyledRow = styled.div` + align-items: center; + align-self: stretch; + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + color: ${({ theme }) => theme.font.color.primary}; + display: flex; + justify-content: space-between; + + padding: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledLeftContent = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(3)}; +`; + +const StyledRightContent = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(0.5)}; +`; + +const StyledCalendarIconContainer = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.light}; + display: flex; +`; + +const StyledLink = styled.a` + align-items: center; + color: ${({ theme }) => theme.font.color.primary}; + display: flex; + text-decoration: none; + :hover { + color: ${({ theme }) => theme.font.color.secondary}; + } +`; + +export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { + const theme = useTheme(); + const fieldContext = useMemo( + () => ({ recoilScopeId: attachment?.id ?? '' }), + [attachment?.id], + ); + + const { deleteOneObject: deleteOneAttachment } = + useDeleteOneObjectRecord({ + objectNameSingular: 'attachment', + }); + + const handleDelete = () => { + deleteOneAttachment(attachment.id); + }; + + return ( + + + + + + {attachment.name} + + + + + + + {formatToHumanReadableDate(attachment.createdAt)} + { + downloadFile(attachment.fullPath, attachment.name); + }} + /> + + + + ); +}; diff --git a/front/src/modules/activities/files/components/Attachments.tsx b/front/src/modules/activities/files/components/Attachments.tsx new file mode 100644 index 000000000..9953431e5 --- /dev/null +++ b/front/src/modules/activities/files/components/Attachments.tsx @@ -0,0 +1,152 @@ +import { ChangeEvent, useRef } from 'react'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { AttachmentList } from '@/activities/files/components/AttachmentList'; +import { useAttachments } from '@/activities/files/hooks/useAttachments'; +import { Attachment } from '@/activities/files/types/Attachment'; +import { getFileType } from '@/activities/files/utils/getFileType'; +import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord'; +import { IconPlus } from '@/ui/display/icon'; +import { Button } from '@/ui/input/button/components/Button'; +import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; + +const StyledTaskGroupEmptyContainer = styled.div` + align-items: center; + align-self: stretch; + display: flex; + flex: 1 0 0; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + justify-content: center; + padding-bottom: ${({ theme }) => theme.spacing(16)}; + padding-left: ${({ theme }) => theme.spacing(4)}; + padding-right: ${({ theme }) => theme.spacing(4)}; + padding-top: ${({ theme }) => theme.spacing(3)}; +`; + +const StyledEmptyTaskGroupTitle = styled.div` + color: ${({ theme }) => theme.font.color.secondary}; + font-size: ${({ theme }) => theme.font.size.xxl}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + line-height: ${({ theme }) => theme.text.lineHeight.md}; +`; + +const StyledEmptyTaskGroupSubTitle = styled.div` + color: ${({ theme }) => theme.font.color.extraLight}; + font-size: ${({ theme }) => theme.font.size.xxl}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + line-height: ${({ theme }) => theme.text.lineHeight.md}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledAttachmentsContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + height: 100%; + overflow: auto; +`; + +const StyledFileInput = styled.input` + display: none; +`; + +export const Attachments = ({ + targetableEntity, +}: { + targetableEntity: ActivityTargetableEntity; +}) => { + const inputFileRef = useRef(null); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const { attachments } = useAttachments(targetableEntity); + + const [uploadFile] = useUploadFileMutation(); + + const { createOneObject: createOneAttachment } = + useCreateOneObjectRecord({ + objectNameSingular: 'attachment', + }); + + const handleFileChange = (e: ChangeEvent) => { + if (e.target.files) onUploadFile?.(e.target.files[0]); + }; + + const handleUploadFileClick = () => { + inputFileRef?.current?.click?.(); + }; + + const onUploadFile = async (file: File) => { + const result = await uploadFile({ + variables: { + file, + fileFolder: FileFolder.Attachment, + }, + }); + + const attachmentUrl = result?.data?.uploadFile; + + if (!attachmentUrl) { + return; + } + if (!createOneAttachment) { + return; + } + + await createOneAttachment({ + authorId: currentWorkspaceMember?.id, + name: file.name, + fullPath: attachmentUrl, + type: getFileType(file.name), + companyId: + targetableEntity.type == 'Company' ? targetableEntity.id : null, + personId: targetableEntity.type == 'Person' ? targetableEntity.id : null, + }); + }; + + if (attachments?.length === 0 && targetableEntity.type !== 'Custom') { + return ( + + + + No files yet + Upload one: + + } + /> + + ); +}; diff --git a/front/src/modules/activities/files/hooks/useAttachments.tsx b/front/src/modules/activities/files/hooks/useAttachments.tsx new file mode 100644 index 000000000..cbe417b4d --- /dev/null +++ b/front/src/modules/activities/files/hooks/useAttachments.tsx @@ -0,0 +1,20 @@ +import { Attachment } from '@/activities/files/types/Attachment'; +import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; + +import { ActivityTargetableEntity } from '../../types/ActivityTargetableEntity'; + +export const useAttachments = (entity: ActivityTargetableEntity) => { + const { objects: attachments } = useFindManyObjectRecords({ + objectNamePlural: 'attachments', + filter: { + [entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id }, + }, + orderBy: { + createdAt: 'DescNullsFirst', + }, + }); + + return { + attachments: attachments as Attachment[], + }; +}; diff --git a/front/src/modules/activities/files/types/Attachment.ts b/front/src/modules/activities/files/types/Attachment.ts new file mode 100644 index 000000000..fbe2a9cc3 --- /dev/null +++ b/front/src/modules/activities/files/types/Attachment.ts @@ -0,0 +1,20 @@ +export type Attachment = { + id: string; + name: string; + fullPath: string; + type: AttachmentType; + companyId: string; + personId: string; + activityId: string; + authorId: string; + createdAt: string; +}; +export type AttachmentType = + | 'Archive' + | 'Audio' + | 'Image' + | 'Presentation' + | 'Spreadsheet' + | 'TextDocument' + | 'Video' + | 'Other'; diff --git a/front/src/modules/activities/files/utils/downloadFile.ts b/front/src/modules/activities/files/utils/downloadFile.ts new file mode 100644 index 000000000..a8f981ca0 --- /dev/null +++ b/front/src/modules/activities/files/utils/downloadFile.ts @@ -0,0 +1,18 @@ +export const downloadFile = (fullPath: string, fileName: string) => { + fetch(process.env.REACT_APP_SERVER_BASE_URL + '/files/' + fullPath) + .then((resp) => + resp.status === 200 + ? resp.blob() + : Promise.reject('Failed downloading file'), + ) + .then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }); +}; diff --git a/front/src/modules/activities/files/utils/getFileType.ts b/front/src/modules/activities/files/utils/getFileType.ts new file mode 100644 index 000000000..a90a29efb --- /dev/null +++ b/front/src/modules/activities/files/utils/getFileType.ts @@ -0,0 +1,55 @@ +import { AttachmentType } 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', + xls: 'Spreadsheet', + xlsb: 'Spreadsheet', + xlsm: 'Spreadsheet', + xlsx: 'Spreadsheet', + xltx: 'Spreadsheet', + csv: 'Spreadsheet', + tsv: 'Spreadsheet', + ods: 'Spreadsheet', + ppt: 'Presentation', + pptx: 'Presentation', + potx: 'Presentation', + odp: 'Presentation', + html: 'Presentation', + png: 'Image', + jpg: 'Image', + jpeg: 'Image', + svg: 'Image', + gif: 'Image', + webp: 'Image', + heif: 'Image', + tif: 'Image', + tiff: 'Image', + bmp: 'Image', + mp4: 'Video', + avi: 'Video', + mov: 'Video', + wmv: 'Video', + mpg: 'Video', + mpeg: 'Video', + mp3: 'Audio', + zip: 'Archive', + tar: 'Archive', + iso: 'Archive', + gz: 'Archive', +}; + +export const getFileType = (fileName: string): AttachmentType => { + const fileExtension = fileName.split('.').at(-1); + if (!fileExtension) { + return 'Other'; + } + return FileExtensionMapping[fileExtension.toLowerCase()] ?? 'Other'; +}; diff --git a/front/src/modules/files/graphql/queries/uploadFile.ts b/front/src/modules/files/graphql/queries/uploadFile.ts new file mode 100644 index 000000000..d07fc1c5e --- /dev/null +++ b/front/src/modules/files/graphql/queries/uploadFile.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const UPLOAD_FILE = gql` + mutation uploadFile($file: Upload!, $fileFolder: FileFolder) { + uploadFile(file: $file, fileFolder: $fileFolder) + } +`; diff --git a/front/src/modules/ui/display/icon/index.ts b/front/src/modules/ui/display/icon/index.ts index dd2252357..6c70f06c1 100644 --- a/front/src/modules/ui/display/icon/index.ts +++ b/front/src/modules/ui/display/icon/index.ts @@ -36,6 +36,7 @@ export { IconCurrencyDollar, IconDatabase, IconDotsVertical, + IconDownload, IconEye, IconEyeOff, IconFileCheck, @@ -64,6 +65,7 @@ export { IconMouse2, IconNotes, IconNumbers, + IconPaperclip, IconPencil, IconPhone, IconPlug, diff --git a/front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx index 129f4162f..650e563af 100644 --- a/front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx +++ b/front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; +import { Attachments } from '@/activities/files/components/Attachments'; import { Notes } from '@/activities/notes/components/Notes'; import { EntityTasks } from '@/activities/tasks/components/EntityTasks'; import { Timeline } from '@/activities/timeline/components/Timeline'; @@ -8,6 +9,7 @@ import { IconCheckbox, IconMail, IconNotes, + IconPaperclip, IconTimelineEvent, } from '@/ui/display/icon'; import { TabList } from '@/ui/layout/tab/components/TabList'; @@ -72,6 +74,13 @@ export const ShowPageRightContainer = ({ hide: !notes, disabled: entity.type === 'Custom', }, + { + id: 'files', + title: 'Files', + Icon: IconPaperclip, + hide: !notes, + disabled: entity.type === 'Custom', + }, { id: 'emails', title: 'Emails', @@ -94,6 +103,7 @@ export const ShowPageRightContainer = ({ {activeTabId === 'timeline' && } {activeTabId === 'tasks' && } {activeTabId === 'notes' && } + {activeTabId === 'files' && } ); }; diff --git a/server/src/database/typeorm-seeds/metadata/field-metadata/attachment.ts b/server/src/database/typeorm-seeds/metadata/field-metadata/attachment.ts index a2b72a759..5d1c56ad4 100644 --- a/server/src/database/typeorm-seeds/metadata/field-metadata/attachment.ts +++ b/server/src/database/typeorm-seeds/metadata/field-metadata/attachment.ts @@ -172,10 +172,12 @@ export const seedAttachmentFieldMetadata = async ( type: FieldMetadataType.RELATION, name: 'author', label: 'Author', - targetColumnMap: {}, + targetColumnMap: { + value: 'authorId', + }, description: 'Attachment author', icon: 'IconCircleUser', - isNullable: false, + isNullable: true, isSystem: false, defaultValue: undefined, }, @@ -204,10 +206,12 @@ export const seedAttachmentFieldMetadata = async ( type: FieldMetadataType.RELATION, name: 'activity', label: 'Activity', - targetColumnMap: {}, + targetColumnMap: { + value: 'activityId', + }, description: 'Attachment activity', icon: 'IconNotes', - isNullable: false, + isNullable: true, isSystem: false, defaultValue: undefined, }, @@ -223,7 +227,7 @@ export const seedAttachmentFieldMetadata = async ( targetColumnMap: {}, description: 'Attachment activity id foreign key', icon: undefined, - isNullable: false, + isNullable: true, isSystem: true, defaultValue: undefined, }, @@ -236,10 +240,12 @@ export const seedAttachmentFieldMetadata = async ( type: FieldMetadataType.RELATION, name: 'person', label: 'Person', - targetColumnMap: {}, + targetColumnMap: { + value: 'personId', + }, description: 'Attachment person', icon: 'IconUser', - isNullable: false, + isNullable: true, isSystem: false, defaultValue: undefined, }, @@ -255,7 +261,7 @@ export const seedAttachmentFieldMetadata = async ( targetColumnMap: {}, description: 'Attachment person id foreign key', icon: undefined, - isNullable: false, + isNullable: true, isSystem: true, defaultValue: undefined, }, @@ -268,10 +274,12 @@ export const seedAttachmentFieldMetadata = async ( type: FieldMetadataType.RELATION, name: 'company', label: 'Company', - targetColumnMap: {}, + targetColumnMap: { + value: 'companyId', + }, description: 'Attachment company', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, isSystem: false, defaultValue: undefined, }, @@ -287,7 +295,7 @@ export const seedAttachmentFieldMetadata = async ( targetColumnMap: {}, description: 'Attachment company id foreign key', icon: undefined, - isNullable: false, + isNullable: true, isSystem: true, defaultValue: undefined, }, diff --git a/server/src/workspace/workspace-manager/standard-objects/attachment.ts b/server/src/workspace/workspace-manager/standard-objects/attachment.ts index e02116b9f..d81a3f2d2 100644 --- a/server/src/workspace/workspace-manager/standard-objects/attachment.ts +++ b/server/src/workspace/workspace-manager/standard-objects/attachment.ts @@ -90,7 +90,7 @@ const attachmentMetadata = { }, description: 'Attachment activity', icon: 'IconNotes', - isNullable: false, + isNullable: true, }, { isCustom: false, @@ -103,7 +103,7 @@ const attachmentMetadata = { }, description: 'Attachment person', icon: 'IconUser', - isNullable: false, + isNullable: true, }, { isCustom: false, @@ -116,7 +116,7 @@ const attachmentMetadata = { }, description: 'Attachment company', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, }, ], };