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:
Aaryan Khandelwal 2023-11-29 14:25:57 +05:30 committed by Aaryan Khandelwal
parent 8662305f0a
commit 223984d900
56 changed files with 637 additions and 1510 deletions

View File

@ -30,7 +30,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
const { const {
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal }, commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
issueDetail: { updateIssue }, projectIssues: { updateIssue },
user: { currentUser }, user: { currentUser },
} = useMobxStore(); } = useMobxStore();

View File

@ -21,7 +21,7 @@ export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store // store
const { const {
issueDetail: { updateIssue }, projectIssues: { updateIssue },
projectMember: { projectMembers }, projectMember: { projectMembers },
} = useMobxStore(); } = useMobxStore();

View File

@ -23,7 +23,7 @@ export const ChangeIssuePriority: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { const {
issueDetail: { updateIssue }, projectIssues: { updateIssue },
} = useMobxStore(); } = useMobxStore();
const submitChanges = async (formData: Partial<IIssue>) => { const submitChanges = async (formData: Partial<IIssue>) => {

View File

@ -24,7 +24,7 @@ export const ChangeIssueState: React.FC<Props> = observer((props) => {
const { const {
projectState: { projectStates }, projectState: { projectStates },
issueDetail: { updateIssue }, projectIssues: { updateIssue },
} = useMobxStore(); } = useMobxStore();
const submitChanges = async (formData: Partial<IIssue>) => { const submitChanges = async (formData: Partial<IIssue>) => {

View File

@ -226,31 +226,12 @@ const activityDetails: {
}, },
icon: <BlockedIcon height="12" width="12" color="#6b7280" />, icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
}, },
duplicate: {
message: (activity) => {
if (activity.old_value === "")
return (
<>
marked this issue as duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed this issue as a duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
icon: <CopyPlus size={12} color="#6b7280" />,
},
cycles: { cycles: {
message: (activity, showIssue, workspaceSlug) => { message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created") if (activity.verb === "created")
return ( return (
<> <>
<span className="flex-shrink-0">added this issue to the cycle</span> <span className="flex-shrink-0">added this issue to the cycle </span>
<a <a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank" target="_blank"
@ -295,6 +276,25 @@ const activityDetails: {
}, },
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />, icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
}, },
duplicate: {
message: (activity) => {
if (activity.old_value === "")
return (
<>
marked this issue as duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed this issue as a duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
icon: <CopyPlus size={12} color="#6b7280" />,
},
description: { description: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
<> <>
@ -354,7 +354,7 @@ const activityDetails: {
return ( return (
<> <>
added a new label{" "} added a new label{" "}
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs w-min whitespace-nowrap"> <span className="inline-flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs w-min whitespace-nowrap">
<LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} /> <LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} />
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span> <span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
</span> </span>
@ -370,7 +370,7 @@ const activityDetails: {
return ( return (
<> <>
removed the label{" "} removed the label{" "}
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs"> <span className="inline-flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs w-min whitespace-nowrap">
<LabelPill labelId={activity.old_identifier ?? ""} workspaceSlug={workspaceSlug} /> <LabelPill labelId={activity.old_identifier ?? ""} workspaceSlug={workspaceSlug} />
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span> <span className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span>
</span> </span>

View File

@ -1,10 +1,13 @@
// ui
import { ExternalLinkIcon, Tooltip } from "@plane/ui";
// icons // icons
import { ExternalLinkIcon } from "@plane/ui";
import { Pencil, Trash2, LinkIcon } from "lucide-react"; import { Pencil, Trash2, LinkIcon } from "lucide-react";
// helpers // helpers
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
// types // types
import { linkDetails, UserAuth } from "types"; import { linkDetails, UserAuth } from "types";
// hooks
import useToast from "hooks/use-toast";
type Props = { type Props = {
links: linkDetails[]; links: linkDetails[];
@ -14,18 +17,37 @@ type Props = {
}; };
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEditLink, userAuth }) => { export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEditLink, userAuth }) => {
// toast
const { setToastAlert } = useToast();
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setToastAlert({
message: "The URL has been successfully copied to your clipboard",
type: "success",
title: "Copied to clipboard",
});
};
return ( return (
<> <>
{links.map((link) => ( {links.map((link) => (
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5"> <div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
<div className="flex items-start justify-between gap-2 w-full"> <div className="flex items-start justify-between gap-2 w-full">
<div className="flex items-start gap-2"> <div className="flex items-start truncate gap-2">
<span className="py-1"> <span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" /> <LinkIcon className="h-3 w-3 flex-shrink-0" />
</span> </span>
<span className="text-xs break-all">{link.title && link.title !== "" ? link.title : link.url}</span> <Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url}>
<span
className="text-xs truncate cursor-pointer"
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
>
{link.title && link.title !== "" ? link.title : link.url}
</span>
</Tooltip>
</div> </div>
{!isNotAllowed && ( {!isNotAllowed && (

View File

@ -130,7 +130,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
)} )}
<div className="flex-grow truncate h-full flex items-center justify-between gap-2"> <div className="flex-grow truncate h-full flex items-center justify-between gap-2">
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<IssueGanttSidebarBlock data={block.data} handleIssue={blockUpdateHandler} /> <IssueGanttSidebarBlock data={block.data} />
</div> </div>
<div className="flex-shrink-0 text-sm text-custom-text-200"> <div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""} {duration} day{duration > 1 ? "s" : ""}

View File

@ -138,7 +138,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
)} )}
<div className="flex-grow truncate h-full flex items-center justify-between gap-2"> <div className="flex-grow truncate h-full flex items-center justify-between gap-2">
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<IssueGanttSidebarBlock data={block.data} handleIssue={blockUpdateHandler} /> <IssueGanttSidebarBlock data={block.data} />
</div> </div>
<div className="flex-shrink-0 text-sm text-custom-text-200"> <div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""} {duration} day{duration > 1 ? "s" : ""}

View File

@ -7,13 +7,13 @@ export * from "./delete-issue-modal";
export * from "./description-form"; export * from "./description-form";
export * from "./form"; export * from "./form";
export * from "./issue-layouts"; export * from "./issue-layouts";
export * from "./issue-peek-overview";
export * from "./main-content"; export * from "./main-content";
export * from "./modal"; export * from "./modal";
export * from "./parent-issues-list-modal"; export * from "./parent-issues-list-modal";
export * from "./sidebar"; export * from "./sidebar";
export * from "./label"; export * from "./label";
export * from "./issue-reaction"; export * from "./issue-reaction";
export * from "./peek-overview";
export * from "./confirm-issue-discard"; export * from "./confirm-issue-discard";
// draft issue // draft issue

View File

@ -1,10 +1,9 @@
import { FC, useCallback } from "react"; import { FC, useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd"; import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart } from "components/issues"; import { CalendarChart, IssuePeekOverview } from "components/issues";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { import {
@ -41,8 +40,11 @@ interface IBaseCalendarRoot {
} }
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
const { issueStore, issuesFilterStore, calendarViewStore, QuickActions, issueActions, viewId, handleDragDrop } = const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, handleDragDrop } = props;
props;
// router
const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
const displayFilters = issuesFilterStore.issueFilters?.displayFilters; const displayFilters = issuesFilterStore.issueFilters?.displayFilters;
@ -67,10 +69,11 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
issueActions[action]!(issue); issueActions[action]!(issue);
} }
}, },
[issueStore] [issueActions]
); );
return ( return (
<>
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<CalendarChart <CalendarChart
@ -78,7 +81,6 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
layout={displayFilters?.calendar?.layout} layout={displayFilters?.calendar?.layout}
showWeekends={displayFilters?.calendar?.show_weekends ?? false} showWeekends={displayFilters?.calendar?.show_weekends ?? false}
handleIssues={handleIssues}
quickActions={(issue) => ( quickActions={(issue) => (
<QuickActions <QuickActions
issue={issue} issue={issue}
@ -100,5 +102,16 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
/> />
</DragDropContext> </DragDropContext>
</div> </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)
}
/>
)}
</>
); );
}); });

View File

@ -9,7 +9,6 @@ import { Spinner } from "@plane/ui";
// types // types
import { ICalendarWeek } from "./types"; import { ICalendarWeek } from "./types";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IGroupedIssues, IIssueResponse } from "store/issues/types"; import { IGroupedIssues, IIssueResponse } from "store/issues/types";
type Props = { type Props = {
@ -17,7 +16,6 @@ type Props = {
groupedIssueIds: IGroupedIssues; groupedIssueIds: IGroupedIssues;
layout: "month" | "week" | undefined; layout: "month" | "week" | undefined;
showWeekends: boolean; showWeekends: boolean;
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode; quickActions: (issue: IIssue) => React.ReactNode;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
@ -29,7 +27,7 @@ type Props = {
}; };
export const CalendarChart: React.FC<Props> = observer((props) => { export const CalendarChart: React.FC<Props> = observer((props) => {
const { issues, groupedIssueIds, layout, showWeekends, handleIssues, quickActions, quickAddCallback, viewId } = props; const { issues, groupedIssueIds, layout, showWeekends, quickActions, quickAddCallback, viewId } = props;
const { calendar: calendarStore } = useMobxStore(); const { calendar: calendarStore } = useMobxStore();
@ -60,7 +58,6 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
enableQuickIssueCreate enableQuickIssueCreate
handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
viewId={viewId} viewId={viewId}
@ -74,7 +71,6 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
enableQuickIssueCreate enableQuickIssueCreate
handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
viewId={viewId} viewId={viewId}

View File

@ -10,14 +10,12 @@ import { renderDateFormat } from "helpers/date-time.helper";
// constants // constants
import { MONTHS_LIST } from "constants/calendar"; import { MONTHS_LIST } from "constants/calendar";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IGroupedIssues, IIssueResponse } from "store/issues/types"; import { IGroupedIssues, IIssueResponse } from "store/issues/types";
type Props = { type Props = {
date: ICalendarDate; date: ICalendarDate;
issues: IIssueResponse | undefined; issues: IIssueResponse | undefined;
groupedIssueIds: IGroupedIssues; groupedIssueIds: IGroupedIssues;
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode; quickActions: (issue: IIssue) => React.ReactNode;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: ( quickAddCallback?: (
@ -30,16 +28,7 @@ type Props = {
}; };
export const CalendarDayTile: React.FC<Props> = observer((props) => { export const CalendarDayTile: React.FC<Props> = observer((props) => {
const { const { date, issues, groupedIssueIds, quickActions, enableQuickIssueCreate, quickAddCallback, viewId } = props;
date,
issues,
groupedIssueIds,
handleIssues,
quickActions,
enableQuickIssueCreate,
quickAddCallback,
viewId,
} = props;
const { issueFilter: issueFilterStore } = useMobxStore(); const { issueFilter: issueFilterStore } = useMobxStore();
@ -81,12 +70,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{...provided.droppableProps} {...provided.droppableProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
<CalendarIssueBlocks <CalendarIssueBlocks issues={issues} issueIdList={issueIdList} quickActions={quickActions} />
issues={issues}
issueIdList={issueIdList}
handleIssues={handleIssues}
quickActions={quickActions}
/>
{enableQuickIssueCreate && ( {enableQuickIssueCreate && (
<div className="py-1 px-2"> <div className="py-1 px-2">
<CalendarQuickAddIssueForm <CalendarQuickAddIssueForm

View File

@ -1,22 +1,31 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Draggable } from "@hello-pangea/dnd"; import { Draggable } from "@hello-pangea/dnd";
// components // components
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IIssueResponse } from "store/issues/types"; import { IIssueResponse } from "store/issues/types";
type Props = { type Props = {
issues: IIssueResponse | undefined; issues: IIssueResponse | undefined;
issueIdList: string[] | null; issueIdList: string[] | null;
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode; quickActions: (issue: IIssue) => React.ReactNode;
}; };
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => { export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { issues, issueIdList, handleIssues, quickActions } = props; const { issues, issueIdList, quickActions } = props;
// router
const router = useRouter();
const handleIssuePeekOverview = (issue: IIssue) => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
};
return ( return (
<> <>
@ -28,22 +37,24 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
<Draggable key={issue.id} draggableId={issue.id} index={index}> <Draggable key={issue.id} draggableId={issue.id} index={index}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className="p-1 px-2 relative" className="p-1 px-2 relative cursor-pointer"
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
onClick={() => handleIssuePeekOverview(issue)}
> >
{issue?.tempId !== undefined && ( {issue?.tempId !== undefined && (
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" /> <div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
)} )}
<div <div
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${ className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center justify-between gap-1.5 border-[0.5px] border-custom-border-100 ${
snapshot.isDragging snapshot.isDragging
? "shadow-custom-shadow-rg bg-custom-background-90" ? "shadow-custom-shadow-rg bg-custom-background-90"
: "bg-custom-background-100 hover:bg-custom-background-90" : "bg-custom-background-100 hover:bg-custom-background-90"
}`} }`}
> >
<div className="flex items-center gap-1.5 h-full">
<span <span
className="h-full w-0.5 rounded flex-shrink-0" className="h-full w-0.5 rounded flex-shrink-0"
style={{ style={{
@ -53,20 +64,19 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
<div className="text-xs text-custom-text-300 flex-shrink-0"> <div className="text-xs text-custom-text-300 flex-shrink-0">
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </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);
}}
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="text-xs truncate">{issue.name}</div> <div className="text-xs truncate">{issue.name}</div>
</Tooltip> </Tooltip>
</IssuePeekOverview> </div>
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div> <div
className="hidden group-hover/calendar-block:block"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{quickActions(issue)}
</div>
</div> </div>
</div> </div>
)} )}

View File

@ -9,14 +9,12 @@ import { renderDateFormat } from "helpers/date-time.helper";
// types // types
import { ICalendarDate, ICalendarWeek } from "./types"; import { ICalendarDate, ICalendarWeek } from "./types";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IGroupedIssues, IIssueResponse } from "store/issues/types"; import { IGroupedIssues, IIssueResponse } from "store/issues/types";
type Props = { type Props = {
issues: IIssueResponse | undefined; issues: IIssueResponse | undefined;
groupedIssueIds: IGroupedIssues; groupedIssueIds: IGroupedIssues;
week: ICalendarWeek | undefined; week: ICalendarWeek | undefined;
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode; quickActions: (issue: IIssue) => React.ReactNode;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: ( quickAddCallback?: (
@ -29,16 +27,7 @@ type Props = {
}; };
export const CalendarWeekDays: React.FC<Props> = observer((props) => { export const CalendarWeekDays: React.FC<Props> = observer((props) => {
const { const { issues, groupedIssueIds, week, quickActions, enableQuickIssueCreate, quickAddCallback, viewId } = props;
issues,
groupedIssueIds,
week,
handleIssues,
quickActions,
enableQuickIssueCreate,
quickAddCallback,
viewId,
} = props;
const { issueFilter: issueFilterStore } = useMobxStore(); const { issueFilter: issueFilterStore } = useMobxStore();
@ -62,7 +51,6 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
date={date} date={date}
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}

View File

@ -1,11 +1,10 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import useProjectDetails from "hooks/use-project-details";
// components // components
import { IssueGanttBlock } from "components/issues"; import { IssueGanttBlock, IssuePeekOverview } from "components/issues";
import { import {
GanttChartRoot, GanttChartRoot,
IBlockUpdateData, IBlockUpdateData,
@ -41,9 +40,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
const { issueFiltersStore, issueStore, viewId } = props; const { issueFiltersStore, issueStore, viewId } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string }; const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
const { projectDetails } = useProjectDetails();
const { const {
user: { currentProjectRole }, user: { currentProjectRole },
@ -61,7 +58,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
//Todo fix sort order in the structure //Todo fix sort order in the structure
issueStore.updateIssue( issueStore.updateIssue(
workspaceSlug, workspaceSlug.toString(),
issue.project, issue.project,
issue.id, issue.id,
{ {
@ -83,7 +80,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
loaderTitle="Issues" loaderTitle="Issues"
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null} blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
blockUpdateHandler={updateIssue} blockUpdateHandler={updateIssue}
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} handleIssue={updateIssue} />} blockToRender={(data: IIssue) => <IssueGanttBlock data={data} />}
sidebarToRender={(props) => ( sidebarToRender={(props) => (
<IssueGanttSidebar <IssueGanttSidebar
{...props} {...props}
@ -98,6 +95,17 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed} enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
/> />
</div> </div>
{workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={(issueToUpdate) => {
// TODO: update the logic here
updateIssue(issueToUpdate as IIssue, {});
}}
/>
)}
</> </>
); );
}); });

View File

@ -1,28 +1,28 @@
import { useRouter } from "next/router";
// ui // ui
import { Tooltip, StateGroupIcon } from "@plane/ui"; import { Tooltip, StateGroupIcon } from "@plane/ui";
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
import { IBlockUpdateData } from "components/gantt-chart";
// helpers // helpers
import { renderShortDate } from "helpers/date-time.helper"; import { renderShortDate } from "helpers/date-time.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
export const IssueGanttBlock = ({ export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
data, const router = useRouter();
handleIssue,
}: { const handleIssuePeekOverview = () => {
data: IIssue; const { query } = router;
handleIssue: (block: IIssue, payload: IBlockUpdateData) => void;
}) => ( router.push({
<IssuePeekOverview pathname: router.pathname,
workspaceSlug={data?.workspace_detail?.slug} query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
projectId={data?.project_detail?.id} });
issueId={data?.id} };
handleIssue={(issueToUpdate) => handleIssue({ ...data, ...issueToUpdate }, {})}
> return (
<div <div
className="flex items-center relative h-full w-full rounded cursor-pointer" className="flex items-center relative h-full w-full rounded cursor-pointer"
style={{ backgroundColor: data?.state_detail?.color }} style={{ backgroundColor: data?.state_detail?.color }}
onClick={handleIssuePeekOverview}
> >
<div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" /> <div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" />
<Tooltip <Tooltip
@ -41,24 +41,24 @@ export const IssueGanttBlock = ({
</Tooltip> </Tooltip>
</Tooltip> </Tooltip>
</div> </div>
</IssuePeekOverview> );
); };
// rendering issues on gantt sidebar // rendering issues on gantt sidebar
export const IssueGanttSidebarBlock = ({ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
data, const router = useRouter();
handleIssue,
}: { const handleIssuePeekOverview = () => {
data: IIssue; const { query } = router;
handleIssue: (block: IIssue, payload: IBlockUpdateData) => void;
}) => ( router.push({
<IssuePeekOverview pathname: router.pathname,
workspaceSlug={data?.workspace_detail?.slug} query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
projectId={data?.project_detail?.id} });
issueId={data?.id} };
handleIssue={(issueToUpdate) => handleIssue({ ...data, ...issueToUpdate }, {})}
> return (
<div className="relative w-full flex items-center gap-2 h-full cursor-pointer"> <div className="relative w-full flex items-center gap-2 h-full cursor-pointer" onClick={handleIssuePeekOverview}>
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} /> <StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
<div className="text-xs text-custom-text-300 flex-shrink-0"> <div className="text-xs text-custom-text-300 flex-shrink-0">
{data?.project_detail?.identifier} {data?.sequence_id} {data?.project_detail?.identifier} {data?.sequence_id}
@ -67,5 +67,5 @@ export const IssueGanttSidebarBlock = ({
<span className="text-sm font-medium flex-grow truncate">{data?.name}</span> <span className="text-sm font-medium flex-grow truncate">{data?.name}</span>
</Tooltip> </Tooltip>
</div> </div>
</IssuePeekOverview> );
); };

View File

@ -1,4 +1,5 @@
import { FC, useCallback, useState } from "react"; import { FC, useCallback, useState } from "react";
import { useRouter } from "next/router";
import { DragDropContext, Droppable } from "@hello-pangea/dnd"; import { DragDropContext, Droppable } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
@ -29,7 +30,7 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
import { KanBan } from "./default"; import { KanBan } from "./default";
import { KanBanSwimLanes } from "./swimlanes"; import { KanBanSwimLanes } from "./swimlanes";
import { EProjectStore } from "store/command-palette.store"; import { EProjectStore } from "store/command-palette.store";
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { IssuePeekOverview } from "components/issues";
export interface IBaseKanBanLayout { export interface IBaseKanBanLayout {
issueStore: issueStore:
@ -79,7 +80,10 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
handleDragDrop, handleDragDrop,
addIssuesToView, addIssuesToView,
} = props; } = props;
// router
const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
// mobx store
const { const {
project: { workspaceProjects }, project: { workspaceProjects },
projectLabel: { projectLabels }, projectLabel: { projectLabels },
@ -134,7 +138,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
issueActions[action]!(issue); issueActions[action]!(issue);
} }
}, },
[issueStore] [issueActions]
); );
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
@ -264,6 +268,17 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
)} )}
</DragDropContext> </DragDropContext>
</div> </div>
{workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={(issueToUpdate) =>
handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, EIssueActions.UPDATE)
}
/>
)}
</> </>
); );
}); });

View File

@ -1,11 +1,12 @@
import { Draggable } from "@hello-pangea/dnd"; import { Draggable } from "@hello-pangea/dnd";
// components // components
import { KanBanProperties } from "./properties"; import { KanBanProperties } from "./properties";
// ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
// types // types
import { IIssueDisplayProperties, IIssue } from "types"; import { IIssueDisplayProperties, IIssue } from "types";
import { EIssueActions } from "../types"; import { EIssueActions } from "../types";
import { useRouter } from "next/router";
interface IssueBlockProps { interface IssueBlockProps {
sub_group_id: string; sub_group_id: string;
@ -33,11 +34,22 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
displayProperties, displayProperties,
isReadOnly, isReadOnly,
} = props; } = props;
// router
const router = useRouter();
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => { const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE); if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
}; };
const handleIssuePeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
};
return ( return (
<> <>
<Draggable draggableId={issue.id} index={index}> <Draggable draggableId={issue.id} index={index}>
@ -47,6 +59,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
onClick={handleIssuePeekOverview}
> >
{issue.tempId !== undefined && ( {issue.tempId !== undefined && (
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" /> <div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
@ -68,23 +81,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </div>
)} )}
<IssuePeekOverview
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}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div> <div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
</Tooltip> </Tooltip>
</IssuePeekOverview>
<div> <div>
<KanBanProperties <KanBanProperties
sub_group_id={sub_group_id} sub_group_id={sub_group_id}

View File

@ -23,6 +23,8 @@ import {
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { IIssueResponse } from "store/issues/types"; import { IIssueResponse } from "store/issues/types";
import { EProjectStore } from "store/command-palette.store"; import { EProjectStore } from "store/command-palette.store";
import { IssuePeekOverview } from "components/issues";
import { useRouter } from "next/router";
enum EIssueActions { enum EIssueActions {
UPDATE = "update", UPDATE = "update",
@ -68,7 +70,10 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
currentStore, currentStore,
addIssuesToView, addIssuesToView,
} = props; } = props;
// router
const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
// mobx store
const { const {
project: projectStore, project: projectStore,
projectMember: { projectMembers }, projectMember: { projectMembers },
@ -144,6 +149,15 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
/> />
</div> </div>
)} )}
{workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={(issueToUpdate) => handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE)}
/>
)}
</> </>
); );
}); });

View File

@ -1,6 +1,6 @@
import { useRouter } from "next/router";
// components // components
import { ListProperties } from "./properties"; import { ListProperties } from "./properties";
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
// ui // ui
import { Spinner, Tooltip } from "@plane/ui"; import { Spinner, Tooltip } from "@plane/ui";
// types // types
@ -19,11 +19,21 @@ interface IssueBlockProps {
export const IssueBlock: React.FC<IssueBlockProps> = (props) => { export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
const { columnId, issue, handleIssues, quickActions, displayProperties, isReadonly } = props; const { columnId, issue, handleIssues, quickActions, displayProperties, isReadonly } = props;
// router
const router = useRouter();
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => { const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
handleIssues(issueToUpdate, EIssueActions.UPDATE); handleIssues(issueToUpdate, EIssueActions.UPDATE);
}; };
const handleIssuePeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
};
return ( return (
<> <>
<div className="text-sm p-3 relative bg-custom-background-100 flex items-center gap-3"> <div className="text-sm p-3 relative bg-custom-background-100 flex items-center gap-3">
@ -36,20 +46,14 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
{issue?.tempId !== undefined && ( {issue?.tempId !== undefined && (
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" /> <div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
)} )}
<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}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="line-clamp-1 text-sm font-medium text-custom-text-100 w-full">{issue.name}</div> <div
className="line-clamp-1 text-sm font-medium text-custom-text-100 w-full cursor-pointer"
onClick={handleIssuePeekOverview}
>
{issue.name}
</div>
</Tooltip> </Tooltip>
</IssuePeekOverview>
<div className="ml-auto flex-shrink-0 flex items-center gap-2"> <div className="ml-auto flex-shrink-0 flex items-center gap-2">
{!issue?.tempId ? ( {!issue?.tempId ? (

View File

@ -77,7 +77,7 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
}; };
globalViewIssuesStore.updateIssueStructure(type ?? globalViewId!.toString(), payload); globalViewIssuesStore.updateIssueStructure(type ?? globalViewId!.toString(), payload);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, data); // issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, data);
}, },
[globalViewId, globalViewIssuesStore, workspaceSlug, issueDetailStore] [globalViewId, globalViewIssuesStore, workspaceSlug, issueDetailStore]
); );

View File

@ -1,12 +1,8 @@
import React, { useState } from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
// components // components
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
// types // types
import { IIssue, IIssueDisplayProperties } from "types"; import { IIssue, IIssueDisplayProperties } from "types";
@ -16,13 +12,6 @@ type Props = {
handleToggleExpand: (issueId: string) => void; handleToggleExpand: (issueId: string) => void;
properties: IIssueDisplayProperties; properties: IIssueDisplayProperties;
quickActions: (issue: IIssue) => React.ReactNode; quickActions: (issue: IIssue) => React.ReactNode;
setIssuePeekOverView: React.Dispatch<
React.SetStateAction<{
workspaceSlug: string;
projectId: string;
issueId: string;
} | null>
>;
disableUserActions: boolean; disableUserActions: boolean;
nestingLevel: number; nestingLevel: number;
}; };
@ -31,40 +20,20 @@ export const IssueColumn: React.FC<Props> = ({
issue, issue,
expanded, expanded,
handleToggleExpand, handleToggleExpand,
setIssuePeekOverView,
properties, properties,
quickActions, quickActions,
disableUserActions, disableUserActions,
nestingLevel, nestingLevel,
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const handleCopyText = () => {
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const handleIssuePeekOverview = (issue: IIssue) => { const handleIssuePeekOverview = (issue: IIssue) => {
const { query } = router; const { query } = router;
setIssuePeekOverView({
workspaceSlug: issue?.workspace_detail?.slug,
projectId: issue?.project_detail?.id,
issueId: issue?.id,
});
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id }, query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
}); });
}; };

View File

@ -6,7 +6,6 @@ import { IssueColumn } from "components/issues";
import useSubIssue from "hooks/use-sub-issue"; import useSubIssue from "hooks/use-sub-issue";
// types // types
import { IIssue, IIssueDisplayProperties } from "types"; import { IIssue, IIssueDisplayProperties } from "types";
import { EIssueActions } from "components/issues/issue-layouts/types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
@ -14,13 +13,6 @@ type Props = {
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>; setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
properties: IIssueDisplayProperties; properties: IIssueDisplayProperties;
quickActions: (issue: IIssue) => React.ReactNode; quickActions: (issue: IIssue) => React.ReactNode;
setIssuePeekOverView: React.Dispatch<
React.SetStateAction<{
workspaceSlug: string;
projectId: string;
issueId: string;
} | null>
>;
disableUserActions: boolean; disableUserActions: boolean;
nestingLevel?: number; nestingLevel?: number;
}; };
@ -29,7 +21,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
issue, issue,
expandedIssues, expandedIssues,
setExpandedIssues, setExpandedIssues,
setIssuePeekOverView,
properties, properties,
quickActions, quickActions,
disableUserActions, disableUserActions,
@ -58,7 +49,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
expanded={isExpanded} expanded={isExpanded}
handleToggleExpand={handleToggleExpand} handleToggleExpand={handleToggleExpand}
properties={properties} properties={properties}
setIssuePeekOverView={setIssuePeekOverView}
disableUserActions={disableUserActions} disableUserActions={disableUserActions}
nestingLevel={nestingLevel} nestingLevel={nestingLevel}
quickActions={quickActions} quickActions={quickActions}
@ -76,7 +66,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
setExpandedIssues={setExpandedIssues} setExpandedIssues={setExpandedIssues}
properties={properties} properties={properties}
quickActions={quickActions} quickActions={quickActions}
setIssuePeekOverView={setIssuePeekOverView}
disableUserActions={disableUserActions} disableUserActions={disableUserActions}
nestingLevel={nestingLevel + 1} nestingLevel={nestingLevel + 1}
/> />

View File

@ -2,8 +2,12 @@ import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues"; import {
import { IssuePeekOverview } from "components/issues/issue-peek-overview"; IssuePeekOverview,
SpreadsheetColumnsList,
SpreadsheetIssuesColumn,
SpreadsheetQuickAddIssueForm,
} from "components/issues";
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// types // types
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types"; import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types";
@ -47,22 +51,14 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
disableUserActions, disableUserActions,
enableQuickCreateIssue, enableQuickCreateIssue,
} = props; } = props;
// states
const [expandedIssues, setExpandedIssues] = useState<string[]>([]); const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const [issuePeekOverview, setIssuePeekOverView] = useState<{
workspaceSlug: string;
projectId: string;
issueId: string;
} | null>(null);
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
// refs
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
// router
const router = useRouter(); const router = useRouter();
const { cycleId, moduleId } = router.query; const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const handleScroll = () => { const handleScroll = () => {
if (!containerRef.current) return; if (!containerRef.current) return;
@ -116,7 +112,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
properties={displayProperties} properties={displayProperties}
quickActions={quickActions} quickActions={quickActions}
disableUserActions={disableUserActions} disableUserActions={disableUserActions}
setIssuePeekOverView={setIssuePeekOverView}
/> />
) : null ) : null
)} )}
@ -185,11 +180,11 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
))} */} ))} */}
</div> </div>
</div> </div>
{issuePeekOverview && ( {workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview <IssuePeekOverview
workspaceSlug={issuePeekOverview?.workspaceSlug} workspaceSlug={workspaceSlug.toString()}
projectId={issuePeekOverview?.projectId} projectId={peekProjectId.toString()}
issueId={issuePeekOverview?.issueId} issueId={peekIssueId.toString()}
handleIssue={(issueToUpdate: any) => handleIssues(issueToUpdate, EIssueActions.UPDATE)} handleIssue={(issueToUpdate: any) => handleIssues(issueToUpdate, EIssueActions.UPDATE)}
/> />
)} )}

View File

@ -17,17 +17,19 @@ import {
SidebarPrioritySelect, SidebarPrioritySelect,
SidebarStateSelect, SidebarStateSelect,
} from "../sidebar-select"; } from "../sidebar-select";
// services
import { IssueService } from "services/issue";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { CustomDatePicker } from "components/ui"; import { CustomDatePicker } from "components/ui";
import { LinkModal, LinksList } from "components/core"; import { LinkModal, LinksList } from "components/core";
// types // types
import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types"; import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types";
// fetch-keys
import { ISSUE_DETAILS } from "constants/fetch-keys"; import { ISSUE_DETAILS } from "constants/fetch-keys";
// services // constants
import { IssueService } from "services/issue"; import { EUserWorkspaceRoles } from "constants/workspace";
interface IPeekOverviewProperties { interface IPeekOverviewProperties {
issue: IIssue; issue: IIssue;
@ -43,8 +45,11 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const [linkModal, setLinkModal] = useState(false); const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const { user: userStore, cycleIssue: cycleIssueStore, moduleIssue: moduleIssueStore } = useMobxStore(); const {
const userRole = userStore.currentProjectRole; user: { currentProjectRole },
cycleIssues: { addIssueToCycle },
moduleIssues: { addIssueToModule },
} = useMobxStore();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -72,15 +77,16 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const handleParent = (_parent: string) => { const handleParent = (_parent: string) => {
issueUpdate({ ...issue, parent: _parent }); issueUpdate({ ...issue, parent: _parent });
}; };
const addIssueToCycle = async (cycleId: string) => { const handleAddIssueToCycle = async (cycleId: string) => {
if (!workspaceSlug || !issue || !cycleId) return; if (!workspaceSlug || !issue || !cycleId) return;
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, [issue.id]);
addIssueToCycle(workspaceSlug.toString(), cycleId, [issue.id]);
}; };
const addIssueToModule = async (moduleId: string) => { const handleAddIssueToModule = async (moduleId: string) => {
if (!workspaceSlug || !issue || !moduleId) return; if (!workspaceSlug || !issue || !moduleId) return;
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, [issue.id]); addIssueToModule(workspaceSlug.toString(), moduleId, [issue.id]);
}; };
const handleLabels = (formData: Partial<IIssue>) => { const handleLabels = (formData: Partial<IIssue>) => {
issueUpdate({ ...issue, ...formData }); issueUpdate({ ...issue, ...formData });
@ -303,7 +309,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div> <div>
<SidebarCycleSelect <SidebarCycleSelect
issueDetail={issue} issueDetail={issue}
handleCycleChange={addIssueToCycle} handleCycleChange={handleAddIssueToCycle}
disabled={disableUserActions} disabled={disableUserActions}
/> />
</div> </div>
@ -317,7 +323,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div> <div>
<SidebarModuleSelect <SidebarModuleSelect
issueDetail={issue} issueDetail={issue}
handleModuleChange={addIssueToModule} handleModuleChange={handleAddIssueToModule}
disabled={disableUserActions} disabled={disableUserActions}
/> />
</div> </div>
@ -370,10 +376,10 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
handleDeleteLink={handleDeleteLink} handleDeleteLink={handleDeleteLink}
handleEditLink={handleEditLink} handleEditLink={handleEditLink}
userAuth={{ userAuth={{
isGuest: userRole === 5, isGuest: currentProjectRole === EUserWorkspaceRoles.GUEST,
isViewer: userRole === 10, isViewer: currentProjectRole === EUserWorkspaceRoles.VIEWER,
isMember: userRole === 15, isMember: currentProjectRole === EUserWorkspaceRoles.MEMBER,
isOwner: userRole === 20, isOwner: currentProjectRole === EUserWorkspaceRoles.ADMIN,
}} }}
/> />
) : null} ) : null}

View File

@ -1,18 +1,17 @@
import { FC, Fragment, ReactNode } from "react"; import { FC, Fragment, ReactNode } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components import useSWR from "swr";
import { IssueView } from "./view"; // mobx store
// hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// types
import { IIssue } from "types";
import { RootStore } from "store/root";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components
import { IssueView } from "./view";
// helpers // helpers
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { IIssue } from "types";
interface IIssuePeekOverview { interface IIssuePeekOverview {
workspaceSlug: string; workspaceSlug: string;
@ -27,7 +26,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { workspaceSlug, projectId, issueId, handleIssue, children, isArchived = false } = props; const { workspaceSlug, projectId, issueId, handleIssue, children, isArchived = false } = props;
const router = useRouter(); const router = useRouter();
const { peekIssueId } = router.query as { peekIssueId: string }; const { peekIssueId } = router.query;
const { const {
user: userStore, user: userStore,
@ -36,18 +35,18 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
archivedIssueDetail: archivedIssueDetailStore, archivedIssueDetail: archivedIssueDetailStore,
archivedIssues: archivedIssuesStore, archivedIssues: archivedIssuesStore,
project: projectStore, project: projectStore,
}: RootStore = useMobxStore(); } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
useSWR( useSWR(
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
? `ISSUE_PEEK_OVERVIEW_${workspaceSlug}_${projectId}_${peekIssueId}` ? `ISSUE_DETAILS_${workspaceSlug}_${projectId}_${peekIssueId}`
: null, : null,
async () => { async () => {
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) { if (workspaceSlug && projectId && issueId && issueId === peekIssueId) {
if (isArchived) await archivedIssueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, issueId); if (isArchived) await archivedIssueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId);
else await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, issueId); else await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, peekIssueId);
} }
} }
); );
@ -76,10 +75,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const isLoading = isArchived ? archivedIssueDetailStore.loader : issueDetailStore.loader; const isLoading = isArchived ? archivedIssueDetailStore.loader : issueDetailStore.loader;
const issueUpdate = (_data: Partial<IIssue>) => { const issueUpdate = (_data: Partial<IIssue>) => {
if (handleIssue) { if (handleIssue) handleIssue(_data);
handleIssue(_data);
issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data);
}
}; };
const issueReactionCreate = (reaction: string) => const issueReactionCreate = (reaction: string) =>
@ -114,6 +110,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
if (query.peekIssueId) { if (query.peekIssueId) {
issueDetailStore.setPeekId(null); issueDetailStore.setPeekId(null);
delete query.peekIssueId; delete query.peekIssueId;
delete query.peekProjectId;
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query }, query: { ...query },

View File

@ -12,7 +12,6 @@ import { DeleteIssueModal } from "../delete-issue-modal";
import { DeleteArchivedIssueModal } from "../delete-archived-issue-modal"; import { DeleteArchivedIssueModal } from "../delete-archived-issue-modal";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { RootStore } from "store/root";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
@ -90,7 +89,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { peekIssueId } = router.query as { peekIssueId: string }; const { peekIssueId } = router.query as { peekIssueId: string };
const { user: userStore, issueDetail: issueDetailStore }: RootStore = useMobxStore(); const { user: userStore, issueDetail: issueDetailStore } = useMobxStore();
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek"); const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
@ -101,7 +100,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const { query } = router; const { query } = router;
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query, peekIssueId: issueId }, query: { ...query, peekIssueId: issueId, peekProjectId: projectId },
}); });
} }
}; };
@ -110,6 +109,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
if (query.peekIssueId) { if (query.peekIssueId) {
issueDetailStore.setPeekId(null); issueDetailStore.setPeekId(null);
delete query.peekIssueId; delete query.peekIssueId;
delete query.peekProjectId;
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query }, query: { ...query },

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -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";

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -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>
</>
);
});

View File

@ -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>
);

View File

@ -1,11 +1,9 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import { IssueService } from "services/issue";
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// ui // ui
import { ContrastIcon, CustomSearchSelect, Tooltip } from "@plane/ui"; import { ContrastIcon, CustomSearchSelect, Tooltip } from "@plane/ui";
@ -21,12 +19,17 @@ type Props = {
}; };
// services // services
const issueService = new IssueService();
const cycleService = new CycleService(); const cycleService = new CycleService();
export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleChange, disabled = false }) => { export const SidebarCycleSelect: React.FC<Props> = (props) => {
const { issueDetail, handleCycleChange, disabled = false } = props;
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId } = router.query;
// mobx store
const {
cycleIssues: { removeIssueFromCycle },
} = useMobxStore();
const { data: incompleteCycles } = useSWR( const { data: incompleteCycles } = useSWR(
workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null,
@ -35,13 +38,12 @@ export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleCh
: null : null
); );
const removeIssueFromCycle = (bridgeId: string, cycleId: string) => { const handleRemoveIssueFromCycle = (bridgeId: string, cycleId: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId || !issueDetail) return;
issueService removeIssueFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId, issueDetail.id, bridgeId)
.removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId, bridgeId)
.then(() => { .then(() => {
mutate(ISSUE_DETAILS(issueId as string)); mutate(ISSUE_DETAILS(issueDetail.id));
mutate(CYCLE_ISSUES(cycleId)); mutate(CYCLE_ISSUES(cycleId));
}) })
@ -70,7 +72,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({ issueDetail, handleCycleCh
value={issueCycle?.cycle_detail.id} value={issueCycle?.cycle_detail.id}
onChange={(value: any) => { onChange={(value: any) => {
value === issueCycle?.cycle_detail.id value === issueCycle?.cycle_detail.id
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "") ? handleRemoveIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
: handleCycleChange(value); : handleCycleChange(value);
}} }}
options={options} options={options}

View File

@ -1,12 +1,13 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { TwitterPicker } from "react-color"; import { TwitterPicker } from "react-color";
// headless ui
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// services // mobx store
import { IssueLabelService } from "services/issue"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useToast from "hooks/use-toast";
// ui // ui
import { Input } from "@plane/ui"; import { Input } from "@plane/ui";
import { IssueLabelSelect } from "../select"; import { IssueLabelSelect } from "../select";
@ -14,9 +15,6 @@ import { IssueLabelSelect } from "../select";
import { Plus, X } from "lucide-react"; import { Plus, X } from "lucide-react";
// types // types
import { IIssue, IIssueLabel } from "types"; import { IIssue, IIssueLabel } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import useToast from "hooks/use-toast";
type Props = { type Props = {
issueDetails: IIssue | undefined; issueDetails: IIssue | undefined;
@ -31,60 +29,46 @@ const defaultValues: Partial<IIssueLabel> = {
color: "#ff0000", color: "#ff0000",
}; };
const issueLabelService = new IssueLabelService(); export const SidebarLabelSelect: React.FC<Props> = observer((props) => {
const { issueDetails, labelList, submitChanges, isNotAllowed, uneditable } = props;
export const SidebarLabelSelect: React.FC<Props> = ({ // states
issueDetails,
labelList,
submitChanges,
isNotAllowed,
uneditable,
}) => {
const [createLabelForm, setCreateLabelForm] = useState(false); const [createLabelForm, setCreateLabelForm] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { setToastAlert } = useToast();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// toast
const { setToastAlert } = useToast();
// mobx store
const {
projectLabel: { projectLabels, createLabel },
} = useMobxStore();
// form info
const { const {
handleSubmit, handleSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
reset, reset,
watch,
control, control,
setFocus, setFocus,
} = useForm<Partial<IIssueLabel>>({ } = useForm<Partial<IIssueLabel>>({
defaultValues, defaultValues,
}); });
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabel[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const handleNewLabel = async (formData: Partial<IIssueLabel>) => { const handleNewLabel = async (formData: Partial<IIssueLabel>) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting) return;
await issueLabelService await createLabel(workspaceSlug.toString(), projectId.toString(), formData)
.createIssueLabel(workspaceSlug as string, projectId as string, formData)
.then((res) => { .then((res) => {
reset(defaultValues); reset(defaultValues);
issueLabelMutate((prevData: any) => [...(prevData ?? []), res], false);
submitChanges({ labels: [...(issueDetails?.labels ?? []), res.id] }); submitChanges({ labels: [...(issueDetails?.labels ?? []), res.id] });
setCreateLabelForm(false); setCreateLabelForm(false);
}) })
.catch((error) => { .catch((error) => {
setToastAlert({ setToastAlert({
title: "Oops!",
type: "error", type: "error",
message: error?.error ?? "Error while adding the label", title: "Error!",
message: error?.error ?? "Something went wrong. Please try again.",
}); });
reset(formData); reset(formData);
}); });
@ -101,7 +85,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
<div className={`flex flex-col gap-3 ${uneditable ? "opacity-60" : ""}`}> <div className={`flex flex-col gap-3 ${uneditable ? "opacity-60" : ""}`}>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{labelList?.map((labelId) => { {labelList?.map((labelId) => {
const label = issueLabels?.find((l) => l.id === labelId); const label = projectLabels?.find((l) => l.id === labelId);
if (label) if (label)
return ( return (
@ -118,7 +102,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
<span <span
className="h-2 w-2 flex-shrink-0 rounded-full" className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000", backgroundColor: label.color ?? "#000000",
}} }}
/> />
{label.name} {label.name}
@ -166,14 +150,18 @@ export const SidebarLabelSelect: React.FC<Props> = ({
{createLabelForm && ( {createLabelForm && (
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}> <form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}>
<div> <div>
<Controller
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<Popover className="relative"> <Popover className="relative">
<> <>
<Popover.Button className="grid place-items-center outline-none"> <Popover.Button className="grid place-items-center outline-none">
{watch("color") && watch("color") !== "" && ( {value && value?.trim() !== "" && (
<span <span
className="h-6 w-6 rounded" className="h-6 w-6 rounded"
style={{ style={{
backgroundColor: watch("color") ?? "black", backgroundColor: value ?? "black",
}} }}
/> />
)} )}
@ -189,17 +177,13 @@ export const SidebarLabelSelect: React.FC<Props> = ({
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute z-10 mt-1.5 max-w-xs px-2 sm:px-0"> <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)} /> <TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
)}
/>
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
</> </>
</Popover> </Popover>
)}
/>
</div> </div>
<Controller <Controller
control={control} control={control}
@ -235,4 +219,4 @@ export const SidebarLabelSelect: React.FC<Props> = ({
)} )}
</div> </div>
); );
}; });

View File

@ -1,14 +1,15 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import { observer } from "mobx-react-lite";
// services import { mutate } from "swr";
import { ModuleService } from "services/module.service"; // mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui // ui
import { CustomSearchSelect, DiceIcon, Tooltip } from "@plane/ui"; import { CustomSearchSelect, DiceIcon, Tooltip } from "@plane/ui";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// fetch-keys // fetch-keys
import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys"; import { ISSUE_DETAILS, MODULE_ISSUES } from "constants/fetch-keys";
type Props = { type Props = {
issueDetail: IIssue | undefined; issueDetail: IIssue | undefined;
@ -16,24 +17,23 @@ type Props = {
disabled?: boolean; disabled?: boolean;
}; };
const moduleService = new ModuleService(); export const SidebarModuleSelect: React.FC<Props> = observer((props) => {
const { issueDetail, handleModuleChange, disabled = false } = props;
export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModuleChange, disabled = false }) => { // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId } = router.query;
// mobx store
const {
module: { projectModules },
moduleIssues: { removeIssueFromModule },
} = useMobxStore();
const { data: modules } = useSWR( const handleRemoveIssueFromModule = (bridgeId: string, moduleId: string) => {
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null, if (!workspaceSlug || !projectId || !issueDetail) return;
workspaceSlug && projectId ? () => moduleService.getModules(workspaceSlug as string, projectId as string) : null
);
const removeIssueFromModule = (bridgeId: string, moduleId: string) => { removeIssueFromModule(workspaceSlug.toString(), projectId.toString(), moduleId, issueDetail.id, bridgeId)
if (!workspaceSlug || !projectId) return;
moduleService
.removeIssueFromModule(workspaceSlug as string, projectId as string, moduleId, bridgeId)
.then(() => { .then(() => {
mutate(ISSUE_DETAILS(issueId as string)); mutate(ISSUE_DETAILS(issueDetail.id));
mutate(MODULE_ISSUES(moduleId)); mutate(MODULE_ISSUES(moduleId));
}) })
@ -42,7 +42,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
}); });
}; };
const options = modules?.map((module) => ({ const options = projectModules?.map((module) => ({
value: module.id, value: module.id,
query: module.name, query: module.name,
content: ( content: (
@ -62,7 +62,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
value={issueModule?.module_detail.id} value={issueModule?.module_detail.id}
onChange={(value: any) => { onChange={(value: any) => {
value === issueModule?.module_detail.id value === issueModule?.module_detail.id
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "") ? handleRemoveIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
: handleModuleChange(value); : handleModuleChange(value);
}} }}
options={options} options={options}
@ -70,7 +70,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
<div> <div>
<Tooltip <Tooltip
position="left" position="left"
tooltipContent={`${modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}`} tooltipContent={`${projectModules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}`}
> >
<button <button
type="button" type="button"
@ -85,7 +85,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
> >
<span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span> <span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span>
<span className="truncate"> <span className="truncate">
{modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"} {projectModules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}
</span> </span>
</span> </span>
</button> </button>
@ -97,4 +97,4 @@ export const SidebarModuleSelect: React.FC<Props> = ({ issueDetail, handleModule
disabled={disabled} disabled={disabled}
/> />
); );
}; });

View File

@ -1,17 +1,15 @@
import { useRouter } from "next/router";
import React from "react"; import React from "react";
// lucide icons
import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
// components // components
import { IssuePeekOverview } from "../issue-peek-overview";
import { SubIssuesRootList } from "./issues-list"; import { SubIssuesRootList } from "./issues-list";
import { IssueProperty } from "./properties"; import { IssueProperty } from "./properties";
import { IssuePeekOverview } from "components/issues";
// ui // ui
import { CustomMenu, Tooltip } from "@plane/ui"; import { CustomMenu, Tooltip } from "@plane/ui";
// types // types
import { IUser, IIssue } from "types"; import { IUser, IIssue } from "types";
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
// fetch keys
export interface ISubIssues { export interface ISubIssues {
workspaceSlug: string; workspaceSlug: string;
@ -47,7 +45,29 @@ export const SubIssues: React.FC<ISubIssues> = ({
copyText, copyText,
handleIssueCrudOperation, handleIssueCrudOperation,
handleUpdateIssue, handleUpdateIssue,
}) => ( }) => {
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={workspaceSlug}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={(issueToUpdate) => handleUpdateIssue(issue, { ...issue, ...issueToUpdate })}
/>
)}
<div> <div>
{issue && ( {issue && (
<div <div
@ -77,16 +97,7 @@ export const SubIssues: React.FC<ISubIssues> = ({
)} )}
</div> </div>
<IssuePeekOverview <div className="w-full flex items-center gap-2 cursor-pointer" onClick={handleIssuePeekOverview}>
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 <div
className="flex-shrink-0 w-[6px] h-[6px] rounded-full" className="flex-shrink-0 w-[6px] h-[6px] rounded-full"
style={{ style={{
@ -100,7 +111,6 @@ export const SubIssues: React.FC<ISubIssues> = ({
<div className="line-clamp-1 text-xs text-custom-text-100 pr-2">{issue?.name}</div> <div className="line-clamp-1 text-xs text-custom-text-100 pr-2">{issue?.name}</div>
</Tooltip> </Tooltip>
</div> </div>
</IssuePeekOverview>
<div className="flex-shrink-0 text-sm"> <div className="flex-shrink-0 text-sm">
<IssueProperty <IssueProperty
@ -182,4 +192,6 @@ export const SubIssues: React.FC<ISubIssues> = ({
/> />
)} )}
</div> </div>
); </>
);
};

View File

@ -1,5 +1,4 @@
import { useEffect } from "react"; import { useEffect } from "react";
// swr
import useSWR from "swr"; import useSWR from "swr";
// components // components
import { SubIssues } from "./issue"; import { SubIssues } from "./issue";
@ -61,10 +60,10 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
handleIssuesLoader({ key: "sub_issues", issueId: parentIssue?.id }); handleIssuesLoader({ key: "sub_issues", issueId: parentIssue?.id });
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [handleIssuesLoader, isLoading, issuesLoader.sub_issues, parentIssue?.id]);
}, [isLoading]);
return ( return (
<>
<div className="relative"> <div className="relative">
{issues && {issues &&
issues.sub_issues && issues.sub_issues &&
@ -93,5 +92,6 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
style={{ left: `${spacingLeft - 12}px` }} style={{ left: `${spacingLeft - 12}px` }}
/> />
</div> </div>
</>
); );
}; };

View File

@ -43,7 +43,11 @@ const issueService = new IssueService();
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => { export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
const { parentIssue, user } = props; const { parentIssue, user } = props;
const { user: userStore, issue: issueStore, issueDetail: issueDetailStore } = useMobxStore(); const {
user: userStore,
issue: { updateIssueStructure },
projectIssues: { updateIssue },
} = useMobxStore();
const userRole = userStore.currentProjectRole; const userRole = userStore.currentProjectRole;
const router = useRouter(); const router = useRouter();
@ -166,10 +170,10 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
...data, ...data,
}; };
issueStore.updateIssueStructure(null, null, payload); updateIssueStructure(null, null, payload);
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
}, },
[issueStore, issueDetailStore, projectId, user, workspaceSlug] [updateIssueStructure, projectId, updateIssue, user, workspaceSlug]
); );
const isEditable = userRole === 5 || userRole === 10 ? false : true; const isEditable = userRole === 5 || userRole === 10 ? false : true;

View File

@ -401,7 +401,6 @@ const ProfileSettingsPage: NextPageWithLayout = () => {
<span className="text-lg tracking-tight">Deactivate account</span> <span className="text-lg tracking-tight">Deactivate account</span>
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} /> <ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
</Disclosure.Button> </Disclosure.Button>
<Transition <Transition
show={open} show={open}
enter="transition duration-100 ease-out" enter="transition duration-100 ease-out"

View File

@ -106,7 +106,14 @@ export class ModuleService extends APIService {
projectId: string, projectId: string,
moduleId: string, moduleId: string,
data: { issues: string[] } data: { issues: string[] }
): Promise<any> { ): Promise<
{
issue: string;
issue_detail: IIssue;
module: string;
module_detail: IModule;
}[]
> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {

View File

@ -250,7 +250,7 @@ export class CycleIssueStore implements ICycleIssueStore {
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
}; };
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {

View File

@ -76,7 +76,7 @@ export class CycleIssueCalendarViewStore implements ICycleIssueCalendarViewStore
this.rootStore.cycleIssue.issues = { ...reorderedIssues }; this.rootStore.cycleIssue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,

View File

@ -289,7 +289,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
this.rootStore.cycleIssue.issues = { ...reorderedIssues }; this.rootStore.cycleIssue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
@ -437,7 +437,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
this.rootStore.cycleIssue.issues = { ...reorderedIssues }; this.rootStore.cycleIssue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,

View File

@ -278,7 +278,7 @@ export class IssueStore implements IIssueStore {
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
}; };
fetchIssues = async ( fetchIssues = async (

View File

@ -75,7 +75,7 @@ export class IssueCalendarViewStore implements IIssueCalendarViewStore {
this.rootStore.issue.issues = { ...reorderedIssues }; this.rootStore.issue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,

View File

@ -1,4 +1,4 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx"; import { observable, action, makeObservable, runInAction, computed, autorun } from "mobx";
// services // services
import { IssueService, IssueReactionService, IssueCommentService } from "services/issue"; import { IssueService, IssueReactionService, IssueCommentService } from "services/issue";
import { NotificationService } from "services/notification.service"; import { NotificationService } from "services/notification.service";
@ -7,8 +7,6 @@ import { RootStore } from "../root";
import { IIssue } from "types"; import { IIssue } from "types";
// constants // constants
import { groupReactionEmojis } from "constants/issue"; import { groupReactionEmojis } from "constants/issue";
// uuid
import { v4 as uuidv4 } from "uuid";
export interface IIssueDetailStore { export interface IIssueDetailStore {
loader: boolean; loader: boolean;
@ -44,11 +42,6 @@ export interface IIssueDetailStore {
// fetch issue details // fetch issue details
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>; fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
// creating issue
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
optimisticallyCreateIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
// updating issue
updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>) => Promise<IIssue>;
// deleting issue // deleting issue
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -146,9 +139,6 @@ export class IssueDetailStore implements IIssueDetailStore {
setPeekId: action, setPeekId: action,
fetchIssueDetails: action, fetchIssueDetails: action,
createIssue: action,
optimisticallyCreateIssue: action,
updateIssue: action,
deleteIssue: action, deleteIssue: action,
fetchPeekIssueDetails: action, fetchPeekIssueDetails: action,
@ -171,6 +161,26 @@ export class IssueDetailStore implements IIssueDetailStore {
removeIssueSubscription: action, removeIssueSubscription: action,
}); });
autorun(() => {
const projectId = this.rootStore?.project.projectId;
const peekId = this.peekId;
if (!projectId || !peekId) return;
const issue = this.rootStore.projectIssues.issues?.[projectId]?.[peekId];
if (issue && issue.id)
runInAction(() => {
this.issues = {
...this.issues,
[issue.id]: {
...this.issues[issue.id],
...issue,
},
};
});
});
this.rootStore = _rootStore; this.rootStore = _rootStore;
this.issueService = new IssueService(); this.issueService = new IssueService();
this.issueReactionService = new IssueReactionService(); this.issueReactionService = new IssueReactionService();
@ -238,111 +248,6 @@ export class IssueDetailStore implements IIssueDetailStore {
} }
}; };
optimisticallyCreateIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => {
const tempId = data?.id || uuidv4();
runInAction(() => {
this.loader = true;
this.error = null;
this.issues = {
...this.issues,
[tempId]: data as IIssue,
};
});
try {
const response = await this.issueService.createIssue(workspaceSlug, projectId, data);
runInAction(() => {
this.loader = false;
this.error = null;
this.issues = {
...this.issues,
[response.id]: response,
};
});
return response;
} catch (error) {
this.loader = false;
this.error = error;
throw error;
}
};
createIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => {
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
const response = await this.issueService.createIssue(workspaceSlug, projectId, data);
runInAction(() => {
this.loader = false;
this.error = null;
this.issues = {
...this.issues,
[response.id]: response,
};
});
return response;
} catch (error) {
this.loader = false;
this.error = error;
throw error;
}
};
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>) => {
const newIssues = { ...this.issues };
newIssues[issueId] = {
...newIssues[issueId],
...data,
};
try {
runInAction(() => {
this.loader = true;
this.error = null;
this.issues = newIssues;
});
const user = this.rootStore.user.currentUser;
if (!user) return;
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
runInAction(() => {
this.loader = false;
this.error = null;
this.issues = {
...this.issues,
[issueId]: {
...this.issues[issueId],
...response,
},
};
});
return response;
} catch (error) {
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
runInAction(() => {
this.loader = false;
this.error = error;
});
return error;
}
};
deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
const newIssues = { ...this.issues }; const newIssues = { ...this.issues };
delete newIssues[issueId]; delete newIssues[issueId];

View File

@ -289,7 +289,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
this.rootStore.issue.issues = { ...reorderedIssues }; this.rootStore.issue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
@ -437,7 +437,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
this.rootStore.issue.issues = { ...reorderedIssues }; this.rootStore.issue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,

View File

@ -54,7 +54,7 @@ export interface IModuleIssuesStore {
moduleId: string, moduleId: string,
issueIds: string[], issueIds: string[],
fetchAfterAddition?: boolean fetchAfterAddition?: boolean
) => Promise<IIssue>; ) => Promise<any>;
removeIssueFromModule: ( removeIssueFromModule: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -193,7 +193,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
try { try {
const response = await this.rootStore.projectIssues.createIssue(workspaceSlug, projectId, data); const response = await this.rootStore.projectIssues.createIssue(workspaceSlug, projectId, data);
const issueToModule = await this.addIssueToModule(workspaceSlug, moduleId, [response.id], false); await this.addIssueToModule(workspaceSlug, moduleId, [response.id], false);
let _issues = this.issues; let _issues = this.issues;
if (!_issues) _issues = {}; if (!_issues) _issues = {};
@ -204,7 +204,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
this.issues = _issues; this.issues = _issues;
}); });
return issueToModule; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
throw error; throw error;
@ -286,7 +286,7 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
const response = await this.createIssue(workspaceSlug, projectId, data, moduleId); const response = await this.createIssue(workspaceSlug, projectId, data, moduleId);
if (this.issues) { if (this.issues && response) {
delete this.issues[moduleId][data.id as keyof IIssue]; delete this.issues[moduleId][data.id as keyof IIssue];
let _issues = { ...this.issues }; let _issues = { ...this.issues };

View File

@ -260,7 +260,7 @@ export class ModuleIssueStore implements IModuleIssueStore {
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
}; };
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {

View File

@ -76,7 +76,7 @@ export class ModuleIssueCalendarViewStore implements IModuleIssueCalendarViewSto
this.rootStore.moduleIssue.issues = { ...reorderedIssues }; this.rootStore.moduleIssue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,

View File

@ -289,7 +289,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
this.rootStore.moduleIssue.issues = { ...reorderedIssues }; this.rootStore.moduleIssue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
@ -437,7 +437,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
this.rootStore.moduleIssue.issues = { ...reorderedIssues }; this.rootStore.moduleIssue.issues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,

View File

@ -76,7 +76,7 @@ export class ProjectViewIssueCalendarViewStore implements IProjectViewIssueCalen
this.rootStore.projectViewIssues.viewIssues = { ...reorderedIssues }; this.rootStore.projectViewIssues.viewIssues = { ...reorderedIssues };
}); });
this.rootStore.issueDetail?.updateIssue( this.rootStore.projectIssues.updateIssue(
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,

View File

@ -278,7 +278,7 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
}; };
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {