mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: add links and permission to perform actions
refactor: divided file into components
This commit is contained in:
parent
065a4a3cf7
commit
b40059ea21
172
web/components/web-view/create-update-link-form.tsx
Normal file
172
web/components/web-view/create-update-link-form.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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";
|
||||||
|
@ -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>
|
||||||
|
64
web/components/web-view/issue-link-list.tsx
Normal file
64
web/components/web-view/issue-link-list.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
69
web/components/web-view/issue-properties-detail.tsx
Normal file
69
web/components/web-view/issue-properties-detail.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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"
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user