chore: link edit functionality (#1895)

This commit is contained in:
Anmol Singh Bhatia 2023-08-18 12:03:31 +05:30 committed by GitHub
parent d74ec7bda9
commit c3c6ba9e34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 220 additions and 48 deletions

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect } from "react";
// react-hook-form // react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -7,12 +7,15 @@ import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// types // types
import type { IIssueLink, ModuleLink } from "types"; import type { IIssueLink, linkDetails, ModuleLink } from "types";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
onFormSubmit: (formData: IIssueLink | ModuleLink) => Promise<void>; data?: linkDetails | null;
status: boolean;
createIssueLink: (formData: IIssueLink | ModuleLink) => Promise<void>;
updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise<void>;
}; };
const defaultValues: IIssueLink | ModuleLink = { const defaultValues: IIssueLink | ModuleLink = {
@ -20,7 +23,14 @@ const defaultValues: IIssueLink | ModuleLink = {
url: "", url: "",
}; };
export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }) => { export const LinkModal: React.FC<Props> = ({
isOpen,
handleClose,
createIssueLink,
updateIssueLink,
status,
data,
}) => {
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -30,11 +40,6 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
defaultValues, defaultValues,
}); });
const onSubmit = async (formData: IIssueLink | ModuleLink) => {
await onFormSubmit({ title: formData.title, url: formData.url });
onClose();
};
const onClose = () => { const onClose = () => {
handleClose(); handleClose();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@ -43,6 +48,27 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
}, 500); }, 500);
}; };
const handleFormSubmit = async (formData: IIssueLink | ModuleLink) => {
if (!data) await createIssueLink({ title: formData.title, url: formData.url });
else await updateIssueLink({ title: formData.title, url: formData.url }, data.id);
onClose();
};
const handleCreateUpdatePage = async (formData: IIssueLink | ModuleLink) => {
await handleFormSubmit(formData);
reset({
...defaultValues,
});
};
useEffect(() => {
reset({
...defaultValues,
...data,
});
}, [data, reset]);
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}> <Dialog as="div" className="relative z-20" onClose={onClose}>
@ -70,14 +96,14 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
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-lg bg-custom-background-100 border border-custom-border-200 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-200 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(handleCreateUpdatePage)}>
<div> <div>
<div className="space-y-5"> <div className="space-y-5">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-custom-text-100" className="text-lg font-medium leading-6 text-custom-text-100"
> >
Add Link {status ? "Update Link" : "Add Link"}
</Dialog.Title> </Dialog.Title>
<div className="mt-2 space-y-3"> <div className="mt-2 space-y-3">
<div> <div>
@ -113,7 +139,13 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
<div className="mt-5 flex justify-end gap-2"> <div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton> <SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}> <PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Adding Link..." : "Add Link"} {status
? isSubmitting
? "Updating Link..."
: "Update Link"
: isSubmitting
? "Adding Link..."
: "Add Link"}
</PrimaryButton> </PrimaryButton>
</div> </div>
</form> </form>

View File

@ -1,25 +1,24 @@
// icons // icons
import { ArrowTopRightOnSquareIcon, LinkIcon, TrashIcon } from "@heroicons/react/24/outline"; import { ArrowTopRightOnSquareIcon, LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
import { Icon } from "components/ui";
// helpers // helpers
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
// types // types
import { IUserLite, UserAuth } from "types"; import { linkDetails, UserAuth } from "types";
type Props = { type Props = {
links: { links: linkDetails[];
id: string;
created_at: Date;
created_by: string;
created_by_detail: IUserLite;
metadata: any;
title: string;
url: string;
}[];
handleDeleteLink: (linkId: string) => void; handleDeleteLink: (linkId: string) => void;
handleEditLink: (link: linkDetails) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }) => { export const LinksList: React.FC<Props> = ({
links,
handleDeleteLink,
handleEditLink,
userAuth,
}) => {
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return ( return (
@ -28,6 +27,13 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
<div key={link.id} className="relative"> <div key={link.id} className="relative">
{!isNotAllowed && ( {!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1"> <div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1">
<button
type="button"
className="grid h-7 w-7 place-items-center rounded bg-custom-background-90 p-1 outline-none hover:bg-custom-background-80"
onClick={() => handleEditLink(link)}
>
<Icon iconName="edit" className="text-custom-text-200" />
</button>
<a <a
href={link.url} href={link.url}
target="_blank" target="_blank"

View File

@ -37,7 +37,7 @@ import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/reac
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
import type { ICycle, IIssue, IIssueLink, IModule } from "types"; import type { ICycle, IIssue, IIssueLink, linkDetails, IModule } from "types";
// fetch-keys // fetch-keys
import { ISSUE_DETAILS } from "constants/fetch-keys"; import { ISSUE_DETAILS } from "constants/fetch-keys";
@ -77,6 +77,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
}) => { }) => {
const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [linkModal, setLinkModal] = useState(false); const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
@ -156,6 +157,43 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
}); });
}; };
const handleUpdateLink = async (formData: IIssueLink, linkId: string) => {
if (!workspaceSlug || !projectId || !issueDetail) return;
const payload = { metadata: {}, ...formData };
const updatedLinks = issueDetail.issue_link.map((l) =>
l.id === linkId
? {
...l,
title: formData.title,
url: formData.url,
}
: l
);
mutate<IIssue>(
ISSUE_DETAILS(issueDetail.id),
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
false
);
await issuesService
.updateIssueLink(
workspaceSlug as string,
projectId as string,
issueDetail.id,
linkId,
payload
)
.then((res) => {
mutate(ISSUE_DETAILS(issueDetail.id));
})
.catch((err) => {
console.log(err);
});
};
const handleDeleteLink = async (linkId: string) => { const handleDeleteLink = async (linkId: string) => {
if (!workspaceSlug || !projectId || !issueDetail) return; if (!workspaceSlug || !projectId || !issueDetail) return;
@ -220,14 +258,25 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
const maxDate = targetDate ? new Date(targetDate) : null; const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate()); maxDate?.setDate(maxDate.getDate());
const handleEditLink = (link: linkDetails) => {
setSelectedLinkToUpdate(link);
setLinkModal(true);
};
const isNotAllowed = memberRole.isGuest || memberRole.isViewer; const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
return ( return (
<> <>
<LinkModal <LinkModal
isOpen={linkModal} isOpen={linkModal}
handleClose={() => setLinkModal(false)} handleClose={() => {
onFormSubmit={handleCreateLink} setLinkModal(false);
setSelectedLinkToUpdate(null);
}}
data={selectedLinkToUpdate}
status={selectedLinkToUpdate ? true : false}
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
/> />
<DeleteIssueModal <DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)} handleClose={() => setDeleteIssueModal(false)}
@ -490,6 +539,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<LinksList <LinksList
links={issueDetail.issue_link} links={issueDetail.issue_link}
handleDeleteLink={handleDeleteLink} handleDeleteLink={handleDeleteLink}
handleEditLink={handleEditLink}
userAuth={memberRole} userAuth={memberRole}
/> />
) : null} ) : null}

View File

@ -37,7 +37,7 @@ import { LinkIcon } from "@heroicons/react/20/solid";
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
// types // types
import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types"; import { ICurrentUserResponse, IIssue, linkDetails, IModule, ModuleLink } from "types";
// fetch-keys // fetch-keys
import { MODULE_DETAILS } from "constants/fetch-keys"; import { MODULE_DETAILS } from "constants/fetch-keys";
// constant // constant
@ -61,6 +61,7 @@ type Props = {
export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIssues, user }) => { export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIssues, user }) => {
const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query; const { workspaceSlug, projectId, moduleId } = router.query;
@ -115,6 +116,37 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
}); });
}; };
const handleUpdateLink = async (formData: ModuleLink, linkId: string) => {
if (!workspaceSlug || !projectId || !module) return;
const payload = { metadata: {}, ...formData };
const updatedLinks = module.link_module.map((l) =>
l.id === linkId
? {
...l,
title: formData.title,
url: formData.url,
}
: l
);
mutate<IModule>(
MODULE_DETAILS(module.id),
(prevData) => ({ ...(prevData as IModule), link_module: updatedLinks }),
false
);
await modulesService
.updateModuleLink(workspaceSlug as string, projectId as string, module.id, linkId, payload)
.then((res) => {
mutate(MODULE_DETAILS(module.id));
})
.catch((err) => {
console.log(err);
});
};
const handleDeleteLink = async (linkId: string) => { const handleDeleteLink = async (linkId: string) => {
if (!workspaceSlug || !projectId || !module) return; if (!workspaceSlug || !projectId || !module) return;
@ -170,12 +202,23 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
? Math.round((module.completed_issues / module.total_issues) * 100) ? Math.round((module.completed_issues / module.total_issues) * 100)
: null; : null;
const handleEditLink = (link: linkDetails) => {
setSelectedLinkToUpdate(link);
setModuleLinkModal(true);
};
return ( return (
<> <>
<LinkModal <LinkModal
isOpen={moduleLinkModal} isOpen={moduleLinkModal}
handleClose={() => setModuleLinkModal(false)} handleClose={() => {
onFormSubmit={handleCreateLink} setModuleLinkModal(false);
setSelectedLinkToUpdate(null);
}}
data={selectedLinkToUpdate}
status={selectedLinkToUpdate ? true : false}
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
/> />
<DeleteModuleModal <DeleteModuleModal
isOpen={moduleDeleteModal} isOpen={moduleDeleteModal}
@ -544,7 +587,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</Disclosure> </Disclosure>
</div> </div>
<div className="flex w-full flex-col border-t border-custom-border-200 px-6 py-6 text-xs"> <div className="flex w-full flex-col border-t border-custom-border-200 px-6 pt-6 pb-10 text-xs">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<h4 className="text-sm font-medium text-custom-text-200">Links</h4> <h4 className="text-sm font-medium text-custom-text-200">Links</h4>
<button <button
@ -558,6 +601,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
{memberRole && module.link_module && module.link_module.length > 0 ? ( {memberRole && module.link_module && module.link_module.length > 0 ? (
<LinksList <LinksList
links={module.link_module} links={module.link_module}
handleEditLink={handleEditLink}
handleDeleteLink={handleDeleteLink} handleDeleteLink={handleDeleteLink}
userAuth={memberRole} userAuth={memberRole}
/> />

View File

@ -459,6 +459,29 @@ class ProjectIssuesServices extends APIService {
}); });
} }
async updateIssueLink(
workspaceSlug: string,
projectId: string,
issueId: string,
linkId: string,
data: {
metadata: any;
title: string;
url: string;
},
): Promise<any> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async deleteIssueLink( async deleteIssueLink(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,

View File

@ -212,6 +212,28 @@ class ProjectIssuesServices extends APIService {
}); });
} }
async updateModuleLink(
workspaceSlug: string,
projectId: string,
moduleId: string,
linkId: string,
data: {
metadata: any;
title: string;
url: string;
},
): Promise<any> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async deleteModuleLink( async deleteModuleLink(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,

View File

@ -56,6 +56,16 @@ export interface IIssueLink {
url: string; url: string;
} }
export interface linkDetails {
created_at: Date;
created_by: string;
created_by_detail: IUserLite;
id: string;
metadata: any;
title: string;
url: string;
}
export interface IIssue { export interface IIssue {
archived_at: string; archived_at: string;
assignees: string[]; assignees: string[];
@ -80,15 +90,7 @@ export interface IIssue {
estimate_point: number | null; estimate_point: number | null;
id: string; id: string;
issue_cycle: IIssueCycle | null; issue_cycle: IIssueCycle | null;
issue_link: { issue_link: linkDetails[];
created_at: Date;
created_by: string;
created_by_detail: IUserLite;
id: string;
metadata: any;
title: string;
url: string;
}[];
issue_module: IIssueModule | null; issue_module: IIssueModule | null;
labels: string[]; labels: string[];
label_details: any[]; label_details: any[];

View File

@ -7,6 +7,7 @@ import type {
IWorkspaceLite, IWorkspaceLite,
IProjectLite, IProjectLite,
IIssueFilterOptions, IIssueFilterOptions,
linkDetails,
} from "types"; } from "types";
export interface IModule { export interface IModule {
@ -26,15 +27,7 @@ export interface IModule {
id: string; id: string;
lead: string | null; lead: string | null;
lead_detail: IUserLite | null; lead_detail: IUserLite | null;
link_module: { link_module: linkDetails[];
created_at: Date;
created_by: string;
created_by_detail: IUserLite;
id: string;
metadata: any;
title: string;
url: string;
}[];
links_list: ModuleLink[]; links_list: ModuleLink[];
members: string[]; members: string[];
members_list: string[]; members_list: string[];