Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views

This commit is contained in:
gurusainath 2024-02-12 17:18:39 +05:30
commit 7e1b90c97a
83 changed files with 1605 additions and 1203 deletions

View File

@ -67,6 +67,7 @@ from .issue import (
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
IssueDetailSerializer,
)
from .module import (

View File

@ -586,7 +586,6 @@ class IssueSerializer(DynamicBaseSerializer):
"id",
"name",
"state_id",
"description_html",
"sort_order",
"completed_at",
"estimate_point",
@ -618,6 +617,13 @@ class IssueSerializer(DynamicBaseSerializer):
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ['description_html']
class IssueLiteSerializer(DynamicBaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"

View File

@ -50,6 +50,7 @@ from plane.app.serializers import (
CommentReactionSerializer,
IssueRelationSerializer,
RelatedIssueSerializer,
IssueDetailSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
@ -267,7 +268,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueSerializer(
IssueDetailSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,

View File

@ -1,6 +1,6 @@
# base requirements
Django==4.2.7
Django==4.2.10
psycopg==3.1.12
djangorestframework==3.14.0
redis==4.6.0
@ -30,7 +30,7 @@ openpyxl==3.1.2
beautifulsoup4==4.12.2
dj-database-url==2.1.0
posthog==3.0.2
cryptography==41.0.6
cryptography==42.0.0
lxml==4.9.3
boto3==1.28.40

View File

@ -42,8 +42,8 @@ export const EditorHeader = (props: IEditorHeader) => {
} = props;
return (
<div className="flex items-center border-b border-custom-border-200 px-5 py-2">
<div className="w-56 flex-shrink-0 lg:w-72">
<div className="flex items-center border-b border-custom-border-200 md:px-5 px-3 py-2">
<div className="md:w-56 flex-shrink-0 lg:w-72 w-fit">
<SummaryPopover
editor={editor}
markings={markings}
@ -52,7 +52,7 @@ export const EditorHeader = (props: IEditorHeader) => {
/>
</div>
<div className="flex-shrink-0">
<div className="flex-shrink-0 hidden md:flex">
{!readonly && uploadFile && (
<FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />
)}

View File

@ -152,7 +152,7 @@ export const PageRenderer = (props: IPageRenderer) => {
);
return (
<div className="w-full pb-64 pl-7 pt-5 page-renderer">
<div className="w-full pb-64 md:pl-7 pl-3 pt-5 page-renderer">
{!readonly ? (
<input
onChange={(e) => handlePageTitleChange(e.target.value)}

View File

@ -40,16 +40,30 @@ export const SummaryPopover: React.FC<Props> = (props) => {
>
<List className="h-4 w-4" />
</button>
{!sidePeekVisible && (
<div
className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
)}
<div className="md:hidden block">
{sidePeekVisible && (
<div
className="z-10 max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
)}
</div>
<div className="hidden md:block">
{!sidePeekVisible && (
<div
className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
)}
</div>
</div>
);
};

View File

@ -10,6 +10,7 @@ import { DocumentDetails } from "src/types/editor-types";
import { PageRenderer } from "src/ui/components/page-renderer";
import { getMenuOptions } from "src/utils/menu-options";
import { useRouter } from "next/router";
import { FixedMenu } from "src";
interface IDocumentEditor {
// document info
@ -149,11 +150,14 @@ const DocumentEditor = ({
documentDetails={documentDetails}
isSubmitting={isSubmitting}
/>
<div className="flex-shrink-0 md:hidden border-b border-custom-border-200 pl-3 py-2">
{uploadFile && <FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />}
</div>
<div className="flex h-full w-full overflow-y-auto frame-renderer">
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72">
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72 hidden md:block">
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
</div>
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)] page-renderer">
<div className="h-full w-full md:w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)] page-renderer">
<PageRenderer
onActionCompleteHandler={onActionCompleteHandler}
hideDragHandle={hideDragHandleOnMouseLeave}

View File

@ -77,7 +77,7 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
}
return (
<div className="flex items-center divide-x divide-custom-border-200">
<div className="flex flex-wrap items-center divide-x divide-custom-border-200">
<div className="flex items-center gap-0.5 pr-2">
{basicMarkItems.map((item) => (
<button

View File

@ -1,35 +1,73 @@
import * as React from "react";
// icons
import { ChevronRight } from "lucide-react";
type BreadcrumbsProps = {
children: any;
children: React.ReactNode;
onBack?: () => void;
};
const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
<div className="flex items-center space-x-2">
{React.Children.map(children, (child, index) => (
<div key={index} className="flex items-center gap-2.5">
{child}
{index !== React.Children.count(children) - 1 && (
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
)}
</div>
))}
</div>
);
const Breadcrumbs = ({ children, onBack }: BreadcrumbsProps) => {
const [isSmallScreen, setIsSmallScreen] = React.useState(false);
React.useEffect(() => {
const handleResize = () => {
setIsSmallScreen(window.innerWidth <= 640); // Adjust this value as per your requirement
};
window.addEventListener("resize", handleResize);
handleResize(); // Call it initially to set the correct state
return () => window.removeEventListener("resize", handleResize);
}, []);
const childrenArray = React.Children.toArray(children);
return (
<div className="flex items-center space-x-2 overflow-hidden">
{!isSmallScreen && (
<>
{childrenArray.map((child, index) => (
<React.Fragment key={index}>
{index > 0 && !isSmallScreen && (
<div className="flex items-center gap-2.5">
<ChevronRight
className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400"
aria-hidden="true"
/>
</div>
)}
<div className={`flex items-center gap-2.5 ${isSmallScreen && index > 0 ? 'hidden sm:flex' : 'flex'}`}>
{child}
</div>
</React.Fragment>
))}
</>
)}
{isSmallScreen && childrenArray.length > 1 && (
<>
<div className="flex items-center gap-2.5">
{onBack && <span onClick={onBack} className="text-custom-text-200">...</span>}
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
</div>
<div className="flex items-center gap-2.5">{childrenArray[childrenArray.length - 1]}</div>
</>
)}
{isSmallScreen && childrenArray.length === 1 && childrenArray}
</div>
);
};
type Props = {
type?: "text" | "component";
component?: React.ReactNode;
link?: JSX.Element;
};
const BreadcrumbItem: React.FC<Props> = (props) => {
const { type = "text", component, link } = props;
return <>{type != "text" ? <div className="flex items-center space-x-2">{component}</div> : link}</>;
return <>{type !== "text" ? <div className="flex items-center space-x-2">{component}</div> : link}</>;
};
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;
export { Breadcrumbs, BreadcrumbItem };
export { Breadcrumbs, BreadcrumbItem };

View File

@ -7,7 +7,7 @@ export const SidebarHamburgerToggle: FC = observer(() => {
const { theme: themStore } = useApplication();
return (
<div
className="w-7 h-7 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
className="w-7 h-7 flex-shrink-0 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
onClick={() => themStore.toggleSidebar()}
>
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />

View File

@ -38,7 +38,7 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
{peekCycle && (
<div
ref={ref}
className="flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300"
className="flex h-full w-full max-w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 fixed md:relative right-0 z-[9]"
style={{
boxShadow:
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",

View File

@ -69,8 +69,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycleDetails.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
@ -175,7 +175,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
/>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="flex h-44 w-full min-w-[250px] flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3 truncate">
<span className="flex-shrink-0">
@ -253,7 +253,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
) : (
<span className="text-xs text-custom-text-400">No due date</span>
)}
<div className="z-10 flex items-center gap-1.5">
<div className="z-[5] flex items-center gap-1.5">
{isEditingAllowed &&
(cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>

View File

@ -204,7 +204,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</Tooltip>
</div>
<button onClick={openCycleOverview} className="invisible z-10 flex-shrink-0 group-hover:visible">
<button onClick={openCycleOverview} className="flex-shrink-0 z-[5] invisible group-hover:visible">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>

View File

@ -1,16 +1,30 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// hooks
import { useApplication, useCycle } from "hooks/store";
// ui
import { Tooltip, ContrastIcon } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { ICycle } from "@plane/types";
export const CycleGanttBlock = ({ data }: { data: ICycle }) => {
type Props = {
cycleId: string;
};
export const CycleGanttBlock: React.FC<Props> = observer((props) => {
const { cycleId } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getCycleById } = useCycle();
// derived values
const cycleDetails = getCycleById(cycleId);
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
const cycleStatus = data.status.toLocaleLowerCase();
return (
<div
className="relative flex h-full w-full items-center rounded"
@ -26,36 +40,45 @@ export const CycleGanttBlock = ({ data }: { data: ICycle }) => {
? "rgb(var(--color-text-200))"
: "",
}}
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)}
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)}
>
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{data?.name}</h5>
<h5>{cycleDetails?.name}</h5>
<div>
{renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.end_date ?? "")}
{renderFormattedDate(cycleDetails?.start_date ?? "")} to{" "}
{renderFormattedDate(cycleDetails?.end_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{cycleDetails?.name}</div>
</Tooltip>
</div>
);
};
});
export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => {
export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { cycleId } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getCycleById } = useCycle();
// derived values
const cycleDetails = getCycleById(cycleId);
const cycleStatus = data.status.toLocaleLowerCase();
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
return (
<div
className="relative flex h-full w-full items-center gap-2"
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)}
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)}
>
<ContrastIcon
className="h-5 w-5 flex-shrink-0"
@ -71,7 +94,7 @@ export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => {
: ""
}`}
/>
<h6 className="flex-grow truncate text-sm font-medium">{data?.name}</h6>
<h6 className="flex-grow truncate text-sm font-medium">{cycleDetails?.name}</h6>
</div>
);
};
});

View File

@ -63,7 +63,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />}
blockToRender={(data: ICycle) => <CycleGanttBlock cycleId={data.id} />}
enableBlockLeftResize={false}
enableBlockRightResize={false}
enableBlockMove={false}

View File

@ -100,7 +100,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
});
})
.catch((_) => {
.catch(() => {
captureCycleEvent({
eventName: CYCLE_UPDATED,
payload: {

View File

@ -65,14 +65,14 @@ export const EmptyState: React.FC<Props> = ({
);
return (
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 px-20">
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
<div
className={cn("flex flex-col gap-5", {
"min-w-[24rem] max-w-[45rem]": size === "sm",
"min-w-[30rem] max-w-[60rem]": size === "lg",
"md:min-w-[24rem] max-w-[45rem]": size === "sm",
"md:min-w-[30rem] max-w-[60rem]": size === "lg",
})}
>
<div className="flex flex-col gap-1.5 flex-shrink-0">{emptyStateHeader}</div>
<div className="flex flex-col gap-1.5 flex-shrink">{emptyStateHeader}</div>
<Image
src={image}

View File

@ -1,3 +1,4 @@
import { observer } from "mobx-react";
import { FC } from "react";
// hooks
import { useIssueDetail } from "hooks/store";
@ -8,6 +9,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
// constants
import { BLOCK_HEIGHT, HEADER_HEIGHT } from "../constants";
export type GanttChartBlocksProps = {
itemsContainerWidth: number;
@ -20,7 +23,7 @@ export type GanttChartBlocksProps = {
showAllBlocks: boolean;
};
export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props) => {
const {
itemsContainerWidth,
blocks,
@ -31,9 +34,10 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
enableBlockMove,
showAllBlocks,
} = props;
const { activeBlock, dispatch } = useChart();
// store hooks
const { peekIssue } = useIssueDetail();
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
@ -77,43 +81,51 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
return (
<div
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
style={{ width: `${itemsContainerWidth}px` }}
className="h-full"
style={{
width: `${itemsContainerWidth}px`,
marginTop: `${HEADER_HEIGHT}px`,
}}
>
{blocks &&
blocks.length > 0 &&
blocks.map((block) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
{blocks?.map((block) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
const isBlockVisibleOnChart = block.start_date && block.target_date;
const isBlockVisibleOnChart = block.start_date && block.target_date;
return (
return (
<div
key={`block-${block.id}`}
className="relative min-w-full w-max"
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
<div
key={`block-${block.id}`}
className={cn(
"h-11",
{ "rounded bg-custom-background-80": activeBlock?.id === block.id },
{
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
}
)}
className={cn("relative h-full", {
"bg-custom-background-80": activeBlock?.id === block.id,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
>
{!isBlockVisibleOnChart && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />}
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
{isBlockVisibleOnChart ? (
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
) : (
<ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
)}
</div>
);
})}
</div>
);
})}
</div>
);
};
});

View File

@ -1 +1 @@
export * from "./blocks-display";
export * from "./blocks-list";

View File

@ -0,0 +1,59 @@
import { Expand, Shrink } from "lucide-react";
// hooks
import { useChart } from "../hooks";
// helpers
import { cn } from "helpers/common.helper";
// types
import { IGanttBlock, TGanttViews } from "../types";
type Props = {
blocks: IGanttBlock[] | null;
fullScreenMode: boolean;
handleChartView: (view: TGanttViews) => void;
handleToday: () => void;
loaderTitle: string;
title: string;
toggleFullScreenMode: () => void;
};
export const GanttChartHeader: React.FC<Props> = (props) => {
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props;
// chart hook
const { currentView, allViews } = useChart();
return (
<div className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2 z-10">
<div className="flex items-center gap-2 text-lg font-medium">{title}</div>
<div className="ml-auto">
<div className="ml-auto text-sm font-medium">{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}</div>
</div>
<div className="flex flex-wrap items-center gap-2">
{allViews?.map((chartView: any) => (
<div
key={chartView?.key}
className={cn("cursor-pointer rounded-sm p-1 px-2 text-xs", {
"bg-custom-background-80": currentView === chartView?.key,
"hover:bg-custom-background-90": currentView !== chartView?.key,
})}
onClick={() => handleChartView(chartView?.key)}
>
{chartView?.title}
</div>
))}
</div>
<button type="button" className="rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80" onClick={handleToday}>
Today
</button>
<button
type="button"
className="flex items-center justify-center rounded-sm border border-custom-border-200 p-1 transition-all hover:bg-custom-background-80"
onClick={toggleFullScreenMode}
>
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
</button>
</div>
);
};

View File

@ -0,0 +1,4 @@
export * from "./views";
export * from "./header";
export * from "./main-content";
export * from "./root";

View File

@ -1,324 +0,0 @@
import { FC, useEffect, useState } from "react";
// icons
// components
import { GanttChartBlocks } from "components/gantt-chart";
// import { GanttSidebar } from "../sidebar";
// import { HourChartView } from "./hours";
// import { DayChartView } from "./day";
// import { WeekChartView } from "./week";
// import { BiWeekChartView } from "./bi-week";
import { MonthChartView } from "./month";
// import { QuarterChartView } from "./quarter";
// import { YearChartView } from "./year";
// icons
import { Expand, Shrink } from "lucide-react";
// views
import {
// generateHourChart,
// generateDayChart,
// generateWeekChart,
// generateBiWeekChart,
generateMonthChart,
// generateQuarterChart,
// generateYearChart,
getNumberOfDaysBetweenTwoDatesInMonth,
// getNumberOfDaysBetweenTwoDatesInQuarter,
// getNumberOfDaysBetweenTwoDatesInYear,
getMonthChartItemPositionWidthInMonth,
} from "../views";
// types
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
// data
import { currentViewDataWithView } from "../data";
// context
import { useChart } from "../hooks";
type ChartViewRootProps = {
border: boolean;
title: string;
loaderTitle: string;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableReorder: boolean;
bottomSpacing: boolean;
showAllBlocks: boolean;
};
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const {
border,
title,
blocks = null,
loaderTitle,
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableReorder,
bottomSpacing,
showAllBlocks,
} = props;
// states
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts
// hooks
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart();
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks && blocks.length > 0
? blocks.map((block: any) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
: [];
useEffect(() => {
if (currentViewData && blocks) setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
}, [currentViewData, blocks]);
// blocks state management ends
const handleChartView = (key: TGanttViews) => updateCurrentViewRenderPayload(null, key);
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
const selectedCurrentView: TGanttViews = view;
const selectedCurrentViewData: ChartDataType | undefined =
selectedCurrentView && selectedCurrentView === currentViewData?.key
? currentViewData
: currentViewDataWithView(view);
if (selectedCurrentViewData === undefined) return;
let currentRender: any;
// if (view === "hours") currentRender = generateHourChart(selectedCurrentViewData, side);
// if (view === "day") currentRender = generateDayChart(selectedCurrentViewData, side);
// if (view === "week") currentRender = generateWeekChart(selectedCurrentViewData, side);
// if (view === "bi_week") currentRender = generateBiWeekChart(selectedCurrentViewData, side);
if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side);
// if (view === "quarter") currentRender = generateQuarterChart(selectedCurrentViewData, side);
// if (selectedCurrentView === "year")
// currentRender = generateYearChart(selectedCurrentViewData, side);
// updating the prevData, currentData and nextData
if (currentRender.payload.length > 0) {
if (side === "left") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: selectedCurrentView,
currentViewData: currentRender.state,
renderView: [...currentRender.payload, ...renderView],
},
});
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else if (side === "right") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...renderView, ...currentRender.payload],
},
});
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...currentRender.payload],
},
});
setItemsContainerWidth(currentRender.scrollWidth);
setTimeout(() => {
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
}, 50);
}
}
};
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
// handling the scroll positioning from left and right
useEffect(() => {
handleToday();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updatingCurrentLeftScrollPosition = (width: number) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
if (!scrollContainer) return;
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
};
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
if (!scrollContainer) return;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
let scrollWidth: number = 0;
let daysDifference: number = 0;
// if (currentView === "hours")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "day")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "bi_week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
if (currentView === "month")
daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "quarter")
// daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date);
// if (currentView === "year")
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
scrollWidth = daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width);
scrollContainer.scrollLeft = scrollWidth;
};
// handling scroll functionality
const onScroll = () => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
if (!scrollContainer) return;
const scrollWidth: number = scrollContainer?.scrollWidth;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
const currentScrollPosition: number = scrollContainer?.scrollLeft;
updateScrollLeft(currentScrollPosition);
const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
if (currentScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
if (currentScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
};
return (
<div
className={`${
fullScreenMode ? `fixed bottom-0 left-0 right-0 top-0 z-[999999] bg-custom-background-100` : `relative`
} ${
border ? `border border-custom-border-200` : ``
} flex h-full select-none flex-col rounded-sm bg-custom-background-100 shadow`}
>
{/* chart header */}
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2">
{title && (
<div className="flex items-center gap-2 text-lg font-medium">
<div>{title}</div>
{/* <div className="text-xs rounded-full px-2 py-1 font-bold border border-custom-primary/75 bg-custom-primary/5 text-custom-text-100">
Gantt View Beta
</div> */}
</div>
)}
<div className="ml-auto">
{blocks === null ? (
<div className="ml-auto text-sm font-medium">Loading...</div>
) : (
<div className="ml-auto text-sm font-medium">
{blocks.length} {loaderTitle}
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
{allViews &&
allViews.length > 0 &&
// eslint-disable-next-line @typescript-eslint/no-unused-vars
allViews.map((_chatView: any, _idx: any) => (
<div
key={_chatView?.key}
className={`cursor-pointer rounded-sm p-1 px-2 text-xs ${
currentView === _chatView?.key ? `bg-custom-background-80` : `hover:bg-custom-background-90`
}`}
onClick={() => handleChartView(_chatView?.key)}
>
{_chatView?.title}
</div>
))}
</div>
<div className="flex items-center gap-1">
<div
className="cursor-pointer rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80"
onClick={handleToday}
>
Today
</div>
</div>
<div
className="flex cursor-pointer items-center justify-center rounded-sm border border-custom-border-200 p-1 transition-all hover:bg-custom-background-80"
onClick={() => setFullScreenMode((prevData) => !prevData)}
>
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
</div>
</div>
{/* content */}
<div
id="gantt-container"
className={`relative flex h-full w-full flex-1 overflow-hidden border-t border-custom-border-200 ${
bottomSpacing ? "mb-8" : ""
}`}
>
<div id="gantt-sidebar" className="flex h-full w-1/4 flex-col border-r border-custom-border-200">
<div className="box-border flex h-[60px] flex-shrink-0 items-end justify-between gap-2 border-b border-custom-border-200 pb-2 pl-10 pr-4 text-sm font-medium text-custom-text-300">
<h6>{title}</h6>
<h6>Duration</h6>
</div>
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
</div>
<div
className="horizontal-scroll-enable relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto"
id="scroll-container"
onScroll={onScroll}
>
{/* {currentView && currentView === "hours" && <HourChartView />} */}
{/* {currentView && currentView === "day" && <DayChartView />} */}
{/* {currentView && currentView === "week" && <WeekChartView />} */}
{/* {currentView && currentView === "bi_week" && <BiWeekChartView />} */}
{currentView && currentView === "month" && <MonthChartView />}
{/* {currentView && currentView === "quarter" && <QuarterChartView />} */}
{/* {currentView && currentView === "year" && <YearChartView />} */}
{/* blocks */}
{currentView && currentViewData && (
<GanttChartBlocks
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
showAllBlocks={showAllBlocks}
/>
)}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,120 @@
// components
import {
BiWeekChartView,
DayChartView,
GanttChartBlocksList,
GanttChartSidebar,
HourChartView,
IBlockUpdateData,
IGanttBlock,
MonthChartView,
QuarterChartView,
TGanttViews,
WeekChartView,
YearChartView,
useChart,
} from "components/gantt-chart";
// helpers
import { cn } from "helpers/common.helper";
type Props = {
blocks: IGanttBlock[] | null;
blockToRender: (data: any) => React.ReactNode;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
bottomSpacing: boolean;
chartBlocks: IGanttBlock[] | null;
enableBlockLeftResize: boolean;
enableBlockMove: boolean;
enableBlockRightResize: boolean;
enableReorder: boolean;
itemsContainerWidth: number;
showAllBlocks: boolean;
sidebarToRender: (props: any) => React.ReactNode;
title: string;
updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void;
};
export const GanttChartMainContent: React.FC<Props> = (props) => {
const {
blocks,
blockToRender,
blockUpdateHandler,
bottomSpacing,
chartBlocks,
enableBlockLeftResize,
enableBlockMove,
enableBlockRightResize,
enableReorder,
itemsContainerWidth,
showAllBlocks,
sidebarToRender,
title,
updateCurrentViewRenderPayload,
} = props;
// chart hook
const { currentView, currentViewData, updateScrollLeft } = useChart();
// handling scroll functionality
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
updateScrollLeft(scrollLeft);
const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth;
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
if (approxRangeRight < 1000) updateCurrentViewRenderPayload("right", currentView);
if (approxRangeLeft < 1000) updateCurrentViewRenderPayload("left", currentView);
};
const CHART_VIEW_COMPONENTS: {
[key in TGanttViews]: React.FC;
} = {
hours: HourChartView,
day: DayChartView,
week: WeekChartView,
bi_week: BiWeekChartView,
month: MonthChartView,
quarter: QuarterChartView,
year: YearChartView,
};
if (!currentView) return null;
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
return (
<div
// DO NOT REMOVE THE ID
id="gantt-container"
className={cn(
"h-full w-full overflow-auto horizontal-scroll-enable flex border-t-[0.5px] border-custom-border-200",
{
"mb-8": bottomSpacing,
}
)}
onScroll={onScroll}
>
<GanttChartSidebar
blocks={blocks}
blockUpdateHandler={blockUpdateHandler}
enableReorder={enableReorder}
sidebarToRender={sidebarToRender}
title={title}
/>
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
<ActiveChartView />
{currentViewData && (
<GanttChartBlocksList
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
showAllBlocks={showAllBlocks}
/>
)}
</div>
</div>
);
};

View File

@ -1,72 +0,0 @@
import { FC } from "react";
// hooks
import { useChart } from "../hooks";
// types
import { IMonthBlock } from "../views";
export const MonthChartView: FC<any> = () => {
const { currentViewData, renderView } = useChart();
const monthBlocks: IMonthBlock[] = renderView;
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100/50">
{monthBlocks &&
monthBlocks.length > 0 &&
monthBlocks.map((block, _idxRoot) => (
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
<div className="h-[60px] w-full">
<div className="relative h-[30px]">
<div className="sticky left-0 inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize">
{block?.title}
</div>
</div>
<div className="flex h-[30px] w-full">
{block?.children &&
block?.children.length > 0 &&
block?.children.map((monthDay, _idx) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="flex-shrink-0 border-b border-custom-border-200 py-1 text-center capitalize"
style={{ width: `${currentViewData?.data.width}px` }}
>
<div className="space-x-1 text-xs">
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
<span className={monthDay.today ? "rounded-full bg-custom-primary-100 px-1 text-white" : ""}>
{monthDay.day}
</span>
</div>
</div>
))}
</div>
</div>
<div className="flex h-full w-full divide-x divide-custom-border-100/50">
{block?.children &&
block?.children.length > 0 &&
block?.children.map((monthDay, _idx) => (
<div
key={`column-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`relative flex h-full w-full flex-1 justify-center ${
["sat", "sun"].includes(monthDay?.dayData?.shortTitle || "") ? `bg-custom-background-90` : ``
}`}
>
{/* {monthDay?.today && (
<div className="absolute top-0 bottom-0 w-[1px] bg-red-500" />
)} */}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -0,0 +1,203 @@
import { FC, useEffect, useState } from "react";
// components
import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart";
// views
import {
generateMonthChart,
getNumberOfDaysBetweenTwoDatesInMonth,
getMonthChartItemPositionWidthInMonth,
} from "../views";
// helpers
import { cn } from "helpers/common.helper";
// types
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
// data
import { currentViewDataWithView } from "../data";
// constants
import { SIDEBAR_WIDTH } from "../constants";
type ChartViewRootProps = {
border: boolean;
title: string;
loaderTitle: string;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableReorder: boolean;
bottomSpacing: boolean;
showAllBlocks: boolean;
};
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const {
border,
title,
blocks = null,
loaderTitle,
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableReorder,
bottomSpacing,
showAllBlocks,
} = props;
// states
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
const [fullScreenMode, setFullScreenMode] = useState(false);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
// hooks
const { currentView, currentViewData, renderView, dispatch } = useChart();
// rendering the block structure
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks
? blocks.map((block: any) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
: [];
useEffect(() => {
if (!currentViewData || !blocks) return;
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
}, [currentViewData, blocks]);
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
const selectedCurrentView: TGanttViews = view;
const selectedCurrentViewData: ChartDataType | undefined =
selectedCurrentView && selectedCurrentView === currentViewData?.key
? currentViewData
: currentViewDataWithView(view);
if (selectedCurrentViewData === undefined) return;
let currentRender: any;
if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side);
// updating the prevData, currentData and nextData
if (currentRender.payload.length > 0) {
if (side === "left") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: selectedCurrentView,
currentViewData: currentRender.state,
renderView: [...currentRender.payload, ...renderView],
},
});
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else if (side === "right") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...renderView, ...currentRender.payload],
},
});
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...currentRender.payload],
},
});
setItemsContainerWidth(currentRender.scrollWidth);
setTimeout(() => {
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
}, 50);
}
}
};
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
// handling the scroll positioning from left and right
useEffect(() => {
handleToday();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updatingCurrentLeftScrollPosition = (width: number) => {
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
if (!scrollContainer) return;
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
};
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
if (!scrollContainer) return;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
let scrollWidth: number = 0;
let daysDifference: number = 0;
// if (currentView === "hours")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "day")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "bi_week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
if (currentView === "month")
daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "quarter")
// daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date);
// if (currentView === "year")
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
scrollWidth =
daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width) + SIDEBAR_WIDTH / 2;
scrollContainer.scrollLeft = scrollWidth;
};
return (
<div
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
"fixed inset-0 z-[999999] bg-custom-background-100": fullScreenMode,
"border-[0.5px] border-custom-border-200": border,
})}
>
<GanttChartHeader
blocks={blocks}
fullScreenMode={fullScreenMode}
toggleFullScreenMode={() => setFullScreenMode((prevData) => !prevData)}
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
handleToday={handleToday}
loaderTitle={loaderTitle}
title={title}
/>
<GanttChartMainContent
blocks={blocks}
blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler}
bottomSpacing={bottomSpacing}
chartBlocks={chartBlocks}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockMove={enableBlockMove}
enableBlockRightResize={enableBlockRightResize}
enableReorder={enableReorder}
itemsContainerWidth={itemsContainerWidth}
showAllBlocks={showAllBlocks}
sidebarToRender={sidebarToRender}
title={title}
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
/>
</div>
);
};

View File

@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "components/gantt-chart";
export const BiWeekChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "../../hooks";
export const DayChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "components/gantt-chart";
export const HourChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -0,0 +1,7 @@
export * from "./bi-week";
export * from "./day";
export * from "./hours";
export * from "./month";
export * from "./quarter";
export * from "./week";
export * from "./year";

View File

@ -0,0 +1,76 @@
import { FC } from "react";
// hooks
import { useChart } from "components/gantt-chart";
// helpers
import { cn } from "helpers/common.helper";
// types
import { IMonthBlock } from "../../views";
// constants
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants";
export const MonthChartView: FC<any> = () => {
// chart hook
const { currentViewData, renderView } = useChart();
const monthBlocks: IMonthBlock[] = renderView;
return (
<>
<div className="absolute top-0 left-0 h-full w-max flex divide-x divide-custom-border-100/50">
{monthBlocks?.map((block, rootIndex) => (
<div key={`month-${block?.month}-${block?.year}`} className="relative">
<div
className="w-full sticky top-0 z-[5] bg-custom-background-100"
style={{
height: `${HEADER_HEIGHT}px`,
}}
>
<div className="h-1/2">
<div
className="sticky inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize"
style={{
left: `${SIDEBAR_WIDTH}px`,
}}
>
{block?.title}
</div>
</div>
<div className="h-1/2 w-full flex">
{block?.children?.map((monthDay, index) => (
<div
key={`sub-title-${rootIndex}-${index}`}
className="flex-shrink-0 border-b-[0.5px] border-custom-border-200 py-1 text-center capitalize"
style={{ width: `${currentViewData?.data.width}px` }}
>
<div className="space-x-1 text-xs">
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
<span
className={cn({
"rounded-full bg-custom-primary-100 px-1 text-white": monthDay.today,
})}
>
{monthDay.day}
</span>
</div>
</div>
))}
</div>
</div>
<div className="h-full w-full flex divide-x divide-custom-border-100/50">
{block?.children?.map((monthDay, index) => (
<div
key={`column-${rootIndex}-${index}`}
className="h-full overflow-hidden"
style={{ width: `${currentViewData?.data.width}px` }}
>
{["sat", "sun"].includes(monthDay?.dayData?.shortTitle) && (
<div className="h-full bg-custom-background-90" />
)}
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "../../hooks";
export const QuarterChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "../../hooks";
export const WeekChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "../../hooks";
export const YearChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -0,0 +1,5 @@
export const BLOCK_HEIGHT = 44;
export const HEADER_HEIGHT = 60;
export const SIDEBAR_WIDTH = 360;

View File

@ -24,6 +24,7 @@ const chartReducer = (state: ChartContextData, action: ChartContextActionPayload
const initialView = "month";
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// states;
const [state, dispatch] = useState<ChartContextData>({
currentView: initialView,
currentViewData: currentViewDataWithView(initialView),
@ -31,23 +32,25 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
allViews: allViewsWithData,
activeBlock: null,
});
const [scrollLeft, setScrollLeft] = useState(0);
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
const newState = chartReducer(state, action);
dispatch(() => newState);
return newState;
};
const updateScrollLeft = (scrollLeft: number) => {
setScrollLeft(scrollLeft);
};
const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft);
return (
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
<ChartContext.Provider
value={{
...state,
scrollLeft,
updateScrollLeft,
dispatch: handleDispatch,
}}
>
{children}
</ChartContext.Provider>
);

View File

@ -1,9 +1,11 @@
import React, { useEffect, useRef, useState } from "react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { ArrowRight } from "lucide-react";
// hooks
import { useChart } from "../hooks";
// types
import { IGanttBlock } from "../types";
import { IGanttBlock, useChart } from "components/gantt-chart";
// helpers
import { cn } from "helpers/common.helper";
// constants
import { SIDEBAR_WIDTH } from "../constants";
type Props = {
block: IGanttBlock;
@ -20,7 +22,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
const [isLeftResizing, setIsLeftResizing] = useState(false);
const [isRightResizing, setIsRightResizing] = useState(false);
const [isMoving, setIsMoving] = useState(false);
const [posFromLeft, setPosFromLeft] = useState<number | null>(null);
const [isHidden, setIsHidden] = useState(true);
// refs
const resizableRef = useRef<HTMLDivElement>(null);
// chart hook
@ -31,12 +33,10 @@ export const ChartDraggable: React.FC<Props> = (props) => {
let delWidth = 0;
const ganttContainer = document.querySelector("#gantt-container") as HTMLElement;
const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLElement;
const ganttContainer = document.querySelector("#gantt-container") as HTMLDivElement;
const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLDivElement;
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
if (!ganttContainer || !ganttSidebar || !scrollContainer) return 0;
if (!ganttContainer || !ganttSidebar) return 0;
const posFromLeft = e.clientX;
// manually scroll to left if reached the left end while dragging
@ -45,7 +45,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
delWidth = -5;
scrollContainer.scrollBy(delWidth, 0);
ganttContainer.scrollBy(delWidth, 0);
} else delWidth = e.movementX;
// manually scroll to right if reached the right end while dragging
@ -55,7 +55,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
delWidth = 5;
scrollContainer.scrollBy(delWidth, 0);
ganttContainer.scrollBy(delWidth, 0);
} else delWidth = e.movementX;
return delWidth;
@ -201,50 +201,61 @@ export const ChartDraggable: React.FC<Props> = (props) => {
};
// scroll to a hidden block
const handleScrollToBlock = () => {
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
if (!scrollContainer || !block.position) return;
// update container's scroll position to the block's position
scrollContainer.scrollLeft = block.position.marginLeft - 4;
};
// update block position from viewport's left end on scroll
useEffect(() => {
const block = resizableRef.current;
if (!block) return;
setPosFromLeft(block.getBoundingClientRect().left);
}, [scrollLeft]);
// check if block is hidden on either side
const isBlockHiddenOnLeft =
block.position?.marginLeft &&
block.position?.width &&
scrollLeft > block.position.marginLeft + block.position.width;
const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth;
useEffect(() => {
const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement;
const resizableBlock = resizableRef.current;
if (!resizableBlock || !intersectionRoot) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
setIsHidden(!entry.isIntersecting);
});
},
{
root: intersectionRoot,
rootMargin: `0px 0px 0px -${SIDEBAR_WIDTH}px`,
}
);
observer.observe(resizableBlock);
return () => {
observer.unobserve(resizableBlock);
};
}, [block.data.name]);
return (
<>
{/* move to left side hidden block button */}
{isBlockHiddenOnLeft && (
<div
className="fixed z-[1] ml-1 mt-1.5 grid h-8 w-8 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
{/* move to the hidden block */}
{isHidden && (
<button
type="button"
className="sticky z-[1] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
style={{
left: `${SIDEBAR_WIDTH + 4}px`,
}}
onClick={handleScrollToBlock}
>
<ArrowLeft className="h-3.5 w-3.5" />
</div>
)}
{/* move to right side hidden block button */}
{isBlockHiddenOnRight && (
<div
className="fixed right-1 z-[1] mt-1.5 grid h-8 w-8 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
onClick={handleScrollToBlock}
>
<ArrowRight className="h-3.5 w-3.5" />
</div>
<ArrowRight
className={cn("h-3.5 w-3.5", {
"rotate-180": isBlockHiddenOnLeft,
})}
/>
</button>
)}
<div
id={`block-${block.id}`}
ref={resizableRef}
className="group relative inline-flex h-full cursor-pointer items-center font-medium transition-all"
style={{
@ -259,17 +270,22 @@ export const ChartDraggable: React.FC<Props> = (props) => {
onMouseDown={handleBlockLeftResize}
onMouseEnter={() => setIsLeftResizing(true)}
onMouseLeave={() => setIsLeftResizing(false)}
className="absolute -left-2.5 top-1/2 z-[3] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md"
className="absolute -left-2.5 top-1/2 -translate-y-1/2 z-[3] h-full w-6 cursor-col-resize rounded-md"
/>
<div
className={`absolute top-1/2 h-7 w-1 -translate-y-1/2 rounded-sm bg-custom-background-100 transition-all duration-300 ${
isLeftResizing ? "-left-2.5" : "left-1"
}`}
className={cn(
"absolute left-1 top-1/2 -translate-y-1/2 h-7 w-1 rounded-sm bg-custom-background-100 transition-all duration-300",
{
"-left-2.5": isLeftResizing,
}
)}
/>
</>
)}
<div
className={`relative z-[2] flex h-8 w-full items-center rounded ${isMoving ? "pointer-events-none" : ""}`}
className={cn("relative z-[2] flex h-8 w-full items-center rounded", {
"pointer-events-none": isMoving,
})}
onMouseDown={handleBlockMove}
>
{blockToRender(block.data)}
@ -281,12 +297,15 @@ export const ChartDraggable: React.FC<Props> = (props) => {
onMouseDown={handleBlockRightResize}
onMouseEnter={() => setIsRightResizing(true)}
onMouseLeave={() => setIsRightResizing(false)}
className="absolute -right-2.5 top-1/2 z-[2] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md"
className="absolute -right-2.5 top-1/2 -translate-y-1/2 z-[2] h-full w-6 cursor-col-resize rounded-md"
/>
<div
className={`absolute top-1/2 h-7 w-1 -translate-y-1/2 rounded-sm bg-custom-background-100 transition-all duration-300 ${
isRightResizing ? "-right-2.5" : "right-1"
}`}
className={cn(
"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-1 rounded-sm bg-custom-background-100 transition-all duration-300",
{
"-right-2.5": isRightResizing,
}
)}
/>
</>
)}

View File

@ -1,4 +1,5 @@
export * from "./blocks";
export * from "./chart";
export * from "./helpers";
export * from "./hooks";
export * from "./root";

View File

@ -1,10 +1,8 @@
import { FC } from "react";
// components
import { ChartViewRoot } from "./chart";
import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// context
import { ChartContextProvider } from "./contexts";
// types
import { IBlockUpdateData, IGanttBlock } from "./types";
type GanttChartRootProps = {
border?: boolean;

View File

@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
@ -9,8 +8,11 @@ import { Loader } from "@plane/ui";
import { CycleGanttSidebarBlock } from "components/cycles";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
title: string;
@ -20,12 +22,8 @@ type Props = {
};
export const CycleGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, blockUpdateHandler, blocks, enableReorder } = props;
const router = useRouter();
const { cycleId } = router.query;
const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
@ -84,12 +82,7 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="mt-3 max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
@ -104,7 +97,9 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
>
{(provided, snapshot) => (
<div
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
@ -112,9 +107,12 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
>
<div
id={`sidebar-block-${block.id}`}
className={`group flex h-full w-full items-center gap-2 rounded-l px-2 pr-4 ${
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
@ -128,9 +126,9 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<CycleGanttSidebarBlock data={block.data} />
<CycleGanttSidebarBlock cycleId={block.data.id} />
</div>
{duration !== undefined && (
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}
</div>

View File

@ -1,4 +1,5 @@
export * from "./cycle-sidebar";
export * from "./module-sidebar";
export * from "./sidebar";
export * from "./project-view-sidebar";
export * from "./cycles";
export * from "./issues";
export * from "./modules";
export * from "./project-views";
export * from "./root";

View File

@ -0,0 +1,195 @@
import { observer } from "mobx-react";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
import { useIssueDetail } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
// components
import { GanttQuickAddIssueForm, IssueGanttSidebarBlock } from "components/issues";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { TIssue } from "@plane/types";
import { BLOCK_HEIGHT } from "../constants";
type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: TIssue,
viewId?: string
) => Promise<TIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
showAllBlocks?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
const {
blockUpdateHandler,
blocks,
enableReorder,
enableQuickIssueCreate,
quickAddCallback,
viewId,
disableIssueCreation,
showAllBlocks = false,
} = props;
const { activeBlock, dispatch } = useChart();
const { peekIssue } = useIssueDetail();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<>
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
const duration =
!block.start_date || !block.target_date
? null
: findTotalDaysInRange(block.start_date, block.target_date);
return (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} />
</div>
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
<span>
{duration} day{duration > 1 ? "s" : ""}
</span>
</div>
)}
</div>
</div>
</div>
)}
</Draggable>
);
})
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
{enableQuickIssueCreate && !disableIssueCreation && (
<GanttQuickAddIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
)}
</>
);
});

View File

@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
@ -9,8 +8,11 @@ import { Loader } from "@plane/ui";
import { ModuleGanttSidebarBlock } from "components/modules";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
title: string;
@ -20,12 +22,8 @@ type Props = {
};
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, blockUpdateHandler, blocks, enableReorder } = props;
const router = useRouter();
const { cycleId } = router.query;
const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
@ -84,12 +82,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="mt-3 max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
@ -104,7 +97,9 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
>
{(provided, snapshot) => (
<div
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
@ -112,9 +107,12 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
>
<div
id={`sidebar-block-${block.id}`}
className={`group flex h-full w-full items-center gap-2 rounded-l px-2 pr-4 ${
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
@ -128,7 +126,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<ModuleGanttSidebarBlock data={block.data} />
<ModuleGanttSidebarBlock moduleId={block.data.id} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-sm text-custom-text-200">

View File

@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
@ -11,6 +10,8 @@ import { IssueGanttSidebarBlock } from "components/issues";
import { findTotalDaysInRange } from "helpers/date-time.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
title: string;
@ -21,12 +22,8 @@ type Props = {
};
export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, blockUpdateHandler, blocks, enableReorder } = props;
const router = useRouter();
const { cycleId } = router.query;
const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
@ -86,7 +83,6 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="mt-3 max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
@ -105,7 +101,10 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
>
{(provided, snapshot) => (
<div
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
className={`${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
@ -129,7 +128,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock data={block.data} />
<IssueGanttSidebarBlock issueId={block.data.id} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-sm text-custom-text-200">

View File

@ -0,0 +1,41 @@
// components
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// constants
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
type Props = {
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableReorder: boolean;
sidebarToRender: (props: any) => React.ReactNode;
title: string;
};
export const GanttChartSidebar: React.FC<Props> = (props) => {
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title } = props;
return (
<div
// DO NOT REMOVE THE ID
id="gantt-sidebar"
className="sticky left-0 z-10 min-h-full h-max flex-shrink-0 border-r-[0.5px] border-custom-border-200"
style={{
width: `${SIDEBAR_WIDTH}px`,
}}
>
<div
className="box-border flex-shrink-0 flex items-end justify-between gap-2 border-b-[0.5px] border-custom-border-200 pb-2 pl-8 pr-4 text-sm font-medium text-custom-text-300 sticky top-0 z-10 bg-custom-background-100"
style={{
height: `${HEADER_HEIGHT}px`,
}}
>
<h6>{title}</h6>
<h6>Duration</h6>
</div>
<div className="min-h-full h-max bg-custom-background-100">
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
</div>
</div>
);
};

View File

@ -1,198 +0,0 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
import { useIssueDetail } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
// components
import { GanttQuickAddIssueForm, IssueGanttSidebarBlock } from "components/issues";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { TIssue } from "@plane/types";
type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: TIssue,
viewId?: string
) => Promise<TIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
showAllBlocks?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = (props) => {
const {
blockUpdateHandler,
blocks,
enableReorder,
enableQuickIssueCreate,
quickAddCallback,
viewId,
disableIssueCreation,
showAllBlocks = false,
} = props;
const router = useRouter();
const { cycleId } = router.query;
const { activeBlock, dispatch } = useChart();
const { peekIssue } = useIssueDetail();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="mt-[12px] max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<>
{blocks ? (
blocks.map((block, index) => {
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
const duration = findTotalDaysInRange(block.start_date, block.target_date);
return (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<div
className={cn(
"h-11",
{ "rounded bg-custom-background-80": snapshot.isDragging },
{
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
}
)}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
id={`sidebar-block-${block.id}`}
className={`group flex h-full w-full items-center gap-2 rounded-l px-2 pr-4 ${
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
}`}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock data={block.data} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
<span>
{duration} day{duration > 1 ? "s" : ""}
</span>
</div>
)}
</div>
</div>
</div>
)}
</Draggable>
);
})
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
{enableQuickIssueCreate && !disableIssueCreation && (
<GanttQuickAddIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
)}
</div>
)}
</Droppable>
</DragDropContext>
);
};

View File

@ -153,7 +153,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
<div className="flex justify-between border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<SidebarHamburgerToggle />
<Breadcrumbs>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
@ -196,7 +196,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
label={
<>
<ContrastIcon className="h-3 w-3" />
{cycleDetails?.name && truncateText(cycleDetails.name, 40)}
<div className=" w-auto max-w-[70px] sm:max-w-[200px] inline-block truncate line-clamp-1 overflow-hidden whitespace-nowrap">
{cycleDetails?.name && cycleDetails.name}
</div>
</>
}
className="ml-1.5 flex-shrink-0"

View File

@ -50,7 +50,7 @@ export const CyclesHeader: FC = observer(() => {
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<Breadcrumbs>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
@ -89,7 +89,7 @@ export const CyclesHeader: FC = observer(() => {
toggleCreateCycleModal(true);
}}
>
Add Cycle
<div className="hidden sm:block">Add</div> Cycle
</Button>
</div>
)}

View File

@ -21,7 +21,7 @@ import { ProjectAnalyticsModal } from "components/analytics";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { BreadcrumbLink } from "components/common";
// ui
import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui";
import { Breadcrumbs, Button, CustomMenu, DiceIcon, LayersIcon } from "@plane/ui";
// icons
import { ArrowRight, PanelRight, Plus } from "lucide-react";
// helpers
@ -156,7 +156,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<div className="flex justify-between border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<SidebarHamburgerToggle />
<Breadcrumbs>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
@ -199,7 +199,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
label={
<>
<DiceIcon className="h-3 w-3" />
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
<div className="w-auto max-w-[70px] sm:max-w-[200px] inline-block truncate line-clamp-1 overflow-hidden whitespace-nowrap">
{moduleDetails?.name && moduleDetails.name}
</div>
</>
}
className="ml-1.5 flex-shrink-0"
@ -251,6 +253,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
Analytics
</Button>
<Button
className="hidden sm:flex"
onClick={() => {
setTrackElement("Module issues page");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
@ -258,7 +261,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
size="sm"
prependIcon={<Plus />}
>
<span className="hidden md:block">Add</span> Issue
Add Issue
</Button>
</>
)}

View File

@ -1,11 +1,11 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
import { GanttChartSquare, LayoutGrid, List, Plus } from "lucide-react";
// hooks
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// ui
import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui";
import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui";
// helper
import { renderEmoji } from "helpers/emoji.helper";
// constants
@ -31,75 +31,101 @@ export const ModulesListHeader: React.FC = observer(() => {
const canUserCreateModule =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails?.emoji ? (
renderEmoji(currentProjectDetails.emoji)
) : currentProjectDetails?.icon_prop ? (
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Modules" icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
<div>
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails?.emoji ? (
renderEmoji(currentProjectDetails.emoji)
) : currentProjectDetails?.icon_prop ? (
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Modules" icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-2">
<div className="items-center gap-1 rounded bg-custom-background-80 p-1 hidden md:flex">
{MODULE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${modulesView == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => setModulesView(layout.key)}
>
<layout.icon
strokeWidth={2}
className={`h-3.5 w-3.5 ${modulesView == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`}
/>
</button>
</Tooltip>
))}
</div>
{canUserCreateModule && (
<Button
variant="primary"
size="sm"
prependIcon={<Plus />}
onClick={() => {
setTrackElement("Modules page");
commandPaletteStore.toggleCreateModuleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Module
</Button>
)}
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
<div className="flex justify-center md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
// placement="bottom-start"
customButton={
<span className="flex items-center gap-2">
{modulesView === 'gantt_chart' ? <GanttChartSquare className="w-3 h-3" /> : modulesView === 'grid' ? <LayoutGrid className="w-3 h-3" /> : <List className="w-3 h-3" />}
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>
</span>
}
customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm"
closeOnSelect
>
{MODULE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
modulesView == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => setModulesView(layout.key)}
>
<layout.icon
strokeWidth={2}
className={`h-3.5 w-3.5 ${
modulesView == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`}
/>
</button>
</Tooltip>
<CustomMenu.MenuItem
onClick={() => setModulesView(layout.key)}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</div>
{canUserCreateModule && (
<Button
variant="primary"
size="sm"
prependIcon={<Plus />}
onClick={() => {
setTrackElement("Modules page");
commandPaletteStore.toggleCreateModuleModal(true);
}}
>
Add Module
</Button>
)}
</CustomMenu>
</div>
</div>
);
});

View File

@ -36,23 +36,34 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails?.emoji ? (
renderEmoji(currentProjectDetails.emoji)
) : currentProjectDetails?.icon_prop ? (
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
}
/>
<span>
<span className="hidden md:block">
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails?.emoji ? (
renderEmoji(currentProjectDetails.emoji)
) : currentProjectDetails?.icon_prop ? (
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
}
/>
</span>
<span className="md:hidden">
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={"..."}
/>
</span>
</span>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={

View File

@ -3,7 +3,7 @@ import useSWR from "swr";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useProject } from "hooks/store";
import { useApplication, useProject } from "hooks/store";
// ui
import { Breadcrumbs, LayersIcon } from "@plane/ui";
// helpers
@ -15,6 +15,8 @@ import { ISSUE_DETAILS } from "constants/fetch-keys";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { BreadcrumbLink } from "components/common";
import { PanelRight } from "lucide-react";
import { cn } from "helpers/common.helper";
// services
const issueService = new IssueService();
@ -25,6 +27,7 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
const { workspaceSlug, projectId, issueId } = router.query;
// store hooks
const { currentProjectDetails, getProjectById } = useProject();
const { theme: themeStore } = useApplication();
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
@ -33,12 +36,14 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
: null
);
const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed;
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<Breadcrumbs>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
@ -85,6 +90,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
</Breadcrumbs>
</div>
</div>
<button className="block md:hidden" onClick={() => themeStore.toggleIssueDetailSidebar()}>
<PanelRight className={cn("w-4 h-4 ", !isSidebarCollapsed ? "text-custom-primary-100" : " text-custom-text-200")} />
</button>
</div>
);
});

View File

@ -2,7 +2,7 @@ import { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
import { Briefcase, Circle, ExternalLink, Plus, Inbox } from "lucide-react";
// hooks
import {
useApplication,
@ -120,35 +120,35 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<Breadcrumbs>
<Breadcrumbs onBack={() => router.back()}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails ? (
currentProjectDetails?.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{renderEmoji(currentProjectDetails.emoji)}
</span>
) : currentProjectDetails?.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(currentProjectDetails.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
) : (
<BreadcrumbLink
href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails ? (
currentProjectDetails?.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" />
{renderEmoji(currentProjectDetails.emoji)}
</span>
) : currentProjectDetails?.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(currentProjectDetails.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
}
/>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" />
</span>
)
}
/>
}
/>
@ -202,18 +202,19 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</FiltersDropdown>
</div>
{currentProjectDetails?.inbox_view && inboxDetails && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxDetails?.id}`}>
<span>
<Button variant="neutral-primary" size="sm" className="relative">
Inbox
{inboxDetails?.pending_issue_count > 0 && (
<span className="absolute -right-1.5 -top-1.5 h-4 w-4 rounded-full border border-custom-sidebar-border-200 bg-custom-sidebar-background-80 text-custom-text-100">
{inboxDetails?.pending_issue_count}
</span>
)}
</Button>
</span>
</Link>
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxDetails?.id}`}>
<span className="hidden md:block" >
<Button variant="neutral-primary" size="sm" className="relative">
Inbox
{inboxDetails?.pending_issue_count > 0 && (
<span className="absolute -right-1.5 -top-1.5 h-4 w-4 rounded-full border border-custom-sidebar-border-200 bg-custom-sidebar-background-80 text-custom-text-100">
{inboxDetails?.pending_issue_count}
</span>
)}
</Button>
</span>
<Inbox className="w-4 h-4 mr-2 text-custom-text-200 block md:hidden" />
</Link>
)}
{canUserCreateIssue && (
<>
@ -228,7 +229,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
size="sm"
prependIcon={<Plus />}
>
Add Issue
<div className="hidden sm:block">Add</div> Issue
</Button>
</>
)}

View File

@ -36,7 +36,7 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<div className="z-50">
<Breadcrumbs>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={

View File

@ -56,7 +56,7 @@ export const ProjectsHeader = observer(() => {
}}
className="items-center"
>
Add Project
<div className="hidden sm:block">Add</div> Project
</Button>
)}
</div>

View File

@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
import useReloadConfirmations from "hooks/use-reload-confirmation";
import debounce from "lodash/debounce";
// components
import { TextArea } from "@plane/ui";
import { Loader, TextArea } from "@plane/ui";
import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor";
// types
import { TIssue } from "@plane/types";
@ -12,6 +12,8 @@ import { TIssueOperations } from "./issue-detail";
// services
import { FileService } from "services/file.service";
import { useMention, useWorkspace } from "hooks/store";
import { observer } from "mobx-react";
import { isNil } from "lodash";
export interface IssueDescriptionFormValues {
name: string;
@ -36,7 +38,7 @@ export interface IssueDetailsProps {
const fileService = new FileService();
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
const { workspaceSlug, projectId, issueId, issue, issueOperations, disabled, isSubmitting, setIsSubmitting } = props;
const workspaceStore = useWorkspace();
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string;
@ -71,12 +73,20 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
// editor rerendering on every save
useEffect(() => {
if (issue.id) {
setLocalIssueDescription({ id: issue.id, description_html: issue.description_html });
setLocalTitleValue(issue.name);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [issue.id]); // TODO: verify the exhaustive-deps warning
useEffect(() => {
if (issue.description_html) {
setLocalIssueDescription((state) => {
if (!isNil(state.description_html)) return state;
return { id: issue.id, description_html: issue.description_html };
});
}
}, [issue.description_html]);
const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<TIssue>) => {
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
@ -167,42 +177,48 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
</div>
<span>{errors.name ? errors.name.message : null}</span>
<div className="relative">
<Controller
name="description_html"
control={control}
render={({ field: { onChange } }) =>
!disabled ? (
<RichTextEditor
cancelUploadImage={fileService.cancelUpload}
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
value={localIssueDescription.description_html}
rerenderOnPropsChange={localIssueDescription}
setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting}
dragDropEnabled
customClassName="min-h-[150px] shadow-sm"
onChange={(description: Object, description_html: string) => {
setShowAlert(true);
setIsSubmitting("submitting");
onChange(description_html);
debouncedFormSave();
}}
mentionSuggestions={mentionSuggestions}
mentionHighlights={mentionHighlights}
/>
) : (
<RichReadOnlyEditor
value={localIssueDescription.description_html}
customClassName="!p-0 !pt-2 text-custom-text-200"
noBorder={disabled}
mentionHighlights={mentionHighlights}
/>
)
}
/>
{issue.description_html ? (
<Controller
name="description_html"
control={control}
render={({ field: { onChange } }) =>
!disabled ? (
<RichTextEditor
cancelUploadImage={fileService.cancelUpload}
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
value={localIssueDescription.description_html}
rerenderOnPropsChange={localIssueDescription}
setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting}
dragDropEnabled
customClassName="min-h-[150px] shadow-sm"
onChange={(description: Object, description_html: string) => {
setShowAlert(true);
setIsSubmitting("submitting");
onChange(description_html);
debouncedFormSave();
}}
mentionSuggestions={mentionSuggestions}
mentionHighlights={mentionHighlights}
/>
) : (
<RichReadOnlyEditor
value={localIssueDescription.description_html}
customClassName="!p-0 !pt-2 text-custom-text-200"
noBorder={disabled}
mentionHighlights={mentionHighlights}
/>
)
}
/>
) : (
<Loader>
<Loader.Item height="150px" />
</Loader>
)}
</div>
</div>
);
};
});

View File

@ -33,7 +33,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
}`}
>
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden={true} />
<div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-center z-10 bg-custom-background-80 text-custom-text-200">
<div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-center z-[4] bg-custom-background-80 text-custom-text-200">
{icon ? icon : <Network className="w-3.5 h-3.5" />}
</div>
<div className="w-full text-custom-text-200">

View File

@ -9,7 +9,7 @@ import { EmptyState } from "components/common";
// images
import emptyIssue from "public/empty-state/issue.svg";
// hooks
import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store";
import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// types
import { TIssue } from "@plane/types";
@ -17,6 +17,7 @@ import { TIssue } from "@plane/types";
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
import { observer } from "mobx-react";
export type TIssueOperations = {
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -52,7 +53,7 @@ export type TIssueDetailRoot = {
is_archived?: boolean;
};
export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
const { workspaceSlug, projectId, issueId, is_archived = false } = props;
// router
const router = useRouter();
@ -76,6 +77,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
const {
membership: { currentProjectRole },
} = useUser();
const { theme: themeStore } = useApplication();
const issueOperations: TIssueOperations = useMemo(
() => ({
@ -347,8 +349,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
}}
/>
) : (
<div className="flex h-full overflow-hidden">
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
<div className="flex w-full h-full overflow-hidden">
<div className="h-full w-full max-w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
<IssueMainContent
workspaceSlug={workspaceSlug}
projectId={projectId}
@ -357,7 +359,10 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
is_editable={!is_archived && is_editable}
/>
</div>
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5">
<div
className="h-full w-full sm:w-1/2 md:w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5 fixed md:relative bg-custom-sidebar-background-100 right-0 z-[5]"
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
>
<IssueDetailsSidebar
workspaceSlug={workspaceSlug}
projectId={projectId}
@ -374,4 +379,4 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
<IssuePeekOverview />
</>
);
};
});

View File

@ -187,9 +187,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
}`}
buttonClassName={`text-sm justify-between ${issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
}`}
hideIcon={issue.assignee_ids?.length === 0}
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
@ -233,8 +232,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
// TODO: add this logic
// showPlaceholderIcon
// TODO: add this logic
// showPlaceholderIcon
/>
</div>
@ -259,8 +258,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
// TODO: add this logic
// showPlaceholderIcon
// TODO: add this logic
// showPlaceholderIcon
/>
</div>

View File

@ -1,5 +1,5 @@
import { FC, useState } from "react";
import { Bell } from "lucide-react";
import { Bell, BellOff } from "lucide-react";
import { observer } from "mobx-react-lite";
// UI
import { Button } from "@plane/ui";
@ -52,12 +52,20 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
<div>
<Button
size="sm"
prependIcon={<Bell className="h-3 w-3" />}
prependIcon={subscription?.subscribed ? <BellOff /> : <Bell className="h-3 w-3" />}
variant="outline-primary"
className="hover:!bg-custom-primary-100/20"
onClick={handleSubscription}
>
{loading ? "Loading..." : subscription?.subscribed ? "Unsubscribe" : "Subscribe"}
{loading ? (
<span>
<span className="hidden sm:block">Loading</span>...
</span>
) : subscription?.subscribed ? (
<div className="hidden sm:block">Unsubscribe</div>
) : (
<div className="hidden sm:block">Subscribe</div>
)}
</Button>
</div>
);

View File

@ -69,7 +69,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
loaderTitle="Issues"
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
blockUpdateHandler={updateIssueBlockStructure}
blockToRender={(data: TIssue) => <IssueGanttBlock data={data} />}
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />}
sidebarToRender={(props) => (
<IssueGanttSidebar
{...props}

View File

@ -1,33 +1,41 @@
import { observer } from "mobx-react";
// hooks
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
// ui
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
// hooks
type Props = {
issueId: string;
};
export const IssueGanttBlock: React.FC<Props> = observer((props) => {
const { issueId } = props;
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getProjectStates } = useProjectState();
const { setPeekIssue } = useIssueDetail();
const {
issue: { getIssueById },
setPeekIssue,
} = useIssueDetail();
// derived values
const issueDetails = getIssueById(issueId);
const stateDetails =
issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id);
const handleIssuePeekOverview = () =>
workspaceSlug &&
data &&
data.project_id &&
data.id &&
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
const stateColor = getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id)?.color || "";
issueDetails &&
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
return (
<div
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={{
backgroundColor: stateColor,
backgroundColor: stateDetails?.color,
}}
onClick={handleIssuePeekOverview}
>
@ -35,58 +43,62 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{data?.name}</h5>
<h5>{issueDetails?.name}</h5>
<div>
{renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.target_date ?? "")}
{renderFormattedDate(issueDetails?.start_date ?? "")} to{" "}
{renderFormattedDate(issueDetails?.target_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100 overflow-hidden">
{issueDetails?.name}
</div>
</Tooltip>
</div>
);
};
});
// rendering issues on gantt sidebar
export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => {
// hooks
const { getProjectStates } = useProjectState();
export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { issueId } = props;
// store hooks
const { getStateById } = useProjectState();
const { getProjectById } = useProject();
const {
router: { workspaceSlug },
} = useApplication();
const { setPeekIssue } = useIssueDetail();
const {
issue: { getIssueById },
setPeekIssue,
} = useIssueDetail();
// derived values
const issueDetails = getIssueById(issueId);
const projectDetails = issueDetails && getProjectById(issueDetails?.project_id);
const stateDetails = issueDetails && getStateById(issueDetails?.state_id);
const handleIssuePeekOverview = () =>
workspaceSlug &&
data &&
data.project_id &&
data.id &&
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
const currentStateDetails =
getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id) || undefined;
issueDetails &&
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
return (
<ControlLink
href={`/${workspaceSlug}/projects/${data.project_id}/issues/${data.id}`}
href={`/${workspaceSlug}/projects/${issueDetails?.project_id}/issues/${issueDetails?.id}`}
target="_blank"
onClick={handleIssuePeekOverview}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<div className="relative flex h-full w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
{currentStateDetails != undefined && (
<StateGroupIcon stateGroup={currentStateDetails?.group} color={currentStateDetails?.color} />
)}
{stateDetails && <StateGroupIcon stateGroup={stateDetails?.group} color={stateDetails?.color} />}
<div className="flex-shrink-0 text-xs text-custom-text-300">
{getProjectById(data?.project_id)?.identifier} {data?.sequence_id}
{projectDetails?.identifier} {issueDetails?.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={data.name}>
<span className="flex-grow truncate text-sm font-medium">{data?.name}</span>
<Tooltip tooltipHeading="Title" tooltipContent={issueDetails?.name}>
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
</Tooltip>
</div>
</ControlLink>
);
};
});

View File

@ -11,6 +11,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { createIssuePayload } from "helpers/issue.helper";
import { cn } from "helpers/common.helper";
// types
import { IProject, TIssue } from "@plane/types";
// constants
@ -138,10 +139,12 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
};
return (
<>
<div
className={`${errors && errors?.name && errors?.name?.message ? `border border-red-500/20 bg-red-500/10` : ``}`}
>
{isOpen ? (
{isOpen ? (
<div
className={cn("sticky bottom-0 z-[1] bg-custom-background-100", {
"border border-red-500/20 bg-red-500/10": errors && errors?.name && errors?.name?.message,
})}
>
<div className="shadow-custom-shadow-sm">
<form
ref={ref}
@ -152,16 +155,17 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
</form>
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
</div>
) : (
<div
className="flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</div>
)}
</div>
</div>
) : (
<button
type="button"
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100 bg-custom-background-100"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</button>
)}
</>
);
});

View File

@ -66,14 +66,14 @@ export const HeaderColumn = (props: Props) => {
}
onMenuClose={onClose}
placement="bottom-end"
closeOnSelect
>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
className={`flex items-center justify-between gap-1.5 px-1 ${selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
@ -87,11 +87,10 @@ export const HeaderColumn = (props: Props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
className={`flex items-center justify-between gap-1.5 px-1 ${selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />

View File

@ -4,6 +4,7 @@ import { useIssueDetail, useProject, useUser } from "hooks/store";
// components
import { IssueDescriptionForm, TIssueOperations } from "components/issues";
import { IssueReaction } from "../issue-detail/reactions";
import { observer } from "mobx-react";
interface IPeekOverviewIssueDetails {
workspaceSlug: string;
@ -15,7 +16,7 @@ interface IPeekOverviewIssueDetails {
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
}
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer((props) => {
const { workspaceSlug, projectId, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props;
// store hooks
const { getProjectById } = useProject();
@ -23,6 +24,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
const {
issue: { getIssueById },
} = useIssueDetail();
// derived values
const issue = getIssueById(issueId);
if (!issue) return <></>;
@ -53,4 +55,4 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
)}
</>
);
};
});

View File

@ -1,52 +1,74 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// hooks
import { useApplication, useModule } from "hooks/store";
// ui
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { IModule } from "@plane/types";
// constants
import { MODULE_STATUS } from "constants/module";
export const ModuleGanttBlock = ({ data }: { data: IModule }) => {
type Props = {
moduleId: string;
};
export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
const { moduleId } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getModuleById } = useModule();
// derived values
const moduleDetails = getModuleById(moduleId);
return (
<div
className="relative flex h-full w-full items-center rounded"
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === data?.status)?.color }}
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data?.id}`)}
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }}
onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)}
>
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{data?.name}</h5>
<h5>{moduleDetails?.name}</h5>
<div>
{renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.target_date ?? "")}
{renderFormattedDate(moduleDetails?.start_date ?? "")} to{" "}
{renderFormattedDate(moduleDetails?.target_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{moduleDetails?.name}</div>
</Tooltip>
</div>
);
};
});
export const ModuleGanttSidebarBlock = ({ data }: { data: IModule }) => {
export const ModuleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { moduleId } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getModuleById } = useModule();
// derived values
const moduleDetails = getModuleById(moduleId);
return (
<div
className="relative flex h-full w-full items-center gap-2"
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data.id}`)}
onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)}
>
<ModuleStatusIcon status={data?.status ?? "backlog"} height="16px" width="16px" />
<h6 className="flex-grow truncate text-sm font-medium">{data.name}</h6>
<ModuleStatusIcon status={moduleDetails?.status ?? "backlog"} height="16px" width="16px" />
<h6 className="flex-grow truncate text-sm font-medium">{moduleDetails?.name}</h6>
</div>
);
};
});

View File

@ -47,7 +47,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
blocks={projectModuleIds ? blockFormat(projectModuleIds) : null}
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
blockToRender={(data: IModule) => <ModuleGanttBlock data={data} />}
blockToRender={(data: IModule) => <ModuleGanttBlock moduleId={data.id} />}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}

View File

@ -147,8 +147,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
? !moduleTotalIssues || moduleTotalIssues === 0
? "0 Issue"
: moduleTotalIssues === moduleDetails.completed_issues
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
: `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
: `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
: "0 Issue";
return (
@ -164,7 +164,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
)}
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
<Link href={`/${workspaceSlug}/projects/${moduleDetails.project}/modules/${moduleDetails.id}`}>
<div className="flex h-44 w-full min-w-[250px] flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div>
<div className="flex items-center justify-between gap-2">
<Tooltip tooltipContent={moduleDetails.name} position="top">
@ -240,7 +240,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
<span className="text-xs text-custom-text-400">No due date</span>
)}
<div className="z-10 flex items-center gap-1.5">
<div className="z-[5] flex items-center gap-1.5">
{isEditingAllowed &&
(moduleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>

View File

@ -154,38 +154,37 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
)}
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
<Link href={`/${workspaceSlug}/projects/${moduleDetails.project}/modules/${moduleDetails.id}`}>
<div className="group flex h-16 w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90">
<div className="flex w-full items-center gap-3 truncate">
<div className="flex items-center gap-4 truncate">
<span className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{completedModuleCheck ? (
progress === 100 ? (
<div className="group flex w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 flex-col sm:flex-row px-5 py-6 text-sm hover:bg-custom-background-90">
<div className="relative flex w-full items-center gap-3 justify-between overflow-hidden">
<div className="relative w-full flex items-center gap-3 overflow-hidden">
<div className="flex items-center gap-4 truncate">
<span className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{completedModuleCheck ? (
progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-sm text-custom-primary-100">{`!`}</span>
)
) : progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-sm text-custom-primary-100">{`!`}</span>
)
) : progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
</span>
<Tooltip tooltipContent={moduleDetails.name} position="top">
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
</span>
<Tooltip tooltipContent={moduleDetails.name} position="top">
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
</div>
<button onClick={openModuleOverview} className="z-[5] hidden flex-shrink-0 group-hover:flex">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
<button onClick={openModuleOverview} className="z-10 hidden flex-shrink-0 group-hover:flex">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
<div className="flex w-full items-center justify-end gap-2.5 md:w-auto md:flex-shrink-0 ">
<div className="flex items-center justify-center">
<div className="flex items-center justify-center flex-shrink-0">
{moduleStatus && (
<span
className="flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs"
className="flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs flex-shrink-0"
style={{
color: moduleStatus.color,
backgroundColor: `${moduleStatus.color}20`,
@ -195,64 +194,70 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
</span>
)}
</div>
</div>
{renderDate && (
<span className="flex w-40 items-center justify-center gap-2 text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
)}
<Tooltip tooltipContent={`${moduleDetails.members_detail.length} Members`}>
<div className="flex w-16 cursor-default items-center justify-center gap-1">
{moduleDetails.members_detail.length > 0 ? (
<AvatarGroup showTooltip={false}>
{moduleDetails.members_detail.map((member) => (
<Avatar key={member.id} name={member.display_name} src={member.avatar} />
))}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{isEditingAllowed &&
(moduleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites} className="z-[1]">
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button>
) : (
<button type="button" onClick={handleAddToFavorites} className="z-[1]">
<Star className="h-3.5 w-3.5 text-custom-text-300" />
</button>
))}
<CustomMenu verticalEllipsis buttonClassName="z-[1]">
{isEditingAllowed && (
<>
<CustomMenu.MenuItem onClick={handleEditModule}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-3 w-3" />
<span>Edit module</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeleteModule}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" />
<span>Delete module</span>
</span>
</CustomMenu.MenuItem>
</>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3 w-3" />
<span>Copy module link</span>
<div className="flex w-full sm:w-auto relative overflow-hidden items-center gap-2.5 justify-between sm:justify-end sm:flex-shrink-0 ">
<div className="text-xs text-custom-text-300">
{renderDate && (
<span className=" text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
<div className="flex-shrink-0 relative flex items-center gap-3">
<Tooltip tooltipContent={`${moduleDetails.members_detail.length} Members`}>
<div className="flex w-10 cursor-default items-center justify-center gap-1">
{moduleDetails.members_detail.length > 0 ? (
<AvatarGroup showTooltip={false}>
{moduleDetails.members_detail.map((member) => (
<Avatar key={member.id} name={member.display_name} src={member.avatar} />
))}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{isEditingAllowed &&
(moduleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites} className="z-[1]">
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button>
) : (
<button type="button" onClick={handleAddToFavorites} className="z-[1]">
<Star className="h-3.5 w-3.5 text-custom-text-300" />
</button>
))}
<CustomMenu verticalEllipsis buttonClassName="z-[1]">
{isEditingAllowed && (
<>
<CustomMenu.MenuItem onClick={handleEditModule}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-3 w-3" />
<span>Edit module</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeleteModule}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" />
<span>Delete module</span>
</span>
</CustomMenu.MenuItem>
</>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3 w-3" />
<span>Copy module link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</div>
</Link>

View File

@ -39,7 +39,7 @@ export const ModulePeekOverview: React.FC<Props> = observer(({ projectId, worksp
{peekModule && (
<div
ref={ref}
className="flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300"
className="flex h-full w-full max-w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 absolute md:relative right-0 z-[9]"
style={{
boxShadow:
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",

View File

@ -323,9 +323,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<CustomSelect
customButton={
<span
className={`flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
className={`flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs ${isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
style={{
color: moduleStatus ? moduleStatus.color : "#a3a3a2",
backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220",
@ -374,15 +373,13 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<>
<Popover.Button
ref={startDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("start_date") ? "" : "text-custom-text-400"
}`}
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${watch("start_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(startDate) ?? "No date selected"}
</span>
@ -430,15 +427,13 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<>
<Popover.Button
ref={endDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("target_date") ? "" : "text-custom-text-400"
}`}
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${watch("target_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(endDate) ?? "No date selected"}
</span>
@ -596,7 +591,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
</div>
</div>
</div>
<div className="relative h-40 w-80">
<div className="relative h-40 w-full max-w-80">
<ProgressChart
distribution={moduleDetails.distribution?.completion_chart ?? {}}
startDate={moduleDetails.start_date}

View File

@ -59,6 +59,11 @@ export const WorkspaceDashboardView = observer(() => {
return (
<>
{currentUser && !currentUser.is_tour_completed && (
<div className="fixed left-0 top-0 z-20 grid h-full w-full place-items-center bg-custom-backdrop bg-opacity-50 transition-opacity">
<TourRoot onComplete={handleTourCompleted} />
</div>
)}
{homeDashboardId && joinedProjectIds ? (
<>
{joinedProjectIds.length > 0 ? (
@ -66,11 +71,7 @@ export const WorkspaceDashboardView = observer(() => {
<IssuePeekOverview />
<div className="space-y-7 p-7 bg-custom-background-90 h-full w-full flex flex-col overflow-y-auto">
{currentUser && <UserGreetingsView user={currentUser} />}
{currentUser && !currentUser.is_tour_completed && (
<div className="fixed left-0 top-0 z-20 grid h-full w-full place-items-center bg-custom-backdrop bg-opacity-50 transition-opacity">
<TourRoot onComplete={handleTourCompleted} />
</div>
)}
<DashboardWidgets />
</div>
</>

View File

@ -104,7 +104,7 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-2xl">
<PageForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} pageStore={pageStore} />
</Dialog.Panel>
</Transition.Child>

View File

@ -67,7 +67,7 @@ export const PageForm: React.FC<Props> = (props) => {
</div>
</div>
</div>
<div className="mt-5 flex items-center justify-between gap-2">
<div className="mt-5 md:flex items-center justify-between gap-2">
<Controller
control={control}
name="access"
@ -100,7 +100,7 @@ export const PageForm: React.FC<Props> = (props) => {
</div>
)}
/>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 justify-end mt-5 md:mt-0">
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
Cancel
</Button>

View File

@ -155,7 +155,7 @@ export const PagesListItem: FC<IPagesListItem> = observer(({ pageId, projectId }
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} pageId={pageId} />
<li>
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${pageId}`}>
<div className="relative rounded p-4 text-custom-text-200 hover:bg-custom-background-80">
<div className="relative rounded p-3 md:p-4 text-custom-text-200 hover:bg-custom-background-80">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 overflow-hidden">
<FileText className="h-4 w-4 shrink-0" />

View File

@ -52,7 +52,7 @@ export const RecentPagesList: FC = observer(() => {
return (
<div key={key}>
<h2 className="sticky top-0 z-[1] mb-2 bg-custom-background-100 text-xl font-semibold capitalize">
<h2 className="sticky top-0 z-[1] mb-2 bg-custom-background-100 text-xl font-semibold capitalize px-3 md:p-0">
{replaceUnderscoreIfSnakeCase(key)}
</h2>
<PagesListView pageIds={recentProjectPages[key]} />

View File

@ -1,4 +1,4 @@
import React, { ReactElement } from "react";
import React, { ReactElement, useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
@ -12,7 +12,7 @@ import { Loader } from "@plane/ui";
// types
import { NextPageWithLayout } from "lib/types";
// fetch-keys
import { useIssueDetail } from "hooks/store";
import { useApplication, useIssueDetail } from "hooks/store";
const IssueDetailsPage: NextPageWithLayout = observer(() => {
// router
@ -23,6 +23,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
fetchIssue,
issue: { getIssueById },
} = useIssueDetail();
const { theme: themeStore } = useApplication();
const { isLoading } = useSWR(
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
@ -34,6 +35,21 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
const issue = getIssueById(issueId?.toString() || "") || undefined;
const issueLoader = !issue || isLoading ? true : false;
useEffect(() => {
const handleToggleIssueDetailSidebar = () => {
if (window && window.innerWidth < 768) {
themeStore.toggleIssueDetailSidebar(true);
}
if (window && themeStore.issueDetailSidebarCollapsed && window.innerWidth >= 768) {
themeStore.toggleIssueDetailSidebar(false);
}
};
window.addEventListener("resize", handleToggleIssueDetailSidebar);
handleToggleIssueDetailSidebar();
return () => window.removeEventListener("resize", handleToggleIssueDetailSidebar);
}, [themeStore]);
return (
<>
{issueLoader ? (

View File

@ -103,6 +103,25 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const mobileTabList = (
<Tab.List as="div" className="flex items-center justify-between border-b border-custom-border-200 px-3 pt-3 mb-4">
<div className="flex flex-wrap items-center gap-4">
{PAGE_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`text-sm outline-none pb-3 ${
selected ? "border-custom-primary-100 text-custom-primary-100 border-b" : ""
}`
}
>
{tab.title}
</Tab>
))}
</div>
</Tab.List>
);
if (loader || archivedPageLoader)
return (
<div className="flex items-center justify-center h-full w-full">
@ -121,8 +140,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
projectId={projectId.toString()}
/>
)}
<div className="flex h-full flex-col space-y-5 overflow-hidden p-6">
<div className="flex justify-between gap-4">
<div className="flex h-full flex-col md:space-y-5 overflow-hidden md:p-6">
<div className="justify-between gap-4 hidden md:flex">
<h3 className="text-2xl font-semibold text-custom-text-100">Pages</h3>
</div>
<Tab.Group
@ -147,7 +166,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
}
}}
>
<Tab.List as="div" className="mb-6 flex items-center justify-between">
<div className="md:hidden">{mobileTabList}</div>
<Tab.List as="div" className="mb-6 items-center justify-between hidden md:flex">
<div className="flex flex-wrap items-center gap-4">
{PAGE_TABS_LIST.map((tab) => (
<Tab

View File

@ -9,11 +9,13 @@ export interface IThemeStore {
sidebarCollapsed: boolean | undefined;
profileSidebarCollapsed: boolean | undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined;
issueDetailSidebarCollapsed: boolean | undefined;
// actions
toggleSidebar: (collapsed?: boolean) => void;
setTheme: (theme: any) => void;
toggleProfileSidebar: (collapsed?: boolean) => void;
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
toggleIssueDetailSidebar: (collapsed?: boolean) => void;
}
export class ThemeStore implements IThemeStore {
@ -22,6 +24,7 @@ export class ThemeStore implements IThemeStore {
theme: string | null = null;
profileSidebarCollapsed: boolean | undefined = undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
issueDetailSidebarCollapsed: boolean | undefined = undefined;
// root store
rootStore;
@ -32,11 +35,13 @@ export class ThemeStore implements IThemeStore {
theme: observable.ref,
profileSidebarCollapsed: observable.ref,
workspaceAnalyticsSidebarCollapsed: observable.ref,
issueDetailSidebarCollapsed: observable.ref,
// action
toggleSidebar: action,
setTheme: action,
toggleProfileSidebar: action,
toggleWorkspaceAnalyticsSidebar: action
toggleWorkspaceAnalyticsSidebar: action,
toggleIssueDetailSidebar: action,
// computed
});
// root store
@ -82,6 +87,15 @@ export class ThemeStore implements IThemeStore {
localStorage.setItem("workspace_analytics_sidebar_collapsed", this.workspaceAnalyticsSidebarCollapsed.toString());
};
toggleIssueDetailSidebar = (collapsed?: boolean) => {
if(collapsed === undefined) {
this.issueDetailSidebarCollapsed = !this.issueDetailSidebarCollapsed;
} else {
this.issueDetailSidebarCollapsed = collapsed;
}
localStorage.setItem("issue_detail_sidebar_collapsed", this.issueDetailSidebarCollapsed.toString());
}
/**
* Sets the user theme and applies it to the platform
* @param _theme

View File

@ -4,6 +4,7 @@ import { IssueArchiveService, IssueService } from "services/issue";
// types
import { IIssueDetail } from "./root.store";
import { TIssue } from "@plane/types";
import { computedFn } from "mobx-utils";
export interface IIssueStoreActions {
// actions
@ -44,10 +45,10 @@ export class IssueStore implements IIssueStore {
}
// helper methods
getIssueById = (issueId: string) => {
getIssueById = computedFn((issueId: string) => {
if (!issueId) return undefined;
return this.rootIssueDetailStore.rootIssueStore.issues.getIssueById(issueId) ?? undefined;
};
});
// actions
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => {
@ -63,12 +64,12 @@ export class IssueStore implements IIssueStore {
if (!issue) throw new Error("Issue not found");
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]);
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue], true);
// store handlers from issue detail
// parent
if (issue && issue?.parent && issue?.parent?.id)
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue?.parent]);
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue.parent]);
// assignees
// labels
// state

View File

@ -10,7 +10,7 @@ export type IIssueStore = {
// observables
issuesMap: Record<string, TIssue>; // Record defines issue_id as key and TIssue as value
// actions
addIssue(issues: TIssue[]): void;
addIssue(issues: TIssue[], shouldReplace?: boolean): void;
updateIssue(issueId: string, issue: Partial<TIssue>): void;
removeIssue(issueId: string): void;
// helper methods
@ -39,11 +39,11 @@ export class IssueStore implements IIssueStore {
* @param {TIssue[]} issues
* @returns {void}
*/
addIssue = (issues: TIssue[]) => {
addIssue = (issues: TIssue[], shouldReplace = false) => {
if (issues && issues.length <= 0) return;
runInAction(() => {
issues.forEach((issue) => {
if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue);
if (!this.issuesMap[issue.id] || shouldReplace) set(this.issuesMap, issue.id, issue);
});
});
};