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 {
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
issueDetail: { updateIssue },
projectIssues: { updateIssue },
user: { currentUser },
} = useMobxStore();

View File

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

View File

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

View File

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

View File

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

View File

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

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">
<IssueGanttSidebarBlock data={block.data} handleIssue={blockUpdateHandler} />
<IssueGanttSidebarBlock data={block.data} />
</div>
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}

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">
<IssueGanttSidebarBlock data={block.data} handleIssue={blockUpdateHandler} />
<IssueGanttSidebarBlock data={block.data} />
</div>
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
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) => {

View File

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

View File

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

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;
this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
this.rootStore.projectIssues.updateIssue(workspaceSlug, issue.project, issue.id, newPayload);
};
fetchIssues = async (

View File

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

View File

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

View File

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

View File

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

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;
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) => {

View File

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

View File

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

View File

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

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;
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) => {