Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions web/components/web-view/create-update-link-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// react
import React from "react";

// next
import { useRouter } from "next/router";

// swr
import { mutate } from "swr";

// react hooks form
import { useForm } from "react-hook-form";

// services
import issuesService from "services/issues.service";

// fetch keys
import { M_ISSUE_DETAILS } from "constants/fetch-keys";

// hooks
import useToast from "hooks/use-toast";

// ui
import { PrimaryButton, Input } from "components/ui";

// types
import type { linkDetails, IIssueLink } from "types";

type Props = {
links?: linkDetails[];
data?: linkDetails;
onSuccess: () => void;
};

export const CreateUpdateLinkForm: React.FC<Props> = (props) => {
const { data, links, onSuccess } = props;

const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;

const { setToastAlert } = useToast();

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
defaultValues: {
title: "",
url: "",
},
});

const onSubmit = async (formData: IIssueLink) => {
if (!workspaceSlug || !projectId || !issueId) return;

const payload = { metadata: {}, ...formData };

if (!data)
await issuesService
.createIssueLink(
workspaceSlug.toString(),
projectId.toString(),
issueId.toString(),
payload
)
.then(() => {
onSuccess();
mutate(
M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString())
);
})
.catch((err) => {
if (err?.status === 400)
setToastAlert({
type: "error",
title: "Error!",
message: "This URL already exists for this issue.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
});
});
else {
const updatedLinks = links?.map((l) =>
l.id === data.id
? {
...l,
title: formData.title,
url: formData.url,
}
: l
);

mutate(
M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString()),
(prevData) => ({ ...prevData, issue_link: updatedLinks }),
false
);

await issuesService
.updateIssueLink(
workspaceSlug.toString(),
projectId.toString(),
issueId.toString(),
data!.id,
payload
)
.then(() => {
onSuccess();
mutate(
M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString())
);
});
}
};

return (
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
<div>
<div className="space-y-5">
<div className="mt-2 space-y-3">
<div>
<Input
id="url"
label="URL"
name="url"
type="url"
placeholder="https://..."
autoComplete="off"
error={errors.url}
register={register}
validations={{
required: "URL is required",
}}
/>
</div>
<div>
<Input
id="title"
label="Title (optional)"
name="title"
type="text"
placeholder="Enter title"
autoComplete="off"
error={errors.title}
register={register}
/>
</div>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<PrimaryButton
type="submit"
loading={isSubmitting}
className="w-full !py-2 text-custom-text-300 !text-base flex items-center justify-center"
>
{data
? isSubmitting
? "Updating Link..."
: "Update Link"
: isSubmitting
? "Adding Link..."
: "Add Link"}
</PrimaryButton>
</div>
</form>
);
};
10 changes: 10 additions & 0 deletions web/components/web-view/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export * from "./web-view-modal";
export * from "./select-state";
export * from "./select-priority";
export * from "./issue-web-view-form";
export * from "./label";
export * from "./sub-issues";
export * from "./issue-attachments";
export * from "./issue-properties-detail";
export * from "./issue-link-list";
export * from "./create-update-link-form";
159 changes: 159 additions & 0 deletions web/components/web-view/issue-attachments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// react
import React, { useState, useCallback } from "react";

// next
import Link from "next/link";
import { useRouter } from "next/router";

// swr
import useSWR, { mutate } from "swr";

// services
import issuesService from "services/issues.service";

// react dropzone
import { useDropzone } from "react-dropzone";

// fetch key
import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";

// hooks
import useToast from "hooks/use-toast";

// icons
import { ChevronRightIcon } from "@heroicons/react/24/outline";

// components
import { Label, WebViewModal } from "components/web-view";

// types
import type { IIssueAttachment } from "types";

type Props = {
allowed: boolean;
};

export const IssueAttachments: React.FC<Props> = (props) => {
const { allowed } = props;

const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;

const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const { setToastAlert } = useToast();

const onDrop = useCallback(
(acceptedFiles: File[]) => {
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,
})
);
setIsLoading(true);

issuesService
.uploadIssueAttachment(
workspaceSlug as string,
projectId as string,
issueId as string,
formData
)
.then((res) => {
mutate<IIssueAttachment[]>(
ISSUE_ATTACHMENTS(issueId as string),
(prevData) => [res, ...(prevData ?? [])],
false
);
mutate(PROJECT_ISSUES_ACTIVITY(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)",
});
});
},
[issueId, projectId, setToastAlert, workspaceSlug]
);

const { getRootProps } = useDropzone({
onDrop,
maxSize: 5 * 1024 * 1024,
disabled: !allowed || isLoading,
});

const { data: attachments } = useSWR<IIssueAttachment[]>(
workspaceSlug && projectId && issueId ? ISSUE_ATTACHMENTS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () =>
issuesService.getIssueAttachment(
workspaceSlug.toString(),
projectId.toString(),
issueId.toString()
)
: null
);

return (
<div>
<WebViewModal isOpen={isOpen} onClose={() => setIsOpen(false)} modalTitle="Insert file">
<div className="space-y-6">
<div
{...getRootProps()}
className={`border-b w-full py-2 text-custom-text-100 px-2 flex justify-between items-center ${
!allowed || isLoading ? "cursor-not-allowed" : "cursor-pointer"
}`}
>
{isLoading ? (
<p className="text-center">Uploading...</p>
) : (
<>
<h3 className="text-lg">Upload</h3>
<ChevronRightIcon className="w-5 h-5" />
</>
)}
</div>
</div>
</WebViewModal>

<Label>Attachments</Label>
<div className="mt-1 space-y-[6px]">
{attachments?.map((attachment) => (
<div
key={attachment.id}
className="px-3 border border-custom-border-200 rounded-[4px] py-2 flex justify-between items-center bg-custom-background-100"
>
<Link href={attachment.asset}>
<a target="_blank" className="text-custom-text-200 truncate">
{attachment.attributes.name}
</a>
</Link>
</div>
))}
<button
type="button"
onClick={() => setIsOpen(true)}
className="bg-custom-primary-100/10 border border-dotted border-custom-primary-100 text-center py-2 w-full text-custom-primary-100"
>
Click to upload file here
</button>
</div>
</div>
);
};
Loading