mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 05:49:40 +02:00
[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
This commit is contained in:
committed by
GitHub
parent
7a2fba6254
commit
4c8a0066ae
@@ -11,6 +11,7 @@ export type TInitiativeAttachment = {
|
||||
name: string;
|
||||
size: number;
|
||||
};
|
||||
asset: string;
|
||||
asset_url: string;
|
||||
Initiative_id: string;
|
||||
// required
|
||||
|
||||
@@ -8,7 +8,7 @@ import { InitiativesDetailsHeader } from "./header";
|
||||
const ProjectDetailLayout = ({ children }: { children: ReactNode }) => (
|
||||
<>
|
||||
<AppHeader header={<InitiativesDetailsHeader />} />
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
<ContentWrapper className="overflow-hidden">{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function ProjectEpicDetailsLayout({ children }: { children: React
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<ProjectEpicDetailsHeader />} />
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
<ContentWrapper className="overflow-hidden">{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,9 @@ export const EpicPeekOverviewHeader: FC<PeekOverviewHeaderProps> = 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<PeekOverviewHeaderProps> = observer((pro
|
||||
if (issue) {
|
||||
await removeIssue(issue.project_id, issue.id).then(() => {
|
||||
// TODO: add toast
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/epics`);
|
||||
setPeekIssue(undefined);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<TAttachmentHelpers, "create">;
|
||||
|
||||
type TInitiativeAttachmentsDetail = {
|
||||
attachmentId: string;
|
||||
attachmentHelpers: TAttachmentOperationsRemoveModal;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const InitiativeAttachmentsDetail: FC<TInitiativeAttachmentsDetail> = 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 (
|
||||
<>
|
||||
<InitiativeAttachmentDeleteModal
|
||||
isOpen={isDeleteInitiativeAttachmentModalOpen}
|
||||
onClose={() => setIsDeleteInitiativeAttachmentModalOpen(false)}
|
||||
attachmentOperations={attachmentHelpers.operations}
|
||||
attachmentId={attachmentId}
|
||||
/>
|
||||
|
||||
<div className="w-full flex items-center justify-between gap-1 rounded-md border border-custom-border-100 bg-custom-background-100 px-4 py-2 text-sm">
|
||||
<Link href={fileURL ?? ""} target="_blank" rel="noopener noreferrer">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-7 w-7">{fileIcon}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip tooltipContent={fileName} isMobile={isMobile}>
|
||||
<span className="text-sm">{truncateText(`${fileName}`, 10)}</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${
|
||||
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||
>
|
||||
<span>
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-custom-text-200">
|
||||
<span>{fileExtension.toUpperCase()}</span>
|
||||
<span>{convertBytesToSize(attachment.attributes.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{!disabled && (
|
||||
<button type="button" onClick={() => setIsDeleteInitiativeAttachmentModalOpen(true)}>
|
||||
<X className="h-4 w-4 text-custom-text-300 hover:text-custom-text-100" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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<Props> = 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 (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(fileURL, "_blank");
|
||||
}}
|
||||
>
|
||||
<div className="group flex items-center justify-between gap-3 h-11 hover:bg-custom-background-90 pl-9 pr-2">
|
||||
<div className="flex items-center gap-3 text-sm truncate">
|
||||
<div className="flex items-center gap-3">{fileIcon}</div>
|
||||
<Tooltip tooltipContent={`${fileName}.${fileExtension}`} isMobile={isMobile}>
|
||||
<p className="text-custom-text-200 font-medium truncate">{`${fileName}.${fileExtension}`}</p>
|
||||
</Tooltip>
|
||||
<span className="flex size-1.5 bg-custom-background-80 rounded-full" />
|
||||
<span className="flex-shrink-0 text-custom-text-400">{convertBytesToSize(attachment.attributes.size)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{attachment?.updated_by && (
|
||||
<>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${
|
||||
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<ButtonAvatars showTooltip userIds={attachment?.updated_by} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CustomMenu ellipsis closeOnSelect placement="bottom-end" disabled={disabled}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDeleteAttachmentModal(attachmentId);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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<Props> = 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 (
|
||||
<div className="flex items-center justify-between gap-3 h-11 bg-custom-background-90 pl-9 pr-2 pointer-events-none">
|
||||
<div className="flex items-center gap-3 text-sm truncate">
|
||||
<div className="flex-shrink-0">{fileIcon}</div>
|
||||
<Tooltip tooltipContent={fileName} isMobile={isMobile}>
|
||||
<p className="text-custom-text-200 font-medium truncate">{fileName}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<span className="flex-shrink-0">
|
||||
<CircularProgressIndicator size={20} strokeWidth={3} percentage={uploadStatus.progress} />
|
||||
</span>
|
||||
<div className="flex-shrink-0 text-sm font-medium">{uploadStatus.progress}% done</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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<Props> = 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 (
|
||||
<div className="flex h-[60px] items-center justify-between gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 px-4 py-2 text-sm pointer-events-none">
|
||||
<div className="flex-shrink-0 flex items-center gap-3">
|
||||
<div className="h-7 w-7">{fileIcon}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip tooltipContent={fileName} isMobile={isMobile}>
|
||||
<span className="text-sm">{truncateText(`${fileName}`, 10)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-custom-text-200">
|
||||
<span>{fileExtension.toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<span className="flex-shrink-0">
|
||||
<CircularProgressIndicator size={20} strokeWidth={3} percentage={uploadStatus.progress} />
|
||||
</span>
|
||||
<div className="flex-shrink-0 text-sm font-medium">{uploadStatus.progress}% done</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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<TInitiativeAttachmentsList> = 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) => (
|
||||
<InitiativeAttachmentsUploadDetails key={uploadStatus.id} uploadStatus={uploadStatus} />
|
||||
))}
|
||||
{initiativeAttachments?.map((attachmentId: string) => (
|
||||
<InitiativeAttachmentsDetail
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
attachmentHelpers={attachmentHelpers}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1 @@
|
||||
export * from "./attachment-detail";
|
||||
export * from "./attachment-upload-details";
|
||||
export * from "./attachments-list";
|
||||
export * from "./delete-attachment-modal";
|
||||
export * from "./root";
|
||||
|
||||
@@ -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<TInitiativeAttachmentRoot> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, initiativeId, disabled = false } = props;
|
||||
// hooks
|
||||
export const InitiativeAttachmentRoot: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, initiativeId, disabled } = props;
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [attachmentDeleteModalId, setAttachmentDeleteModalId] = useState<string | null>(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 (
|
||||
<div className="relative py-3 space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<InitiativeAttachmentsList
|
||||
initiativeId={initiativeId}
|
||||
disabled={disabled}
|
||||
attachmentHelpers={attachmentHelpers}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{uploadStatus?.map((uploadStatus) => <AttachmentsUploadItem key={uploadStatus.id} uploadStatus={uploadStatus} />)}
|
||||
{initiativeAttachments && (
|
||||
<>
|
||||
{attachmentDeleteModalId && (
|
||||
<InitiativeAttachmentDeleteModal
|
||||
isOpen={Boolean(attachmentDeleteModalId)}
|
||||
onClose={() => toggleDeleteAttachmentModal(null)}
|
||||
attachmentOperations={attachmentOperations}
|
||||
attachmentId={attachmentDeleteModalId}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`relative flex flex-col ${isDragActive && initiativeAttachments.length < 3 ? "min-h-[200px]" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive && (
|
||||
<div className="absolute flex items-center justify-center left-0 top-0 h-full w-full bg-custom-background-90/75 z-30 ">
|
||||
<div className="flex items-center justify-center p-1 rounded-md bg-custom-background-100">
|
||||
<div className="flex flex-col justify-center items-center px-5 py-6 rounded-md border border-dashed border-custom-border-300">
|
||||
<UploadCloud className="size-7" />
|
||||
<span className="text-sm text-custom-text-300">Drag and drop anywhere to upload</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{initiativeAttachments?.map((attachmentId) => (
|
||||
<InitiativeAttachmentsListItem
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
toggleDeleteAttachmentModal={toggleDeleteAttachmentModal}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -75,6 +75,7 @@ export const ProjectItem = observer((props: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
itemClassName="overflow-visible"
|
||||
isMobile={isMobile}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
|
||||
@@ -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<Props> = 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<Props> = observer((props) => {
|
||||
setIsLoading(true);
|
||||
attachmentOperations
|
||||
.create(currentFile)
|
||||
.then(() => {
|
||||
setLastCollapsibleAction("attachments");
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
@@ -72,9 +80,16 @@ export const InitiativeAttachmentActionButton: FC<Props> = observer((props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<button {...getRootProps()} type="button" disabled={disabled}>
|
||||
<input {...getInputProps()} />
|
||||
{customButton ? customButton : <Plus className="h-4 w-4" />}
|
||||
</button>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
// TODO: Remove extra div and move event propagation to button
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<button {...getRootProps()} type="button" disabled={disabled}>
|
||||
<input {...getInputProps()} />
|
||||
{customButton ? customButton : <Plus className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<LayoutRoot
|
||||
|
||||
@@ -108,7 +108,7 @@ export const InitiativeSidebarCommentsRoot: FC<Props> = 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,
|
||||
{
|
||||
|
||||
@@ -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<TAttachmentHelpers, "create">;
|
||||
|
||||
type TProjectAttachmentsDetail = {
|
||||
attachmentId: string;
|
||||
attachmentHelpers: TAttachmentOperationsRemoveModal;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectAttachmentsDetail: FC<TProjectAttachmentsDetail> = 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 (
|
||||
<>
|
||||
<ProjectAttachmentDeleteModal
|
||||
isOpen={isDeleteProjectAttachmentModalOpen}
|
||||
onClose={() => setIsDeleteProjectAttachmentModalOpen(false)}
|
||||
attachmentOperations={attachmentHelpers.operations}
|
||||
attachmentId={attachmentId}
|
||||
/>
|
||||
|
||||
<div className="w-full flex items-center justify-between gap-1 rounded-md border border-custom-border-100 bg-custom-background-100 px-4 py-2 text-sm">
|
||||
<Link href={fileURL ?? ""} target="_blank" rel="noopener noreferrer">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-7 w-7">{fileIcon}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip tooltipContent={fileName} isMobile={isMobile}>
|
||||
<span className="text-sm">{truncateText(`${fileName}`, 10)}</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${
|
||||
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||
>
|
||||
<span>
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-custom-text-200">
|
||||
<span>{fileExtension.toUpperCase()}</span>
|
||||
<span>{convertBytesToSize(attachment.attributes.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{!disabled && (
|
||||
<button type="button" onClick={() => setIsDeleteProjectAttachmentModalOpen(true)}>
|
||||
<X className="h-4 w-4 text-custom-text-300 hover:text-custom-text-100" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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<Props> = 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 (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(fileURL, "_blank");
|
||||
}}
|
||||
>
|
||||
<div className="group flex items-center justify-between gap-3 h-11 hover:bg-custom-background-90 pl-9 pr-2">
|
||||
<div className="flex items-center gap-3 text-sm truncate">
|
||||
<div className="flex items-center gap-3">{fileIcon}</div>
|
||||
<Tooltip tooltipContent={`${fileName}.${fileExtension}`} isMobile={isMobile}>
|
||||
<p className="text-custom-text-200 font-medium truncate">{`${fileName}.${fileExtension}`}</p>
|
||||
</Tooltip>
|
||||
<span className="flex size-1.5 bg-custom-background-80 rounded-full" />
|
||||
<span className="flex-shrink-0 text-custom-text-400">{convertBytesToSize(attachment.attributes.size)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{attachment?.updated_by && (
|
||||
<>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${
|
||||
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<ButtonAvatars showTooltip userIds={attachment?.updated_by} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CustomMenu ellipsis closeOnSelect placement="bottom-end" disabled={disabled}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDeleteAttachmentModal(attachmentId);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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<Props> = 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 (
|
||||
<div className="flex items-center justify-between gap-3 h-11 bg-custom-background-90 pl-9 pr-2 pointer-events-none">
|
||||
<div className="flex items-center gap-3 text-sm truncate">
|
||||
<div className="flex-shrink-0">{fileIcon}</div>
|
||||
<Tooltip tooltipContent={fileName} isMobile={isMobile}>
|
||||
<p className="text-custom-text-200 font-medium truncate">{fileName}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<span className="flex-shrink-0">
|
||||
<CircularProgressIndicator size={20} strokeWidth={3} percentage={uploadStatus.progress} />
|
||||
</span>
|
||||
<div className="flex-shrink-0 text-sm font-medium">{uploadStatus.progress}% done</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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<Props> = 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 (
|
||||
<div className="flex h-[60px] items-center justify-between gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 px-4 py-2 text-sm pointer-events-none">
|
||||
<div className="flex-shrink-0 flex items-center gap-3">
|
||||
<div className="h-7 w-7">{fileIcon}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip tooltipContent={fileName} isMobile={isMobile}>
|
||||
<span className="text-sm">{truncateText(`${fileName}`, 10)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-custom-text-200">
|
||||
<span>{fileExtension.toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<span className="flex-shrink-0">
|
||||
<CircularProgressIndicator size={20} strokeWidth={3} percentage={uploadStatus.progress} />
|
||||
</span>
|
||||
<div className="flex-shrink-0 text-sm font-medium">{uploadStatus.progress}% done</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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<TProjectAttachmentsList> = 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) => (
|
||||
<ProjectAttachmentsUploadDetails key={uploadStatus.id} uploadStatus={uploadStatus} />
|
||||
))}
|
||||
{projectAttachments?.map((attachmentId: string) => (
|
||||
<ProjectAttachmentsDetail
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
attachmentHelpers={attachmentHelpers}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1 @@
|
||||
export * from "./attachment-detail";
|
||||
export * from "./attachment-upload-details";
|
||||
export * from "./attachments-list";
|
||||
export * from "./delete-attachment-modal";
|
||||
export * from "./root";
|
||||
|
||||
@@ -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<TProjectAttachmentRoot> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, projectId, disabled = false } = props;
|
||||
// hooks
|
||||
export const ProjectAttachmentRoot: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, disabled } = props;
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [attachmentDeleteModalId, setAttachmentDeleteModalId] = useState<string | null>(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 (
|
||||
<div className="relative py-3 space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<ProjectAttachmentsList projectId={projectId} disabled={disabled} attachmentHelpers={attachmentHelpers} />
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{uploadStatus?.map((uploadStatus) => <AttachmentsUploadItem key={uploadStatus.id} uploadStatus={uploadStatus} />)}
|
||||
{projectAttachments && (
|
||||
<>
|
||||
{attachmentDeleteModalId && (
|
||||
<ProjectAttachmentDeleteModal
|
||||
isOpen={Boolean(attachmentDeleteModalId)}
|
||||
onClose={() => toggleDeleteAttachmentModal(null)}
|
||||
attachmentOperations={attachmentOperations}
|
||||
attachmentId={attachmentDeleteModalId}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`relative flex flex-col ${isDragActive && projectAttachments.length < 3 ? "min-h-[200px]" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive && (
|
||||
<div className="absolute flex items-center justify-center left-0 top-0 h-full w-full bg-custom-background-90/75 z-30 ">
|
||||
<div className="flex items-center justify-center p-1 rounded-md bg-custom-background-100">
|
||||
<div className="flex flex-col justify-center items-center px-5 py-6 rounded-md border border-dashed border-custom-border-300">
|
||||
<UploadCloud className="size-7" />
|
||||
<span className="text-sm text-custom-text-300">Drag and drop anywhere to upload</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{projectAttachments?.map((attachmentId) => (
|
||||
<ProjectAttachmentsListItem
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
toggleDeleteAttachmentModal={toggleDeleteAttachmentModal}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@ export class InitiativeStore implements IInitiativeStore {
|
||||
};
|
||||
|
||||
setLastCollapsibleAction = (section: InitiativeCollapsible) => {
|
||||
this.openCollapsibleSection = [section];
|
||||
this.openCollapsibleSection = [...this.openCollapsibleSection, section];
|
||||
};
|
||||
|
||||
toggleOpenCollapsibleSection = (section: InitiativeCollapsible) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type TProjectAttachment = {
|
||||
name: string;
|
||||
size: number;
|
||||
};
|
||||
asset: string;
|
||||
asset_url: string;
|
||||
project_id: string;
|
||||
// required
|
||||
|
||||
Reference in New Issue
Block a user