Attachments (#2716)
* create attachment site * add deletion * - fix person create attachment * - add presentation type - add some more file endings - various fixes
This commit is contained in:
@ -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 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<FileFolder>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type UploadFileMutation = { __typename?: 'Mutation', uploadFile: string };
|
||||||
|
|
||||||
export type UploadImageMutationVariables = Exact<{
|
export type UploadImageMutationVariables = Exact<{
|
||||||
file: Scalars['Upload'];
|
file: Scalars['Upload'];
|
||||||
fileFolder?: InputMaybe<FileFolder>;
|
fileFolder?: InputMaybe<FileFolder>;
|
||||||
@ -1126,6 +1134,38 @@ export function useGetClientConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOp
|
|||||||
export type GetClientConfigQueryHookResult = ReturnType<typeof useGetClientConfigQuery>;
|
export type GetClientConfigQueryHookResult = ReturnType<typeof useGetClientConfigQuery>;
|
||||||
export type GetClientConfigLazyQueryHookResult = ReturnType<typeof useGetClientConfigLazyQuery>;
|
export type GetClientConfigLazyQueryHookResult = ReturnType<typeof useGetClientConfigLazyQuery>;
|
||||||
export type GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
|
export type GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
|
||||||
|
export const UploadFileDocument = gql`
|
||||||
|
mutation uploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||||
|
uploadFile(file: $file, fileFolder: $fileFolder)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type UploadFileMutationFn = Apollo.MutationFunction<UploadFileMutation, UploadFileMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __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<UploadFileMutation, UploadFileMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<UploadFileMutation, UploadFileMutationVariables>(UploadFileDocument, options);
|
||||||
|
}
|
||||||
|
export type UploadFileMutationHookResult = ReturnType<typeof useUploadFileMutation>;
|
||||||
|
export type UploadFileMutationResult = Apollo.MutationResult<UploadFileMutation>;
|
||||||
|
export type UploadFileMutationOptions = Apollo.BaseMutationOptions<UploadFileMutation, UploadFileMutationVariables>;
|
||||||
export const UploadImageDocument = gql`
|
export const UploadImageDocument = gql`
|
||||||
mutation uploadImage($file: Upload!, $fileFolder: FileFolder) {
|
mutation uploadImage($file: Upload!, $fileFolder: FileFolder) {
|
||||||
uploadImage(file: $file, fileFolder: $fileFolder)
|
uploadImage(file: $file, fileFolder: $fileFolder)
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||||
|
<Dropdown
|
||||||
|
clickableComponent={
|
||||||
|
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
<DropdownMenu width="160px">
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<MenuItem
|
||||||
|
text="Download"
|
||||||
|
LeftIcon={IconDownload}
|
||||||
|
onClick={handleDownload}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text="Delete"
|
||||||
|
accent="danger"
|
||||||
|
LeftIcon={IconTrash}
|
||||||
|
onClick={handleDelete}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
}
|
||||||
|
dropdownHotkeyScope={{
|
||||||
|
scope: dropdownScopeId,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownScope>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<StyledIconContainer background={IconColors[attachment.type]}>
|
||||||
|
{Icon && <Icon size={theme.icon.size.sm} />}
|
||||||
|
</StyledIconContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 && (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledTitleBar>
|
||||||
|
<StyledTitle>
|
||||||
|
{title} <StyledCount>{attachments.length}</StyledCount>
|
||||||
|
</StyledTitle>
|
||||||
|
{button}
|
||||||
|
</StyledTitleBar>
|
||||||
|
<StyledAttachmentContainer>
|
||||||
|
{attachments.map((attachment) => (
|
||||||
|
<AttachmentRow key={attachment.id} attachment={attachment} />
|
||||||
|
))}
|
||||||
|
</StyledAttachmentContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
104
front/src/modules/activities/files/components/AttachmentRow.tsx
Normal file
104
front/src/modules/activities/files/components/AttachmentRow.tsx
Normal file
@ -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<Attachment>({
|
||||||
|
objectNameSingular: 'attachment',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
deleteOneAttachment(attachment.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldContext.Provider value={fieldContext as GenericFieldContextType}>
|
||||||
|
<StyledRow>
|
||||||
|
<StyledLeftContent>
|
||||||
|
<AttachmentIcon attachment={attachment} />
|
||||||
|
<StyledLink
|
||||||
|
href={
|
||||||
|
process.env.REACT_APP_SERVER_BASE_URL +
|
||||||
|
'/files/' +
|
||||||
|
attachment.fullPath
|
||||||
|
}
|
||||||
|
target="__blank"
|
||||||
|
>
|
||||||
|
{attachment.name}
|
||||||
|
</StyledLink>
|
||||||
|
</StyledLeftContent>
|
||||||
|
<StyledRightContent>
|
||||||
|
<StyledCalendarIconContainer>
|
||||||
|
<IconCalendar size={theme.icon.size.md} />
|
||||||
|
</StyledCalendarIconContainer>
|
||||||
|
{formatToHumanReadableDate(attachment.createdAt)}
|
||||||
|
<AttachmentDropdown
|
||||||
|
scopeKey={attachment.id}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onDownload={() => {
|
||||||
|
downloadFile(attachment.fullPath, attachment.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledRightContent>
|
||||||
|
</StyledRow>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
152
front/src/modules/activities/files/components/Attachments.tsx
Normal file
152
front/src/modules/activities/files/components/Attachments.tsx
Normal file
@ -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<HTMLInputElement>(null);
|
||||||
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
const { attachments } = useAttachments(targetableEntity);
|
||||||
|
|
||||||
|
const [uploadFile] = useUploadFileMutation();
|
||||||
|
|
||||||
|
const { createOneObject: createOneAttachment } =
|
||||||
|
useCreateOneObjectRecord<Attachment>({
|
||||||
|
objectNameSingular: 'attachment',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<StyledTaskGroupEmptyContainer>
|
||||||
|
<StyledFileInput
|
||||||
|
ref={inputFileRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledEmptyTaskGroupTitle>No files yet</StyledEmptyTaskGroupTitle>
|
||||||
|
<StyledEmptyTaskGroupSubTitle>Upload one:</StyledEmptyTaskGroupSubTitle>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="Add file"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleUploadFileClick}
|
||||||
|
/>
|
||||||
|
</StyledTaskGroupEmptyContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledAttachmentsContainer>
|
||||||
|
<StyledFileInput
|
||||||
|
ref={inputFileRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
<AttachmentList
|
||||||
|
title="All"
|
||||||
|
attachments={attachments ?? []}
|
||||||
|
button={
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
title="Add file"
|
||||||
|
onClick={handleUploadFileClick}
|
||||||
|
></Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledAttachmentsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
20
front/src/modules/activities/files/hooks/useAttachments.tsx
Normal file
20
front/src/modules/activities/files/hooks/useAttachments.tsx
Normal file
@ -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[],
|
||||||
|
};
|
||||||
|
};
|
||||||
20
front/src/modules/activities/files/types/Attachment.ts
Normal file
20
front/src/modules/activities/files/types/Attachment.ts
Normal file
@ -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';
|
||||||
18
front/src/modules/activities/files/utils/downloadFile.ts
Normal file
18
front/src/modules/activities/files/utils/downloadFile.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
55
front/src/modules/activities/files/utils/getFileType.ts
Normal file
55
front/src/modules/activities/files/utils/getFileType.ts
Normal file
@ -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';
|
||||||
|
};
|
||||||
7
front/src/modules/files/graphql/queries/uploadFile.ts
Normal file
7
front/src/modules/files/graphql/queries/uploadFile.ts
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -36,6 +36,7 @@ export {
|
|||||||
IconCurrencyDollar,
|
IconCurrencyDollar,
|
||||||
IconDatabase,
|
IconDatabase,
|
||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
|
IconDownload,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconEyeOff,
|
IconEyeOff,
|
||||||
IconFileCheck,
|
IconFileCheck,
|
||||||
@ -64,6 +65,7 @@ export {
|
|||||||
IconMouse2,
|
IconMouse2,
|
||||||
IconNotes,
|
IconNotes,
|
||||||
IconNumbers,
|
IconNumbers,
|
||||||
|
IconPaperclip,
|
||||||
IconPencil,
|
IconPencil,
|
||||||
IconPhone,
|
IconPhone,
|
||||||
IconPlug,
|
IconPlug,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { Attachments } from '@/activities/files/components/Attachments';
|
||||||
import { Notes } from '@/activities/notes/components/Notes';
|
import { Notes } from '@/activities/notes/components/Notes';
|
||||||
import { EntityTasks } from '@/activities/tasks/components/EntityTasks';
|
import { EntityTasks } from '@/activities/tasks/components/EntityTasks';
|
||||||
import { Timeline } from '@/activities/timeline/components/Timeline';
|
import { Timeline } from '@/activities/timeline/components/Timeline';
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
IconCheckbox,
|
IconCheckbox,
|
||||||
IconMail,
|
IconMail,
|
||||||
IconNotes,
|
IconNotes,
|
||||||
|
IconPaperclip,
|
||||||
IconTimelineEvent,
|
IconTimelineEvent,
|
||||||
} from '@/ui/display/icon';
|
} from '@/ui/display/icon';
|
||||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
@ -72,6 +74,13 @@ export const ShowPageRightContainer = ({
|
|||||||
hide: !notes,
|
hide: !notes,
|
||||||
disabled: entity.type === 'Custom',
|
disabled: entity.type === 'Custom',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'files',
|
||||||
|
title: 'Files',
|
||||||
|
Icon: IconPaperclip,
|
||||||
|
hide: !notes,
|
||||||
|
disabled: entity.type === 'Custom',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'emails',
|
id: 'emails',
|
||||||
title: 'Emails',
|
title: 'Emails',
|
||||||
@ -94,6 +103,7 @@ export const ShowPageRightContainer = ({
|
|||||||
{activeTabId === 'timeline' && <Timeline entity={entity} />}
|
{activeTabId === 'timeline' && <Timeline entity={entity} />}
|
||||||
{activeTabId === 'tasks' && <EntityTasks entity={entity} />}
|
{activeTabId === 'tasks' && <EntityTasks entity={entity} />}
|
||||||
{activeTabId === 'notes' && <Notes entity={entity} />}
|
{activeTabId === 'notes' && <Notes entity={entity} />}
|
||||||
|
{activeTabId === 'files' && <Attachments targetableEntity={entity} />}
|
||||||
</StyledShowPageRightContainer>
|
</StyledShowPageRightContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -172,10 +172,12 @@ export const seedAttachmentFieldMetadata = async (
|
|||||||
type: FieldMetadataType.RELATION,
|
type: FieldMetadataType.RELATION,
|
||||||
name: 'author',
|
name: 'author',
|
||||||
label: 'Author',
|
label: 'Author',
|
||||||
targetColumnMap: {},
|
targetColumnMap: {
|
||||||
|
value: 'authorId',
|
||||||
|
},
|
||||||
description: 'Attachment author',
|
description: 'Attachment author',
|
||||||
icon: 'IconCircleUser',
|
icon: 'IconCircleUser',
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
},
|
},
|
||||||
@ -204,10 +206,12 @@ export const seedAttachmentFieldMetadata = async (
|
|||||||
type: FieldMetadataType.RELATION,
|
type: FieldMetadataType.RELATION,
|
||||||
name: 'activity',
|
name: 'activity',
|
||||||
label: 'Activity',
|
label: 'Activity',
|
||||||
targetColumnMap: {},
|
targetColumnMap: {
|
||||||
|
value: 'activityId',
|
||||||
|
},
|
||||||
description: 'Attachment activity',
|
description: 'Attachment activity',
|
||||||
icon: 'IconNotes',
|
icon: 'IconNotes',
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
},
|
},
|
||||||
@ -223,7 +227,7 @@ export const seedAttachmentFieldMetadata = async (
|
|||||||
targetColumnMap: {},
|
targetColumnMap: {},
|
||||||
description: 'Attachment activity id foreign key',
|
description: 'Attachment activity id foreign key',
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
},
|
},
|
||||||
@ -236,10 +240,12 @@ export const seedAttachmentFieldMetadata = async (
|
|||||||
type: FieldMetadataType.RELATION,
|
type: FieldMetadataType.RELATION,
|
||||||
name: 'person',
|
name: 'person',
|
||||||
label: 'Person',
|
label: 'Person',
|
||||||
targetColumnMap: {},
|
targetColumnMap: {
|
||||||
|
value: 'personId',
|
||||||
|
},
|
||||||
description: 'Attachment person',
|
description: 'Attachment person',
|
||||||
icon: 'IconUser',
|
icon: 'IconUser',
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
},
|
},
|
||||||
@ -255,7 +261,7 @@ export const seedAttachmentFieldMetadata = async (
|
|||||||
targetColumnMap: {},
|
targetColumnMap: {},
|
||||||
description: 'Attachment person id foreign key',
|
description: 'Attachment person id foreign key',
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
},
|
},
|
||||||
@ -268,10 +274,12 @@ export const seedAttachmentFieldMetadata = async (
|
|||||||
type: FieldMetadataType.RELATION,
|
type: FieldMetadataType.RELATION,
|
||||||
name: 'company',
|
name: 'company',
|
||||||
label: 'Company',
|
label: 'Company',
|
||||||
targetColumnMap: {},
|
targetColumnMap: {
|
||||||
|
value: 'companyId',
|
||||||
|
},
|
||||||
description: 'Attachment company',
|
description: 'Attachment company',
|
||||||
icon: 'IconBuildingSkyscraper',
|
icon: 'IconBuildingSkyscraper',
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
},
|
},
|
||||||
@ -287,7 +295,7 @@ export const seedAttachmentFieldMetadata = async (
|
|||||||
targetColumnMap: {},
|
targetColumnMap: {},
|
||||||
description: 'Attachment company id foreign key',
|
description: 'Attachment company id foreign key',
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const attachmentMetadata = {
|
|||||||
},
|
},
|
||||||
description: 'Attachment activity',
|
description: 'Attachment activity',
|
||||||
icon: 'IconNotes',
|
icon: 'IconNotes',
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
@ -103,7 +103,7 @@ const attachmentMetadata = {
|
|||||||
},
|
},
|
||||||
description: 'Attachment person',
|
description: 'Attachment person',
|
||||||
icon: 'IconUser',
|
icon: 'IconUser',
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
@ -116,7 +116,7 @@ const attachmentMetadata = {
|
|||||||
},
|
},
|
||||||
description: 'Attachment company',
|
description: 'Attachment company',
|
||||||
icon: 'IconBuildingSkyscraper',
|
icon: 'IconBuildingSkyscraper',
|
||||||
isNullable: false,
|
isNullable: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user