feat: issue attachments feature (#717)

* chore: issue attachment services added

* feat: attachment icons added

* chore: fetch-key and icons export

* feat: issue attachment upload section added

* feat: issue attachment list section added

* feat: date helper function added

* style: responsiveness added

* feat: attachment info added

* style: attachment overflow fix

* style: cursor pointer added

* chore: issue attachment interface

* style: uploading state added

* feat: delete issue attachment modal

* style: style improvement and refactor

* style: consistent icon added , chore: refactor the code

* fix: js icon import fix

* fix: build fix

* chore: refactor code
This commit is contained in:
Anmol Singh Bhatia 2023-04-06 15:07:11 +05:30 committed by GitHub
parent 86ec46db2c
commit 14dd498d08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 664 additions and 19 deletions

View File

@ -1,19 +1,46 @@
import React from "react";
import {
CssIcon,
CsvIcon,
DocIcon,
FigmaIcon,
HtmlIcon,
JavaScriptIcon,
JpgIcon,
PdfIcon,
PngIcon,
SheetIcon,
SvgIcon,
TxtIcon,
} from "components/icons";
import type { Props } from "./types";
export const getFileIcon = (fileType: string) => {
switch (fileType) {
case "pdf":
return <PdfIcon height={28} width={28} />;
case "csv":
return <CsvIcon height={28} width={28} />;
case "xlsx":
return <SheetIcon height={28} width={28} />;
case "css":
return <CssIcon height={28} width={28} />;
case "doc":
return <DocIcon height={28} width={28} />;
case "fig":
return <FigmaIcon height={28} width={28} />;
case "html":
return <HtmlIcon height={28} width={28} />;
case "png":
return <PngIcon height={28} width={28} />;
case "jpg":
return <JpgIcon height={28} width={28} />;
case "js":
return <JavaScriptIcon height={28} width={28} />;
case "txt":
return <TxtIcon height={28} width={28} />;
case "svg":
return <SvgIcon height={28} width={28} />;
export const AttachmentIcon: React.FC<Props> = ({ width, height, className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.75 20C18.4 20 18.1042 19.8792 17.8625 19.6375C17.6208 19.3958 17.5 19.1 17.5 18.75V5.25C17.5 4.9 17.6208 4.60417 17.8625 4.3625C18.1042 4.12083 18.4 4 18.75 4C19.1 4 19.3958 4.12083 19.6375 4.3625C19.8792 4.60417 20 4.9 20 5.25V18.75C20 19.1 19.8792 19.3958 19.6375 19.6375C19.3958 19.8792 19.1 20 18.75 20ZM6.275 20C6.09167 20 5.92083 19.9667 5.7625 19.9C5.60417 19.8333 5.47083 19.7458 5.3625 19.6375C5.25417 19.5292 5.16667 19.3958 5.1 19.2375C5.03333 19.0792 5 18.9167 5 18.75V15.25C5 14.9 5.12083 14.6042 5.3625 14.3625C5.60417 14.1208 5.9 14 6.25 14C6.6 14 6.89583 14.1208 7.1375 14.3625C7.37917 14.6042 7.5 14.9 7.5 15.25V18.75C7.5 18.9167 7.46667 19.0792 7.4 19.2375C7.33333 19.3958 7.24583 19.5292 7.1375 19.6375C7.02917 19.7458 6.9 19.8333 6.75 19.9C6.6 19.9667 6.44167 20 6.275 20ZM12.5 20C12.15 20 11.8542 19.8792 11.6125 19.6375C11.3708 19.3958 11.25 19.1 11.25 18.75V10.25C11.25 9.9 11.3708 9.60417 11.6125 9.3625C11.8542 9.12083 12.15 9 12.5 9C12.85 9 13.1458 9.12083 13.3875 9.3625C13.6292 9.60417 13.75 9.9 13.75 10.25V18.75C13.75 19.1 13.6292 19.3958 13.3875 19.6375C13.1458 19.8792 12.85 20 12.5 20Z"
fill="#212529"
/>
</svg>
);
default:
return <DocIcon height={28} width={28}/>;
}
};

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import CssFileIcon from "public/attachment/css-icon.png";
export const CssIcon: React.FC<Props> = ({ width, height }) => (
<Image src={CssFileIcon} height={height} width={width} alt="CssFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import CSVFileIcon from "public/attachment/csv-icon.png";
export const CsvIcon: React.FC<Props> = ({ width , height }) => (
<Image src={CSVFileIcon} height={height} width={width} alt="CSVFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import DocFileIcon from "public/attachment/doc-icon.png";
export const DocIcon: React.FC<Props> = ({ width , height }) => (
<Image src={DocFileIcon} height={height} width={width} alt="DocFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import FigmaFileIcon from "public/attachment/figma-icon.png";
export const FigmaIcon: React.FC<Props> = ({ width , height }) => (
<Image src={FigmaFileIcon} height={height} width={width} alt="FigmaFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import HtmlFileIcon from "public/attachment/html-icon.png";
export const HtmlIcon: React.FC<Props> = ({ width, height }) => (
<Image src={HtmlFileIcon} height={height} width={width} alt="HtmlFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import ImgFileIcon from "public/attachment/img-icon.png";
export const ImgIcon: React.FC<Props> = ({ width , height }) => (
<Image src={ImgFileIcon} height={height} width={width} alt="ImgFileIcon" />
);

View File

@ -57,3 +57,16 @@ export * from "./import-layers";
export * from "./check";
export * from "./water-drop-icon";
export * from "./transfer-icon";
export * from "./pdf-file-icon";
export * from "./csv-file-icon";
export * from "./sheet-file-icon";
export * from "./doc-file-icon";
export * from "./html-file-icon";
export * from "./css-file-icon";
export * from "./js-file-icon";
export * from "./figma-file-icon";
export * from "./img-file-icon";
export * from "./png-file-icon";
export * from "./jpg-file-icon";
export * from "./svg-file-icon";
export * from "./txt-file-icon";

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import JpgFileIcon from "public/attachment/jpg-icon.png";
export const JpgIcon: React.FC<Props> = ({ width, height }) => (
<Image src={JpgFileIcon} height={height} width={width} alt="JpgFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import JsFileIcon from "public/attachment/js-icon.png";
export const JavaScriptIcon: React.FC<Props> = ({ width, height }) => (
<Image src={JsFileIcon} height={height} width={width} alt="JsFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import PDFFileIcon from "public/attachment/pdf-icon.png";
export const PdfIcon: React.FC<Props> = ({ width , height }) => (
<Image src={PDFFileIcon} height={height} width={width} alt="PDFFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import PngFileIcon from "public/attachment/png-icon.png";
export const PngIcon: React.FC<Props> = ({ width, height }) => (
<Image src={PngFileIcon} height={height} width={width} alt="PngFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import SheetFileIcon from "public/attachment/excel-icon.png";
export const SheetIcon: React.FC<Props> = ({ width, height }) => (
<Image src={SheetFileIcon} height={height} width={width} alt="SheetFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import SvgFileIcon from "public/attachment/svg-icon.png";
export const SvgIcon: React.FC<Props> = ({ width, height }) => (
<Image src={SvgFileIcon} height={height} width={width} alt="SvgFileIcon" />
);

View File

@ -0,0 +1,9 @@
import React from "react";
import Image from "next/image";
import type { Props } from "./types";
import TxtFileIcon from "public/attachment/txt-icon.png";
export const TxtIcon: React.FC<Props> = ({ width, height }) => (
<Image src={TxtFileIcon} height={height} width={width} alt="TxtFileIcon" />
);

View File

@ -0,0 +1,100 @@
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-dropzone
import { useDropzone } from "react-dropzone";
// toast
import useToast from "hooks/use-toast";
// fetch key
import { ISSUE_ATTACHMENTS } from "constants/fetch-keys";
// services
import issuesService from "services/issues.service";
// type
import { IIssueAttachment } from "types";
const maxFileSize = 5 * 1024 * 1024; // 5 MB
export const IssueAttachmentUpload = () => {
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { setToastAlert } = useToast();
const onDrop = useCallback((acceptedFiles: File[]) => {
setIsLoading(true);
if (!acceptedFiles[0] || !workspaceSlug) return;
const formData = new FormData();
formData.append("asset", acceptedFiles[0]);
formData.append(
"attributes",
JSON.stringify({
name: acceptedFiles[0].name,
size: acceptedFiles[0].size,
})
);
issuesService
.uploadIssueAttachment(
workspaceSlug as string,
projectId as string,
issueId as string,
formData
)
.then((res) => {
mutate<IIssueAttachment[]>(ISSUE_ATTACHMENTS(issueId as string));
setToastAlert({
type: "success",
title: "Success!",
message: "File added successfully.",
});
setIsLoading(false);
})
.catch((err) => {
setIsLoading(false);
setToastAlert({
type: "error",
title: "error!",
message: "Something went wrong. please check file type & size (max 5 MB)",
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
onDrop,
maxSize: maxFileSize,
multiple: false,
});
const fileError =
fileRejections.length > 0
? `Invalid file type or size (max ${maxFileSize / 1024 / 1024} MB)`
: null;
return (
<div
{...getRootProps()}
className={`flex items-center justify-center h-[60px] cursor-pointer border-2 border-dashed border-theme text-blue-500 bg-blue-500/5 text-sm rounded-md px-4 ${
isDragActive ? "bg-theme/10" : ""
} ${isDragReject ? "bg-red-100" : ""}`}
>
<input {...getInputProps()} />
<span className="flex items-center gap-2">
{isDragActive ? (
<p>Drop here...</p>
) : isLoading ? (
<p className="text-center">Uploading....</p>
) : (
<p className="text-center">Drag & Drop or Click to add new file</p>
)}
{fileError && <p className="text-red-500 mt-2">{fileError}</p>}
</span>
</div>
);
};

View File

@ -0,0 +1,111 @@
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
// ui
import { Tooltip } from "components/ui";
import { DeleteAttachmentModal } from "./delete-attachment-modal";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
import { ExclamationIcon, getFileIcon } from "components/icons";
// services
import issuesService from "services/issues.service";
import projectService from "services/project.service";
// fetch-key
import { ISSUE_ATTACHMENTS, PROJECT_MEMBERS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
import { renderLongDateFormat } from "helpers/date-time.helper";
import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper";
// type
import { IIssueAttachment } from "types";
export const IssueAttachments = () => {
const [deleteAttachment, setDeleteAttachment] = useState<IIssueAttachment | null>(null);
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { data: attachments } = useSWR<IIssueAttachment[]>(
workspaceSlug && projectId && issueId ? ISSUE_ATTACHMENTS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () =>
issuesService.getIssueAttachment(
workspaceSlug as string,
projectId as string,
issueId as string
)
: null
);
const { data: people } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
return (
<>
<DeleteAttachmentModal
isOpen={attachmentDeleteModal}
setIsOpen={setAttachmentDeleteModal}
data={deleteAttachment}
/>
{attachments &&
attachments.length > 0 &&
attachments.map((file) => (
<div
key={file.id}
className="flex items-center justify-between h-[60px] gap-1 px-4 py-2 text-sm border border-gray-200 bg-white rounded-md"
>
<Link href={file.asset}>
<a target="_blank">
<div className="flex items-center gap-3">
<div className="h-7 w-7">{getFileIcon(getFileExtension(file.asset))}</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Tooltip theme="dark" tooltipContent={getFileName(file.attributes.name)}>
<span className="text-sm">
{truncateText(`${getFileName(file.attributes.name)}`, 10)}
</span>
</Tooltip>
<Tooltip
theme="dark"
tooltipContent={`${
people?.find((person) => person.member.id === file.updated_by)?.member
.first_name ?? ""
} uploaded on ${renderLongDateFormat(file.updated_at)}`}
>
<span>
<ExclamationIcon className="h-3 w-3" />
</span>
</Tooltip>
</div>
<div className="flex items-center gap-3 text-gray-500 text-xs">
<span>{getFileExtension(file.asset).toUpperCase()}</span>
<span>{convertBytesToSize(file.attributes.size)}</span>
</div>
</div>
</div>
</a>
</Link>
<button
onClick={() => {
setDeleteAttachment(file);
setAttachmentDeleteModal(true);
}}
>
<XMarkIcon className="h-4 w-4 text-gray-500 hover:text-gray-800" />
</button>
</div>
))}
</>
);
};

View File

@ -0,0 +1,144 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { SecondaryButton, DangerButton } from "components/ui";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// helper
import { getFileName } from "helpers/attachment.helper";
// types
import type { IIssueAttachment } from "types";
// fetch-keys
import { ISSUE_ATTACHMENTS } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
data: IIssueAttachment | null;
};
export const DeleteAttachmentModal: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { setToastAlert } = useToast();
const handleClose = () => {
setIsOpen(false);
setIsDeleteLoading(false);
};
const handleDeletion = async (assetId: string) => {
setIsDeleteLoading(true);
if (!workspaceSlug || !projectId || !data) return;
mutate<IIssueAttachment[]>(ISSUE_ATTACHMENTS(issueId as string));
await issuesService
.deleteIssueAttachment(
workspaceSlug as string,
projectId as string,
issueId as string,
assetId as string
)
.then((res) => {
mutate(ISSUE_ATTACHMENTS(issueId as string));
setToastAlert({
type: "success",
title: "Success!",
message: "File removed successfully.",
});
handleClose();
})
.catch((err) => {
setToastAlert({
type: "error",
title: "error!",
message: "Something went wrong please try again.",
});
setIsDeleteLoading(false);
});
};
return (
data && (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
Delete Attachment
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete attachment-{" "}
<span className="font-bold">{getFileName(data.attributes.name)}</span>?
This attachment will be permanently removed. This action cannot be
undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 bg-gray-50 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={() => handleDeletion(data.id)} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}
</DangerButton>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
);
};

View File

@ -9,3 +9,6 @@ export * from "./my-issues-list-item";
export * from "./parent-issues-list-modal";
export * from "./sidebar";
export * from "./sub-issues-list";
export * from "./attachment-upload";
export * from "./attachments";
export * from "./delete-attachment-modal";

View File

@ -115,6 +115,7 @@ export const VIEW_DETAILS = (viewId: string) => `VIEW_DETAILS_${viewId.toUpperCa
// Issues
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
export const ISSUE_ATTACHMENTS = (issueId: string) => `ISSUE_ATTACHMENTS_${issueId.toUpperCase()}`;
// integrations
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";

View File

@ -0,0 +1,22 @@
export const getFileExtension = (filename: string) =>
filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2);
export const getFileName = (fileName: string) => {
const dotIndex = fileName.lastIndexOf(".");
const nameWithoutExtension = fileName.substring(0, dotIndex);
return nameWithoutExtension;
};
export const convertBytesToSize = (bytes: number) => {
let size;
if (bytes < 1024 * 1024) {
size = Math.round(bytes / 1024) + " KB";
} else {
size = Math.round(bytes / (1024 * 1024)) + " MB";
}
return size;
};

View File

@ -89,7 +89,10 @@ export const timeAgo = (time: any) => {
return time;
};
export const getDateRangeStatus = (startDate: string | null | undefined, endDate: string | null | undefined) => {
export const getDateRangeStatus = (
startDate: string | null | undefined,
endDate: string | null | undefined
) => {
if (!startDate || !endDate) return "draft";
const today = renderDateFormat(new Date());
@ -173,3 +176,37 @@ export const renderShortTime = (date: string | Date) => {
export const isDateRangeValid = (startDate: string, endDate: string) =>
new Date(startDate) < new Date(endDate);
export const renderLongDateFormat = (dateString: string) => {
const date = new Date(dateString);
const day = date.getDate();
const year = date.getFullYear();
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const monthIndex = date.getMonth();
const monthName = monthNames[monthIndex];
const suffixes = ["st", "nd", "rd", "th"];
let suffix = "";
if (day === 1 || day === 21 || day === 31) {
suffix = suffixes[0];
} else if (day === 2 || day === 22) {
suffix = suffixes[1];
} else if (day === 3 || day === 23) {
suffix = suffixes[2];
} else {
suffix = suffixes[3];
}
return `${day}${suffix} ${monthName} ${year}`;
};

View File

@ -20,6 +20,8 @@ import {
IssueDetailsSidebar,
IssueActivitySection,
AddComment,
IssueAttachmentUpload,
IssueAttachments,
} from "components/issues";
// ui
import { Loader, CustomMenu } from "components/ui";
@ -198,6 +200,13 @@ const IssueDetailsPage: NextPage<UserAuth> = (props) => {
<SubIssuesList parentIssue={issueDetails} userAuth={props} />
</div>
</div>
<div className="flex flex-col gap-3 py-3">
<h3 className="text-lg">Attachments</h3>
<div className="grid gap-3 grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<IssueAttachmentUpload/>
<IssueAttachments />
</div>
</div>
<div className="space-y-5 bg-secondary pt-3">
<h3 className="text-lg">Comments/Activity</h3>
<IssueActivitySection />

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -440,6 +440,51 @@ class ProjectIssuesServices extends APIService {
throw error?.response?.data;
});
}
async uploadIssueAttachment(
workspaceSlug: string,
projectId: string,
issueId: string,
file: FormData
): Promise<any> {
return this.mediaUpload(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/`,
file
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getIssueAttachment(
workspaceSlug: string,
projectId: string,
issueId: string
): Promise<any> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async deleteIssueAttachment(
workspaceSlug: string,
projectId: string,
issueId: string,
assetId: string
): Promise<any> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/${assetId}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
export default new ProjectIssuesServices();

View File

@ -256,3 +256,19 @@ export interface IIssueViewOptions {
filters: IIssueFilterOptions;
target_date: string;
}
export interface IIssueAttachment {
asset: string;
attributes: {
name: string;
size: number;
};
created_at: string;
created_by: string;
id: string;
issue: string;
project: string;
updated_at: string;
updated_by: string;
workspace: string;
}