feat: init
This commit is contained in:
107
apps/web/core/components/issues/attachment/attachment-detail.tsx
Normal file
107
apps/web/core/components/issues/attachment/attachment-detail.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { AlertCircle, X } from "lucide-react";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import {
|
||||
convertBytesToSize,
|
||||
getFileExtension,
|
||||
getFileName,
|
||||
getFileURL,
|
||||
renderFormattedDate,
|
||||
truncateText,
|
||||
} from "@plane/utils";
|
||||
// icons
|
||||
//
|
||||
import { getFileIcon } from "@/components/icons";
|
||||
// components
|
||||
import { IssueAttachmentDeleteModal } from "@/components/issues/attachment/delete-attachment-modal";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
import type { TAttachmentHelpers } from "../issue-detail-widgets/attachments/helper";
|
||||
|
||||
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentHelpers, "create">;
|
||||
|
||||
type TIssueAttachmentsDetail = {
|
||||
attachmentId: string;
|
||||
attachmentHelpers: TAttachmentOperationsRemoveModal;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((props) => {
|
||||
// props
|
||||
const { attachmentId, attachmentHelpers, disabled } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const {
|
||||
attachment: { getAttachmentById },
|
||||
} = useIssueDetail();
|
||||
// state
|
||||
const [isDeleteIssueAttachmentModalOpen, setIsDeleteIssueAttachmentModalOpen] = 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 (
|
||||
<>
|
||||
{isDeleteIssueAttachmentModalOpen && (
|
||||
<IssueAttachmentDeleteModal
|
||||
isOpen={isDeleteIssueAttachmentModalOpen}
|
||||
onClose={() => setIsDeleteIssueAttachmentModalOpen(false)}
|
||||
attachmentOperations={attachmentHelpers.operations}
|
||||
attachmentId={attachmentId}
|
||||
/>
|
||||
)}
|
||||
<div className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 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={() => setIsDeleteIssueAttachmentModalOpen(true)}>
|
||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { FC } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { FileRejection } from "react-dropzone";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UploadCloud } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TIssueServiceType } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
// types
|
||||
import type { TAttachmentHelpers } from "../issue-detail-widgets/attachments/helper";
|
||||
// components
|
||||
import { IssueAttachmentsListItem } from "./attachment-list-item";
|
||||
import { IssueAttachmentsUploadItem } from "./attachment-list-upload-item";
|
||||
// types
|
||||
import { IssueAttachmentDeleteModal } from "./delete-attachment-modal";
|
||||
|
||||
type TIssueAttachmentItemList = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
attachmentHelpers: TAttachmentHelpers;
|
||||
disabled?: boolean;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
};
|
||||
|
||||
export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
attachmentHelpers,
|
||||
disabled,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
// store hooks
|
||||
const {
|
||||
attachment: { getAttachmentsByIssueId },
|
||||
attachmentDeleteModalId,
|
||||
toggleDeleteAttachmentModal,
|
||||
fetchActivities,
|
||||
} = useIssueDetail(issueServiceType);
|
||||
const { operations: attachmentOperations, snapshot: attachmentSnapshot } = attachmentHelpers;
|
||||
const { create: createAttachment } = attachmentOperations;
|
||||
const { uploadStatus } = attachmentSnapshot;
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// derived values
|
||||
const issueAttachments = getAttachmentsByIssueId(issueId);
|
||||
|
||||
// handlers
|
||||
const handleFetchPropertyActivities = useCallback(() => {
|
||||
fetchActivities(workspaceSlug, projectId, issueId);
|
||||
}, [fetchActivities, workspaceSlug, projectId, issueId]);
|
||||
|
||||
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;
|
||||
|
||||
setIsUploading(true);
|
||||
createAttachment(currentFile)
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("toast.error"),
|
||||
message: t("attachment.error"),
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
handleFetchPropertyActivities();
|
||||
setIsUploading(false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("toast.error"),
|
||||
message:
|
||||
totalAttachedFiles > 1
|
||||
? t("attachment.only_one_file_allowed")
|
||||
: t("attachment.file_size_limit", { size: maxFileSize / 1024 / 1024 }),
|
||||
});
|
||||
return;
|
||||
},
|
||||
[createAttachment, maxFileSize, workspaceSlug, handleFetchPropertyActivities]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
maxSize: maxFileSize,
|
||||
multiple: false,
|
||||
disabled: isUploading || disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{uploadStatus?.map((uploadStatus) => (
|
||||
<IssueAttachmentsUploadItem key={uploadStatus.id} uploadStatus={uploadStatus} />
|
||||
))}
|
||||
{issueAttachments && (
|
||||
<>
|
||||
{attachmentDeleteModalId && (
|
||||
<IssueAttachmentDeleteModal
|
||||
isOpen={Boolean(attachmentDeleteModalId)}
|
||||
onClose={() => toggleDeleteAttachmentModal(null)}
|
||||
attachmentOperations={attachmentOperations}
|
||||
attachmentId={attachmentDeleteModalId}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`relative flex flex-col ${isDragActive && issueAttachments.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">{t("attachment.drag_and_drop")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueAttachments?.map((attachmentId) => (
|
||||
<IssueAttachmentsListItem
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trash } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { TIssueServiceType } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate } from "@plane/utils";
|
||||
// components
|
||||
//
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { getFileIcon } from "@/components/icons";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type TIssueAttachmentsListItem = {
|
||||
attachmentId: string;
|
||||
disabled?: boolean;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
};
|
||||
|
||||
export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer((props) => {
|
||||
const { t } = useTranslation();
|
||||
// props
|
||||
const { attachmentId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const {
|
||||
attachment: { getAttachmentById },
|
||||
toggleDeleteAttachmentModal,
|
||||
} = useIssueDetail(issueServiceType);
|
||||
// derived values
|
||||
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
||||
const fileName = getFileName(attachment?.attributes.name ?? "");
|
||||
const fileExtension = getFileExtension(attachment?.attributes.name ?? "");
|
||||
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?.created_by && (
|
||||
<>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${
|
||||
getUserDetails(attachment?.created_by)?.display_name ?? ""
|
||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<ButtonAvatars showTooltip userIds={attachment?.created_by} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CustomMenu ellipsis closeOnSelect placement="bottom-end" disabled={disabled}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
toggleDeleteAttachmentModal(attachmentId);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>{t("common.actions.delete")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { CircularProgressIndicator } from "@plane/ui";
|
||||
// components
|
||||
import { getFileExtension } from "@plane/utils";
|
||||
import { getFileIcon } from "@/components/icons";
|
||||
// helpers
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
import type { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store";
|
||||
|
||||
type Props = {
|
||||
uploadStatus: TAttachmentUploadStatus;
|
||||
};
|
||||
|
||||
export const IssueAttachmentsUploadItem: 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>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { CircularProgressIndicator } from "@plane/ui";
|
||||
import { getFileExtension, truncateText } from "@plane/utils";
|
||||
// ui
|
||||
// icons
|
||||
import { getFileIcon } from "@/components/icons";
|
||||
// helpers
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
import type { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store";
|
||||
|
||||
type Props = {
|
||||
uploadStatus: TAttachmentUploadStatus;
|
||||
};
|
||||
|
||||
export const IssueAttachmentsUploadDetails: 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-[2px] 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>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
// types
|
||||
import type { TAttachmentOperations } from "../issue-detail-widgets/attachments/helper";
|
||||
|
||||
type TAttachmentOperationsModal = Pick<TAttachmentOperations, "create">;
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
disabled?: boolean;
|
||||
attachmentOperations: TAttachmentOperationsModal;
|
||||
};
|
||||
|
||||
export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, disabled = false, attachmentOperations } = props;
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
const currentFile: File = acceptedFiles[0];
|
||||
if (!currentFile || !workspaceSlug) return;
|
||||
|
||||
setIsLoading(true);
|
||||
attachmentOperations.create(currentFile).finally(() => setIsLoading(false));
|
||||
},
|
||||
[attachmentOperations, workspaceSlug]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
||||
onDrop,
|
||||
maxSize: maxFileSize,
|
||||
multiple: false,
|
||||
disabled: isLoading || disabled,
|
||||
});
|
||||
|
||||
const fileError =
|
||||
fileRejections.length > 0 ? `Invalid file type or size (max ${maxFileSize / 1024 / 1024} MB)` : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`flex h-[60px] items-center justify-center rounded-md border-2 border-dashed bg-custom-primary/5 px-4 text-xs text-custom-primary ${
|
||||
isDragActive ? "border-custom-primary bg-custom-primary/10" : "border-custom-border-200"
|
||||
} ${isDragReject ? "bg-red-100" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<span className="flex items-center gap-2">
|
||||
{isDragActive ? (
|
||||
<p>Drop here...</p>
|
||||
) : fileError ? (
|
||||
<p className="text-center text-red-500">{fileError}</p>
|
||||
) : isLoading ? (
|
||||
<p className="text-center">Uploading...</p>
|
||||
) : (
|
||||
<p className="text-center">Click or drag a file here</p>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// types
|
||||
import type { TAttachmentHelpers } from "../issue-detail-widgets/attachments/helper";
|
||||
// components
|
||||
import { IssueAttachmentsDetail } from "./attachment-detail";
|
||||
import { IssueAttachmentsUploadDetails } from "./attachment-upload-details";
|
||||
|
||||
type TIssueAttachmentsList = {
|
||||
issueId: string;
|
||||
attachmentHelpers: TAttachmentHelpers;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props) => {
|
||||
const { issueId, attachmentHelpers, disabled } = props;
|
||||
// store hooks
|
||||
const {
|
||||
attachment: { getAttachmentsByIssueId },
|
||||
} = useIssueDetail();
|
||||
// derived values
|
||||
const { snapshot: attachmentSnapshot } = attachmentHelpers;
|
||||
const { uploadStatus } = attachmentSnapshot;
|
||||
const issueAttachments = getAttachmentsByIssueId(issueId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{uploadStatus?.map((uploadStatus) => (
|
||||
<IssueAttachmentsUploadDetails key={uploadStatus.id} uploadStatus={uploadStatus} />
|
||||
))}
|
||||
{issueAttachments?.map((attachmentId) => (
|
||||
<IssueAttachmentsDetail
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
attachmentHelpers={attachmentHelpers}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane-i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import type { TIssueServiceType } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore } from "@plane/ui";
|
||||
// helper
|
||||
import { getFileName } from "@plane/utils";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// types
|
||||
import type { TAttachmentOperations } from "../issue-detail-widgets/attachments/helper";
|
||||
|
||||
export type TAttachmentOperationsRemoveModal = Pick<TAttachmentOperations, "remove">;
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
attachmentId: string;
|
||||
attachmentOperations: TAttachmentOperationsRemoveModal;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
};
|
||||
|
||||
export const IssueAttachmentDeleteModal: FC<Props> = observer((props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onClose, attachmentId, attachmentOperations, issueServiceType = EIssueServiceType.ISSUES } = props;
|
||||
// states
|
||||
const [loader, setLoader] = useState(false);
|
||||
|
||||
// store hooks
|
||||
const {
|
||||
attachment: { getAttachmentById },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
// derived values
|
||||
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
||||
|
||||
// handlers
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setLoader(false);
|
||||
};
|
||||
|
||||
const handleDeletion = async (assetId: string) => {
|
||||
setLoader(true);
|
||||
attachmentOperations.remove(assetId).finally(() => handleClose());
|
||||
};
|
||||
|
||||
if (!attachment) return <></>;
|
||||
return (
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={() => handleDeletion(attachment.id)}
|
||||
isSubmitting={loader}
|
||||
isOpen={isOpen}
|
||||
title={t("attachment.delete")}
|
||||
content={
|
||||
<>
|
||||
{/* TODO: Translate here */}
|
||||
Are you sure you want to delete attachment-{" "}
|
||||
<span className="font-bold">{getFileName(attachment.attributes.name)}</span>? This attachment will be
|
||||
permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
1
apps/web/core/components/issues/attachment/index.ts
Normal file
1
apps/web/core/components/issues/attachment/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
37
apps/web/core/components/issues/attachment/root.tsx
Normal file
37
apps/web/core/components/issues/attachment/root.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useAttachmentOperations } from "../issue-detail-widgets/attachments/helper";
|
||||
// components
|
||||
import { IssueAttachmentUpload } from "./attachment-upload";
|
||||
import { IssueAttachmentsList } from "./attachments-list";
|
||||
|
||||
export type TIssueAttachmentRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// hooks
|
||||
const attachmentHelpers = useAttachmentOperations(workspaceSlug, projectId, issueId);
|
||||
|
||||
return (
|
||||
<div className="relative py-3 space-y-3">
|
||||
<h3 className="text-lg">Attachments</h3>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<IssueAttachmentUpload
|
||||
workspaceSlug={workspaceSlug}
|
||||
disabled={disabled}
|
||||
attachmentOperations={attachmentHelpers.operations}
|
||||
/>
|
||||
<IssueAttachmentsList issueId={issueId} disabled={disabled} attachmentHelpers={attachmentHelpers} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user