From 4c8a0066ae3dedf5f6d6c2e39017184bdd8fa482 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:32:04 +0530 Subject: [PATCH] [WEB-3100 | WEB-3098 | WEB-3089] fix: platform fixes and improvements (#2173) * fix: initiative and epics detail layout scroll * chore: initiative and project overview store updated * chore: initiatve attachment improvements * chore: initative and project overview attachment improvements * chore: code clean up * fix: initiative comment attachment upload * fix: initiative project list dropdown placement * fix: epic peek view delete * fix: initiative empty state cta permission * fix: initiative permission updated --- packages/types/src/initiatives/initiatives.ts | 1 + .../(detail)/[initiativeId]/layout.tsx | 2 +- .../[projectId]/epics/(detail)/layout.tsx | 2 +- .../components/epics/peek-overview/header.tsx | 4 +- .../attachment/attachment-detail.tsx | 98 ------------- .../attachment/attachment-list-item.tsx | 98 +++++++++++++ .../attachment-list-upload-item.tsx | 45 ++++++ .../attachment/attachment-upload-details.tsx | 54 ------- .../attachment/attachments-list.tsx | 40 ----- .../collapsible-section/attachment/index.ts | 4 - .../collapsible-section/attachment/root.tsx | 137 +++++++++++++++--- .../projects/project-list-item/index.tsx | 1 + .../main/info-section/attachment-button.tsx | 23 ++- .../components/initiatives/details/root.tsx | 5 +- .../details/sidebar/comment-tab-root.tsx | 2 +- .../attachment/attachment-detail.tsx | 98 ------------- .../attachment/attachment-list-item.tsx | 98 +++++++++++++ .../attachment-list-upload-item.tsx | 45 ++++++ .../attachment/attachment-upload-details.tsx | 54 ------- .../attachment/attachments-list.tsx | 41 ------ .../collaspible-section/attachment/index.ts | 4 - .../collaspible-section/attachment/root.tsx | 132 +++++++++++++++-- web/ee/store/initiatives/initiatives.store.ts | 2 +- web/ee/types/projects/project_attachment.ts | 1 + 24 files changed, 556 insertions(+), 435 deletions(-) delete mode 100644 web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-detail.tsx create mode 100644 web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-list-item.tsx create mode 100644 web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-list-upload-item.tsx delete mode 100644 web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-upload-details.tsx delete mode 100644 web/ee/components/initiatives/details/main/collapsible-section/attachment/attachments-list.tsx delete mode 100644 web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-detail.tsx create mode 100644 web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-list-item.tsx create mode 100644 web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-list-upload-item.tsx delete mode 100644 web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-upload-details.tsx delete mode 100644 web/ee/components/project-overview/details/main/collaspible-section/attachment/attachments-list.tsx diff --git a/packages/types/src/initiatives/initiatives.ts b/packages/types/src/initiatives/initiatives.ts index 0f8b528f52..cd54ef84db 100644 --- a/packages/types/src/initiatives/initiatives.ts +++ b/packages/types/src/initiatives/initiatives.ts @@ -11,6 +11,7 @@ export type TInitiativeAttachment = { name: string; size: number; }; + asset: string; asset_url: string; Initiative_id: string; // required diff --git a/web/app/[workspaceSlug]/(projects)/initiatives/(detail)/[initiativeId]/layout.tsx b/web/app/[workspaceSlug]/(projects)/initiatives/(detail)/[initiativeId]/layout.tsx index f81ea7b974..19b4e27e1f 100644 --- a/web/app/[workspaceSlug]/(projects)/initiatives/(detail)/[initiativeId]/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/initiatives/(detail)/[initiativeId]/layout.tsx @@ -8,7 +8,7 @@ import { InitiativesDetailsHeader } from "./header"; const ProjectDetailLayout = ({ children }: { children: ReactNode }) => ( <> } /> - {children} + {children} > ); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(detail)/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(detail)/layout.tsx index 53c1e31e33..1b83201630 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(detail)/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/epics/(detail)/layout.tsx @@ -8,7 +8,7 @@ export default function ProjectEpicDetailsLayout({ children }: { children: React return ( <> } /> - {children} + {children} > ); } diff --git a/web/ee/components/epics/peek-overview/header.tsx b/web/ee/components/epics/peek-overview/header.tsx index be59799903..cfa86e0879 100644 --- a/web/ee/components/epics/peek-overview/header.tsx +++ b/web/ee/components/epics/peek-overview/header.tsx @@ -91,7 +91,9 @@ export const EpicPeekOverviewHeader: FC = observer((pro // store hooks const { issue: { getIssueById }, + setPeekIssue, } = useIssueDetail(EIssueServiceType.EPICS); + const { updateIssue, removeIssue } = useIssuesActions(EIssuesStoreType.EPIC); const { allowPermissions } = useUserPermissions(); const { epicDetailSidebarCollapsed, toggleEpicDetailSidebar } = useAppTheme(); @@ -119,7 +121,7 @@ export const EpicPeekOverviewHeader: FC = observer((pro if (issue) { await removeIssue(issue.project_id, issue.id).then(() => { // TODO: add toast - router.push(`/${workspaceSlug}/projects/${projectId}/epics`); + setPeekIssue(undefined); }); } }; diff --git a/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-detail.tsx b/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-detail.tsx deleted file mode 100644 index 4cc29ee12f..0000000000 --- a/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-detail.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -import { FC, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { AlertCircle, X } from "lucide-react"; -// ui -import { Tooltip } from "@plane/ui"; -// icons -import { getFileIcon } from "@/components/icons"; -// components -// helpers -import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; -import { truncateText } from "@/helpers/string.helper"; -// hooks -import { useMember } from "@/hooks/store"; -import { usePlatformOS } from "@/hooks/use-platform-os"; -import { useInitiativeAttachments } from "@/plane-web/hooks/store"; -// local components -import { InitiativeAttachmentDeleteModal } from "./delete-attachment-modal"; -import { TAttachmentHelpers } from "./use-attachments"; - -type TAttachmentOperationsRemoveModal = Exclude; - -type TInitiativeAttachmentsDetail = { - attachmentId: string; - attachmentHelpers: TAttachmentOperationsRemoveModal; - disabled?: boolean; -}; - -export const InitiativeAttachmentsDetail: FC = observer((props) => { - // props - const { attachmentId, attachmentHelpers, disabled } = props; - // store hooks - const { getUserDetails } = useMember(); - const { getAttachmentById } = useInitiativeAttachments(); - // state - const [isDeleteInitiativeAttachmentModalOpen, setIsDeleteInitiativeAttachmentModalOpen] = useState(false); - // derived values - const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined; - const fileName = getFileName(attachment?.attributes.name ?? ""); - const fileExtension = getFileExtension(attachment?.asset_url ?? ""); - const fileIcon = getFileIcon(fileExtension, 28); - const fileURL = getFileURL(attachment?.asset_url ?? ""); - // hooks - const { isMobile } = usePlatformOS(); - - if (!attachment) return <>>; - - return ( - <> - setIsDeleteInitiativeAttachmentModalOpen(false)} - attachmentOperations={attachmentHelpers.operations} - attachmentId={attachmentId} - /> - - - - - {fileIcon} - - - - {truncateText(`${fileName}`, 10)} - - - - - - - - - - {fileExtension.toUpperCase()} - {convertBytesToSize(attachment.attributes.size)} - - - - - - {!disabled && ( - setIsDeleteInitiativeAttachmentModalOpen(true)}> - - - )} - - > - ); -}); diff --git a/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-list-item.tsx b/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-list-item.tsx new file mode 100644 index 0000000000..b80780831a --- /dev/null +++ b/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-list-item.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { Trash } from "lucide-react"; +// ui +import { CustomMenu, Tooltip } from "@plane/ui"; +// components +import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; +import { getFileIcon } from "@/components/icons"; +// helpers +import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper"; +import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useMember } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +import { useInitiativeAttachments } from "@/plane-web/hooks/store"; + +type Props = { + attachmentId: string; + disabled?: boolean; + toggleDeleteAttachmentModal: (attachmentId: string | null) => void; +}; + +export const InitiativeAttachmentsListItem: FC = observer((props) => { + // props + const { attachmentId, disabled, toggleDeleteAttachmentModal } = props; + // store hooks + const { getUserDetails } = useMember(); + const { getAttachmentById } = useInitiativeAttachments(); + + // derived values + const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined; + const fileName = getFileName(attachment?.attributes.name ?? ""); + const fileExtension = getFileExtension(attachment?.asset ?? ""); + const fileIcon = getFileIcon(fileExtension, 18); + const fileURL = getFileURL(attachment?.asset_url ?? ""); + // hooks + const { isMobile } = usePlatformOS(); + + if (!attachment) return <>>; + + return ( + <> + { + e.preventDefault(); + e.stopPropagation(); + window.open(fileURL, "_blank"); + }} + > + + + {fileIcon} + + {`${fileName}.${fileExtension}`} + + + {convertBytesToSize(attachment.attributes.size)} + + + + {attachment?.updated_by && ( + <> + + + + + + > + )} + + + { + e.preventDefault(); + e.stopPropagation(); + toggleDeleteAttachmentModal(attachmentId); + }} + > + + + Delete + + + + + + + > + ); +}); diff --git a/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-list-upload-item.tsx b/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-list-upload-item.tsx new file mode 100644 index 0000000000..d414896641 --- /dev/null +++ b/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-list-upload-item.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { observer } from "mobx-react"; +// ui +import { CircularProgressIndicator, Tooltip } from "@plane/ui"; +// components +import { getFileIcon } from "@/components/icons"; +// helpers +import { getFileExtension } from "@/helpers/attachment.helper"; +// hooks +import { usePlatformOS } from "@/hooks/use-platform-os"; +// types +import { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store"; + +type Props = { + uploadStatus: TAttachmentUploadStatus; +}; + +export const AttachmentsUploadItem: React.FC = observer((props) => { + // props + const { uploadStatus } = props; + // derived values + const fileName = uploadStatus.name; + const fileExtension = getFileExtension(uploadStatus.name ?? ""); + const fileIcon = getFileIcon(fileExtension, 18); + // hooks + const { isMobile } = usePlatformOS(); + + return ( + + + {fileIcon} + + {fileName} + + + + + + + {uploadStatus.progress}% done + + + ); +}); diff --git a/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-upload-details.tsx b/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-upload-details.tsx deleted file mode 100644 index f47d134bdf..0000000000 --- a/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachment-upload-details.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { observer } from "mobx-react"; -// ui -import { CircularProgressIndicator, Tooltip } from "@plane/ui"; -// icons -import { getFileIcon } from "@/components/icons"; -// helpers -import { getFileExtension } from "@/helpers/attachment.helper"; -import { truncateText } from "@/helpers/string.helper"; -// hooks -import { usePlatformOS } from "@/hooks/use-platform-os"; -// plane web -import { TAttachmentUploadStatus } from "@/plane-web/store/initiatives/initiative-attachment.store"; - -type Props = { - uploadStatus: TAttachmentUploadStatus; -}; - -export const InitiativeAttachmentsUploadDetails: React.FC = observer((props) => { - // props - const { uploadStatus } = props; - // derived values - const fileName = uploadStatus.name; - const fileExtension = getFileExtension(uploadStatus.name ?? ""); - const fileIcon = getFileIcon(fileExtension, 28); - // hooks - const { isMobile } = usePlatformOS(); - - return ( - - - {fileIcon} - - - - {truncateText(`${fileName}`, 10)} - - - - - {fileExtension.toUpperCase()} - - - - - - - - {uploadStatus.progress}% done - - - ); -}); diff --git a/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachments-list.tsx b/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachments-list.tsx deleted file mode 100644 index 21e9eab96f..0000000000 --- a/web/ee/components/initiatives/details/main/collapsible-section/attachment/attachments-list.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react"; -// hooks -import { useInitiativeAttachments } from "@/plane-web/hooks/store"; -// local components -import { InitiativeAttachmentsDetail } from "./attachment-detail"; -import { InitiativeAttachmentsUploadDetails } from "./attachment-upload-details"; -import { TAttachmentHelpers } from "./use-attachments"; - -type TInitiativeAttachmentsList = { - initiativeId: string; - attachmentHelpers: TAttachmentHelpers; - disabled?: boolean; -}; - -export const InitiativeAttachmentsList: FC = observer((props) => { - const { initiativeId, attachmentHelpers, disabled } = props; - // store hooks - const { getAttachmentsByInitiativeId } = useInitiativeAttachments(); - // derived values - const { snapshot: attachmentSnapshot } = attachmentHelpers; - const { uploadStatus } = attachmentSnapshot; - const initiativeAttachments = getAttachmentsByInitiativeId(initiativeId); - - return ( - <> - {uploadStatus?.map((uploadStatus) => ( - - ))} - {initiativeAttachments?.map((attachmentId: string) => ( - - ))} - > - ); -}); diff --git a/web/ee/components/initiatives/details/main/collapsible-section/attachment/index.ts b/web/ee/components/initiatives/details/main/collapsible-section/attachment/index.ts index 1af6815925..1efe34c51e 100644 --- a/web/ee/components/initiatives/details/main/collapsible-section/attachment/index.ts +++ b/web/ee/components/initiatives/details/main/collapsible-section/attachment/index.ts @@ -1,5 +1 @@ -export * from "./attachment-detail"; -export * from "./attachment-upload-details"; -export * from "./attachments-list"; -export * from "./delete-attachment-modal"; export * from "./root"; diff --git a/web/ee/components/initiatives/details/main/collapsible-section/attachment/root.tsx b/web/ee/components/initiatives/details/main/collapsible-section/attachment/root.tsx index 72740c4ac1..602ab4db92 100644 --- a/web/ee/components/initiatives/details/main/collapsible-section/attachment/root.tsx +++ b/web/ee/components/initiatives/details/main/collapsible-section/attachment/root.tsx @@ -1,32 +1,133 @@ "use client"; - -import { FC } from "react"; +import { FC, useCallback, useState } from "react"; import { observer } from "mobx-react"; -// components -import { InitiativeAttachmentsList } from "./attachments-list"; +import { FileRejection, useDropzone } from "react-dropzone"; +import { UploadCloud } from "lucide-react"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; +// plane web hooks +import { useInitiativeAttachments } from "@/plane-web/hooks/store"; +import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; +import { useFileSize } from "@/plane-web/hooks/use-file-size"; +// local components +import { InitiativeAttachmentsListItem } from "./attachment-list-item"; +import { AttachmentsUploadItem } from "./attachment-list-upload-item"; +import { InitiativeAttachmentDeleteModal } from "./delete-attachment-modal"; import { useAttachmentOperations } from "./use-attachments"; -export type TInitiativeAttachmentRoot = { +type Props = { workspaceSlug: string; initiativeId: string; disabled?: boolean; }; -export const InitiativeAttachmentRoot: FC = observer((props) => { - // props - const { workspaceSlug, initiativeId, disabled = false } = props; - // hooks +export const InitiativeAttachmentRoot: FC = observer((props) => { + const { workspaceSlug, initiativeId, disabled } = props; + // states + const [isLoading, setIsLoading] = useState(false); + const [attachmentDeleteModalId, setAttachmentDeleteModalId] = useState(null); + // store hooks const attachmentHelpers = useAttachmentOperations(workspaceSlug, initiativeId); + const { getAttachmentsByInitiativeId } = useInitiativeAttachments(); + + // helpers + const { operations: attachmentOperations, snapshot: attachmentSnapshot } = attachmentHelpers; + + // file size + const { maxFileSize } = useFileSize(); + + // derived values + const initiativeAttachments = getAttachmentsByInitiativeId(initiativeId); + const { uploadStatus } = attachmentSnapshot; + + // handlers + const onDrop = useCallback( + (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + const totalAttachedFiles = acceptedFiles.length + rejectedFiles.length; + + if (rejectedFiles.length === 0) { + const currentFile: File = acceptedFiles[0]; + if (!currentFile || !workspaceSlug) return; + + setIsLoading(true); + attachmentOperations + .create(currentFile) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "File could not be attached. Try uploading again.", + }); + }) + .finally(() => { + setIsLoading(false); + }); + return; + } + + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: + totalAttachedFiles > 1 + ? "Only one file can be uploaded at a time." + : `File must be of ${maxFileSize / 1024 / 1024}MB or less in size.`, + }); + return; + }, + [attachmentOperations, maxFileSize, workspaceSlug] + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + maxSize: maxFileSize, + multiple: false, + disabled: isLoading || disabled, + }); + + const toggleDeleteAttachmentModal = (attachmentId: string | null) => { + setAttachmentDeleteModalId(attachmentId); + }; return ( - - - - - + <> + {uploadStatus?.map((uploadStatus) => )} + {initiativeAttachments && ( + <> + {attachmentDeleteModalId && ( + toggleDeleteAttachmentModal(null)} + attachmentOperations={attachmentOperations} + attachmentId={attachmentDeleteModalId} + /> + )} + + + {isDragActive && ( + + + + + Drag and drop anywhere to upload + + + + )} + {initiativeAttachments?.map((attachmentId) => ( + + ))} + + > + )} + > ); }); diff --git a/web/ee/components/initiatives/details/main/collapsible-section/projects/project-list-item/index.tsx b/web/ee/components/initiatives/details/main/collapsible-section/projects/project-list-item/index.tsx index 2706df1e84..d1a3338e87 100644 --- a/web/ee/components/initiatives/details/main/collapsible-section/projects/project-list-item/index.tsx +++ b/web/ee/components/initiatives/details/main/collapsible-section/projects/project-list-item/index.tsx @@ -75,6 +75,7 @@ export const ProjectItem = observer((props: Props) => { } + itemClassName="overflow-visible" isMobile={isMobile} parentRef={parentRef} /> diff --git a/web/ee/components/initiatives/details/main/info-section/attachment-button.tsx b/web/ee/components/initiatives/details/main/info-section/attachment-button.tsx index 6f49486742..7b1775bbc6 100644 --- a/web/ee/components/initiatives/details/main/info-section/attachment-button.tsx +++ b/web/ee/components/initiatives/details/main/info-section/attachment-button.tsx @@ -7,6 +7,7 @@ import { Plus } from "lucide-react"; // plane ui import { TOAST_TYPE, setToast } from "@plane/ui"; // plane web +import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; import { useFileSize } from "@/plane-web/hooks/use-file-size"; // helpers import { useAttachmentOperations } from "../collapsible-section/attachment/use-attachments"; @@ -22,6 +23,10 @@ export const InitiativeAttachmentActionButton: FC = observer((props) => { const { workspaceSlug, initiativeId, customButton, disabled = false } = props; // state const [isLoading, setIsLoading] = useState(false); + // store hooks + const { + initiative: { setLastCollapsibleAction }, + } = useInitiatives(); // file size const { maxFileSize } = useFileSize(); // operations @@ -38,6 +43,9 @@ export const InitiativeAttachmentActionButton: FC = observer((props) => { setIsLoading(true); attachmentOperations .create(currentFile) + .then(() => { + setLastCollapsibleAction("attachments"); + }) .catch(() => { setToast({ type: TOAST_TYPE.ERROR, @@ -72,9 +80,16 @@ export const InitiativeAttachmentActionButton: FC = observer((props) => { }); return ( - - - {customButton ? customButton : } - + { + // TODO: Remove extra div and move event propagation to button + e.stopPropagation(); + }} + > + + + {customButton ? customButton : } + + ); }); diff --git a/web/ee/components/initiatives/details/root.tsx b/web/ee/components/initiatives/details/root.tsx index 17521ada7c..d977820d97 100644 --- a/web/ee/components/initiatives/details/root.tsx +++ b/web/ee/components/initiatives/details/root.tsx @@ -25,7 +25,10 @@ export const InitiativeDetailRoot = observer((props: Props) => { // derived values const initiative = getInitiativeById(initiativeId); - const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + const isEditable = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); return ( = observer((props) => { }, uploadCommentAsset: async (file, commentId) => { try { - if (!workspaceSlug || !commentId) throw new Error("Missing fields"); + if (!workspaceSlug) throw new Error("Missing fields"); const res = await fileService.uploadWorkspaceAsset( workspaceSlug, { diff --git a/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-detail.tsx b/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-detail.tsx deleted file mode 100644 index 0eee886575..0000000000 --- a/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-detail.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -import { FC, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { AlertCircle, X } from "lucide-react"; -// ui -import { Tooltip } from "@plane/ui"; -// icons -import { getFileIcon } from "@/components/icons"; -// components -// helpers -import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; -import { truncateText } from "@/helpers/string.helper"; -// hooks -import { useMember } from "@/hooks/store"; -import { usePlatformOS } from "@/hooks/use-platform-os"; -// types -import { useProjectAttachments } from "@/plane-web/hooks/store/projects/use-project-attachments"; -import { ProjectAttachmentDeleteModal } from "./delete-attachment-modal"; -import { TAttachmentHelpers } from "./use-attachments"; - -type TAttachmentOperationsRemoveModal = Exclude; - -type TProjectAttachmentsDetail = { - attachmentId: string; - attachmentHelpers: TAttachmentOperationsRemoveModal; - disabled?: boolean; -}; - -export const ProjectAttachmentsDetail: FC = observer((props) => { - // props - const { attachmentId, attachmentHelpers, disabled } = props; - // store hooks - const { getUserDetails } = useMember(); - const { getAttachmentById } = useProjectAttachments(); - // state - const [isDeleteProjectAttachmentModalOpen, setIsDeleteProjectAttachmentModalOpen] = useState(false); - // derived values - const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined; - const fileName = getFileName(attachment?.attributes.name ?? ""); - const fileExtension = getFileExtension(attachment?.asset_url ?? ""); - const fileIcon = getFileIcon(fileExtension, 28); - const fileURL = getFileURL(attachment?.asset_url ?? ""); - // hooks - const { isMobile } = usePlatformOS(); - - if (!attachment) return <>>; - - return ( - <> - setIsDeleteProjectAttachmentModalOpen(false)} - attachmentOperations={attachmentHelpers.operations} - attachmentId={attachmentId} - /> - - - - - {fileIcon} - - - - {truncateText(`${fileName}`, 10)} - - - - - - - - - - {fileExtension.toUpperCase()} - {convertBytesToSize(attachment.attributes.size)} - - - - - - {!disabled && ( - setIsDeleteProjectAttachmentModalOpen(true)}> - - - )} - - > - ); -}); diff --git a/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-list-item.tsx b/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-list-item.tsx new file mode 100644 index 0000000000..b012417772 --- /dev/null +++ b/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-list-item.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { Trash } from "lucide-react"; +// ui +import { CustomMenu, Tooltip } from "@plane/ui"; +// components +import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; +import { getFileIcon } from "@/components/icons"; +// helpers +import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper"; +import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useMember } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane web +import { useProjectAttachments } from "@/plane-web/hooks/store/projects/use-project-attachments"; + +type Props = { + attachmentId: string; + disabled?: boolean; + toggleDeleteAttachmentModal: (attachmentId: string | null) => void; +}; + +export const ProjectAttachmentsListItem: FC = observer((props) => { + // props + const { attachmentId, disabled, toggleDeleteAttachmentModal } = props; + // store hooks + const { getUserDetails } = useMember(); + const { getAttachmentById } = useProjectAttachments(); + const { isMobile } = usePlatformOS(); + + // derived values + const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined; + const fileName = getFileName(attachment?.attributes.name ?? ""); + const fileExtension = getFileExtension(attachment?.asset ?? ""); + const fileIcon = getFileIcon(fileExtension, 18); + const fileURL = getFileURL(attachment?.asset_url ?? ""); + + if (!attachment) return <>>; + + return ( + <> + { + e.preventDefault(); + e.stopPropagation(); + window.open(fileURL, "_blank"); + }} + > + + + {fileIcon} + + {`${fileName}.${fileExtension}`} + + + {convertBytesToSize(attachment.attributes.size)} + + + + {attachment?.updated_by && ( + <> + + + + + + > + )} + + + { + e.preventDefault(); + e.stopPropagation(); + toggleDeleteAttachmentModal(attachmentId); + }} + > + + + Delete + + + + + + + > + ); +}); diff --git a/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-list-upload-item.tsx b/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-list-upload-item.tsx new file mode 100644 index 0000000000..d414896641 --- /dev/null +++ b/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-list-upload-item.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { observer } from "mobx-react"; +// ui +import { CircularProgressIndicator, Tooltip } from "@plane/ui"; +// components +import { getFileIcon } from "@/components/icons"; +// helpers +import { getFileExtension } from "@/helpers/attachment.helper"; +// hooks +import { usePlatformOS } from "@/hooks/use-platform-os"; +// types +import { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store"; + +type Props = { + uploadStatus: TAttachmentUploadStatus; +}; + +export const AttachmentsUploadItem: React.FC = observer((props) => { + // props + const { uploadStatus } = props; + // derived values + const fileName = uploadStatus.name; + const fileExtension = getFileExtension(uploadStatus.name ?? ""); + const fileIcon = getFileIcon(fileExtension, 18); + // hooks + const { isMobile } = usePlatformOS(); + + return ( + + + {fileIcon} + + {fileName} + + + + + + + {uploadStatus.progress}% done + + + ); +}); diff --git a/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-upload-details.tsx b/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-upload-details.tsx deleted file mode 100644 index 8d855ca299..0000000000 --- a/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachment-upload-details.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { observer } from "mobx-react"; -// ui -import { CircularProgressIndicator, Tooltip } from "@plane/ui"; -// icons -import { getFileIcon } from "@/components/icons"; -// helpers -import { getFileExtension } from "@/helpers/attachment.helper"; -import { truncateText } from "@/helpers/string.helper"; -// hooks -import { usePlatformOS } from "@/hooks/use-platform-os"; -import { TAttachmentUploadStatus } from "@/plane-web/store/projects/project-details/attachment.store"; -// types - -type Props = { - uploadStatus: TAttachmentUploadStatus; -}; - -export const ProjectAttachmentsUploadDetails: React.FC = observer((props) => { - // props - const { uploadStatus } = props; - // derived values - const fileName = uploadStatus.name; - const fileExtension = getFileExtension(uploadStatus.name ?? ""); - const fileIcon = getFileIcon(fileExtension, 28); - // hooks - const { isMobile } = usePlatformOS(); - - return ( - - - {fileIcon} - - - - {truncateText(`${fileName}`, 10)} - - - - - {fileExtension.toUpperCase()} - - - - - - - - {uploadStatus.progress}% done - - - ); -}); diff --git a/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachments-list.tsx b/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachments-list.tsx deleted file mode 100644 index 704956b083..0000000000 --- a/web/ee/components/project-overview/details/main/collaspible-section/attachment/attachments-list.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react"; -// hooks -// types -// components -import { useProjectAttachments } from "@/plane-web/hooks/store/projects/use-project-attachments"; -import { ProjectAttachmentsDetail } from "./attachment-detail"; -import { ProjectAttachmentsUploadDetails } from "./attachment-upload-details"; -import { TAttachmentHelpers } from "./use-attachments"; - -type TProjectAttachmentsList = { - projectId: string; - attachmentHelpers: TAttachmentHelpers; - disabled?: boolean; -}; - -export const ProjectAttachmentsList: FC = observer((props) => { - const { projectId, attachmentHelpers, disabled } = props; - // store hooks - const { getAttachmentsByProjectId } = useProjectAttachments(); - // derived values - const { snapshot: attachmentSnapshot } = attachmentHelpers; - const { uploadStatus } = attachmentSnapshot; - const projectAttachments = getAttachmentsByProjectId(projectId); - - return ( - <> - {uploadStatus?.map((uploadStatus) => ( - - ))} - {projectAttachments?.map((attachmentId: string) => ( - - ))} - > - ); -}); diff --git a/web/ee/components/project-overview/details/main/collaspible-section/attachment/index.ts b/web/ee/components/project-overview/details/main/collaspible-section/attachment/index.ts index 1af6815925..1efe34c51e 100644 --- a/web/ee/components/project-overview/details/main/collaspible-section/attachment/index.ts +++ b/web/ee/components/project-overview/details/main/collaspible-section/attachment/index.ts @@ -1,5 +1 @@ -export * from "./attachment-detail"; -export * from "./attachment-upload-details"; -export * from "./attachments-list"; -export * from "./delete-attachment-modal"; export * from "./root"; diff --git a/web/ee/components/project-overview/details/main/collaspible-section/attachment/root.tsx b/web/ee/components/project-overview/details/main/collaspible-section/attachment/root.tsx index 5477da8694..f2f3c4e4a4 100644 --- a/web/ee/components/project-overview/details/main/collaspible-section/attachment/root.tsx +++ b/web/ee/components/project-overview/details/main/collaspible-section/attachment/root.tsx @@ -1,28 +1,132 @@ "use client"; - -import { FC } from "react"; +import { FC, useCallback, useState } from "react"; import { observer } from "mobx-react"; -// components -import { ProjectAttachmentsList } from "./attachments-list"; +import { FileRejection, useDropzone } from "react-dropzone"; +import { UploadCloud } from "lucide-react"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; +// plane web hooks +import { useProjectAttachments } from "@/plane-web/hooks/store/projects/use-project-attachments"; +import { useFileSize } from "@/plane-web/hooks/use-file-size"; +// local components +import { ProjectAttachmentsListItem } from "./attachment-list-item"; +import { AttachmentsUploadItem } from "./attachment-list-upload-item"; +import { ProjectAttachmentDeleteModal } from "./delete-attachment-modal"; import { useAttachmentOperations } from "./use-attachments"; -export type TProjectAttachmentRoot = { +type Props = { workspaceSlug: string; projectId: string; disabled?: boolean; }; -export const ProjectAttachmentRoot: FC = observer((props) => { - // props - const { workspaceSlug, projectId, disabled = false } = props; - // hooks +export const ProjectAttachmentRoot: FC = observer((props) => { + const { workspaceSlug, projectId, disabled } = props; + // states + const [isLoading, setIsLoading] = useState(false); + const [attachmentDeleteModalId, setAttachmentDeleteModalId] = useState(null); + // store hooks const attachmentHelpers = useAttachmentOperations(workspaceSlug, projectId); + const { getAttachmentsByProjectId } = useProjectAttachments(); + + // helpers + const { operations: attachmentOperations, snapshot: attachmentSnapshot } = attachmentHelpers; + + // file size + const { maxFileSize } = useFileSize(); + + // derived values + const projectAttachments = getAttachmentsByProjectId(projectId); + const { uploadStatus } = attachmentSnapshot; + + // handlers + const onDrop = useCallback( + (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + const totalAttachedFiles = acceptedFiles.length + rejectedFiles.length; + + if (rejectedFiles.length === 0) { + const currentFile: File = acceptedFiles[0]; + if (!currentFile || !workspaceSlug) return; + + setIsLoading(true); + attachmentOperations + .create(currentFile) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "File could not be attached. Try uploading again.", + }); + }) + .finally(() => { + setIsLoading(false); + }); + return; + } + + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: + totalAttachedFiles > 1 + ? "Only one file can be uploaded at a time." + : `File must be of ${maxFileSize / 1024 / 1024}MB or less in size.`, + }); + return; + }, + [attachmentOperations, maxFileSize, workspaceSlug] + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + maxSize: maxFileSize, + multiple: false, + disabled: isLoading || disabled, + }); + + const toggleDeleteAttachmentModal = (attachmentId: string | null) => { + setAttachmentDeleteModalId(attachmentId); + }; return ( - - - - - + <> + {uploadStatus?.map((uploadStatus) => )} + {projectAttachments && ( + <> + {attachmentDeleteModalId && ( + toggleDeleteAttachmentModal(null)} + attachmentOperations={attachmentOperations} + attachmentId={attachmentDeleteModalId} + /> + )} + + + {isDragActive && ( + + + + + Drag and drop anywhere to upload + + + + )} + {projectAttachments?.map((attachmentId) => ( + + ))} + + > + )} + > ); }); diff --git a/web/ee/store/initiatives/initiatives.store.ts b/web/ee/store/initiatives/initiatives.store.ts index b8eea9073a..df580f5b18 100644 --- a/web/ee/store/initiatives/initiatives.store.ts +++ b/web/ee/store/initiatives/initiatives.store.ts @@ -146,7 +146,7 @@ export class InitiativeStore implements IInitiativeStore { }; setLastCollapsibleAction = (section: InitiativeCollapsible) => { - this.openCollapsibleSection = [section]; + this.openCollapsibleSection = [...this.openCollapsibleSection, section]; }; toggleOpenCollapsibleSection = (section: InitiativeCollapsible) => { diff --git a/web/ee/types/projects/project_attachment.ts b/web/ee/types/projects/project_attachment.ts index 447ee4edcb..6bdab5ad82 100644 --- a/web/ee/types/projects/project_attachment.ts +++ b/web/ee/types/projects/project_attachment.ts @@ -6,6 +6,7 @@ export type TProjectAttachment = { name: string; size: number; }; + asset: string; asset_url: string; project_id: string; // required
{`${fileName}.${fileExtension}`}
{fileName}