feat: add links and permission to perform actions

refactor: divided file into components
This commit is contained in:
dakshesh14 2023-09-05 17:06:17 +05:30
parent 065a4a3cf7
commit b40059ea21
7 changed files with 336 additions and 63 deletions

View File

@ -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>
);
};

View File

@ -5,3 +5,6 @@ export * from "./issue-web-view-form";
export * from "./label"; export * from "./label";
export * from "./sub-issues"; export * from "./sub-issues";
export * from "./issue-attachments"; export * from "./issue-attachments";
export * from "./issue-properties-detail";
export * from "./issue-link-list";
export * from "./create-update-link-form";

View File

@ -29,7 +29,13 @@ import { Label, WebViewModal } from "components/web-view";
// types // types
import type { IIssueAttachment } from "types"; import type { IIssueAttachment } from "types";
export const IssueAttachments: React.FC = () => { type Props = {
allowed: boolean;
};
export const IssueAttachments: React.FC<Props> = (props) => {
const { allowed } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
@ -89,6 +95,7 @@ export const IssueAttachments: React.FC = () => {
const { getRootProps } = useDropzone({ const { getRootProps } = useDropzone({
onDrop, onDrop,
maxSize: 5 * 1024 * 1024, maxSize: 5 * 1024 * 1024,
disabled: !allowed || isLoading,
}); });
const { data: attachments } = useSWR<IIssueAttachment[]>( const { data: attachments } = useSWR<IIssueAttachment[]>(
@ -109,7 +116,9 @@ export const IssueAttachments: React.FC = () => {
<div className="space-y-6"> <div className="space-y-6">
<div <div
{...getRootProps()} {...getRootProps()}
className="border-b w-full py-2 text-custom-text-100 px-2 flex justify-between items-center" 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 ? ( {isLoading ? (
<p className="text-center">Uploading...</p> <p className="text-center">Uploading...</p>

View File

@ -0,0 +1,64 @@
// react
import React, { useState } from "react";
// next
import Link from "next/link";
// icons
import { LinkIcon, PlusIcon } from "@heroicons/react/24/outline";
// components
import { Label, WebViewModal, CreateUpdateLinkForm } from "components/web-view";
// ui
import { SecondaryButton } from "components/ui";
// types
import type { linkDetails } from "types";
type Props = {
allowed: boolean;
links?: linkDetails[];
};
export const IssueLinks: React.FC<Props> = (props) => {
const { links, allowed } = props;
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<WebViewModal isOpen={isOpen} onClose={() => setIsOpen(false)} modalTitle="Add Link">
<CreateUpdateLinkForm links={links} onSuccess={() => setIsOpen(false)} />
</WebViewModal>
<Label>Attachments</Label>
<div className="mt-1 space-y-[6px]">
{links?.map((link) => (
<div
key={link.id}
className="px-3 border border-custom-border-200 rounded-[4px] py-2 flex justify-between items-center bg-custom-background-100"
>
<Link href={link.url}>
<a target="_blank" className="text-custom-text-200 truncate">
<span>
<LinkIcon className="w-4 h-4 inline-block mr-1" />
</span>
<span>{link.title}</span>
</a>
</Link>
</div>
))}
<SecondaryButton
type="button"
disabled={!allowed}
onClick={() => setIsOpen(true)}
className="w-full !py-2 text-custom-text-300 !text-base flex items-center justify-center"
>
<PlusIcon className="w-4 h-4 inline-block mr-1" />
<span>Add</span>
</SecondaryButton>
</div>
</div>
);
};

View File

@ -0,0 +1,69 @@
// react
import React from "react";
// react hook forms
import { Controller } from "react-hook-form";
// ui
import { Icon } from "components/ui";
// components
import { Label, StateSelect, PrioritySelect } from "components/web-view";
// types
import type { IIssue } from "types";
type Props = {
control: any;
submitChanges: (data: Partial<IIssue>) => Promise<void>;
};
export const IssuePropertiesDetail: React.FC<Props> = (props) => {
const { control, submitChanges } = props;
return (
<div>
<Label>Details</Label>
<div className="space-y-2 mb-[6px]">
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
<div className="flex items-center gap-1">
<Icon iconName="grid_view" />
<span className="text-sm text-custom-text-200">State</span>
</div>
<div>
<Controller
control={control}
name="state"
render={({ field: { value } }) => (
<StateSelect
value={value}
onChange={(val: string) => submitChanges({ state: val })}
/>
)}
/>
</div>
</div>
</div>
<div className="space-y-2">
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
<div className="flex items-center gap-1">
<Icon iconName="grid_view" />
<span className="text-sm text-custom-text-200">Priority</span>
</div>
<div>
<Controller
control={control}
name="priority"
render={({ field: { value } }) => (
<PrioritySelect
value={value}
onChange={(val: string) => submitChanges({ priority: val })}
/>
)}
/>
</div>
</div>
</div>
</div>
);
};

View File

@ -36,7 +36,7 @@ export const WebViewModal = (props: Props) => {
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" /> <div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed bottom-0 left-0 w-full z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center text-center sm:items-center"> <div className="flex min-h-full items-end justify-center text-center sm:items-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
@ -47,7 +47,7 @@ export const WebViewModal = (props: Props) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-none rounded-tr-[4px] rounded-tl-[4px] bg-custom-background-100 p-6 text-left shadow-xl transition-all sm:my-8 w-full sm:p-6"> <Dialog.Panel className="relative transform overflow-hidden rounded-none rounded-tr-[4px] rounded-tl-[4px] bg-custom-background-100 p-6 text-left shadow-xl transition-all sm:mt-8 w-full">
<div className="flex justify-between items-center w-full"> <div className="flex justify-between items-center w-full">
<Dialog.Title <Dialog.Title
as="h3" as="h3"

View File

@ -8,7 +8,7 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// react hook forms // react hook forms
import { useForm, Controller } from "react-hook-form"; import { useForm } from "react-hook-form";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
@ -18,41 +18,37 @@ import { M_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import { useProjectMyMembership } from "contexts/project-member.context"; import useProjectMembers from "hooks/use-project-members";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// ui // ui
import { Spinner, Icon } from "components/ui"; import { Spinner } from "components/ui";
// components // components
import { import {
StateSelect,
PrioritySelect,
IssueWebViewForm, IssueWebViewForm,
SubIssueList, SubIssueList,
IssueAttachments, IssueAttachments,
IssuePropertiesDetail,
IssueLinks,
} from "components/web-view"; } from "components/web-view";
// types // types
import type { IIssue } from "types"; import type { IIssue } from "types";
const Label: React.FC<
React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>
> = (props) => (
<label className="block text-base font-medium mb-[5px]" {...props}>
{props.children}
</label>
);
const MobileWebViewIssueDetail = () => { const MobileWebViewIssueDetail = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
const { memberRole } = useProjectMyMembership(); const memberRole = useProjectMembers(
workspaceSlug as string,
projectId as string,
!!workspaceSlug && !!projectId
);
const isAllowed = memberRole.isMember || memberRole.isOwner; const isAllowed = Boolean(memberRole.isMember || memberRole.isOwner);
const { user } = useUser(); const { user } = useUser();
@ -161,51 +157,11 @@ const MobileWebViewIssueDetail = () => {
<SubIssueList issueDetails={issueDetails!} /> <SubIssueList issueDetails={issueDetails!} />
<div> <IssuePropertiesDetail control={control} submitChanges={submitChanges} />
<Label>Details</Label>
<div className="space-y-2 mb-[6px]">
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
<div className="flex items-center gap-1">
<Icon iconName="grid_view" />
<span className="text-sm text-custom-text-200">State</span>
</div>
<div>
<Controller
control={control}
name="state"
render={({ field: { value } }) => (
<StateSelect
value={value}
onChange={(val: string) => submitChanges({ state: val })}
/>
)}
/>
</div>
</div>
</div>
<div className="space-y-2">
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
<div className="flex items-center gap-1">
<Icon iconName="grid_view" />
<span className="text-sm text-custom-text-200">Priority</span>
</div>
<div>
<Controller
control={control}
name="priority"
render={({ field: { value } }) => (
<PrioritySelect
value={value}
onChange={(val: string) => submitChanges({ priority: val })}
/>
)}
/>
</div>
</div>
</div>
</div>
<IssueAttachments /> <IssueAttachments allowed={isAllowed} />
<IssueLinks allowed={isAllowed} links={issueDetails?.issue_link} />
</div> </div>
</DefaultLayout> </DefaultLayout>
); );