Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -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 {
|
||||
FieldContext,
|
||||
GenericFieldContextType,
|
||||
} from '@/object-record/field/contexts/FieldContext';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { IconCalendar } from '@/ui/display/icon';
|
||||
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 { deleteOneRecord: deleteOneAttachment } =
|
||||
useDeleteOneRecord<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>
|
||||
);
|
||||
};
|
||||
@ -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 { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
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 { createOneRecord: createOneAttachment } =
|
||||
useCreateOneRecord<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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user