forked from github/plane
223984d900
* chore: autorun for the issue detail store * fix: labels mutation * chore: remove old peek overview code * chore: move add to cycle and module logic to store * fix: build errors * chore: add peekProjectId query param for the peek overview * chore: update profile layout * fix: multiple workspaces * style: Issue activity and link design improvements in Peek overview. * fix issue with labels not occupying full widht. * fix links overflow issue. * add tooltip in links to display entire link. * add functionality to copy links to clipboard. * chore: peek overview for all the layouts * fix: build errors --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
import { FC, useState } from "react";
|
|
import { mutate } from "swr";
|
|
import { useRouter } from "next/router";
|
|
import { observer } from "mobx-react-lite";
|
|
// mobx store
|
|
import { useMobxStore } from "lib/mobx/store-provider";
|
|
// ui icons
|
|
import { DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
|
import { CalendarDays, ContrastIcon, Link2, Plus, Signal, Tag, Triangle, User2 } from "lucide-react";
|
|
import {
|
|
SidebarAssigneeSelect,
|
|
SidebarCycleSelect,
|
|
SidebarEstimateSelect,
|
|
SidebarLabelSelect,
|
|
SidebarModuleSelect,
|
|
SidebarParentSelect,
|
|
SidebarPrioritySelect,
|
|
SidebarStateSelect,
|
|
} from "../sidebar-select";
|
|
// services
|
|
import { IssueService } from "services/issue";
|
|
// hooks
|
|
import useToast from "hooks/use-toast";
|
|
// components
|
|
import { CustomDatePicker } from "components/ui";
|
|
import { LinkModal, LinksList } from "components/core";
|
|
// types
|
|
import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types";
|
|
// fetch-keys
|
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
|
// constants
|
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
|
|
|
interface IPeekOverviewProperties {
|
|
issue: IIssue;
|
|
issueUpdate: (issue: Partial<IIssue>) => void;
|
|
disableUserActions: boolean;
|
|
}
|
|
|
|
const issueService = new IssueService();
|
|
|
|
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((props) => {
|
|
const { issue, issueUpdate, disableUserActions } = props;
|
|
// states
|
|
const [linkModal, setLinkModal] = useState(false);
|
|
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
|
|
|
const {
|
|
user: { currentProjectRole },
|
|
cycleIssues: { addIssueToCycle },
|
|
moduleIssues: { addIssueToModule },
|
|
} = useMobxStore();
|
|
|
|
const router = useRouter();
|
|
const { workspaceSlug, projectId } = router.query;
|
|
|
|
const { setToastAlert } = useToast();
|
|
|
|
const handleState = (_state: string) => {
|
|
issueUpdate({ ...issue, state: _state });
|
|
};
|
|
const handlePriority = (_priority: TIssuePriorities) => {
|
|
issueUpdate({ ...issue, priority: _priority });
|
|
};
|
|
const handleAssignee = (_assignees: string[]) => {
|
|
issueUpdate({ ...issue, assignees: _assignees });
|
|
};
|
|
const handleEstimate = (_estimate: number | null) => {
|
|
issueUpdate({ ...issue, estimate_point: _estimate });
|
|
};
|
|
const handleStartDate = (_startDate: string | null) => {
|
|
issueUpdate({ ...issue, start_date: _startDate });
|
|
};
|
|
const handleTargetDate = (_targetDate: string | null) => {
|
|
issueUpdate({ ...issue, target_date: _targetDate });
|
|
};
|
|
const handleParent = (_parent: string) => {
|
|
issueUpdate({ ...issue, parent: _parent });
|
|
};
|
|
const handleAddIssueToCycle = async (cycleId: string) => {
|
|
if (!workspaceSlug || !issue || !cycleId) return;
|
|
|
|
addIssueToCycle(workspaceSlug.toString(), cycleId, [issue.id]);
|
|
};
|
|
|
|
const handleAddIssueToModule = async (moduleId: string) => {
|
|
if (!workspaceSlug || !issue || !moduleId) return;
|
|
|
|
addIssueToModule(workspaceSlug.toString(), moduleId, [issue.id]);
|
|
};
|
|
const handleLabels = (formData: Partial<IIssue>) => {
|
|
issueUpdate({ ...issue, ...formData });
|
|
};
|
|
|
|
const handleCreateLink = async (formData: IIssueLink) => {
|
|
if (!workspaceSlug || !projectId || !issue) return;
|
|
|
|
const payload = { metadata: {}, ...formData };
|
|
|
|
await issueService
|
|
.createIssueLink(workspaceSlug as string, projectId as string, issue.id, payload)
|
|
.then(() => mutate(ISSUE_DETAILS(issue.id)))
|
|
.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.",
|
|
});
|
|
});
|
|
};
|
|
|
|
const handleUpdateLink = async (formData: IIssueLink, linkId: string) => {
|
|
if (!workspaceSlug || !projectId || !issue) return;
|
|
|
|
const payload = { metadata: {}, ...formData };
|
|
|
|
const updatedLinks = issue.issue_link.map((l) =>
|
|
l.id === linkId
|
|
? {
|
|
...l,
|
|
title: formData.title,
|
|
url: formData.url,
|
|
}
|
|
: l
|
|
);
|
|
|
|
mutate<IIssue>(
|
|
ISSUE_DETAILS(issue.id),
|
|
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
|
false
|
|
);
|
|
|
|
await issueService
|
|
.updateIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId, payload)
|
|
.then(() => {
|
|
mutate(ISSUE_DETAILS(issue.id));
|
|
})
|
|
.catch((err) => {
|
|
console.log(err);
|
|
});
|
|
};
|
|
|
|
const handleEditLink = (link: linkDetails) => {
|
|
setSelectedLinkToUpdate(link);
|
|
setLinkModal(true);
|
|
};
|
|
|
|
const handleDeleteLink = async (linkId: string) => {
|
|
if (!workspaceSlug || !projectId || !issue) return;
|
|
|
|
const updatedLinks = issue.issue_link.filter((l) => l.id !== linkId);
|
|
|
|
mutate<IIssue>(
|
|
ISSUE_DETAILS(issue.id),
|
|
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
|
false
|
|
);
|
|
|
|
await issueService
|
|
.deleteIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId)
|
|
.then(() => {
|
|
mutate(ISSUE_DETAILS(issue.id));
|
|
})
|
|
.catch((err) => {
|
|
console.log(err);
|
|
});
|
|
};
|
|
|
|
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
|
minDate?.setDate(minDate.getDate());
|
|
|
|
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
|
maxDate?.setDate(maxDate.getDate());
|
|
|
|
return (
|
|
<>
|
|
<LinkModal
|
|
isOpen={linkModal}
|
|
handleClose={() => {
|
|
setLinkModal(false);
|
|
setSelectedLinkToUpdate(null);
|
|
}}
|
|
data={selectedLinkToUpdate}
|
|
status={selectedLinkToUpdate ? true : false}
|
|
createIssueLink={handleCreateLink}
|
|
updateIssueLink={handleUpdateLink}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<div className="flex flex-col gap-5 py-5 w-full">
|
|
{/* state */}
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
|
<p>State</p>
|
|
</div>
|
|
<div>
|
|
<SidebarStateSelect value={issue?.state || ""} onChange={handleState} disabled={disableUserActions} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* assignee */}
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
|
<p>Assignees</p>
|
|
</div>
|
|
<div>
|
|
<SidebarAssigneeSelect
|
|
value={issue.assignees || []}
|
|
onChange={handleAssignee}
|
|
disabled={disableUserActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* priority */}
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<Signal className="h-4 w-4 flex-shrink-0" />
|
|
<p>Priority</p>
|
|
</div>
|
|
<div>
|
|
<SidebarPrioritySelect
|
|
value={issue.priority || ""}
|
|
onChange={handlePriority}
|
|
disabled={disableUserActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* estimate */}
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<Triangle className="h-4 w-4 flex-shrink-0 " />
|
|
<p>Estimate</p>
|
|
</div>
|
|
<div>
|
|
<SidebarEstimateSelect
|
|
value={issue.estimate_point}
|
|
onChange={handleEstimate}
|
|
disabled={disableUserActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* start date */}
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
|
<p>Start date</p>
|
|
</div>
|
|
<div>
|
|
<CustomDatePicker
|
|
placeholder="Start date"
|
|
value={issue.start_date}
|
|
onChange={handleStartDate}
|
|
className="bg-custom-background-80 !rounded border-none !px-2.5 !py-0.5"
|
|
maxDate={maxDate ?? undefined}
|
|
disabled={disableUserActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* due date */}
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
|
<p>Due date</p>
|
|
</div>
|
|
<div>
|
|
<CustomDatePicker
|
|
placeholder="Due date"
|
|
value={issue.target_date}
|
|
onChange={handleTargetDate}
|
|
className="bg-custom-background-80 !rounded border-none !px-2.5 !py-0.5"
|
|
minDate={minDate ?? undefined}
|
|
disabled={disableUserActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* parent */}
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<User2 className="h-4 w-4 flex-shrink-0" />
|
|
<p>Parent</p>
|
|
</div>
|
|
<div>
|
|
<SidebarParentSelect onChange={handleParent} issueDetails={issue} disabled={disableUserActions} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<span className="border-t border-custom-border-200" />
|
|
|
|
<div className="flex flex-col gap-5 py-5 w-full">
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<ContrastIcon className="h-4 w-4 flex-shrink-0" />
|
|
<p>Cycle</p>
|
|
</div>
|
|
<div>
|
|
<SidebarCycleSelect
|
|
issueDetail={issue}
|
|
handleCycleChange={handleAddIssueToCycle}
|
|
disabled={disableUserActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
|
<p>Module</p>
|
|
</div>
|
|
<div>
|
|
<SidebarModuleSelect
|
|
issueDetail={issue}
|
|
handleModuleChange={handleAddIssueToModule}
|
|
disabled={disableUserActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-40 text-sm flex-shrink-0">
|
|
<Tag className="h-4 w-4 flex-shrink-0" />
|
|
<p>Label</p>
|
|
</div>
|
|
<div className="flex flex-col gap-3 w-full">
|
|
<SidebarLabelSelect
|
|
issueDetails={issue}
|
|
labelList={issue.labels}
|
|
submitChanges={handleLabels}
|
|
isNotAllowed={disableUserActions}
|
|
uneditable={disableUserActions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<span className="border-t border-custom-border-200" />
|
|
|
|
<div className="flex flex-col gap-5 pt-5 w-full">
|
|
<div className="flex flex-col gap-2 w-full">
|
|
<div className="flex items-center gap-2 w-80">
|
|
<div className="flex items-center gap-2 w-40 text-sm">
|
|
<Link2 className="h-4 w-4 flex-shrink-0" />
|
|
<p>Links</p>
|
|
</div>
|
|
<div>
|
|
{!disableUserActions && (
|
|
<button
|
|
type="button"
|
|
className={`flex ${
|
|
disableUserActions ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
|
|
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`}
|
|
onClick={() => setLinkModal(true)}
|
|
disabled={false}
|
|
>
|
|
<Plus className="h-3 w-3" /> New
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-3">
|
|
{issue?.issue_link && issue.issue_link.length > 0 ? (
|
|
<LinksList
|
|
links={issue.issue_link}
|
|
handleDeleteLink={handleDeleteLink}
|
|
handleEditLink={handleEditLink}
|
|
userAuth={{
|
|
isGuest: currentProjectRole === EUserWorkspaceRoles.GUEST,
|
|
isViewer: currentProjectRole === EUserWorkspaceRoles.VIEWER,
|
|
isMember: currentProjectRole === EUserWorkspaceRoles.MEMBER,
|
|
isOwner: currentProjectRole === EUserWorkspaceRoles.ADMIN,
|
|
}}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
});
|