forked from github/plane
chore: issue peek overview (#2918)
* 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>
This commit is contained in:
parent
b30e41f324
commit
220389e74e
@ -30,7 +30,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
|
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
|
||||||
issueDetail: { updateIssue },
|
projectIssues: { updateIssue },
|
||||||
user: { currentUser },
|
user: { currentUser },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
issueDetail: { updateIssue },
|
projectIssues: { updateIssue },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export const ChangeIssuePriority: React.FC<Props> = observer((props) => {
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issueDetail: { updateIssue },
|
projectIssues: { updateIssue },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const submitChanges = async (formData: Partial<IIssue>) => {
|
const submitChanges = async (formData: Partial<IIssue>) => {
|
||||||
|
@ -24,7 +24,7 @@ export const ChangeIssueState: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
projectState: { projectStates },
|
projectState: { projectStates },
|
||||||
issueDetail: { updateIssue },
|
projectIssues: { updateIssue },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const submitChanges = async (formData: Partial<IIssue>) => {
|
const submitChanges = async (formData: Partial<IIssue>) => {
|
||||||
|
@ -226,31 +226,12 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||||
},
|
},
|
||||||
duplicate: {
|
|
||||||
message: (activity) => {
|
|
||||||
if (activity.old_value === "")
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
marked this issue as duplicate of{" "}
|
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
else
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
removed this issue as a duplicate of{" "}
|
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: <CopyPlus size={12} color="#6b7280" />,
|
|
||||||
},
|
|
||||||
cycles: {
|
cycles: {
|
||||||
message: (activity, showIssue, workspaceSlug) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
if (activity.verb === "created")
|
if (activity.verb === "created")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className="flex-shrink-0">added this issue to the cycle</span>
|
<span className="flex-shrink-0">added this issue to the cycle </span>
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -295,6 +276,25 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
duplicate: {
|
||||||
|
message: (activity) => {
|
||||||
|
if (activity.old_value === "")
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
marked this issue as duplicate of{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
removed this issue as a duplicate of{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: <CopyPlus size={12} color="#6b7280" />,
|
||||||
|
},
|
||||||
description: {
|
description: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
<>
|
<>
|
||||||
@ -354,7 +354,7 @@ const activityDetails: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
added a new label{" "}
|
added a new label{" "}
|
||||||
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs w-min whitespace-nowrap">
|
<span className="inline-flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs w-min whitespace-nowrap">
|
||||||
<LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} />
|
<LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} />
|
||||||
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
|
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
|
||||||
</span>
|
</span>
|
||||||
@ -370,7 +370,7 @@ const activityDetails: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
removed the label{" "}
|
removed the label{" "}
|
||||||
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
<span className="inline-flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs w-min whitespace-nowrap">
|
||||||
<LabelPill labelId={activity.old_identifier ?? ""} workspaceSlug={workspaceSlug} />
|
<LabelPill labelId={activity.old_identifier ?? ""} workspaceSlug={workspaceSlug} />
|
||||||
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span>
|
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
// ui
|
||||||
|
import { ExternalLinkIcon, Tooltip } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ExternalLinkIcon } from "@plane/ui";
|
|
||||||
import { Pencil, Trash2, LinkIcon } from "lucide-react";
|
import { Pencil, Trash2, LinkIcon } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { timeAgo } from "helpers/date-time.helper";
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { linkDetails, UserAuth } from "types";
|
import { linkDetails, UserAuth } from "types";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
links: linkDetails[];
|
links: linkDetails[];
|
||||||
@ -14,18 +17,37 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEditLink, userAuth }) => {
|
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEditLink, userAuth }) => {
|
||||||
|
// toast
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setToastAlert({
|
||||||
|
message: "The URL has been successfully copied to your clipboard",
|
||||||
|
type: "success",
|
||||||
|
title: "Copied to clipboard",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
|
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
|
||||||
<div className="flex items-start justify-between gap-2 w-full">
|
<div className="flex items-start justify-between gap-2 w-full">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start truncate gap-2">
|
||||||
<span className="py-1">
|
<span className="py-1">
|
||||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs break-all">{link.title && link.title !== "" ? link.title : link.url}</span>
|
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url}>
|
||||||
|
<span
|
||||||
|
className="text-xs truncate cursor-pointer"
|
||||||
|
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
|
||||||
|
>
|
||||||
|
{link.title && link.title !== "" ? link.title : link.url}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isNotAllowed && (
|
{!isNotAllowed && (
|
||||||
|
@ -130,7 +130,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="flex-grow truncate h-full flex items-center justify-between gap-2">
|
<div className="flex-grow truncate h-full flex items-center justify-between gap-2">
|
||||||
<div className="flex-grow truncate">
|
<div className="flex-grow truncate">
|
||||||
<IssueGanttSidebarBlock data={block.data} handleIssue={blockUpdateHandler} />
|
<IssueGanttSidebarBlock data={block.data} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||||
{duration} day{duration > 1 ? "s" : ""}
|
{duration} day{duration > 1 ? "s" : ""}
|
||||||
|
@ -138,7 +138,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="flex-grow truncate h-full flex items-center justify-between gap-2">
|
<div className="flex-grow truncate h-full flex items-center justify-between gap-2">
|
||||||
<div className="flex-grow truncate">
|
<div className="flex-grow truncate">
|
||||||
<IssueGanttSidebarBlock data={block.data} handleIssue={blockUpdateHandler} />
|
<IssueGanttSidebarBlock data={block.data} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||||
{duration} day{duration > 1 ? "s" : ""}
|
{duration} day{duration > 1 ? "s" : ""}
|
||||||
|
@ -7,13 +7,13 @@ export * from "./delete-issue-modal";
|
|||||||
export * from "./description-form";
|
export * from "./description-form";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
export * from "./issue-layouts";
|
export * from "./issue-layouts";
|
||||||
|
export * from "./issue-peek-overview";
|
||||||
export * from "./main-content";
|
export * from "./main-content";
|
||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
export * from "./parent-issues-list-modal";
|
export * from "./parent-issues-list-modal";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./issue-reaction";
|
export * from "./issue-reaction";
|
||||||
export * from "./peek-overview";
|
|
||||||
export * from "./confirm-issue-discard";
|
export * from "./confirm-issue-discard";
|
||||||
|
|
||||||
// draft issue
|
// draft issue
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { FC, useCallback } from "react";
|
import { FC, useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// components
|
// components
|
||||||
import { CalendarChart } from "components/issues";
|
import { CalendarChart, IssuePeekOverview } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import {
|
import {
|
||||||
@ -41,8 +40,11 @@ interface IBaseCalendarRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||||
const { issueStore, issuesFilterStore, calendarViewStore, QuickActions, issueActions, viewId, handleDragDrop } =
|
const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, handleDragDrop } = props;
|
||||||
props;
|
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||||
|
|
||||||
const displayFilters = issuesFilterStore.issueFilters?.displayFilters;
|
const displayFilters = issuesFilterStore.issueFilters?.displayFilters;
|
||||||
|
|
||||||
@ -67,38 +69,49 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
|||||||
issueActions[action]!(issue);
|
issueActions[action]!(issue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[issueStore]
|
[issueActions]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
<>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||||
<CalendarChart
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
issues={issues}
|
<CalendarChart
|
||||||
groupedIssueIds={groupedIssueIds}
|
issues={issues}
|
||||||
layout={displayFilters?.calendar?.layout}
|
groupedIssueIds={groupedIssueIds}
|
||||||
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
layout={displayFilters?.calendar?.layout}
|
||||||
handleIssues={handleIssues}
|
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
||||||
quickActions={(issue) => (
|
quickActions={(issue) => (
|
||||||
<QuickActions
|
<QuickActions
|
||||||
issue={issue}
|
issue={issue}
|
||||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)}
|
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)}
|
||||||
handleUpdate={
|
handleUpdate={
|
||||||
issueActions[EIssueActions.UPDATE]
|
issueActions[EIssueActions.UPDATE]
|
||||||
? async (data) => handleIssues(issue.target_date ?? "", data, EIssueActions.UPDATE)
|
? async (data) => handleIssues(issue.target_date ?? "", data, EIssueActions.UPDATE)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
handleRemoveFromView={
|
handleRemoveFromView={
|
||||||
issueActions[EIssueActions.REMOVE]
|
issueActions[EIssueActions.REMOVE]
|
||||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE)
|
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
quickAddCallback={issueStore.quickAddIssue}
|
quickAddCallback={issueStore.quickAddIssue}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
|
/>
|
||||||
|
</DragDropContext>
|
||||||
|
</div>
|
||||||
|
{workspaceSlug && peekIssueId && peekProjectId && (
|
||||||
|
<IssuePeekOverview
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={peekProjectId.toString()}
|
||||||
|
issueId={peekIssueId.toString()}
|
||||||
|
handleIssue={(issueToUpdate) =>
|
||||||
|
handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, EIssueActions.UPDATE)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</DragDropContext>
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,6 @@ import { Spinner } from "@plane/ui";
|
|||||||
// types
|
// types
|
||||||
import { ICalendarWeek } from "./types";
|
import { ICalendarWeek } from "./types";
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import { EIssueActions } from "../types";
|
|
||||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -17,7 +16,6 @@ type Props = {
|
|||||||
groupedIssueIds: IGroupedIssues;
|
groupedIssueIds: IGroupedIssues;
|
||||||
layout: "month" | "week" | undefined;
|
layout: "month" | "week" | undefined;
|
||||||
showWeekends: boolean;
|
showWeekends: boolean;
|
||||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
|
||||||
quickActions: (issue: IIssue) => React.ReactNode;
|
quickActions: (issue: IIssue) => React.ReactNode;
|
||||||
quickAddCallback?: (
|
quickAddCallback?: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -29,7 +27,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarChart: React.FC<Props> = observer((props) => {
|
export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||||
const { issues, groupedIssueIds, layout, showWeekends, handleIssues, quickActions, quickAddCallback, viewId } = props;
|
const { issues, groupedIssueIds, layout, showWeekends, quickActions, quickAddCallback, viewId } = props;
|
||||||
|
|
||||||
const { calendar: calendarStore } = useMobxStore();
|
const { calendar: calendarStore } = useMobxStore();
|
||||||
|
|
||||||
@ -60,7 +58,6 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
groupedIssueIds={groupedIssueIds}
|
groupedIssueIds={groupedIssueIds}
|
||||||
enableQuickIssueCreate
|
enableQuickIssueCreate
|
||||||
handleIssues={handleIssues}
|
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
@ -74,7 +71,6 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
groupedIssueIds={groupedIssueIds}
|
groupedIssueIds={groupedIssueIds}
|
||||||
enableQuickIssueCreate
|
enableQuickIssueCreate
|
||||||
handleIssues={handleIssues}
|
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
|
@ -10,14 +10,12 @@ import { renderDateFormat } from "helpers/date-time.helper";
|
|||||||
// constants
|
// constants
|
||||||
import { MONTHS_LIST } from "constants/calendar";
|
import { MONTHS_LIST } from "constants/calendar";
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import { EIssueActions } from "../types";
|
|
||||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
date: ICalendarDate;
|
date: ICalendarDate;
|
||||||
issues: IIssueResponse | undefined;
|
issues: IIssueResponse | undefined;
|
||||||
groupedIssueIds: IGroupedIssues;
|
groupedIssueIds: IGroupedIssues;
|
||||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
|
||||||
quickActions: (issue: IIssue) => React.ReactNode;
|
quickActions: (issue: IIssue) => React.ReactNode;
|
||||||
enableQuickIssueCreate?: boolean;
|
enableQuickIssueCreate?: boolean;
|
||||||
quickAddCallback?: (
|
quickAddCallback?: (
|
||||||
@ -30,16 +28,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const { date, issues, groupedIssueIds, quickActions, enableQuickIssueCreate, quickAddCallback, viewId } = props;
|
||||||
date,
|
|
||||||
issues,
|
|
||||||
groupedIssueIds,
|
|
||||||
handleIssues,
|
|
||||||
quickActions,
|
|
||||||
enableQuickIssueCreate,
|
|
||||||
quickAddCallback,
|
|
||||||
viewId,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||||
|
|
||||||
@ -81,12 +70,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
|||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
<CalendarIssueBlocks
|
<CalendarIssueBlocks issues={issues} issueIdList={issueIdList} quickActions={quickActions} />
|
||||||
issues={issues}
|
|
||||||
issueIdList={issueIdList}
|
|
||||||
handleIssues={handleIssues}
|
|
||||||
quickActions={quickActions}
|
|
||||||
/>
|
|
||||||
{enableQuickIssueCreate && (
|
{enableQuickIssueCreate && (
|
||||||
<div className="py-1 px-2">
|
<div className="py-1 px-2">
|
||||||
<CalendarQuickAddIssueForm
|
<CalendarQuickAddIssueForm
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Draggable } from "@hello-pangea/dnd";
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
// components
|
// components
|
||||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import { EIssueActions } from "../types";
|
|
||||||
import { IIssueResponse } from "store/issues/types";
|
import { IIssueResponse } from "store/issues/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues: IIssueResponse | undefined;
|
issues: IIssueResponse | undefined;
|
||||||
issueIdList: string[] | null;
|
issueIdList: string[] | null;
|
||||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
|
||||||
quickActions: (issue: IIssue) => React.ReactNode;
|
quickActions: (issue: IIssue) => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||||
const { issues, issueIdList, handleIssues, quickActions } = props;
|
const { issues, issueIdList, quickActions } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleIssuePeekOverview = (issue: IIssue) => {
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -28,45 +37,46 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className="p-1 px-2 relative"
|
className="p-1 px-2 relative cursor-pointer"
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
>
|
>
|
||||||
{issue?.tempId !== undefined && (
|
{issue?.tempId !== undefined && (
|
||||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
|
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center justify-between gap-1.5 border-[0.5px] border-custom-border-100 ${
|
||||||
snapshot.isDragging
|
snapshot.isDragging
|
||||||
? "shadow-custom-shadow-rg bg-custom-background-90"
|
? "shadow-custom-shadow-rg bg-custom-background-90"
|
||||||
: "bg-custom-background-100 hover:bg-custom-background-90"
|
: "bg-custom-background-100 hover:bg-custom-background-90"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<div className="flex items-center gap-1.5 h-full">
|
||||||
className="h-full w-0.5 rounded flex-shrink-0"
|
<span
|
||||||
style={{
|
className="h-full w-0.5 rounded flex-shrink-0"
|
||||||
backgroundColor: issue.state_detail.color,
|
style={{
|
||||||
}}
|
backgroundColor: issue.state_detail.color,
|
||||||
/>
|
}}
|
||||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
/>
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||||
</div>
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
<IssuePeekOverview
|
</div>
|
||||||
workspaceSlug={issue?.workspace_detail?.slug}
|
|
||||||
projectId={issue?.project_detail?.id}
|
|
||||||
issueId={issue?.id}
|
|
||||||
// TODO: add the logic here
|
|
||||||
handleIssue={(issueToUpdate) => {
|
|
||||||
handleIssues(issue.target_date ?? "", { ...issue, ...issueToUpdate }, EIssueActions.UPDATE);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<div className="text-xs truncate">{issue.name}</div>
|
<div className="text-xs truncate">{issue.name}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IssuePeekOverview>
|
</div>
|
||||||
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
|
<div
|
||||||
|
className="hidden group-hover/calendar-block:block"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{quickActions(issue)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -9,14 +9,12 @@ import { renderDateFormat } from "helpers/date-time.helper";
|
|||||||
// types
|
// types
|
||||||
import { ICalendarDate, ICalendarWeek } from "./types";
|
import { ICalendarDate, ICalendarWeek } from "./types";
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import { EIssueActions } from "../types";
|
|
||||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues: IIssueResponse | undefined;
|
issues: IIssueResponse | undefined;
|
||||||
groupedIssueIds: IGroupedIssues;
|
groupedIssueIds: IGroupedIssues;
|
||||||
week: ICalendarWeek | undefined;
|
week: ICalendarWeek | undefined;
|
||||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
|
||||||
quickActions: (issue: IIssue) => React.ReactNode;
|
quickActions: (issue: IIssue) => React.ReactNode;
|
||||||
enableQuickIssueCreate?: boolean;
|
enableQuickIssueCreate?: boolean;
|
||||||
quickAddCallback?: (
|
quickAddCallback?: (
|
||||||
@ -29,16 +27,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const { issues, groupedIssueIds, week, quickActions, enableQuickIssueCreate, quickAddCallback, viewId } = props;
|
||||||
issues,
|
|
||||||
groupedIssueIds,
|
|
||||||
week,
|
|
||||||
handleIssues,
|
|
||||||
quickActions,
|
|
||||||
enableQuickIssueCreate,
|
|
||||||
quickAddCallback,
|
|
||||||
viewId,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||||
|
|
||||||
@ -62,7 +51,6 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
|||||||
date={date}
|
date={date}
|
||||||
issues={issues}
|
issues={issues}
|
||||||
groupedIssueIds={groupedIssueIds}
|
groupedIssueIds={groupedIssueIds}
|
||||||
handleIssues={handleIssues}
|
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
|
||||||
// components
|
// components
|
||||||
import { IssueGanttBlock } from "components/issues";
|
import { IssueGanttBlock, IssuePeekOverview } from "components/issues";
|
||||||
import {
|
import {
|
||||||
GanttChartRoot,
|
GanttChartRoot,
|
||||||
IBlockUpdateData,
|
IBlockUpdateData,
|
||||||
@ -41,9 +40,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
const { issueFiltersStore, issueStore, viewId } = props;
|
const { issueFiltersStore, issueStore, viewId } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string };
|
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||||
|
|
||||||
const { projectDetails } = useProjectDetails();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
user: { currentProjectRole },
|
user: { currentProjectRole },
|
||||||
@ -61,7 +58,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
|
|
||||||
//Todo fix sort order in the structure
|
//Todo fix sort order in the structure
|
||||||
issueStore.updateIssue(
|
issueStore.updateIssue(
|
||||||
workspaceSlug,
|
workspaceSlug.toString(),
|
||||||
issue.project,
|
issue.project,
|
||||||
issue.id,
|
issue.id,
|
||||||
{
|
{
|
||||||
@ -83,7 +80,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
loaderTitle="Issues"
|
loaderTitle="Issues"
|
||||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||||
blockUpdateHandler={updateIssue}
|
blockUpdateHandler={updateIssue}
|
||||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} handleIssue={updateIssue} />}
|
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} />}
|
||||||
sidebarToRender={(props) => (
|
sidebarToRender={(props) => (
|
||||||
<IssueGanttSidebar
|
<IssueGanttSidebar
|
||||||
{...props}
|
{...props}
|
||||||
@ -98,6 +95,17 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{workspaceSlug && peekIssueId && peekProjectId && (
|
||||||
|
<IssuePeekOverview
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={peekProjectId.toString()}
|
||||||
|
issueId={peekIssueId.toString()}
|
||||||
|
handleIssue={(issueToUpdate) => {
|
||||||
|
// TODO: update the logic here
|
||||||
|
updateIssue(issueToUpdate as IIssue, {});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, StateGroupIcon } from "@plane/ui";
|
import { Tooltip, StateGroupIcon } from "@plane/ui";
|
||||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
|
||||||
import { IBlockUpdateData } from "components/gantt-chart";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDate } from "helpers/date-time.helper";
|
import { renderShortDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export const IssueGanttBlock = ({
|
export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
|
||||||
data,
|
const router = useRouter();
|
||||||
handleIssue,
|
|
||||||
}: {
|
const handleIssuePeekOverview = () => {
|
||||||
data: IIssue;
|
const { query } = router;
|
||||||
handleIssue: (block: IIssue, payload: IBlockUpdateData) => void;
|
|
||||||
}) => (
|
router.push({
|
||||||
<IssuePeekOverview
|
pathname: router.pathname,
|
||||||
workspaceSlug={data?.workspace_detail?.slug}
|
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
|
||||||
projectId={data?.project_detail?.id}
|
});
|
||||||
issueId={data?.id}
|
};
|
||||||
handleIssue={(issueToUpdate) => handleIssue({ ...data, ...issueToUpdate }, {})}
|
|
||||||
>
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center relative h-full w-full rounded cursor-pointer"
|
className="flex items-center relative h-full w-full rounded cursor-pointer"
|
||||||
style={{ backgroundColor: data?.state_detail?.color }}
|
style={{ backgroundColor: data?.state_detail?.color }}
|
||||||
|
onClick={handleIssuePeekOverview}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -41,24 +41,24 @@ export const IssueGanttBlock = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</IssuePeekOverview>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
// rendering issues on gantt sidebar
|
// rendering issues on gantt sidebar
|
||||||
export const IssueGanttSidebarBlock = ({
|
export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
|
||||||
data,
|
const router = useRouter();
|
||||||
handleIssue,
|
|
||||||
}: {
|
const handleIssuePeekOverview = () => {
|
||||||
data: IIssue;
|
const { query } = router;
|
||||||
handleIssue: (block: IIssue, payload: IBlockUpdateData) => void;
|
|
||||||
}) => (
|
router.push({
|
||||||
<IssuePeekOverview
|
pathname: router.pathname,
|
||||||
workspaceSlug={data?.workspace_detail?.slug}
|
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
|
||||||
projectId={data?.project_detail?.id}
|
});
|
||||||
issueId={data?.id}
|
};
|
||||||
handleIssue={(issueToUpdate) => handleIssue({ ...data, ...issueToUpdate }, {})}
|
|
||||||
>
|
return (
|
||||||
<div className="relative w-full flex items-center gap-2 h-full cursor-pointer">
|
<div className="relative w-full flex items-center gap-2 h-full cursor-pointer" onClick={handleIssuePeekOverview}>
|
||||||
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
|
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
|
||||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||||
{data?.project_detail?.identifier} {data?.sequence_id}
|
{data?.project_detail?.identifier} {data?.sequence_id}
|
||||||
@ -67,5 +67,5 @@ export const IssueGanttSidebarBlock = ({
|
|||||||
<span className="text-sm font-medium flex-grow truncate">{data?.name}</span>
|
<span className="text-sm font-medium flex-grow truncate">{data?.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</IssuePeekOverview>
|
);
|
||||||
);
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { FC, useCallback, useState } from "react";
|
import { FC, useCallback, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { DragDropContext, Droppable } from "@hello-pangea/dnd";
|
import { DragDropContext, Droppable } from "@hello-pangea/dnd";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
// mobx store
|
||||||
@ -29,7 +30,7 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
|||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
import { EProjectStore } from "store/command-palette.store";
|
import { EProjectStore } from "store/command-palette.store";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import { IssuePeekOverview } from "components/issues";
|
||||||
|
|
||||||
export interface IBaseKanBanLayout {
|
export interface IBaseKanBanLayout {
|
||||||
issueStore:
|
issueStore:
|
||||||
@ -79,7 +80,10 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
handleDragDrop,
|
handleDragDrop,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
} = props;
|
} = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||||
|
// mobx store
|
||||||
const {
|
const {
|
||||||
project: { workspaceProjects },
|
project: { workspaceProjects },
|
||||||
projectLabel: { projectLabels },
|
projectLabel: { projectLabels },
|
||||||
@ -134,7 +138,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
issueActions[action]!(issue);
|
issueActions[action]!(issue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[issueStore]
|
[issueActions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||||
@ -264,6 +268,17 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
)}
|
)}
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{workspaceSlug && peekIssueId && peekProjectId && (
|
||||||
|
<IssuePeekOverview
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={peekProjectId.toString()}
|
||||||
|
issueId={peekIssueId.toString()}
|
||||||
|
handleIssue={(issueToUpdate) =>
|
||||||
|
handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, EIssueActions.UPDATE)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Draggable } from "@hello-pangea/dnd";
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
// components
|
// components
|
||||||
import { KanBanProperties } from "./properties";
|
import { KanBanProperties } from "./properties";
|
||||||
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayProperties, IIssue } from "types";
|
import { IIssueDisplayProperties, IIssue } from "types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
sub_group_id: string;
|
sub_group_id: string;
|
||||||
@ -33,11 +34,22 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
|||||||
displayProperties,
|
displayProperties,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
} = props;
|
} = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
||||||
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
|
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleIssuePeekOverview = () => {
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Draggable draggableId={issue.id} index={index}>
|
<Draggable draggableId={issue.id} index={index}>
|
||||||
@ -47,6 +59,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
|||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
|
onClick={handleIssuePeekOverview}
|
||||||
>
|
>
|
||||||
{issue.tempId !== undefined && (
|
{issue.tempId !== undefined && (
|
||||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||||
@ -68,23 +81,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
|||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<IssuePeekOverview
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
workspaceSlug={issue?.workspace_detail?.slug}
|
<div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
|
||||||
projectId={issue?.project_detail?.id}
|
</Tooltip>
|
||||||
issueId={issue?.id}
|
|
||||||
handleIssue={(issueToUpdate) => {
|
|
||||||
handleIssues(
|
|
||||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
|
||||||
!columnId && columnId === "null" ? null : columnId,
|
|
||||||
{ ...issue, ...issueToUpdate },
|
|
||||||
EIssueActions.UPDATE
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
|
||||||
<div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
|
|
||||||
</Tooltip>
|
|
||||||
</IssuePeekOverview>
|
|
||||||
<div>
|
<div>
|
||||||
<KanBanProperties
|
<KanBanProperties
|
||||||
sub_group_id={sub_group_id}
|
sub_group_id={sub_group_id}
|
||||||
|
@ -23,6 +23,8 @@ import {
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { IIssueResponse } from "store/issues/types";
|
import { IIssueResponse } from "store/issues/types";
|
||||||
import { EProjectStore } from "store/command-palette.store";
|
import { EProjectStore } from "store/command-palette.store";
|
||||||
|
import { IssuePeekOverview } from "components/issues";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
enum EIssueActions {
|
enum EIssueActions {
|
||||||
UPDATE = "update",
|
UPDATE = "update",
|
||||||
@ -68,7 +70,10 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
currentStore,
|
currentStore,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
} = props;
|
} = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||||
|
// mobx store
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
@ -144,6 +149,15 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{workspaceSlug && peekIssueId && peekProjectId && (
|
||||||
|
<IssuePeekOverview
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={peekProjectId.toString()}
|
||||||
|
issueId={peekIssueId.toString()}
|
||||||
|
handleIssue={(issueToUpdate) => handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
// components
|
// components
|
||||||
import { ListProperties } from "./properties";
|
import { ListProperties } from "./properties";
|
||||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, Tooltip } from "@plane/ui";
|
import { Spinner, Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -19,11 +19,21 @@ interface IssueBlockProps {
|
|||||||
|
|
||||||
export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||||
const { columnId, issue, handleIssues, quickActions, displayProperties, isReadonly } = props;
|
const { columnId, issue, handleIssues, quickActions, displayProperties, isReadonly } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
|
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
|
||||||
handleIssues(issueToUpdate, EIssueActions.UPDATE);
|
handleIssues(issueToUpdate, EIssueActions.UPDATE);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleIssuePeekOverview = () => {
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-sm p-3 relative bg-custom-background-100 flex items-center gap-3">
|
<div className="text-sm p-3 relative bg-custom-background-100 flex items-center gap-3">
|
||||||
@ -36,20 +46,14 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
|||||||
{issue?.tempId !== undefined && (
|
{issue?.tempId !== undefined && (
|
||||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||||
)}
|
)}
|
||||||
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<IssuePeekOverview
|
<div
|
||||||
workspaceSlug={issue?.workspace_detail?.slug}
|
className="line-clamp-1 text-sm font-medium text-custom-text-100 w-full cursor-pointer"
|
||||||
projectId={issue?.project_detail?.id}
|
onClick={handleIssuePeekOverview}
|
||||||
issueId={issue?.id}
|
>
|
||||||
isArchived={issue?.archived_at !== null}
|
{issue.name}
|
||||||
handleIssue={(issueToUpdate) => {
|
</div>
|
||||||
handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE);
|
</Tooltip>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
|
||||||
<div className="line-clamp-1 text-sm font-medium text-custom-text-100 w-full">{issue.name}</div>
|
|
||||||
</Tooltip>
|
|
||||||
</IssuePeekOverview>
|
|
||||||
|
|
||||||
<div className="ml-auto flex-shrink-0 flex items-center gap-2">
|
<div className="ml-auto flex-shrink-0 flex items-center gap-2">
|
||||||
{!issue?.tempId ? (
|
{!issue?.tempId ? (
|
||||||
|
@ -77,7 +77,7 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
globalViewIssuesStore.updateIssueStructure(type ?? globalViewId!.toString(), payload);
|
globalViewIssuesStore.updateIssueStructure(type ?? globalViewId!.toString(), payload);
|
||||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, data);
|
// issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, data);
|
||||||
},
|
},
|
||||||
[globalViewId, globalViewIssuesStore, workspaceSlug, issueDetailStore]
|
[globalViewId, globalViewIssuesStore, workspaceSlug, issueDetailStore]
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// components
|
// components
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// helpers
|
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueDisplayProperties } from "types";
|
import { IIssue, IIssueDisplayProperties } from "types";
|
||||||
|
|
||||||
@ -16,13 +12,6 @@ type Props = {
|
|||||||
handleToggleExpand: (issueId: string) => void;
|
handleToggleExpand: (issueId: string) => void;
|
||||||
properties: IIssueDisplayProperties;
|
properties: IIssueDisplayProperties;
|
||||||
quickActions: (issue: IIssue) => React.ReactNode;
|
quickActions: (issue: IIssue) => React.ReactNode;
|
||||||
setIssuePeekOverView: React.Dispatch<
|
|
||||||
React.SetStateAction<{
|
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
issueId: string;
|
|
||||||
} | null>
|
|
||||||
>;
|
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
nestingLevel: number;
|
nestingLevel: number;
|
||||||
};
|
};
|
||||||
@ -31,40 +20,20 @@ export const IssueColumn: React.FC<Props> = ({
|
|||||||
issue,
|
issue,
|
||||||
expanded,
|
expanded,
|
||||||
handleToggleExpand,
|
handleToggleExpand,
|
||||||
setIssuePeekOverView,
|
|
||||||
properties,
|
properties,
|
||||||
quickActions,
|
quickActions,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
nestingLevel,
|
nestingLevel,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
// router
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const handleCopyText = () => {
|
|
||||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link Copied!",
|
|
||||||
message: "Issue link copied to clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIssuePeekOverview = (issue: IIssue) => {
|
const handleIssuePeekOverview = (issue: IIssue) => {
|
||||||
const { query } = router;
|
const { query } = router;
|
||||||
setIssuePeekOverView({
|
|
||||||
workspaceSlug: issue?.workspace_detail?.slug,
|
|
||||||
projectId: issue?.project_detail?.id,
|
|
||||||
issueId: issue?.id,
|
|
||||||
});
|
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query, peekIssueId: issue?.id },
|
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ import { IssueColumn } from "components/issues";
|
|||||||
import useSubIssue from "hooks/use-sub-issue";
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueDisplayProperties } from "types";
|
import { IIssue, IIssueDisplayProperties } from "types";
|
||||||
import { EIssueActions } from "components/issues/issue-layouts/types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
@ -14,13 +13,6 @@ type Props = {
|
|||||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
properties: IIssueDisplayProperties;
|
properties: IIssueDisplayProperties;
|
||||||
quickActions: (issue: IIssue) => React.ReactNode;
|
quickActions: (issue: IIssue) => React.ReactNode;
|
||||||
setIssuePeekOverView: React.Dispatch<
|
|
||||||
React.SetStateAction<{
|
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
issueId: string;
|
|
||||||
} | null>
|
|
||||||
>;
|
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
nestingLevel?: number;
|
nestingLevel?: number;
|
||||||
};
|
};
|
||||||
@ -29,7 +21,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
issue,
|
issue,
|
||||||
expandedIssues,
|
expandedIssues,
|
||||||
setExpandedIssues,
|
setExpandedIssues,
|
||||||
setIssuePeekOverView,
|
|
||||||
properties,
|
properties,
|
||||||
quickActions,
|
quickActions,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
@ -58,7 +49,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
expanded={isExpanded}
|
expanded={isExpanded}
|
||||||
handleToggleExpand={handleToggleExpand}
|
handleToggleExpand={handleToggleExpand}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
setIssuePeekOverView={setIssuePeekOverView}
|
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
nestingLevel={nestingLevel}
|
nestingLevel={nestingLevel}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
@ -76,7 +66,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
setIssuePeekOverView={setIssuePeekOverView}
|
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
nestingLevel={nestingLevel + 1}
|
nestingLevel={nestingLevel + 1}
|
||||||
/>
|
/>
|
||||||
|
@ -2,8 +2,12 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues";
|
import {
|
||||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
IssuePeekOverview,
|
||||||
|
SpreadsheetColumnsList,
|
||||||
|
SpreadsheetIssuesColumn,
|
||||||
|
SpreadsheetQuickAddIssueForm,
|
||||||
|
} from "components/issues";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types";
|
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types";
|
||||||
@ -47,22 +51,14 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
disableUserActions,
|
disableUserActions,
|
||||||
enableQuickCreateIssue,
|
enableQuickCreateIssue,
|
||||||
} = props;
|
} = props;
|
||||||
|
// states
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
const [issuePeekOverview, setIssuePeekOverView] = useState<{
|
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
issueId: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
// refs
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { cycleId, moduleId } = router.query;
|
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
@ -116,7 +112,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
properties={displayProperties}
|
properties={displayProperties}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
setIssuePeekOverView={setIssuePeekOverView}
|
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
)}
|
)}
|
||||||
@ -185,11 +180,11 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
))} */}
|
))} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{issuePeekOverview && (
|
{workspaceSlug && peekIssueId && peekProjectId && (
|
||||||
<IssuePeekOverview
|
<IssuePeekOverview
|
||||||
workspaceSlug={issuePeekOverview?.workspaceSlug}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={issuePeekOverview?.projectId}
|
projectId={peekProjectId.toString()}
|
||||||
issueId={issuePeekOverview?.issueId}
|
issueId={peekIssueId.toString()}
|
||||||
handleIssue={(issueToUpdate: any) => handleIssues(issueToUpdate, EIssueActions.UPDATE)}
|
handleIssue={(issueToUpdate: any) => handleIssues(issueToUpdate, EIssueActions.UPDATE)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -17,17 +17,19 @@ import {
|
|||||||
SidebarPrioritySelect,
|
SidebarPrioritySelect,
|
||||||
SidebarStateSelect,
|
SidebarStateSelect,
|
||||||
} from "../sidebar-select";
|
} from "../sidebar-select";
|
||||||
|
// services
|
||||||
|
import { IssueService } from "services/issue";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
import { LinkModal, LinksList } from "components/core";
|
import { LinkModal, LinksList } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types";
|
import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types";
|
||||||
|
// fetch-keys
|
||||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
// services
|
// constants
|
||||||
import { IssueService } from "services/issue";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
interface IPeekOverviewProperties {
|
interface IPeekOverviewProperties {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
@ -43,8 +45,11 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
const [linkModal, setLinkModal] = useState(false);
|
const [linkModal, setLinkModal] = useState(false);
|
||||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
||||||
|
|
||||||
const { user: userStore, cycleIssue: cycleIssueStore, moduleIssue: moduleIssueStore } = useMobxStore();
|
const {
|
||||||
const userRole = userStore.currentProjectRole;
|
user: { currentProjectRole },
|
||||||
|
cycleIssues: { addIssueToCycle },
|
||||||
|
moduleIssues: { addIssueToModule },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -72,15 +77,16 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
const handleParent = (_parent: string) => {
|
const handleParent = (_parent: string) => {
|
||||||
issueUpdate({ ...issue, parent: _parent });
|
issueUpdate({ ...issue, parent: _parent });
|
||||||
};
|
};
|
||||||
const addIssueToCycle = async (cycleId: string) => {
|
const handleAddIssueToCycle = async (cycleId: string) => {
|
||||||
if (!workspaceSlug || !issue || !cycleId) return;
|
if (!workspaceSlug || !issue || !cycleId) return;
|
||||||
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, [issue.id]);
|
|
||||||
|
addIssueToCycle(workspaceSlug.toString(), cycleId, [issue.id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addIssueToModule = async (moduleId: string) => {
|
const handleAddIssueToModule = async (moduleId: string) => {
|
||||||
if (!workspaceSlug || !issue || !moduleId) return;
|
if (!workspaceSlug || !issue || !moduleId) return;
|
||||||
|
|
||||||
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, [issue.id]);
|
addIssueToModule(workspaceSlug.toString(), moduleId, [issue.id]);
|
||||||
};
|
};
|
||||||
const handleLabels = (formData: Partial<IIssue>) => {
|
const handleLabels = (formData: Partial<IIssue>) => {
|
||||||
issueUpdate({ ...issue, ...formData });
|
issueUpdate({ ...issue, ...formData });
|
||||||
@ -303,7 +309,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<div>
|
<div>
|
||||||
<SidebarCycleSelect
|
<SidebarCycleSelect
|
||||||
issueDetail={issue}
|
issueDetail={issue}
|
||||||
handleCycleChange={addIssueToCycle}
|
handleCycleChange={handleAddIssueToCycle}
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -317,7 +323,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<div>
|
<div>
|
||||||
<SidebarModuleSelect
|
<SidebarModuleSelect
|
||||||
issueDetail={issue}
|
issueDetail={issue}
|
||||||
handleModuleChange={addIssueToModule}
|
handleModuleChange={handleAddIssueToModule}
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -370,10 +376,10 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
handleDeleteLink={handleDeleteLink}
|
handleDeleteLink={handleDeleteLink}
|
||||||
handleEditLink={handleEditLink}
|
handleEditLink={handleEditLink}
|
||||||
userAuth={{
|
userAuth={{
|
||||||
isGuest: userRole === 5,
|
isGuest: currentProjectRole === EUserWorkspaceRoles.GUEST,
|
||||||
isViewer: userRole === 10,
|
isViewer: currentProjectRole === EUserWorkspaceRoles.VIEWER,
|
||||||
isMember: userRole === 15,
|
isMember: currentProjectRole === EUserWorkspaceRoles.MEMBER,
|
||||||
isOwner: userRole === 20,
|
isOwner: currentProjectRole === EUserWorkspaceRoles.ADMIN,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import { FC, Fragment, ReactNode } from "react";
|
import { FC, Fragment, ReactNode } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
import useSWR from "swr";
|
||||||
import { IssueView } from "./view";
|
// mobx store
|
||||||
// hooks
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// types
|
|
||||||
import { IIssue } from "types";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
// components
|
||||||
|
import { IssueView } from "./view";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
interface IIssuePeekOverview {
|
interface IIssuePeekOverview {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -27,7 +26,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
const { workspaceSlug, projectId, issueId, handleIssue, children, isArchived = false } = props;
|
const { workspaceSlug, projectId, issueId, handleIssue, children, isArchived = false } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { peekIssueId } = router.query as { peekIssueId: string };
|
const { peekIssueId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
user: userStore,
|
user: userStore,
|
||||||
@ -36,18 +35,18 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
archivedIssueDetail: archivedIssueDetailStore,
|
archivedIssueDetail: archivedIssueDetailStore,
|
||||||
archivedIssues: archivedIssuesStore,
|
archivedIssues: archivedIssuesStore,
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
}: RootStore = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
|
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
|
||||||
? `ISSUE_PEEK_OVERVIEW_${workspaceSlug}_${projectId}_${peekIssueId}`
|
? `ISSUE_DETAILS_${workspaceSlug}_${projectId}_${peekIssueId}`
|
||||||
: null,
|
: null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) {
|
if (workspaceSlug && projectId && issueId && issueId === peekIssueId) {
|
||||||
if (isArchived) await archivedIssueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, issueId);
|
if (isArchived) await archivedIssueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId);
|
||||||
else await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, issueId);
|
else await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -76,10 +75,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
const isLoading = isArchived ? archivedIssueDetailStore.loader : issueDetailStore.loader;
|
const isLoading = isArchived ? archivedIssueDetailStore.loader : issueDetailStore.loader;
|
||||||
|
|
||||||
const issueUpdate = (_data: Partial<IIssue>) => {
|
const issueUpdate = (_data: Partial<IIssue>) => {
|
||||||
if (handleIssue) {
|
if (handleIssue) handleIssue(_data);
|
||||||
handleIssue(_data);
|
|
||||||
issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueReactionCreate = (reaction: string) =>
|
const issueReactionCreate = (reaction: string) =>
|
||||||
@ -114,6 +110,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
if (query.peekIssueId) {
|
if (query.peekIssueId) {
|
||||||
issueDetailStore.setPeekId(null);
|
issueDetailStore.setPeekId(null);
|
||||||
delete query.peekIssueId;
|
delete query.peekIssueId;
|
||||||
|
delete query.peekProjectId;
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query },
|
query: { ...query },
|
||||||
|
@ -12,7 +12,6 @@ import { DeleteIssueModal } from "../delete-issue-modal";
|
|||||||
import { DeleteArchivedIssueModal } from "../delete-archived-issue-modal";
|
import { DeleteArchivedIssueModal } from "../delete-archived-issue-modal";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import { RootStore } from "store/root";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
@ -90,7 +89,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { peekIssueId } = router.query as { peekIssueId: string };
|
const { peekIssueId } = router.query as { peekIssueId: string };
|
||||||
|
|
||||||
const { user: userStore, issueDetail: issueDetailStore }: RootStore = useMobxStore();
|
const { user: userStore, issueDetail: issueDetailStore } = useMobxStore();
|
||||||
|
|
||||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
@ -101,7 +100,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
const { query } = router;
|
const { query } = router;
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query, peekIssueId: issueId },
|
query: { ...query, peekIssueId: issueId, peekProjectId: projectId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -110,6 +109,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
if (query.peekIssueId) {
|
if (query.peekIssueId) {
|
||||||
issueDetailStore.setPeekId(null);
|
issueDetailStore.setPeekId(null);
|
||||||
delete query.peekIssueId;
|
delete query.peekIssueId;
|
||||||
|
delete query.peekProjectId;
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query },
|
query: { ...query },
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
// components
|
|
||||||
import {
|
|
||||||
PeekOverviewHeader,
|
|
||||||
PeekOverviewIssueActivity,
|
|
||||||
PeekOverviewIssueDetails,
|
|
||||||
PeekOverviewIssueProperties,
|
|
||||||
TPeekOverviewModes,
|
|
||||||
} from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { Loader } from "@plane/ui";
|
|
||||||
// types
|
|
||||||
import { IIssue } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
handleClose: () => void;
|
|
||||||
handleDeleteIssue: () => void;
|
|
||||||
handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
|
|
||||||
issue: IIssue | undefined;
|
|
||||||
mode: TPeekOverviewModes;
|
|
||||||
readOnly: boolean;
|
|
||||||
setMode: (mode: TPeekOverviewModes) => void;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FullScreenPeekView: React.FC<Props> = ({
|
|
||||||
handleClose,
|
|
||||||
handleDeleteIssue,
|
|
||||||
handleUpdateIssue,
|
|
||||||
issue,
|
|
||||||
mode,
|
|
||||||
readOnly,
|
|
||||||
setMode,
|
|
||||||
workspaceSlug,
|
|
||||||
}) => (
|
|
||||||
<div className="h-full w-full grid grid-cols-10 divide-x divide-custom-border-200 overflow-hidden">
|
|
||||||
<div className="h-full w-full flex flex-col col-span-7 overflow-hidden">
|
|
||||||
<div className="w-full p-5">
|
|
||||||
<PeekOverviewHeader
|
|
||||||
handleClose={handleClose}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
issue={issue}
|
|
||||||
mode={mode}
|
|
||||||
setMode={setMode}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{issue ? (
|
|
||||||
<div className="h-full w-full px-6 overflow-y-auto">
|
|
||||||
{/* issue title and description */}
|
|
||||||
<div className="w-full">
|
|
||||||
<PeekOverviewIssueDetails
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issue={issue}
|
|
||||||
readOnly={readOnly}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* divider */}
|
|
||||||
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
|
|
||||||
{/* issue activity/comments */}
|
|
||||||
<div className="w-full pb-5">
|
|
||||||
<PeekOverviewIssueActivity workspaceSlug={workspaceSlug} issue={issue} readOnly={readOnly} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Loader className="px-6">
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<div className="space-y-2 mt-3">
|
|
||||||
<Loader.Item height="20px" width="70%" />
|
|
||||||
<Loader.Item height="20px" width="60%" />
|
|
||||||
<Loader.Item height="20px" width="60%" />
|
|
||||||
</div>
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 h-full w-full overflow-y-auto">
|
|
||||||
{/* issue properties */}
|
|
||||||
<div className="w-full px-6 py-5">
|
|
||||||
{issue ? (
|
|
||||||
<PeekOverviewIssueProperties
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issue={issue}
|
|
||||||
mode="full"
|
|
||||||
readOnly={readOnly}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Loader className="mt-11 space-y-4">
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
@ -1,110 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// ui
|
|
||||||
import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui";
|
|
||||||
// icons
|
|
||||||
import { LinkIcon, MoveDiagonal, MoveRight, Trash2 } from "lucide-react";
|
|
||||||
// helpers
|
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
|
||||||
// types
|
|
||||||
import { IIssue } from "types";
|
|
||||||
import { TPeekOverviewModes } from "./layout";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
handleClose: () => void;
|
|
||||||
handleDeleteIssue: () => void;
|
|
||||||
issue: IIssue | undefined;
|
|
||||||
mode: TPeekOverviewModes;
|
|
||||||
setMode: (mode: TPeekOverviewModes) => void;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const peekModes: {
|
|
||||||
key: TPeekOverviewModes;
|
|
||||||
icon: any;
|
|
||||||
label: string;
|
|
||||||
}[] = [
|
|
||||||
{ key: "side", icon: SidePanelIcon, label: "Side Peek" },
|
|
||||||
{
|
|
||||||
key: "modal",
|
|
||||||
icon: CenterPanelIcon,
|
|
||||||
label: "Modal Peek",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "full",
|
|
||||||
icon: FullScreenPanelIcon,
|
|
||||||
label: "Full Screen Peek",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const PeekOverviewHeader: React.FC<Props> = ({
|
|
||||||
issue,
|
|
||||||
handleClose,
|
|
||||||
handleDeleteIssue,
|
|
||||||
mode,
|
|
||||||
setMode,
|
|
||||||
workspaceSlug,
|
|
||||||
}) => {
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
|
||||||
const urlToCopy = window.location.href;
|
|
||||||
|
|
||||||
copyTextToClipboard(urlToCopy).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link copied!",
|
|
||||||
message: "Issue link copied to clipboard",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentMode = peekModes.find((m) => m.key === mode);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{mode === "side" && (
|
|
||||||
<button type="button" onClick={handleClose}>
|
|
||||||
<MoveRight className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${issue?.project}/issues/${issue?.id}`}>
|
|
||||||
<a>
|
|
||||||
<MoveDiagonal className="h-3.5 w-3.5" />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<CustomSelect
|
|
||||||
value={mode}
|
|
||||||
onChange={(val: TPeekOverviewModes) => setMode(val)}
|
|
||||||
customButton={
|
|
||||||
<button type="button" className={`grid place-items-center ${mode === "full" ? "rotate-45" : ""}`}>
|
|
||||||
{currentMode && <currentMode.icon className="h-3.5 w-3.5" />}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{peekModes.map((mode) => (
|
|
||||||
<CustomSelect.Option key={mode.key} value={mode.key}>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<mode.icon className={`h-4 w-4 flex-shrink-0 -my-1 ${mode.key === "full" ? "rotate-45" : ""}`} />
|
|
||||||
{mode.label}
|
|
||||||
</div>
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
</div>
|
|
||||||
{(mode === "side" || mode === "modal") && (
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
|
||||||
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
|
||||||
<LinkIcon className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={handleDeleteIssue}>
|
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
export * from "./full-screen-peek-view";
|
|
||||||
export * from "./header";
|
|
||||||
export * from "./issue-activity";
|
|
||||||
export * from "./issue-details";
|
|
||||||
export * from "./issue-properties";
|
|
||||||
export * from "./layout";
|
|
||||||
export * from "./side-peek-view";
|
|
@ -1,88 +0,0 @@
|
|||||||
import useSWR, { mutate } from "swr";
|
|
||||||
// services
|
|
||||||
import { IssueCommentService, IssueService } from "services/issue";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
|
||||||
// components
|
|
||||||
import { AddComment, IssueActivitySection } from "components/issues";
|
|
||||||
// types
|
|
||||||
import { IIssue, IIssueComment } from "types";
|
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
workspaceSlug: string;
|
|
||||||
issue: IIssue;
|
|
||||||
readOnly: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const issueCommentService = new IssueCommentService();
|
|
||||||
const issueService = new IssueService();
|
|
||||||
|
|
||||||
export const PeekOverviewIssueActivity: React.FC<Props> = (props) => {
|
|
||||||
const { workspaceSlug, issue } = props;
|
|
||||||
// toast
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const { projectDetails } = useProjectDetails();
|
|
||||||
|
|
||||||
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
|
||||||
workspaceSlug && issue ? PROJECT_ISSUES_ACTIVITY(issue.id) : null,
|
|
||||||
workspaceSlug && issue
|
|
||||||
? () => issueService.getIssueActivities(workspaceSlug.toString(), issue?.project, issue?.id)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCommentUpdate = async (commentId: string, data: Partial<IIssueComment>) => {
|
|
||||||
if (!workspaceSlug || !issue) return;
|
|
||||||
|
|
||||||
await issueCommentService
|
|
||||||
.patchIssueComment(workspaceSlug as string, issue.project, issue.id, commentId, data)
|
|
||||||
.then(() => mutateIssueActivity());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommentDelete = async (commentId: string) => {
|
|
||||||
if (!workspaceSlug || !issue) return;
|
|
||||||
|
|
||||||
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
|
||||||
|
|
||||||
await issueCommentService
|
|
||||||
.deleteIssueComment(workspaceSlug as string, issue.project, issue.id, commentId)
|
|
||||||
.then(() => mutateIssueActivity());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddComment = async (formData: IIssueComment) => {
|
|
||||||
if (!workspaceSlug || !issue) return;
|
|
||||||
|
|
||||||
await issueCommentService
|
|
||||||
.createIssueComment(workspaceSlug.toString(), issue.project, issue.id, formData)
|
|
||||||
.then(() => {
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issue.id));
|
|
||||||
})
|
|
||||||
.catch(() =>
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Comment could not be posted. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium">Activity</h4>
|
|
||||||
<div className="mt-4">
|
|
||||||
<IssueActivitySection
|
|
||||||
activity={issueActivity}
|
|
||||||
handleCommentUpdate={handleCommentUpdate}
|
|
||||||
handleCommentDelete={handleCommentDelete}
|
|
||||||
showAccessSpecifier={projectDetails && projectDetails.is_deployed}
|
|
||||||
/>
|
|
||||||
<div className="mt-4">
|
|
||||||
<AddComment onSubmit={handleAddComment} showAccessSpecifier={projectDetails && projectDetails.is_deployed} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,30 +0,0 @@
|
|||||||
// components
|
|
||||||
import { IssueDescriptionForm, IssueReaction } from "components/issues";
|
|
||||||
// types
|
|
||||||
import { IIssue } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
|
|
||||||
issue: IIssue;
|
|
||||||
readOnly: boolean;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PeekOverviewIssueDetails: React.FC<Props> = ({ handleUpdateIssue, issue, readOnly, workspaceSlug }) => (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h6 className="font-medium text-custom-text-200">
|
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
|
||||||
</h6>
|
|
||||||
<IssueDescriptionForm
|
|
||||||
handleFormSubmit={handleUpdateIssue}
|
|
||||||
isAllowed={!readOnly}
|
|
||||||
issue={{
|
|
||||||
name: issue.name,
|
|
||||||
description_html: issue.description_html,
|
|
||||||
project_id: issue.project_detail.id,
|
|
||||||
}}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
<IssueReaction workspaceSlug={workspaceSlug} issueId={issue.id} projectId={issue.project} />
|
|
||||||
</div>
|
|
||||||
);
|
|
@ -1,187 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
// components
|
|
||||||
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// components
|
|
||||||
import {
|
|
||||||
SidebarAssigneeSelect,
|
|
||||||
SidebarPrioritySelect,
|
|
||||||
SidebarStateSelect,
|
|
||||||
TPeekOverviewModes,
|
|
||||||
} from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { CustomDatePicker } from "components/ui";
|
|
||||||
// helpers
|
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
|
||||||
// types
|
|
||||||
import { IIssue } from "types";
|
|
||||||
import { CalendarDays, LinkIcon, Signal, Trash2 } from "lucide-react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
handleDeleteIssue: () => void;
|
|
||||||
handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
|
|
||||||
issue: IIssue;
|
|
||||||
mode: TPeekOverviewModes;
|
|
||||||
readOnly: boolean;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PeekOverviewIssueProperties: FC<Props> = (props) => {
|
|
||||||
const { handleDeleteIssue, handleUpdateIssue, issue, mode, readOnly } = props;
|
|
||||||
// toast
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const startDate = issue.start_date;
|
|
||||||
const targetDate = issue.target_date;
|
|
||||||
|
|
||||||
const minDate = startDate ? new Date(startDate) : null;
|
|
||||||
minDate?.setDate(minDate.getDate());
|
|
||||||
|
|
||||||
const maxDate = targetDate ? new Date(targetDate) : null;
|
|
||||||
maxDate?.setDate(maxDate.getDate());
|
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
|
||||||
const urlToCopy = window.location.href;
|
|
||||||
|
|
||||||
copyTextToClipboard(urlToCopy).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link copied!",
|
|
||||||
message: "Issue link copied to clipboard",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={mode === "full" ? "divide-y divide-custom-border-200" : ""}>
|
|
||||||
{mode === "full" && (
|
|
||||||
<div className="flex justify-between gap-2 pb-3">
|
|
||||||
<h6 className="flex items-center gap-2 font-medium">
|
|
||||||
<StateGroupIcon stateGroup={issue.state_detail.group} color={issue.state_detail.color} />
|
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
|
||||||
</h6>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
|
||||||
<LinkIcon className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={handleDeleteIssue}>
|
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={`space-y-4 ${mode === "full" ? "pt-3" : ""}`}>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
|
||||||
<DoubleCircleIcon className="h-3.5 w-3.5 flex-shrink-0" />
|
|
||||||
<span className="flex-grow truncate">State</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-3/4">
|
|
||||||
<SidebarStateSelect
|
|
||||||
value={issue.state}
|
|
||||||
onChange={(val: string) => handleUpdateIssue({ state: val })}
|
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
|
||||||
<UserGroupIcon className="h-3.5 w-3.5" />
|
|
||||||
<span className="flex-grow truncate">Assignees</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-3/4">
|
|
||||||
<SidebarAssigneeSelect
|
|
||||||
value={issue.assignees}
|
|
||||||
onChange={(val: string[]) => handleUpdateIssue({ assignees: val })}
|
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
|
||||||
<Signal className="h-3.5 w-3.5" />
|
|
||||||
<span className="flex-grow truncate">Priority</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-3/4">
|
|
||||||
<SidebarPrioritySelect
|
|
||||||
value={issue.priority}
|
|
||||||
onChange={(val) => handleUpdateIssue({ priority: val })}
|
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
|
||||||
<span className="flex-grow truncate">Start date</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CustomDatePicker
|
|
||||||
placeholder="Select start date"
|
|
||||||
value={issue.start_date}
|
|
||||||
onChange={(val) =>
|
|
||||||
handleUpdateIssue({
|
|
||||||
start_date: val,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="bg-custom-background-80 border-none"
|
|
||||||
maxDate={maxDate ?? undefined}
|
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
|
||||||
<span className="flex-grow truncate">Due date</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CustomDatePicker
|
|
||||||
placeholder="Select due date"
|
|
||||||
value={issue.target_date}
|
|
||||||
onChange={(val) =>
|
|
||||||
handleUpdateIssue({
|
|
||||||
target_date: val,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="bg-custom-background-80 border-none"
|
|
||||||
minDate={minDate ?? undefined}
|
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <div className="flex items-center gap-2 text-sm">
|
|
||||||
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
|
||||||
<Icon iconName="change_history" className="!text-base flex-shrink-0" />
|
|
||||||
<span className="flex-grow truncate">Estimate</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-3/4">
|
|
||||||
<SidebarEstimateSelect
|
|
||||||
value={issue.estimate_point}
|
|
||||||
onChange={(val: number | null) =>handleUpdateIssue({ estimate_point: val })}
|
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
{/* <Disclosure as="div">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Disclosure.Button
|
|
||||||
as="button"
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-1 text-sm text-custom-text-200"
|
|
||||||
>
|
|
||||||
Show {open ? "Less" : "More"}
|
|
||||||
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-base" />
|
|
||||||
</Disclosure.Button>
|
|
||||||
<Disclosure.Panel as="div" className="mt-4 space-y-4">
|
|
||||||
Disclosure Panel
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,188 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { mutate } from "swr";
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
// components
|
|
||||||
import { DeleteIssueModal, FullScreenPeekView, SidePeekView } from "components/issues";
|
|
||||||
// types
|
|
||||||
import { IIssue } from "types";
|
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
handleMutation?: () => void;
|
|
||||||
projectId: string;
|
|
||||||
readOnly: boolean;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TPeekOverviewModes = "side" | "modal" | "full";
|
|
||||||
|
|
||||||
export const IssuePeekOverview: React.FC<Props> = observer(({ handleMutation, projectId, readOnly, workspaceSlug }) => {
|
|
||||||
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
|
|
||||||
const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
|
|
||||||
const [peekOverviewMode, setPeekOverviewMode] = useState<TPeekOverviewModes>("side");
|
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { peekIssue } = router.query;
|
|
||||||
|
|
||||||
const { issueDetail: issueDetailStore } = useMobxStore();
|
|
||||||
const { deleteIssue, fetchIssueDetails, issues, updateIssue } = issueDetailStore;
|
|
||||||
|
|
||||||
const issue = issues[peekIssue?.toString() ?? ""];
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
const { query } = router;
|
|
||||||
delete query.peekIssue;
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: { ...query },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateIssue = async (formData: Partial<IIssue>) => {
|
|
||||||
if (!issue) return;
|
|
||||||
|
|
||||||
await updateIssue(workspaceSlug, projectId, issue.id, formData);
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issue.id));
|
|
||||||
if (handleMutation) handleMutation();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteIssue = async () => {
|
|
||||||
if (!issue) return;
|
|
||||||
|
|
||||||
await deleteIssue(workspaceSlug, projectId, issue.id);
|
|
||||||
if (handleMutation) handleMutation();
|
|
||||||
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!peekIssue) return;
|
|
||||||
|
|
||||||
fetchIssueDetails(workspaceSlug, projectId, peekIssue.toString());
|
|
||||||
}, [fetchIssueDetails, peekIssue, projectId, workspaceSlug]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (peekIssue) {
|
|
||||||
if (peekOverviewMode === "side") {
|
|
||||||
setIsSidePeekOpen(true);
|
|
||||||
setIsModalPeekOpen(false);
|
|
||||||
} else {
|
|
||||||
setIsModalPeekOpen(true);
|
|
||||||
setIsSidePeekOpen(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIsSidePeekOpen(false);
|
|
||||||
setIsModalPeekOpen(false);
|
|
||||||
}
|
|
||||||
}, [peekIssue, peekOverviewMode]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{issue && (
|
|
||||||
<DeleteIssueModal
|
|
||||||
isOpen={deleteIssueModal}
|
|
||||||
handleClose={() => setDeleteIssueModal(false)}
|
|
||||||
data={issue}
|
|
||||||
onSubmit={handleDeleteIssue}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Transition.Root appear show={isSidePeekOpen} as={React.Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
|
||||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition-transform duration-300"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transition-transform duration-200"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="fixed z-20 bg-custom-background-100 top-0 right-0 h-full w-1/2 shadow-custom-shadow-md">
|
|
||||||
<SidePeekView
|
|
||||||
handleClose={handleClose}
|
|
||||||
handleDeleteIssue={() => setDeleteIssueModal(true)}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issue={issue}
|
|
||||||
mode={peekOverviewMode}
|
|
||||||
readOnly={readOnly}
|
|
||||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
<Transition.Root appear show={isModalPeekOpen} as={React.Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Dialog.Panel>
|
|
||||||
<div
|
|
||||||
className={`fixed z-20 bg-custom-background-100 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg shadow-custom-shadow-xl transition-all duration-300 ${
|
|
||||||
peekOverviewMode === "modal" ? "h-[70%] w-3/5" : "h-[95%] w-[95%]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{peekOverviewMode === "modal" && (
|
|
||||||
<SidePeekView
|
|
||||||
handleClose={handleClose}
|
|
||||||
handleDeleteIssue={() => setDeleteIssueModal(true)}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issue={issue}
|
|
||||||
mode={peekOverviewMode}
|
|
||||||
readOnly={readOnly}
|
|
||||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{peekOverviewMode === "full" && (
|
|
||||||
<FullScreenPeekView
|
|
||||||
handleClose={handleClose}
|
|
||||||
handleDeleteIssue={() => setDeleteIssueModal(true)}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issue={issue}
|
|
||||||
mode={peekOverviewMode}
|
|
||||||
readOnly={readOnly}
|
|
||||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,86 +0,0 @@
|
|||||||
// components
|
|
||||||
import {
|
|
||||||
PeekOverviewHeader,
|
|
||||||
PeekOverviewIssueActivity,
|
|
||||||
PeekOverviewIssueDetails,
|
|
||||||
PeekOverviewIssueProperties,
|
|
||||||
TPeekOverviewModes,
|
|
||||||
} from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { Loader } from "@plane/ui";
|
|
||||||
// types
|
|
||||||
import { IIssue } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
handleClose: () => void;
|
|
||||||
handleDeleteIssue: () => void;
|
|
||||||
handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
|
|
||||||
issue: IIssue | undefined;
|
|
||||||
mode: TPeekOverviewModes;
|
|
||||||
readOnly: boolean;
|
|
||||||
setMode: (mode: TPeekOverviewModes) => void;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SidePeekView: React.FC<Props> = ({
|
|
||||||
handleClose,
|
|
||||||
handleDeleteIssue,
|
|
||||||
handleUpdateIssue,
|
|
||||||
issue,
|
|
||||||
mode,
|
|
||||||
readOnly,
|
|
||||||
setMode,
|
|
||||||
workspaceSlug,
|
|
||||||
}) => (
|
|
||||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
|
||||||
<div className="w-full p-5">
|
|
||||||
<PeekOverviewHeader
|
|
||||||
handleClose={handleClose}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
issue={issue}
|
|
||||||
mode={mode}
|
|
||||||
setMode={setMode}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{issue ? (
|
|
||||||
<div className="h-full w-full px-6 overflow-y-auto">
|
|
||||||
{/* issue title and description */}
|
|
||||||
<div className="w-full">
|
|
||||||
<PeekOverviewIssueDetails
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issue={issue}
|
|
||||||
readOnly={readOnly}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* issue properties */}
|
|
||||||
<div className="w-full mt-10">
|
|
||||||
<PeekOverviewIssueProperties
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issue={issue}
|
|
||||||
mode={mode}
|
|
||||||
readOnly={readOnly}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* divider */}
|
|
||||||
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
|
|
||||||
{/* issue activity/comments */}
|
|
||||||
<div className="w-full pb-5">
|
|
||||||
{issue && <PeekOverviewIssueActivity workspaceSlug={workspaceSlug} issue={issue} readOnly={readOnly} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Loader className="px-6">
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<div className="space-y-2 mt-3">
|
|
||||||
<Loader.Item height="20px" width="70%" />
|
|
||||||
<Loader.Item height="20px" width="60%" />
|
|
||||||
<Loader.Item height="20px" width="60%" />
|
|
||||||
</div>
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
@ -1,11 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "services/issue";
|
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
// ui
|
// ui
|
||||||
import { ContrastIcon, CustomSearchSelect, Tooltip } from "@plane/ui";
|
import { ContrastIcon, CustomSearchSelect, Tooltip } from "@plane/ui";
|
||||||
@ -21,12 +19,17 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const issueService = new IssueService();
|
|
||||||
const cycleService = new CycleService();
|
const cycleService = new CycleService();
|
||||||
|
|
||||||
export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleChange, disabled = false }) => {
|
export const SidebarCycleSelect: React.FC<Props> = (props) => {
|
||||||
|
const { issueDetail, handleCycleChange, disabled = false } = props;
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// mobx store
|
||||||
|
const {
|
||||||
|
cycleIssues: { removeIssueFromCycle },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const { data: incompleteCycles } = useSWR(
|
const { data: incompleteCycles } = useSWR(
|
||||||
workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null,
|
||||||
@ -35,13 +38,12 @@ export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleCh
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeIssueFromCycle = (bridgeId: string, cycleId: string) => {
|
const handleRemoveIssueFromCycle = (bridgeId: string, cycleId: string) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId || !issueDetail) return;
|
||||||
|
|
||||||
issueService
|
removeIssueFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId, issueDetail.id, bridgeId)
|
||||||
.removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId, bridgeId)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(ISSUE_DETAILS(issueId as string));
|
mutate(ISSUE_DETAILS(issueDetail.id));
|
||||||
|
|
||||||
mutate(CYCLE_ISSUES(cycleId));
|
mutate(CYCLE_ISSUES(cycleId));
|
||||||
})
|
})
|
||||||
@ -70,7 +72,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleCh
|
|||||||
value={issueCycle?.cycle_detail.id}
|
value={issueCycle?.cycle_detail.id}
|
||||||
onChange={(value: any) => {
|
onChange={(value: any) => {
|
||||||
value === issueCycle?.cycle_detail.id
|
value === issueCycle?.cycle_detail.id
|
||||||
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
|
? handleRemoveIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
|
||||||
: handleCycleChange(value);
|
: handleCycleChange(value);
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
// headless ui
|
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// services
|
// mobx store
|
||||||
import { IssueLabelService } from "services/issue";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Input } from "@plane/ui";
|
import { Input } from "@plane/ui";
|
||||||
import { IssueLabelSelect } from "../select";
|
import { IssueLabelSelect } from "../select";
|
||||||
@ -14,9 +15,6 @@ import { IssueLabelSelect } from "../select";
|
|||||||
import { Plus, X } from "lucide-react";
|
import { Plus, X } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueLabel } from "types";
|
import { IIssue, IIssueLabel } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueDetails: IIssue | undefined;
|
issueDetails: IIssue | undefined;
|
||||||
@ -31,60 +29,46 @@ const defaultValues: Partial<IIssueLabel> = {
|
|||||||
color: "#ff0000",
|
color: "#ff0000",
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueLabelService = new IssueLabelService();
|
export const SidebarLabelSelect: React.FC<Props> = observer((props) => {
|
||||||
|
const { issueDetails, labelList, submitChanges, isNotAllowed, uneditable } = props;
|
||||||
export const SidebarLabelSelect: React.FC<Props> = ({
|
// states
|
||||||
issueDetails,
|
|
||||||
labelList,
|
|
||||||
submitChanges,
|
|
||||||
isNotAllowed,
|
|
||||||
uneditable,
|
|
||||||
}) => {
|
|
||||||
const [createLabelForm, setCreateLabelForm] = useState(false);
|
const [createLabelForm, setCreateLabelForm] = useState(false);
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// toast
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
// mobx store
|
||||||
|
const {
|
||||||
|
projectLabel: { projectLabels, createLabel },
|
||||||
|
} = useMobxStore();
|
||||||
|
// form info
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
reset,
|
reset,
|
||||||
watch,
|
|
||||||
control,
|
control,
|
||||||
setFocus,
|
setFocus,
|
||||||
} = useForm<Partial<IIssueLabel>>({
|
} = useForm<Partial<IIssueLabel>>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabel[]>(
|
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleNewLabel = async (formData: Partial<IIssueLabel>) => {
|
const handleNewLabel = async (formData: Partial<IIssueLabel>) => {
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
|
|
||||||
await issueLabelService
|
await createLabel(workspaceSlug.toString(), projectId.toString(), formData)
|
||||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData)
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
|
|
||||||
issueLabelMutate((prevData: any) => [...(prevData ?? []), res], false);
|
|
||||||
|
|
||||||
submitChanges({ labels: [...(issueDetails?.labels ?? []), res.id] });
|
submitChanges({ labels: [...(issueDetails?.labels ?? []), res.id] });
|
||||||
|
|
||||||
setCreateLabelForm(false);
|
setCreateLabelForm(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Oops!",
|
|
||||||
type: "error",
|
type: "error",
|
||||||
message: error?.error ?? "Error while adding the label",
|
title: "Error!",
|
||||||
|
message: error?.error ?? "Something went wrong. Please try again.",
|
||||||
});
|
});
|
||||||
reset(formData);
|
reset(formData);
|
||||||
});
|
});
|
||||||
@ -101,7 +85,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
|||||||
<div className={`flex flex-col gap-3 ${uneditable ? "opacity-60" : ""}`}>
|
<div className={`flex flex-col gap-3 ${uneditable ? "opacity-60" : ""}`}>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{labelList?.map((labelId) => {
|
{labelList?.map((labelId) => {
|
||||||
const label = issueLabels?.find((l) => l.id === labelId);
|
const label = projectLabels?.find((l) => l.id === labelId);
|
||||||
|
|
||||||
if (label)
|
if (label)
|
||||||
return (
|
return (
|
||||||
@ -118,7 +102,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
|||||||
<span
|
<span
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
backgroundColor: label.color ?? "#000000",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{label.name}
|
{label.name}
|
||||||
@ -166,40 +150,40 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
|||||||
{createLabelForm && (
|
{createLabelForm && (
|
||||||
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}>
|
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}>
|
||||||
<div>
|
<div>
|
||||||
<Popover className="relative">
|
<Controller
|
||||||
<>
|
name="color"
|
||||||
<Popover.Button className="grid place-items-center outline-none">
|
control={control}
|
||||||
{watch("color") && watch("color") !== "" && (
|
render={({ field: { value, onChange } }) => (
|
||||||
<span
|
<Popover className="relative">
|
||||||
className="h-6 w-6 rounded"
|
<>
|
||||||
style={{
|
<Popover.Button className="grid place-items-center outline-none">
|
||||||
backgroundColor: watch("color") ?? "black",
|
{value && value?.trim() !== "" && (
|
||||||
}}
|
<span
|
||||||
/>
|
className="h-6 w-6 rounded"
|
||||||
)}
|
style={{
|
||||||
</Popover.Button>
|
backgroundColor: value ?? "black",
|
||||||
|
}}
|
||||||
<Transition
|
/>
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute z-10 mt-1.5 max-w-xs px-2 sm:px-0">
|
|
||||||
<Controller
|
|
||||||
name="color"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
|
|
||||||
)}
|
)}
|
||||||
/>
|
</Popover.Button>
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
<Transition
|
||||||
</>
|
as={React.Fragment}
|
||||||
</Popover>
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 translate-y-1"
|
||||||
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
leave="transition ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute z-10 mt-1.5 max-w-xs px-2 sm:px-0">
|
||||||
|
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -235,4 +219,4 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import { observer } from "mobx-react-lite";
|
||||||
// services
|
import { mutate } from "swr";
|
||||||
import { ModuleService } from "services/module.service";
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// ui
|
// ui
|
||||||
import { CustomSearchSelect, DiceIcon, Tooltip } from "@plane/ui";
|
import { CustomSearchSelect, DiceIcon, Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
|
import { ISSUE_DETAILS, MODULE_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueDetail: IIssue | undefined;
|
issueDetail: IIssue | undefined;
|
||||||
@ -16,24 +17,23 @@ type Props = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const moduleService = new ModuleService();
|
export const SidebarModuleSelect: React.FC<Props> = observer((props) => {
|
||||||
|
const { issueDetail, handleModuleChange, disabled = false } = props;
|
||||||
export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModuleChange, disabled = false }) => {
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// mobx store
|
||||||
|
const {
|
||||||
|
module: { projectModules },
|
||||||
|
moduleIssues: { removeIssueFromModule },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const { data: modules } = useSWR(
|
const handleRemoveIssueFromModule = (bridgeId: string, moduleId: string) => {
|
||||||
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
|
if (!workspaceSlug || !projectId || !issueDetail) return;
|
||||||
workspaceSlug && projectId ? () => moduleService.getModules(workspaceSlug as string, projectId as string) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeIssueFromModule = (bridgeId: string, moduleId: string) => {
|
removeIssueFromModule(workspaceSlug.toString(), projectId.toString(), moduleId, issueDetail.id, bridgeId)
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
moduleService
|
|
||||||
.removeIssueFromModule(workspaceSlug as string, projectId as string, moduleId, bridgeId)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(ISSUE_DETAILS(issueId as string));
|
mutate(ISSUE_DETAILS(issueDetail.id));
|
||||||
|
|
||||||
mutate(MODULE_ISSUES(moduleId));
|
mutate(MODULE_ISSUES(moduleId));
|
||||||
})
|
})
|
||||||
@ -42,7 +42,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = modules?.map((module) => ({
|
const options = projectModules?.map((module) => ({
|
||||||
value: module.id,
|
value: module.id,
|
||||||
query: module.name,
|
query: module.name,
|
||||||
content: (
|
content: (
|
||||||
@ -62,7 +62,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
|||||||
value={issueModule?.module_detail.id}
|
value={issueModule?.module_detail.id}
|
||||||
onChange={(value: any) => {
|
onChange={(value: any) => {
|
||||||
value === issueModule?.module_detail.id
|
value === issueModule?.module_detail.id
|
||||||
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
|
? handleRemoveIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
|
||||||
: handleModuleChange(value);
|
: handleModuleChange(value);
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
@ -70,7 +70,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
|||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="left"
|
position="left"
|
||||||
tooltipContent={`${modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}`}
|
tooltipContent={`${projectModules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -85,7 +85,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
|||||||
>
|
>
|
||||||
<span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span>
|
<span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}
|
{projectModules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -97,4 +97,4 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
// lucide icons
|
|
||||||
import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
import { IssuePeekOverview } from "../issue-peek-overview";
|
|
||||||
import { SubIssuesRootList } from "./issues-list";
|
import { SubIssuesRootList } from "./issues-list";
|
||||||
import { IssueProperty } from "./properties";
|
import { IssueProperty } from "./properties";
|
||||||
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Tooltip } from "@plane/ui";
|
import { CustomMenu, Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IUser, IIssue } from "types";
|
import { IUser, IIssue } from "types";
|
||||||
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||||
// fetch keys
|
|
||||||
|
|
||||||
export interface ISubIssues {
|
export interface ISubIssues {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -47,139 +45,153 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
|||||||
copyText,
|
copyText,
|
||||||
handleIssueCrudOperation,
|
handleIssueCrudOperation,
|
||||||
handleUpdateIssue,
|
handleUpdateIssue,
|
||||||
}) => (
|
}) => {
|
||||||
<div>
|
const router = useRouter();
|
||||||
{issue && (
|
const { peekProjectId, peekIssueId } = router.query;
|
||||||
<div
|
|
||||||
className="relative flex items-center gap-2 py-1 px-2 w-full h-full hover:bg-custom-background-90 group transition-all border-b border-custom-border-100"
|
|
||||||
style={{ paddingLeft: `${spacingLeft}px` }}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 w-[22px] h-[22px]">
|
|
||||||
{issue?.sub_issues_count > 0 && (
|
|
||||||
<>
|
|
||||||
{issuesLoader.sub_issues.includes(issue?.id) ? (
|
|
||||||
<div className="w-full h-full flex justify-center items-center rounded-sm bg-custom-background-80 transition-all cursor-not-allowed">
|
|
||||||
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="w-full h-full flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer"
|
|
||||||
onClick={() => handleIssuesLoader({ key: "visibility", issueId: issue?.id })}
|
|
||||||
>
|
|
||||||
{issuesLoader && issuesLoader.visibility.includes(issue?.id) ? (
|
|
||||||
<ChevronDown width={14} strokeWidth={2} />
|
|
||||||
) : (
|
|
||||||
<ChevronRight width={14} strokeWidth={2} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
const handleIssuePeekOverview = () => {
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{workspaceSlug && peekProjectId && peekIssueId && peekIssueId === issue.id && (
|
||||||
<IssuePeekOverview
|
<IssuePeekOverview
|
||||||
workspaceSlug={issue?.workspace_detail?.slug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={issue?.project_detail?.id}
|
projectId={peekProjectId.toString()}
|
||||||
issueId={issue?.id}
|
issueId={peekIssueId.toString()}
|
||||||
handleIssue={(issueToUpdate) => {
|
handleIssue={(issueToUpdate) => handleUpdateIssue(issue, { ...issue, ...issueToUpdate })}
|
||||||
console.log("issueToUpdate", issueToUpdate);
|
/>
|
||||||
handleUpdateIssue(issue, { ...issue, ...issueToUpdate });
|
)}
|
||||||
}}
|
<div>
|
||||||
>
|
{issue && (
|
||||||
<div className="w-full flex items-center gap-2 cursor-pointer">
|
<div
|
||||||
<div
|
className="relative flex items-center gap-2 py-1 px-2 w-full h-full hover:bg-custom-background-90 group transition-all border-b border-custom-border-100"
|
||||||
className="flex-shrink-0 w-[6px] h-[6px] rounded-full"
|
style={{ paddingLeft: `${spacingLeft}px` }}
|
||||||
style={{
|
>
|
||||||
backgroundColor: issue.state_detail.color,
|
<div className="flex-shrink-0 w-[22px] h-[22px]">
|
||||||
}}
|
{issue?.sub_issues_count > 0 && (
|
||||||
/>
|
<>
|
||||||
<div className="flex-shrink-0 text-xs text-custom-text-200">
|
{issuesLoader.sub_issues.includes(issue?.id) ? (
|
||||||
{issue.project_detail.identifier}-{issue?.sequence_id}
|
<div className="w-full h-full flex justify-center items-center rounded-sm bg-custom-background-80 transition-all cursor-not-allowed">
|
||||||
|
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="w-full h-full flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer"
|
||||||
|
onClick={() => handleIssuesLoader({ key: "visibility", issueId: issue?.id })}
|
||||||
|
>
|
||||||
|
{issuesLoader && issuesLoader.visibility.includes(issue?.id) ? (
|
||||||
|
<ChevronDown width={14} strokeWidth={2} />
|
||||||
|
) : (
|
||||||
|
<ChevronRight width={14} strokeWidth={2} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={`${issue?.name}`}>
|
|
||||||
<div className="line-clamp-1 text-xs text-custom-text-100 pr-2">{issue?.name}</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</IssuePeekOverview>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 text-sm">
|
<div className="w-full flex items-center gap-2 cursor-pointer" onClick={handleIssuePeekOverview}>
|
||||||
<IssueProperty
|
<div
|
||||||
|
className="flex-shrink-0 w-[6px] h-[6px] rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex-shrink-0 text-xs text-custom-text-200">
|
||||||
|
{issue.project_detail.identifier}-{issue?.sequence_id}
|
||||||
|
</div>
|
||||||
|
<Tooltip tooltipHeading="Title" tooltipContent={`${issue?.name}`}>
|
||||||
|
<div className="line-clamp-1 text-xs text-custom-text-100 pr-2">{issue?.name}</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 text-sm">
|
||||||
|
<IssueProperty
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
parentIssue={parentIssue}
|
||||||
|
issue={issue}
|
||||||
|
user={user}
|
||||||
|
editable={editable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 text-sm">
|
||||||
|
<CustomMenu width="auto" ellipsis>
|
||||||
|
{editable && (
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleIssueCrudOperation("edit", parentIssue?.id, issue)}>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<Pencil width={14} strokeWidth={2} />
|
||||||
|
<span>Edit issue</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editable && (
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleIssueCrudOperation("delete", parentIssue?.id, issue)}>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<Trash width={14} strokeWidth={2} />
|
||||||
|
<span>Delete issue</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<LinkIcon width={14} strokeWidth={2} />
|
||||||
|
<span>Copy issue link</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editable && (
|
||||||
|
<>
|
||||||
|
{issuesLoader.delete.includes(issue?.id) ? (
|
||||||
|
<div className="flex-shrink-0 w-[22px] h-[22px] rounded-sm bg-red-200/10 text-red-500 transition-all cursor-not-allowed overflow-hidden flex justify-center items-center">
|
||||||
|
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 invisible group-hover:visible w-[22px] h-[22px] rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer overflow-hidden flex justify-center items-center"
|
||||||
|
onClick={() => {
|
||||||
|
handleIssuesLoader({ key: "delete", issueId: issue?.id });
|
||||||
|
removeIssueFromSubIssues(parentIssue?.id, issue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X width={14} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && (
|
||||||
|
<SubIssuesRootList
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
parentIssue={parentIssue}
|
projectId={projectId}
|
||||||
issue={issue}
|
parentIssue={issue}
|
||||||
|
spacingLeft={spacingLeft + 22}
|
||||||
user={user}
|
user={user}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
||||||
|
issuesLoader={issuesLoader}
|
||||||
|
handleIssuesLoader={handleIssuesLoader}
|
||||||
|
copyText={copyText}
|
||||||
|
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 text-sm">
|
|
||||||
<CustomMenu width="auto" ellipsis>
|
|
||||||
{editable && (
|
|
||||||
<CustomMenu.MenuItem onClick={() => handleIssueCrudOperation("edit", parentIssue?.id, issue)}>
|
|
||||||
<div className="flex items-center justify-start gap-2">
|
|
||||||
<Pencil width={14} strokeWidth={2} />
|
|
||||||
<span>Edit issue</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{editable && (
|
|
||||||
<CustomMenu.MenuItem onClick={() => handleIssueCrudOperation("delete", parentIssue?.id, issue)}>
|
|
||||||
<div className="flex items-center justify-start gap-2">
|
|
||||||
<Trash width={14} strokeWidth={2} />
|
|
||||||
<span>Delete issue</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-start gap-2">
|
|
||||||
<LinkIcon width={14} strokeWidth={2} />
|
|
||||||
<span>Copy issue link</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{editable && (
|
|
||||||
<>
|
|
||||||
{issuesLoader.delete.includes(issue?.id) ? (
|
|
||||||
<div className="flex-shrink-0 w-[22px] h-[22px] rounded-sm bg-red-200/10 text-red-500 transition-all cursor-not-allowed overflow-hidden flex justify-center items-center">
|
|
||||||
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 invisible group-hover:visible w-[22px] h-[22px] rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer overflow-hidden flex justify-center items-center"
|
|
||||||
onClick={() => {
|
|
||||||
handleIssuesLoader({ key: "delete", issueId: issue?.id });
|
|
||||||
removeIssueFromSubIssues(parentIssue?.id, issue);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X width={14} strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
);
|
||||||
{issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && (
|
};
|
||||||
<SubIssuesRootList
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
parentIssue={issue}
|
|
||||||
spacingLeft={spacingLeft + 22}
|
|
||||||
user={user}
|
|
||||||
editable={editable}
|
|
||||||
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
|
||||||
issuesLoader={issuesLoader}
|
|
||||||
handleIssuesLoader={handleIssuesLoader}
|
|
||||||
copyText={copyText}
|
|
||||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
// swr
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
import { SubIssues } from "./issue";
|
import { SubIssues } from "./issue";
|
||||||
@ -61,37 +60,38 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
|||||||
handleIssuesLoader({ key: "sub_issues", issueId: parentIssue?.id });
|
handleIssuesLoader({ key: "sub_issues", issueId: parentIssue?.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [handleIssuesLoader, isLoading, issuesLoader.sub_issues, parentIssue?.id]);
|
||||||
}, [isLoading]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<>
|
||||||
{issues &&
|
<div className="relative">
|
||||||
issues.sub_issues &&
|
{issues &&
|
||||||
issues.sub_issues.length > 0 &&
|
issues.sub_issues &&
|
||||||
issues.sub_issues.map((issue: IIssue) => (
|
issues.sub_issues.length > 0 &&
|
||||||
<SubIssues
|
issues.sub_issues.map((issue: IIssue) => (
|
||||||
key={`${issue?.id}`}
|
<SubIssues
|
||||||
workspaceSlug={workspaceSlug}
|
key={`${issue?.id}`}
|
||||||
projectId={projectId}
|
workspaceSlug={workspaceSlug}
|
||||||
parentIssue={parentIssue}
|
projectId={projectId}
|
||||||
issue={issue}
|
parentIssue={parentIssue}
|
||||||
spacingLeft={spacingLeft}
|
issue={issue}
|
||||||
user={user}
|
spacingLeft={spacingLeft}
|
||||||
editable={editable}
|
user={user}
|
||||||
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
editable={editable}
|
||||||
issuesLoader={issuesLoader}
|
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
||||||
handleIssuesLoader={handleIssuesLoader}
|
issuesLoader={issuesLoader}
|
||||||
copyText={copyText}
|
handleIssuesLoader={handleIssuesLoader}
|
||||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
copyText={copyText}
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||||
/>
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 bottom-0 ${spacingLeft > 10 ? `border-l border-custom-border-100` : ``}`}
|
className={`absolute top-0 bottom-0 ${spacingLeft > 10 ? `border-l border-custom-border-100` : ``}`}
|
||||||
style={{ left: `${spacingLeft - 12}px` }}
|
style={{ left: `${spacingLeft - 12}px` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -43,7 +43,11 @@ const issueService = new IssueService();
|
|||||||
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
||||||
const { parentIssue, user } = props;
|
const { parentIssue, user } = props;
|
||||||
|
|
||||||
const { user: userStore, issue: issueStore, issueDetail: issueDetailStore } = useMobxStore();
|
const {
|
||||||
|
user: userStore,
|
||||||
|
issue: { updateIssueStructure },
|
||||||
|
projectIssues: { updateIssue },
|
||||||
|
} = useMobxStore();
|
||||||
const userRole = userStore.currentProjectRole;
|
const userRole = userStore.currentProjectRole;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -166,10 +170,10 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
issueStore.updateIssueStructure(null, null, payload);
|
updateIssueStructure(null, null, payload);
|
||||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
||||||
},
|
},
|
||||||
[issueStore, issueDetailStore, projectId, user, workspaceSlug]
|
[updateIssueStructure, projectId, updateIssue, user, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEditable = userRole === 5 || userRole === 10 ? false : true;
|
const isEditable = userRole === 5 || userRole === 10 ? false : true;
|
||||||
|
@ -401,7 +401,6 @@ const ProfileSettingsPage: NextPageWithLayout = () => {
|
|||||||
<span className="text-lg tracking-tight">Deactivate account</span>
|
<span className="text-lg tracking-tight">Deactivate account</span>
|
||||||
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
|
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
enter="transition duration-100 ease-out"
|
enter="transition duration-100 ease-out"
|
||||||
|
@ -106,7 +106,14 @@ export class ModuleService extends APIService {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
data: { issues: string[] }
|
data: { issues: string[] }
|
||||||
): Promise<any> {
|
): Promise<
|
||||||
|
{
|
||||||
|
issue: string;
|
||||||
|
issue_detail: IIssue;
|
||||||
|
module: string;
|
||||||
|
module_detail: IModule;
|
||||||
|
}[]
|
||||||
|
> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -250,7 +250,7 @@ export class CycleIssueStore implements ICycleIssueStore {
|
|||||||
|
|
||||||
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||||
|
|
||||||
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
||||||
|
@ -76,7 +76,7 @@ export class CycleIssueCalendarViewStore implements ICycleIssueCalendarViewStore
|
|||||||
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
|
@ -289,7 +289,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
|
|||||||
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
@ -437,7 +437,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
|
|||||||
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
|
@ -278,7 +278,7 @@ export class IssueStore implements IIssueStore {
|
|||||||
|
|
||||||
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||||
|
|
||||||
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchIssues = async (
|
fetchIssues = async (
|
||||||
|
@ -75,7 +75,7 @@ export class IssueCalendarViewStore implements IIssueCalendarViewStore {
|
|||||||
this.rootStore.issue.issues = { ...reorderedIssues };
|
this.rootStore.issue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
import { observable, action, makeObservable, runInAction, computed, autorun } from "mobx";
|
||||||
// services
|
// services
|
||||||
import { IssueService, IssueReactionService, IssueCommentService } from "services/issue";
|
import { IssueService, IssueReactionService, IssueCommentService } from "services/issue";
|
||||||
import { NotificationService } from "services/notification.service";
|
import { NotificationService } from "services/notification.service";
|
||||||
@ -7,8 +7,6 @@ import { RootStore } from "../root";
|
|||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { groupReactionEmojis } from "constants/issue";
|
import { groupReactionEmojis } from "constants/issue";
|
||||||
// uuid
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
export interface IIssueDetailStore {
|
export interface IIssueDetailStore {
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
@ -44,11 +42,6 @@ export interface IIssueDetailStore {
|
|||||||
|
|
||||||
// fetch issue details
|
// fetch issue details
|
||||||
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
||||||
// creating issue
|
|
||||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
|
|
||||||
optimisticallyCreateIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
|
|
||||||
// updating issue
|
|
||||||
updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>) => Promise<IIssue>;
|
|
||||||
// deleting issue
|
// deleting issue
|
||||||
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
|
|
||||||
@ -146,9 +139,6 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
setPeekId: action,
|
setPeekId: action,
|
||||||
|
|
||||||
fetchIssueDetails: action,
|
fetchIssueDetails: action,
|
||||||
createIssue: action,
|
|
||||||
optimisticallyCreateIssue: action,
|
|
||||||
updateIssue: action,
|
|
||||||
deleteIssue: action,
|
deleteIssue: action,
|
||||||
|
|
||||||
fetchPeekIssueDetails: action,
|
fetchPeekIssueDetails: action,
|
||||||
@ -171,6 +161,26 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
removeIssueSubscription: action,
|
removeIssueSubscription: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
autorun(() => {
|
||||||
|
const projectId = this.rootStore?.project.projectId;
|
||||||
|
const peekId = this.peekId;
|
||||||
|
|
||||||
|
if (!projectId || !peekId) return;
|
||||||
|
|
||||||
|
const issue = this.rootStore.projectIssues.issues?.[projectId]?.[peekId];
|
||||||
|
|
||||||
|
if (issue && issue.id)
|
||||||
|
runInAction(() => {
|
||||||
|
this.issues = {
|
||||||
|
...this.issues,
|
||||||
|
[issue.id]: {
|
||||||
|
...this.issues[issue.id],
|
||||||
|
...issue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
this.issueReactionService = new IssueReactionService();
|
this.issueReactionService = new IssueReactionService();
|
||||||
@ -238,111 +248,6 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
optimisticallyCreateIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => {
|
|
||||||
const tempId = data?.id || uuidv4();
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = true;
|
|
||||||
this.error = null;
|
|
||||||
this.issues = {
|
|
||||||
...this.issues,
|
|
||||||
[tempId]: data as IIssue,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.issueService.createIssue(workspaceSlug, projectId, data);
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = false;
|
|
||||||
this.error = null;
|
|
||||||
this.issues = {
|
|
||||||
...this.issues,
|
|
||||||
[response.id]: response,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
this.loader = false;
|
|
||||||
this.error = error;
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
createIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => {
|
|
||||||
try {
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = true;
|
|
||||||
this.error = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await this.issueService.createIssue(workspaceSlug, projectId, data);
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = false;
|
|
||||||
this.error = null;
|
|
||||||
this.issues = {
|
|
||||||
...this.issues,
|
|
||||||
[response.id]: response,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
this.loader = false;
|
|
||||||
this.error = error;
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>) => {
|
|
||||||
const newIssues = { ...this.issues };
|
|
||||||
newIssues[issueId] = {
|
|
||||||
...newIssues[issueId],
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = true;
|
|
||||||
this.error = null;
|
|
||||||
this.issues = newIssues;
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = this.rootStore.user.currentUser;
|
|
||||||
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = false;
|
|
||||||
this.error = null;
|
|
||||||
this.issues = {
|
|
||||||
...this.issues,
|
|
||||||
[issueId]: {
|
|
||||||
...this.issues[issueId],
|
|
||||||
...response,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.loader = false;
|
|
||||||
this.error = error;
|
|
||||||
});
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
const newIssues = { ...this.issues };
|
const newIssues = { ...this.issues };
|
||||||
delete newIssues[issueId];
|
delete newIssues[issueId];
|
||||||
|
@ -289,7 +289,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
|||||||
this.rootStore.issue.issues = { ...reorderedIssues };
|
this.rootStore.issue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
@ -437,7 +437,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
|||||||
this.rootStore.issue.issues = { ...reorderedIssues };
|
this.rootStore.issue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
|
@ -54,7 +54,7 @@ export interface IModuleIssuesStore {
|
|||||||
moduleId: string,
|
moduleId: string,
|
||||||
issueIds: string[],
|
issueIds: string[],
|
||||||
fetchAfterAddition?: boolean
|
fetchAfterAddition?: boolean
|
||||||
) => Promise<IIssue>;
|
) => Promise<any>;
|
||||||
removeIssueFromModule: (
|
removeIssueFromModule: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -193,7 +193,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.rootStore.projectIssues.createIssue(workspaceSlug, projectId, data);
|
const response = await this.rootStore.projectIssues.createIssue(workspaceSlug, projectId, data);
|
||||||
const issueToModule = await this.addIssueToModule(workspaceSlug, moduleId, [response.id], false);
|
await this.addIssueToModule(workspaceSlug, moduleId, [response.id], false);
|
||||||
|
|
||||||
let _issues = this.issues;
|
let _issues = this.issues;
|
||||||
if (!_issues) _issues = {};
|
if (!_issues) _issues = {};
|
||||||
@ -204,7 +204,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
|
|||||||
this.issues = _issues;
|
this.issues = _issues;
|
||||||
});
|
});
|
||||||
|
|
||||||
return issueToModule;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
|
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
|
||||||
throw error;
|
throw error;
|
||||||
@ -286,7 +286,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
|
|||||||
|
|
||||||
const response = await this.createIssue(workspaceSlug, projectId, data, moduleId);
|
const response = await this.createIssue(workspaceSlug, projectId, data, moduleId);
|
||||||
|
|
||||||
if (this.issues) {
|
if (this.issues && response) {
|
||||||
delete this.issues[moduleId][data.id as keyof IIssue];
|
delete this.issues[moduleId][data.id as keyof IIssue];
|
||||||
|
|
||||||
let _issues = { ...this.issues };
|
let _issues = { ...this.issues };
|
||||||
|
@ -260,7 +260,7 @@ export class ModuleIssueStore implements IModuleIssueStore {
|
|||||||
|
|
||||||
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||||
|
|
||||||
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
||||||
|
@ -76,7 +76,7 @@ export class ModuleIssueCalendarViewStore implements IModuleIssueCalendarViewSto
|
|||||||
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
|
@ -289,7 +289,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
|
|||||||
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
@ -437,7 +437,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
|
|||||||
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
|
@ -76,7 +76,7 @@ export class ProjectViewIssueCalendarViewStore implements IProjectViewIssueCalen
|
|||||||
this.rootStore.projectViewIssues.viewIssues = { ...reorderedIssues };
|
this.rootStore.projectViewIssues.viewIssues = { ...reorderedIssues };
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issueDetail?.updateIssue(
|
this.rootStore.projectIssues.updateIssue(
|
||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
|
@ -278,7 +278,7 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
|
|||||||
|
|
||||||
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||||
|
|
||||||
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user