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
7cd02401c1
commit
1be1b9f4a3
@ -30,7 +30,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
||||
|
||||
const {
|
||||
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
|
||||
issueDetail: { updateIssue },
|
||||
projectIssues: { updateIssue },
|
||||
user: { currentUser },
|
||||
} = useMobxStore();
|
||||
|
||||
|
@ -21,7 +21,7 @@ export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store
|
||||
const {
|
||||
issueDetail: { updateIssue },
|
||||
projectIssues: { updateIssue },
|
||||
projectMember: { projectMembers },
|
||||
} = useMobxStore();
|
||||
|
||||
|
@ -23,7 +23,7 @@ export const ChangeIssuePriority: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
issueDetail: { updateIssue },
|
||||
projectIssues: { updateIssue },
|
||||
} = useMobxStore();
|
||||
|
||||
const submitChanges = async (formData: Partial<IIssue>) => {
|
||||
|
@ -24,7 +24,7 @@ export const ChangeIssueState: React.FC<Props> = observer((props) => {
|
||||
|
||||
const {
|
||||
projectState: { projectStates },
|
||||
issueDetail: { updateIssue },
|
||||
projectIssues: { updateIssue },
|
||||
} = useMobxStore();
|
||||
|
||||
const submitChanges = async (formData: Partial<IIssue>) => {
|
||||
|
@ -226,31 +226,12 @@ const activityDetails: {
|
||||
},
|
||||
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: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
if (activity.verb === "created")
|
||||
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
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
@ -295,6 +276,25 @@ const activityDetails: {
|
||||
},
|
||||
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: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
@ -354,7 +354,7 @@ const activityDetails: {
|
||||
return (
|
||||
<>
|
||||
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} />
|
||||
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
@ -370,7 +370,7 @@ const activityDetails: {
|
||||
return (
|
||||
<>
|
||||
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} />
|
||||
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span>
|
||||
</span>
|
||||
|
@ -1,10 +1,13 @@
|
||||
// ui
|
||||
import { ExternalLinkIcon, Tooltip } from "@plane/ui";
|
||||
// icons
|
||||
import { ExternalLinkIcon } from "@plane/ui";
|
||||
import { Pencil, Trash2, LinkIcon } from "lucide-react";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { linkDetails, UserAuth } from "types";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
type Props = {
|
||||
links: linkDetails[];
|
||||
@ -14,18 +17,37 @@ type Props = {
|
||||
};
|
||||
|
||||
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEditLink, userAuth }) => {
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
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 (
|
||||
<>
|
||||
{links.map((link) => (
|
||||
<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 gap-2">
|
||||
<div className="flex items-start truncate gap-2">
|
||||
<span className="py-1">
|
||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||
</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>
|
||||
|
||||
{!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">
|
||||
<IssueGanttSidebarBlock data={block.data} handleIssue={blockUpdateHandler} />
|
||||
<IssueGanttSidebarBlock data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{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">
|
||||
<IssueGanttSidebarBlock data={block.data} handleIssue={blockUpdateHandler} />
|
||||
<IssueGanttSidebarBlock data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
|
@ -7,13 +7,13 @@ export * from "./delete-issue-modal";
|
||||
export * from "./description-form";
|
||||
export * from "./form";
|
||||
export * from "./issue-layouts";
|
||||
export * from "./issue-peek-overview";
|
||||
export * from "./main-content";
|
||||
export * from "./modal";
|
||||
export * from "./parent-issues-list-modal";
|
||||
export * from "./sidebar";
|
||||
export * from "./label";
|
||||
export * from "./issue-reaction";
|
||||
export * from "./peek-overview";
|
||||
export * from "./confirm-issue-discard";
|
||||
|
||||
// draft issue
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarChart } from "components/issues";
|
||||
import { CalendarChart, IssuePeekOverview } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import {
|
||||
@ -41,8 +40,11 @@ interface IBaseCalendarRoot {
|
||||
}
|
||||
|
||||
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
const { issueStore, issuesFilterStore, calendarViewStore, QuickActions, issueActions, viewId, handleDragDrop } =
|
||||
props;
|
||||
const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, handleDragDrop } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||
|
||||
const displayFilters = issuesFilterStore.issueFilters?.displayFilters;
|
||||
|
||||
@ -67,38 +69,49 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
issueActions[action]!(issue);
|
||||
}
|
||||
},
|
||||
[issueStore]
|
||||
[issueActions]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
layout={displayFilters?.calendar?.layout}
|
||||
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(issue) => (
|
||||
<QuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)}
|
||||
handleUpdate={
|
||||
issueActions[EIssueActions.UPDATE]
|
||||
? async (data) => handleIssues(issue.target_date ?? "", data, EIssueActions.UPDATE)
|
||||
: undefined
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE]
|
||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
<>
|
||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
layout={displayFilters?.calendar?.layout}
|
||||
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
||||
quickActions={(issue) => (
|
||||
<QuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)}
|
||||
handleUpdate={
|
||||
issueActions[EIssueActions.UPDATE]
|
||||
? async (data) => handleIssues(issue.target_date ?? "", data, EIssueActions.UPDATE)
|
||||
: undefined
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE]
|
||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
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
|
||||
import { ICalendarWeek } from "./types";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||
|
||||
type Props = {
|
||||
@ -17,7 +16,6 @@ type Props = {
|
||||
groupedIssueIds: IGroupedIssues;
|
||||
layout: "month" | "week" | undefined;
|
||||
showWeekends: boolean;
|
||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
@ -29,7 +27,7 @@ type 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();
|
||||
|
||||
@ -60,7 +58,6 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
enableQuickIssueCreate
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
@ -74,7 +71,6 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
enableQuickIssueCreate
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
|
@ -10,14 +10,12 @@ import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// constants
|
||||
import { MONTHS_LIST } from "constants/calendar";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||
|
||||
type Props = {
|
||||
date: ICalendarDate;
|
||||
issues: IIssueResponse | undefined;
|
||||
groupedIssueIds: IGroupedIssues;
|
||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
quickAddCallback?: (
|
||||
@ -30,16 +28,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
date,
|
||||
issues,
|
||||
groupedIssueIds,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
const { date, issues, groupedIssueIds, quickActions, enableQuickIssueCreate, quickAddCallback, viewId } = props;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
@ -81,12 +70,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<CalendarIssueBlocks
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
/>
|
||||
<CalendarIssueBlocks issues={issues} issueIdList={issueIdList} quickActions={quickActions} />
|
||||
{enableQuickIssueCreate && (
|
||||
<div className="py-1 px-2">
|
||||
<CalendarQuickAddIssueForm
|
||||
|
@ -1,22 +1,31 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Draggable } from "@hello-pangea/dnd";
|
||||
// components
|
||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssueResponse } from "store/issues/types";
|
||||
|
||||
type Props = {
|
||||
issues: IIssueResponse | undefined;
|
||||
issueIdList: string[] | null;
|
||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
@ -28,45 +37,46 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className="p-1 px-2 relative"
|
||||
className="p-1 px-2 relative cursor-pointer"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
>
|
||||
{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={`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
|
||||
? "shadow-custom-shadow-rg bg-custom-background-90"
|
||||
: "bg-custom-background-100 hover:bg-custom-background-90"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="h-full w-0.5 rounded flex-shrink-0"
|
||||
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>
|
||||
<IssuePeekOverview
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 h-full">
|
||||
<span
|
||||
className="h-full w-0.5 rounded flex-shrink-0"
|
||||
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>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<div className="text-xs truncate">{issue.name}</div>
|
||||
</Tooltip>
|
||||
</IssuePeekOverview>
|
||||
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
|
||||
</div>
|
||||
<div
|
||||
className="hidden group-hover/calendar-block:block"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{quickActions(issue)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -9,14 +9,12 @@ import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICalendarDate, ICalendarWeek } from "./types";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||
|
||||
type Props = {
|
||||
issues: IIssueResponse | undefined;
|
||||
groupedIssueIds: IGroupedIssues;
|
||||
week: ICalendarWeek | undefined;
|
||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
quickAddCallback?: (
|
||||
@ -29,16 +27,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
issues,
|
||||
groupedIssueIds,
|
||||
week,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
const { issues, groupedIssueIds, week, quickActions, enableQuickIssueCreate, quickAddCallback, viewId } = props;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
@ -62,7 +51,6 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
date={date}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
quickAddCallback={quickAddCallback}
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { IssueGanttBlock } from "components/issues";
|
||||
import { IssueGanttBlock, IssuePeekOverview } from "components/issues";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
@ -41,9 +40,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
const { issueFiltersStore, issueStore, viewId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||
|
||||
const {
|
||||
user: { currentProjectRole },
|
||||
@ -61,7 +58,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
|
||||
//Todo fix sort order in the structure
|
||||
issueStore.updateIssue(
|
||||
workspaceSlug,
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
issue.id,
|
||||
{
|
||||
@ -83,7 +80,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
blockUpdateHandler={updateIssue}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} handleIssue={updateIssue} />}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} />}
|
||||
sidebarToRender={(props) => (
|
||||
<IssueGanttSidebar
|
||||
{...props}
|
||||
@ -98,6 +95,17 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
||||
/>
|
||||
</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
|
||||
import { Tooltip, StateGroupIcon } from "@plane/ui";
|
||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
import { IBlockUpdateData } from "components/gantt-chart";
|
||||
// helpers
|
||||
import { renderShortDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
export const IssueGanttBlock = ({
|
||||
data,
|
||||
handleIssue,
|
||||
}: {
|
||||
data: IIssue;
|
||||
handleIssue: (block: IIssue, payload: IBlockUpdateData) => void;
|
||||
}) => (
|
||||
<IssuePeekOverview
|
||||
workspaceSlug={data?.workspace_detail?.slug}
|
||||
projectId={data?.project_detail?.id}
|
||||
issueId={data?.id}
|
||||
handleIssue={(issueToUpdate) => handleIssue({ ...data, ...issueToUpdate }, {})}
|
||||
>
|
||||
export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleIssuePeekOverview = () => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center relative h-full w-full rounded cursor-pointer"
|
||||
style={{ backgroundColor: data?.state_detail?.color }}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" />
|
||||
<Tooltip
|
||||
@ -41,24 +41,24 @@ export const IssueGanttBlock = ({
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</IssuePeekOverview>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
// rendering issues on gantt sidebar
|
||||
export const IssueGanttSidebarBlock = ({
|
||||
data,
|
||||
handleIssue,
|
||||
}: {
|
||||
data: IIssue;
|
||||
handleIssue: (block: IIssue, payload: IBlockUpdateData) => void;
|
||||
}) => (
|
||||
<IssuePeekOverview
|
||||
workspaceSlug={data?.workspace_detail?.slug}
|
||||
projectId={data?.project_detail?.id}
|
||||
issueId={data?.id}
|
||||
handleIssue={(issueToUpdate) => handleIssue({ ...data, ...issueToUpdate }, {})}
|
||||
>
|
||||
<div className="relative w-full flex items-center gap-2 h-full cursor-pointer">
|
||||
export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleIssuePeekOverview = () => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<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} />
|
||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||
{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>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</IssuePeekOverview>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FC, useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { DragDropContext, Droppable } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
@ -29,7 +30,7 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { IssuePeekOverview } from "components/issues";
|
||||
|
||||
export interface IBaseKanBanLayout {
|
||||
issueStore:
|
||||
@ -79,7 +80,10 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
handleDragDrop,
|
||||
addIssuesToView,
|
||||
} = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||
// mobx store
|
||||
const {
|
||||
project: { workspaceProjects },
|
||||
projectLabel: { projectLabels },
|
||||
@ -134,7 +138,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
issueActions[action]!(issue);
|
||||
}
|
||||
},
|
||||
[issueStore]
|
||||
[issueActions]
|
||||
);
|
||||
|
||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
@ -264,6 +268,17 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
)}
|
||||
</DragDropContext>
|
||||
</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";
|
||||
// components
|
||||
import { KanBanProperties } from "./properties";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
// types
|
||||
import { IIssueDisplayProperties, IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
interface IssueBlockProps {
|
||||
sub_group_id: string;
|
||||
@ -33,11 +34,22 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
displayProperties,
|
||||
isReadOnly,
|
||||
} = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
||||
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 (
|
||||
<>
|
||||
<Draggable draggableId={issue.id} index={index}>
|
||||
@ -47,6 +59,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
{issue.tempId !== undefined && (
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
<IssuePeekOverview
|
||||
workspaceSlug={issue?.workspace_detail?.slug}
|
||||
projectId={issue?.project_detail?.id}
|
||||
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>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<KanBanProperties
|
||||
sub_group_id={sub_group_id}
|
||||
|
@ -23,6 +23,8 @@ import {
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { IIssueResponse } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { IssuePeekOverview } from "components/issues";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
enum EIssueActions {
|
||||
UPDATE = "update",
|
||||
@ -68,7 +70,10 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
currentStore,
|
||||
addIssuesToView,
|
||||
} = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||
// mobx store
|
||||
const {
|
||||
project: projectStore,
|
||||
projectMember: { projectMembers },
|
||||
@ -144,6 +149,15 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
/>
|
||||
</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
|
||||
import { ListProperties } from "./properties";
|
||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
// ui
|
||||
import { Spinner, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
@ -19,11 +19,21 @@ interface IssueBlockProps {
|
||||
|
||||
export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
const { columnId, issue, handleIssues, quickActions, displayProperties, isReadonly } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
|
||||
handleIssues(issueToUpdate, EIssueActions.UPDATE);
|
||||
};
|
||||
|
||||
const handleIssuePeekOverview = () => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 && (
|
||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||
)}
|
||||
|
||||
<IssuePeekOverview
|
||||
workspaceSlug={issue?.workspace_detail?.slug}
|
||||
projectId={issue?.project_detail?.id}
|
||||
issueId={issue?.id}
|
||||
isArchived={issue?.archived_at !== null}
|
||||
handleIssue={(issueToUpdate) => {
|
||||
handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<div
|
||||
className="line-clamp-1 text-sm font-medium text-custom-text-100 w-full cursor-pointer"
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
{issue.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div className="ml-auto flex-shrink-0 flex items-center gap-2">
|
||||
{!issue?.tempId ? (
|
||||
|
@ -77,7 +77,7 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
|
||||
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]
|
||||
);
|
||||
|
@ -1,12 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayProperties } from "types";
|
||||
|
||||
@ -16,13 +12,6 @@ type Props = {
|
||||
handleToggleExpand: (issueId: string) => void;
|
||||
properties: IIssueDisplayProperties;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
setIssuePeekOverView: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
} | null>
|
||||
>;
|
||||
disableUserActions: boolean;
|
||||
nestingLevel: number;
|
||||
};
|
||||
@ -31,40 +20,20 @@ export const IssueColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
expanded,
|
||||
handleToggleExpand,
|
||||
setIssuePeekOverView,
|
||||
properties,
|
||||
quickActions,
|
||||
disableUserActions,
|
||||
nestingLevel,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// router
|
||||
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 { query } = router;
|
||||
setIssuePeekOverView({
|
||||
workspaceSlug: issue?.workspace_detail?.slug,
|
||||
projectId: issue?.project_detail?.id,
|
||||
issueId: issue?.id,
|
||||
});
|
||||
|
||||
router.push({
|
||||
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";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayProperties } from "types";
|
||||
import { EIssueActions } from "components/issues/issue-layouts/types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
@ -14,13 +13,6 @@ type Props = {
|
||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
properties: IIssueDisplayProperties;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
setIssuePeekOverView: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
} | null>
|
||||
>;
|
||||
disableUserActions: boolean;
|
||||
nestingLevel?: number;
|
||||
};
|
||||
@ -29,7 +21,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
expandedIssues,
|
||||
setExpandedIssues,
|
||||
setIssuePeekOverView,
|
||||
properties,
|
||||
quickActions,
|
||||
disableUserActions,
|
||||
@ -58,7 +49,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
||||
expanded={isExpanded}
|
||||
handleToggleExpand={handleToggleExpand}
|
||||
properties={properties}
|
||||
setIssuePeekOverView={setIssuePeekOverView}
|
||||
disableUserActions={disableUserActions}
|
||||
nestingLevel={nestingLevel}
|
||||
quickActions={quickActions}
|
||||
@ -76,7 +66,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
properties={properties}
|
||||
quickActions={quickActions}
|
||||
setIssuePeekOverView={setIssuePeekOverView}
|
||||
disableUserActions={disableUserActions}
|
||||
nestingLevel={nestingLevel + 1}
|
||||
/>
|
||||
|
@ -2,8 +2,12 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues";
|
||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
import {
|
||||
IssuePeekOverview,
|
||||
SpreadsheetColumnsList,
|
||||
SpreadsheetIssuesColumn,
|
||||
SpreadsheetQuickAddIssueForm,
|
||||
} from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types";
|
||||
@ -47,22 +51,14 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
disableUserActions,
|
||||
enableQuickCreateIssue,
|
||||
} = props;
|
||||
|
||||
// states
|
||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||
const [issuePeekOverview, setIssuePeekOverView] = useState<{
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
} | null>(null);
|
||||
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
// refs
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
|
||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!containerRef.current) return;
|
||||
@ -116,7 +112,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
properties={displayProperties}
|
||||
quickActions={quickActions}
|
||||
disableUserActions={disableUserActions}
|
||||
setIssuePeekOverView={setIssuePeekOverView}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
@ -185,11 +180,11 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
))} */}
|
||||
</div>
|
||||
</div>
|
||||
{issuePeekOverview && (
|
||||
{workspaceSlug && peekIssueId && peekProjectId && (
|
||||
<IssuePeekOverview
|
||||
workspaceSlug={issuePeekOverview?.workspaceSlug}
|
||||
projectId={issuePeekOverview?.projectId}
|
||||
issueId={issuePeekOverview?.issueId}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={peekProjectId.toString()}
|
||||
issueId={peekIssueId.toString()}
|
||||
handleIssue={(issueToUpdate: any) => handleIssues(issueToUpdate, EIssueActions.UPDATE)}
|
||||
/>
|
||||
)}
|
||||
|
@ -17,17 +17,19 @@ import {
|
||||
SidebarPrioritySelect,
|
||||
SidebarStateSelect,
|
||||
} from "../sidebar-select";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// components
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { LinkModal, LinksList } from "components/core";
|
||||
// types
|
||||
import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
interface IPeekOverviewProperties {
|
||||
issue: IIssue;
|
||||
@ -43,8 +45,11 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
const [linkModal, setLinkModal] = useState(false);
|
||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
||||
|
||||
const { user: userStore, cycleIssue: cycleIssueStore, moduleIssue: moduleIssueStore } = useMobxStore();
|
||||
const userRole = userStore.currentProjectRole;
|
||||
const {
|
||||
user: { currentProjectRole },
|
||||
cycleIssues: { addIssueToCycle },
|
||||
moduleIssues: { addIssueToModule },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -72,15 +77,16 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
const handleParent = (_parent: string) => {
|
||||
issueUpdate({ ...issue, parent: _parent });
|
||||
};
|
||||
const addIssueToCycle = async (cycleId: string) => {
|
||||
const handleAddIssueToCycle = async (cycleId: string) => {
|
||||
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;
|
||||
|
||||
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, [issue.id]);
|
||||
addIssueToModule(workspaceSlug.toString(), moduleId, [issue.id]);
|
||||
};
|
||||
const handleLabels = (formData: Partial<IIssue>) => {
|
||||
issueUpdate({ ...issue, ...formData });
|
||||
@ -303,7 +309,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
<div>
|
||||
<SidebarCycleSelect
|
||||
issueDetail={issue}
|
||||
handleCycleChange={addIssueToCycle}
|
||||
handleCycleChange={handleAddIssueToCycle}
|
||||
disabled={disableUserActions}
|
||||
/>
|
||||
</div>
|
||||
@ -317,7 +323,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
<div>
|
||||
<SidebarModuleSelect
|
||||
issueDetail={issue}
|
||||
handleModuleChange={addIssueToModule}
|
||||
handleModuleChange={handleAddIssueToModule}
|
||||
disabled={disableUserActions}
|
||||
/>
|
||||
</div>
|
||||
@ -370,10 +376,10 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
handleDeleteLink={handleDeleteLink}
|
||||
handleEditLink={handleEditLink}
|
||||
userAuth={{
|
||||
isGuest: userRole === 5,
|
||||
isViewer: userRole === 10,
|
||||
isMember: userRole === 15,
|
||||
isOwner: userRole === 20,
|
||||
isGuest: currentProjectRole === EUserWorkspaceRoles.GUEST,
|
||||
isViewer: currentProjectRole === EUserWorkspaceRoles.VIEWER,
|
||||
isMember: currentProjectRole === EUserWorkspaceRoles.MEMBER,
|
||||
isOwner: currentProjectRole === EUserWorkspaceRoles.ADMIN,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -1,18 +1,17 @@
|
||||
import { FC, Fragment, ReactNode } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssueView } from "./view";
|
||||
// hooks
|
||||
import useSWR from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { RootStore } from "store/root";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { IssueView } from "./view";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
interface IIssuePeekOverview {
|
||||
workspaceSlug: string;
|
||||
@ -27,7 +26,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, handleIssue, children, isArchived = false } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { peekIssueId } = router.query as { peekIssueId: string };
|
||||
const { peekIssueId } = router.query;
|
||||
|
||||
const {
|
||||
user: userStore,
|
||||
@ -36,18 +35,18 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
archivedIssueDetail: archivedIssueDetailStore,
|
||||
archivedIssues: archivedIssuesStore,
|
||||
project: projectStore,
|
||||
}: RootStore = useMobxStore();
|
||||
} = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
|
||||
? `ISSUE_PEEK_OVERVIEW_${workspaceSlug}_${projectId}_${peekIssueId}`
|
||||
? `ISSUE_DETAILS_${workspaceSlug}_${projectId}_${peekIssueId}`
|
||||
: null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) {
|
||||
if (isArchived) await archivedIssueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, issueId);
|
||||
else await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, issueId);
|
||||
if (workspaceSlug && projectId && issueId && issueId === peekIssueId) {
|
||||
if (isArchived) await archivedIssueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId);
|
||||
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 issueUpdate = (_data: Partial<IIssue>) => {
|
||||
if (handleIssue) {
|
||||
handleIssue(_data);
|
||||
issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data);
|
||||
}
|
||||
if (handleIssue) handleIssue(_data);
|
||||
};
|
||||
|
||||
const issueReactionCreate = (reaction: string) =>
|
||||
@ -114,6 +110,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
if (query.peekIssueId) {
|
||||
issueDetailStore.setPeekId(null);
|
||||
delete query.peekIssueId;
|
||||
delete query.peekProjectId;
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query },
|
||||
|
@ -12,7 +12,6 @@ import { DeleteIssueModal } from "../delete-issue-modal";
|
||||
import { DeleteArchivedIssueModal } from "../delete-archived-issue-modal";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { RootStore } from "store/root";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
@ -90,7 +89,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
const router = useRouter();
|
||||
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 [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
@ -101,7 +100,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
const { query } = router;
|
||||
router.push({
|
||||
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) {
|
||||
issueDetailStore.setPeekId(null);
|
||||
delete query.peekIssueId;
|
||||
delete query.peekProjectId;
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
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 { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
import { CycleService } from "services/cycle.service";
|
||||
// ui
|
||||
import { ContrastIcon, CustomSearchSelect, Tooltip } from "@plane/ui";
|
||||
@ -21,12 +19,17 @@ type Props = {
|
||||
};
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
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 { workspaceSlug, projectId, issueId } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// mobx store
|
||||
const {
|
||||
cycleIssues: { removeIssueFromCycle },
|
||||
} = useMobxStore();
|
||||
|
||||
const { data: incompleteCycles } = useSWR(
|
||||
workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null,
|
||||
@ -35,13 +38,12 @@ export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleCh
|
||||
: null
|
||||
);
|
||||
|
||||
const removeIssueFromCycle = (bridgeId: string, cycleId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const handleRemoveIssueFromCycle = (bridgeId: string, cycleId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetail) return;
|
||||
|
||||
issueService
|
||||
.removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId, bridgeId)
|
||||
removeIssueFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId, issueDetail.id, bridgeId)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(ISSUE_DETAILS(issueDetail.id));
|
||||
|
||||
mutate(CYCLE_ISSUES(cycleId));
|
||||
})
|
||||
@ -70,7 +72,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleCh
|
||||
value={issueCycle?.cycle_detail.id}
|
||||
onChange={(value: any) => {
|
||||
value === issueCycle?.cycle_detail.id
|
||||
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
|
||||
? handleRemoveIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
|
||||
: handleCycleChange(value);
|
||||
}}
|
||||
options={options}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { TwitterPicker } from "react-color";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { IssueLabelService } from "services/issue";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Input } from "@plane/ui";
|
||||
import { IssueLabelSelect } from "../select";
|
||||
@ -14,9 +15,6 @@ import { IssueLabelSelect } from "../select";
|
||||
import { Plus, X } from "lucide-react";
|
||||
// types
|
||||
import { IIssue, IIssueLabel } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
type Props = {
|
||||
issueDetails: IIssue | undefined;
|
||||
@ -31,60 +29,46 @@ const defaultValues: Partial<IIssueLabel> = {
|
||||
color: "#ff0000",
|
||||
};
|
||||
|
||||
const issueLabelService = new IssueLabelService();
|
||||
|
||||
export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
issueDetails,
|
||||
labelList,
|
||||
submitChanges,
|
||||
isNotAllowed,
|
||||
uneditable,
|
||||
}) => {
|
||||
export const SidebarLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const { issueDetails, labelList, submitChanges, isNotAllowed, uneditable } = props;
|
||||
// states
|
||||
const [createLabelForm, setCreateLabelForm] = useState(false);
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
// mobx store
|
||||
const {
|
||||
projectLabel: { projectLabels, createLabel },
|
||||
} = useMobxStore();
|
||||
// form info
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset,
|
||||
watch,
|
||||
control,
|
||||
setFocus,
|
||||
} = useForm<Partial<IIssueLabel>>({
|
||||
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>) => {
|
||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||
|
||||
await issueLabelService
|
||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData)
|
||||
await createLabel(workspaceSlug.toString(), projectId.toString(), formData)
|
||||
.then((res) => {
|
||||
reset(defaultValues);
|
||||
|
||||
issueLabelMutate((prevData: any) => [...(prevData ?? []), res], false);
|
||||
|
||||
submitChanges({ labels: [...(issueDetails?.labels ?? []), res.id] });
|
||||
|
||||
setCreateLabelForm(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setToastAlert({
|
||||
title: "Oops!",
|
||||
type: "error",
|
||||
message: error?.error ?? "Error while adding the label",
|
||||
title: "Error!",
|
||||
message: error?.error ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
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-wrap gap-1">
|
||||
{labelList?.map((labelId) => {
|
||||
const label = issueLabels?.find((l) => l.id === labelId);
|
||||
const label = projectLabels?.find((l) => l.id === labelId);
|
||||
|
||||
if (label)
|
||||
return (
|
||||
@ -118,7 +102,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
backgroundColor: label.color ?? "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
@ -166,40 +150,40 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
{createLabelForm && (
|
||||
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}>
|
||||
<div>
|
||||
<Popover className="relative">
|
||||
<>
|
||||
<Popover.Button className="grid place-items-center outline-none">
|
||||
{watch("color") && watch("color") !== "" && (
|
||||
<span
|
||||
className="h-6 w-6 rounded"
|
||||
style={{
|
||||
backgroundColor: watch("color") ?? "black",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Popover.Button>
|
||||
|
||||
<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)} />
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Popover className="relative">
|
||||
<>
|
||||
<Popover.Button className="grid place-items-center outline-none">
|
||||
{value && value?.trim() !== "" && (
|
||||
<span
|
||||
className="h-6 w-6 rounded"
|
||||
style={{
|
||||
backgroundColor: value ?? "black",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
</Popover>
|
||||
</Popover.Button>
|
||||
|
||||
<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">
|
||||
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
</Popover>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
@ -235,4 +219,4 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
// services
|
||||
import { ModuleService } from "services/module.service";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { CustomSearchSelect, DiceIcon, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
|
||||
import { ISSUE_DETAILS, MODULE_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issueDetail: IIssue | undefined;
|
||||
@ -16,24 +17,23 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const moduleService = new ModuleService();
|
||||
|
||||
export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModuleChange, disabled = false }) => {
|
||||
export const SidebarModuleSelect: React.FC<Props> = observer((props) => {
|
||||
const { issueDetail, handleModuleChange, disabled = false } = props;
|
||||
// router
|
||||
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(
|
||||
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId ? () => moduleService.getModules(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
const handleRemoveIssueFromModule = (bridgeId: string, moduleId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetail) return;
|
||||
|
||||
const removeIssueFromModule = (bridgeId: string, moduleId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
moduleService
|
||||
.removeIssueFromModule(workspaceSlug as string, projectId as string, moduleId, bridgeId)
|
||||
removeIssueFromModule(workspaceSlug.toString(), projectId.toString(), moduleId, issueDetail.id, bridgeId)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
mutate(ISSUE_DETAILS(issueDetail.id));
|
||||
|
||||
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,
|
||||
query: module.name,
|
||||
content: (
|
||||
@ -62,7 +62,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
||||
value={issueModule?.module_detail.id}
|
||||
onChange={(value: any) => {
|
||||
value === issueModule?.module_detail.id
|
||||
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
|
||||
? handleRemoveIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
|
||||
: handleModuleChange(value);
|
||||
}}
|
||||
options={options}
|
||||
@ -70,7 +70,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
||||
<div>
|
||||
<Tooltip
|
||||
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
|
||||
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="truncate">
|
||||
{modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}
|
||||
{projectModules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@ -97,4 +97,4 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
// lucide icons
|
||||
import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
||||
// components
|
||||
import { IssuePeekOverview } from "../issue-peek-overview";
|
||||
import { SubIssuesRootList } from "./issues-list";
|
||||
import { IssueProperty } from "./properties";
|
||||
import { IssuePeekOverview } from "components/issues";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IUser, IIssue } from "types";
|
||||
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||
// fetch keys
|
||||
|
||||
export interface ISubIssues {
|
||||
workspaceSlug: string;
|
||||
@ -47,139 +45,153 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
||||
copyText,
|
||||
handleIssueCrudOperation,
|
||||
handleUpdateIssue,
|
||||
}) => (
|
||||
<div>
|
||||
{issue && (
|
||||
<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 router = useRouter();
|
||||
const { peekProjectId, peekIssueId } = router.query;
|
||||
|
||||
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
|
||||
workspaceSlug={issue?.workspace_detail?.slug}
|
||||
projectId={issue?.project_detail?.id}
|
||||
issueId={issue?.id}
|
||||
handleIssue={(issueToUpdate) => {
|
||||
console.log("issueToUpdate", issueToUpdate);
|
||||
handleUpdateIssue(issue, { ...issue, ...issueToUpdate });
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 cursor-pointer">
|
||||
<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}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={peekProjectId.toString()}
|
||||
issueId={peekIssueId.toString()}
|
||||
handleIssue={(issueToUpdate) => handleUpdateIssue(issue, { ...issue, ...issueToUpdate })}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{issue && (
|
||||
<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>
|
||||
<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">
|
||||
<IssueProperty
|
||||
<div className="w-full flex items-center gap-2 cursor-pointer" onClick={handleIssuePeekOverview}>
|
||||
<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}
|
||||
parentIssue={parentIssue}
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
parentIssue={issue}
|
||||
spacingLeft={spacingLeft + 22}
|
||||
user={user}
|
||||
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>
|
||||
)}
|
||||
|
||||
{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";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { SubIssues } from "./issue";
|
||||
@ -61,37 +60,38 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
||||
handleIssuesLoader({ key: "sub_issues", issueId: parentIssue?.id });
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
}, [handleIssuesLoader, isLoading, issuesLoader.sub_issues, parentIssue?.id]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{issues &&
|
||||
issues.sub_issues &&
|
||||
issues.sub_issues.length > 0 &&
|
||||
issues.sub_issues.map((issue: IIssue) => (
|
||||
<SubIssues
|
||||
key={`${issue?.id}`}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssue={parentIssue}
|
||||
issue={issue}
|
||||
spacingLeft={spacingLeft}
|
||||
user={user}
|
||||
editable={editable}
|
||||
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
||||
issuesLoader={issuesLoader}
|
||||
handleIssuesLoader={handleIssuesLoader}
|
||||
copyText={copyText}
|
||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
/>
|
||||
))}
|
||||
<>
|
||||
<div className="relative">
|
||||
{issues &&
|
||||
issues.sub_issues &&
|
||||
issues.sub_issues.length > 0 &&
|
||||
issues.sub_issues.map((issue: IIssue) => (
|
||||
<SubIssues
|
||||
key={`${issue?.id}`}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssue={parentIssue}
|
||||
issue={issue}
|
||||
spacingLeft={spacingLeft}
|
||||
user={user}
|
||||
editable={editable}
|
||||
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
||||
issuesLoader={issuesLoader}
|
||||
handleIssuesLoader={handleIssuesLoader}
|
||||
copyText={copyText}
|
||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div
|
||||
className={`absolute top-0 bottom-0 ${spacingLeft > 10 ? `border-l border-custom-border-100` : ``}`}
|
||||
style={{ left: `${spacingLeft - 12}px` }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 bottom-0 ${spacingLeft > 10 ? `border-l border-custom-border-100` : ``}`}
|
||||
style={{ left: `${spacingLeft - 12}px` }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -43,7 +43,11 @@ const issueService = new IssueService();
|
||||
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((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 router = useRouter();
|
||||
@ -166,10 +170,10 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
||||
...data,
|
||||
};
|
||||
|
||||
issueStore.updateIssueStructure(null, null, payload);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
||||
updateIssueStructure(null, null, payload);
|
||||
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;
|
||||
|
@ -401,7 +401,6 @@ const ProfileSettingsPage: NextPageWithLayout = () => {
|
||||
<span className="text-lg tracking-tight">Deactivate account</span>
|
||||
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
|
||||
</Disclosure.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
|
@ -106,7 +106,14 @@ export class ModuleService extends APIService {
|
||||
projectId: string,
|
||||
moduleId: 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)
|
||||
.then((response) => response?.data)
|
||||
.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;
|
||||
|
||||
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) => {
|
||||
|
@ -76,7 +76,7 @@ export class CycleIssueCalendarViewStore implements ICycleIssueCalendarViewStore
|
||||
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
updateIssue.issueId,
|
||||
|
@ -289,7 +289,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
|
||||
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
updateIssue.issueId,
|
||||
@ -437,7 +437,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
|
||||
this.rootStore.cycleIssue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
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;
|
||||
|
||||
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
||||
this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
|
||||
};
|
||||
|
||||
fetchIssues = async (
|
||||
|
@ -75,7 +75,7 @@ export class IssueCalendarViewStore implements IIssueCalendarViewStore {
|
||||
this.rootStore.issue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
updateIssue.issueId,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
import { observable, action, makeObservable, runInAction, computed, autorun } from "mobx";
|
||||
// services
|
||||
import { IssueService, IssueReactionService, IssueCommentService } from "services/issue";
|
||||
import { NotificationService } from "services/notification.service";
|
||||
@ -7,8 +7,6 @@ import { RootStore } from "../root";
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { groupReactionEmojis } from "constants/issue";
|
||||
// uuid
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export interface IIssueDetailStore {
|
||||
loader: boolean;
|
||||
@ -44,11 +42,6 @@ export interface IIssueDetailStore {
|
||||
|
||||
// fetch issue details
|
||||
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
|
||||
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
|
||||
@ -146,9 +139,6 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
setPeekId: action,
|
||||
|
||||
fetchIssueDetails: action,
|
||||
createIssue: action,
|
||||
optimisticallyCreateIssue: action,
|
||||
updateIssue: action,
|
||||
deleteIssue: action,
|
||||
|
||||
fetchPeekIssueDetails: action,
|
||||
@ -171,6 +161,26 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
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.issueService = new IssueService();
|
||||
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) => {
|
||||
const newIssues = { ...this.issues };
|
||||
delete newIssues[issueId];
|
||||
|
@ -289,7 +289,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
||||
this.rootStore.issue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
updateIssue.issueId,
|
||||
@ -437,7 +437,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
||||
this.rootStore.issue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
updateIssue.issueId,
|
||||
|
@ -54,7 +54,7 @@ export interface IModuleIssuesStore {
|
||||
moduleId: string,
|
||||
issueIds: string[],
|
||||
fetchAfterAddition?: boolean
|
||||
) => Promise<IIssue>;
|
||||
) => Promise<any>;
|
||||
removeIssueFromModule: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@ -193,7 +193,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
|
||||
|
||||
try {
|
||||
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;
|
||||
if (!_issues) _issues = {};
|
||||
@ -204,7 +204,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
|
||||
this.issues = _issues;
|
||||
});
|
||||
|
||||
return issueToModule;
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
|
||||
throw error;
|
||||
@ -286,7 +286,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
|
||||
|
||||
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];
|
||||
|
||||
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;
|
||||
|
||||
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) => {
|
||||
|
@ -76,7 +76,7 @@ export class ModuleIssueCalendarViewStore implements IModuleIssueCalendarViewSto
|
||||
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
updateIssue.issueId,
|
||||
|
@ -289,7 +289,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
|
||||
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
updateIssue.issueId,
|
||||
@ -437,7 +437,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
|
||||
this.rootStore.moduleIssue.issues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
updateIssue.issueId,
|
||||
|
@ -76,7 +76,7 @@ export class ProjectViewIssueCalendarViewStore implements IProjectViewIssueCalen
|
||||
this.rootStore.projectViewIssues.viewIssues = { ...reorderedIssues };
|
||||
});
|
||||
|
||||
this.rootStore.issueDetail?.updateIssue(
|
||||
this.rootStore.projectIssues.updateIssue(
|
||||
updateIssue.workspaceSlug,
|
||||
updateIssue.projectId,
|
||||
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;
|
||||
|
||||
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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user