feat: init
This commit is contained in:
78
apps/web/core/components/gantt-chart/chart/header.tsx
Normal file
78
apps/web/core/components/gantt-chart/chart/header.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { Expand, Shrink } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane
|
||||
import type { TGanttViews } from "@plane/types";
|
||||
import { Row } from "@plane/ui";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { VIEWS_LIST } from "@/components/gantt-chart/data";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { GANTT_BREADCRUMBS_HEIGHT } from "../constants";
|
||||
|
||||
type Props = {
|
||||
blockIds: string[];
|
||||
fullScreenMode: boolean;
|
||||
handleChartView: (view: TGanttViews) => void;
|
||||
handleToday: () => void;
|
||||
loaderTitle: string;
|
||||
toggleFullScreenMode: () => void;
|
||||
showToday: boolean;
|
||||
};
|
||||
|
||||
export const GanttChartHeader: React.FC<Props> = observer((props) => {
|
||||
const { t } = useTranslation();
|
||||
const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode, showToday } =
|
||||
props;
|
||||
// chart hook
|
||||
const { currentView } = useTimeLineChartStore();
|
||||
|
||||
return (
|
||||
<Row
|
||||
className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap py-2"
|
||||
style={{ height: `${GANTT_BREADCRUMBS_HEIGHT}px` }}
|
||||
>
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto text-sm font-medium">
|
||||
{blockIds ? `${blockIds.length} ${loaderTitle}` : t("common.loading")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{VIEWS_LIST.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)}
|
||||
>
|
||||
{t(chartView?.i18n_title)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showToday && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80"
|
||||
onClick={handleToday}
|
||||
>
|
||||
{t("common.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>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
4
apps/web/core/components/gantt-chart/chart/index.ts
Normal file
4
apps/web/core/components/gantt-chart/chart/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./views";
|
||||
export * from "./header";
|
||||
export * from "./main-content";
|
||||
export * from "./root";
|
||||
235
apps/web/core/components/gantt-chart/chart/main-content.tsx
Normal file
235
apps/web/core/components/gantt-chart/chart/main-content.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react";
|
||||
import type {
|
||||
ChartDataType,
|
||||
IBlockUpdateData,
|
||||
IBlockUpdateDependencyData,
|
||||
IGanttBlock,
|
||||
TGanttViews,
|
||||
} from "@plane/types";
|
||||
import { cn, getDate } from "@plane/utils";
|
||||
// components
|
||||
import { MultipleSelectGroup } from "@/components/core/multiple-select";
|
||||
import { GanttChartSidebar, MonthChartView, QuarterChartView, WeekChartView } from "@/components/gantt-chart";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
// plane web components
|
||||
import { TimelineDependencyPaths, TimelineDraggablePath } from "@/plane-web/components/gantt-chart";
|
||||
import { GanttChartRowList } from "@/plane-web/components/gantt-chart/blocks/block-row-list";
|
||||
import { GanttChartBlocksList } from "@/plane-web/components/gantt-chart/blocks/blocks-list";
|
||||
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues/bulk-operations";
|
||||
// plane web hooks
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
//
|
||||
import { DEFAULT_BLOCK_WIDTH, GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
|
||||
import { getItemPositionWidth } from "../views";
|
||||
import { TimelineDragHelper } from "./timeline-drag-helper";
|
||||
|
||||
type Props = {
|
||||
blockIds: string[];
|
||||
canLoadMoreBlocks?: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
bottomSpacing: boolean;
|
||||
enableBlockLeftResize: boolean | ((blockId: string) => boolean);
|
||||
enableBlockMove: boolean | ((blockId: string) => boolean);
|
||||
enableBlockRightResize: boolean | ((blockId: string) => boolean);
|
||||
enableReorder: boolean | ((blockId: string) => boolean);
|
||||
enableSelection: boolean | ((blockId: string) => boolean);
|
||||
enableAddBlock: boolean | ((blockId: string) => boolean);
|
||||
enableDependency: boolean | ((blockId: string) => boolean);
|
||||
itemsContainerWidth: number;
|
||||
showAllBlocks: boolean;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
title: string;
|
||||
updateCurrentViewRenderPayload: (
|
||||
direction: "left" | "right",
|
||||
currentView: TGanttViews,
|
||||
targetDate?: Date
|
||||
) => ChartDataType | undefined;
|
||||
quickAdd?: React.ReactNode | undefined;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
|
||||
export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blockIds,
|
||||
loadMoreBlocks,
|
||||
blockToRender,
|
||||
blockUpdateHandler,
|
||||
bottomSpacing,
|
||||
enableBlockLeftResize,
|
||||
enableBlockMove,
|
||||
enableBlockRightResize,
|
||||
enableReorder,
|
||||
enableAddBlock,
|
||||
enableSelection,
|
||||
enableDependency,
|
||||
itemsContainerWidth,
|
||||
showAllBlocks,
|
||||
sidebarToRender,
|
||||
title,
|
||||
canLoadMoreBlocks,
|
||||
updateCurrentViewRenderPayload,
|
||||
quickAdd,
|
||||
updateBlockDates,
|
||||
isEpic = false,
|
||||
} = props;
|
||||
// refs
|
||||
const ganttContainerRef = useRef<HTMLDivElement>(null);
|
||||
// chart hook
|
||||
const { currentView, currentViewData } = useTimeLineChartStore();
|
||||
// plane web hooks
|
||||
const isBulkOperationsEnabled = useBulkOperationStatus();
|
||||
|
||||
// Enable Auto Scroll for Ganttlist
|
||||
useEffect(() => {
|
||||
const element = ganttContainerRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
autoScrollForElements({
|
||||
element,
|
||||
getAllowedAxis: () => "vertical",
|
||||
canScroll: ({ source }) => source.data.dragInstanceId === "GANTT_REORDER",
|
||||
})
|
||||
);
|
||||
}, [ganttContainerRef?.current]);
|
||||
|
||||
// handling scroll functionality
|
||||
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
|
||||
|
||||
const approxRangeLeft = scrollLeft;
|
||||
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
|
||||
const calculatedRangeRight = itemsContainerWidth - (scrollLeft + clientWidth);
|
||||
|
||||
if (approxRangeRight < clientWidth || calculatedRangeRight < clientWidth) {
|
||||
updateCurrentViewRenderPayload("right", currentView);
|
||||
}
|
||||
if (approxRangeLeft < clientWidth) {
|
||||
updateCurrentViewRenderPayload("left", currentView);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollToBlock = (block: IGanttBlock) => {
|
||||
const scrollContainer = ganttContainerRef.current as HTMLDivElement;
|
||||
const scrollToEndDate = !block.start_date && block.target_date;
|
||||
const scrollToDate = block.start_date ? getDate(block.start_date) : getDate(block.target_date);
|
||||
let chartData;
|
||||
|
||||
if (!scrollContainer || !currentViewData || !scrollToDate) return;
|
||||
|
||||
if (scrollToDate.getTime() < currentViewData.data.startDate.getTime()) {
|
||||
chartData = updateCurrentViewRenderPayload("left", currentView, scrollToDate);
|
||||
} else if (scrollToDate.getTime() > currentViewData.data.endDate.getTime()) {
|
||||
chartData = updateCurrentViewRenderPayload("right", currentView, scrollToDate);
|
||||
}
|
||||
// update container's scroll position to the block's position
|
||||
const updatedPosition = getItemPositionWidth(chartData ?? currentViewData, block);
|
||||
|
||||
setTimeout(() => {
|
||||
if (updatedPosition)
|
||||
scrollContainer.scrollLeft = updatedPosition.marginLeft - 4 - (scrollToEndDate ? DEFAULT_BLOCK_WIDTH : 0);
|
||||
});
|
||||
};
|
||||
|
||||
const CHART_VIEW_COMPONENTS: {
|
||||
[key in TGanttViews]: React.FC;
|
||||
} = {
|
||||
week: WeekChartView,
|
||||
month: MonthChartView,
|
||||
quarter: QuarterChartView,
|
||||
};
|
||||
|
||||
if (!currentView) return null;
|
||||
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimelineDragHelper ganttContainerRef={ganttContainerRef} />
|
||||
<MultipleSelectGroup
|
||||
containerRef={ganttContainerRef}
|
||||
entities={{
|
||||
[GANTT_SELECT_GROUP]: blockIds ?? [],
|
||||
}}
|
||||
disabled={!isBulkOperationsEnabled || isEpic}
|
||||
>
|
||||
{(helpers) => (
|
||||
<>
|
||||
<div
|
||||
// DO NOT REMOVE THE ID
|
||||
id="gantt-container"
|
||||
className={cn(
|
||||
"h-full w-full overflow-auto vertical-scrollbar horizontal-scrollbar scrollbar-lg flex border-t-[0.5px] border-custom-border-200",
|
||||
{
|
||||
"mb-8": bottomSpacing,
|
||||
}
|
||||
)}
|
||||
ref={ganttContainerRef}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<GanttChartSidebar
|
||||
blockIds={blockIds}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableReorder={enableReorder}
|
||||
enableSelection={enableSelection}
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
quickAdd={quickAdd}
|
||||
selectionHelpers={helpers}
|
||||
isEpic={isEpic}
|
||||
/>
|
||||
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||
<ActiveChartView />
|
||||
{currentViewData && (
|
||||
<div
|
||||
className="relative h-full"
|
||||
style={{
|
||||
width: `${itemsContainerWidth}px`,
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
paddingBottom: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<GanttChartRowList
|
||||
blockIds={blockIds}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
handleScrollToBlock={handleScrollToBlock}
|
||||
enableAddBlock={enableAddBlock}
|
||||
showAllBlocks={showAllBlocks}
|
||||
selectionHelpers={helpers}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
/>
|
||||
<TimelineDependencyPaths isEpic={isEpic} />
|
||||
<TimelineDraggablePath />
|
||||
<GanttChartBlocksList
|
||||
blockIds={blockIds}
|
||||
blockToRender={blockToRender}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
enableDependency={enableDependency}
|
||||
showAllBlocks={showAllBlocks}
|
||||
updateBlockDates={updateBlockDates}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||
</>
|
||||
)}
|
||||
</MultipleSelectGroup>
|
||||
</>
|
||||
);
|
||||
});
|
||||
219
apps/web/core/components/gantt-chart/chart/root.tsx
Normal file
219
apps/web/core/components/gantt-chart/chart/root.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { createPortal } from "react-dom";
|
||||
// plane imports
|
||||
// components
|
||||
import type { ChartDataType, IBlockUpdateData, IBlockUpdateDependencyData, TGanttViews } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
import { GanttChartHeader, GanttChartMainContent } from "@/components/gantt-chart";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { SIDEBAR_WIDTH } from "../constants";
|
||||
import { currentViewDataWithView } from "../data";
|
||||
import type { IMonthBlock, IMonthView, IWeekBlock } from "../views";
|
||||
import { getNumberOfDaysBetweenTwoDates, monthView, quarterView, weekView } from "../views";
|
||||
|
||||
type ChartViewRootProps = {
|
||||
border: boolean;
|
||||
title: string;
|
||||
loaderTitle: string;
|
||||
blockIds: string[];
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
enableBlockLeftResize: boolean | ((blockId: string) => boolean);
|
||||
enableBlockRightResize: boolean | ((blockId: string) => boolean);
|
||||
enableBlockMove: boolean | ((blockId: string) => boolean);
|
||||
enableReorder: boolean | ((blockId: string) => boolean);
|
||||
enableAddBlock: boolean | ((blockId: string) => boolean);
|
||||
enableSelection: boolean | ((blockId: string) => boolean);
|
||||
enableDependency: boolean | ((blockId: string) => boolean);
|
||||
bottomSpacing: boolean;
|
||||
showAllBlocks: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
quickAdd?: React.ReactNode | undefined;
|
||||
showToday: boolean;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
|
||||
const timelineViewHelpers = {
|
||||
week: weekView,
|
||||
month: monthView,
|
||||
quarter: quarterView,
|
||||
};
|
||||
|
||||
export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
const {
|
||||
border,
|
||||
title,
|
||||
blockIds,
|
||||
loadMoreBlocks,
|
||||
loaderTitle,
|
||||
blockUpdateHandler,
|
||||
sidebarToRender,
|
||||
blockToRender,
|
||||
canLoadMoreBlocks,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
enableReorder,
|
||||
enableAddBlock,
|
||||
enableSelection,
|
||||
enableDependency,
|
||||
bottomSpacing,
|
||||
showAllBlocks,
|
||||
quickAdd,
|
||||
showToday,
|
||||
updateBlockDates,
|
||||
isEpic = false,
|
||||
} = props;
|
||||
// states
|
||||
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
|
||||
const [fullScreenMode, setFullScreenMode] = useState(false);
|
||||
// hooks
|
||||
const {
|
||||
currentView,
|
||||
currentViewData,
|
||||
renderView,
|
||||
updateCurrentView,
|
||||
updateCurrentViewData,
|
||||
updateRenderView,
|
||||
updateAllBlocksOnChartChangeWhileDragging,
|
||||
} = useTimeLineChartStore();
|
||||
const { data } = useUserProfile();
|
||||
const startOfWeek = data?.start_of_the_week;
|
||||
|
||||
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews, targetDate?: Date) => {
|
||||
const selectedCurrentView: TGanttViews = view;
|
||||
const selectedCurrentViewData: ChartDataType | undefined =
|
||||
selectedCurrentView && selectedCurrentView === currentViewData?.key
|
||||
? currentViewData
|
||||
: currentViewDataWithView(view);
|
||||
|
||||
if (selectedCurrentViewData === undefined) return;
|
||||
|
||||
const currentViewHelpers = timelineViewHelpers[selectedCurrentView];
|
||||
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate, startOfWeek);
|
||||
const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as (
|
||||
a: IWeekBlock[] | IMonthView | IMonthBlock[],
|
||||
b: IWeekBlock[] | IMonthView | IMonthBlock[]
|
||||
) => IWeekBlock[] | IMonthView | IMonthBlock[];
|
||||
|
||||
// updating the prevData, currentData and nextData
|
||||
if (currentRender.payload) {
|
||||
updateCurrentViewData(currentRender.state);
|
||||
|
||||
if (side === "left") {
|
||||
updateCurrentView(selectedCurrentView);
|
||||
updateRenderView(mergeRenderPayloads(currentRender.payload, renderView));
|
||||
updateItemsContainerWidth(currentRender.scrollWidth);
|
||||
if (!targetDate) updateCurrentLeftScrollPosition(currentRender.scrollWidth);
|
||||
updateAllBlocksOnChartChangeWhileDragging(currentRender.scrollWidth);
|
||||
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
||||
} else if (side === "right") {
|
||||
updateCurrentView(view);
|
||||
updateRenderView(mergeRenderPayloads(renderView, currentRender.payload));
|
||||
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
||||
} else {
|
||||
updateCurrentView(view);
|
||||
updateRenderView(currentRender.payload);
|
||||
setItemsContainerWidth(currentRender.scrollWidth);
|
||||
setTimeout(() => {
|
||||
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
return currentRender.state;
|
||||
};
|
||||
|
||||
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
|
||||
|
||||
// handling the scroll positioning from left and right
|
||||
useEffect(() => {
|
||||
handleToday();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const updateItemsContainerWidth = (width: number) => {
|
||||
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||
if (!scrollContainer) return;
|
||||
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
|
||||
};
|
||||
|
||||
const updateCurrentLeftScrollPosition = (width: number) => {
|
||||
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||
if (!scrollContainer) return;
|
||||
|
||||
scrollContainer.scrollLeft = 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;
|
||||
daysDifference = getNumberOfDaysBetweenTwoDates(currentState.data.startDate, date);
|
||||
|
||||
scrollWidth =
|
||||
Math.abs(daysDifference) * currentState.data.dayWidth -
|
||||
(clientVisibleWidth / 2 - currentState.data.dayWidth) +
|
||||
SIDEBAR_WIDTH / 2;
|
||||
|
||||
scrollContainer.scrollLeft = scrollWidth;
|
||||
};
|
||||
|
||||
const portalContainer = document.getElementById("full-screen-portal") as HTMLElement;
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
|
||||
"inset-0 z-[25] bg-custom-background-100": fullScreenMode,
|
||||
"border-[0.5px] border-custom-border-200": border,
|
||||
})}
|
||||
>
|
||||
<GanttChartHeader
|
||||
blockIds={blockIds}
|
||||
fullScreenMode={fullScreenMode}
|
||||
toggleFullScreenMode={() => setFullScreenMode((prevData) => !prevData)}
|
||||
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
||||
handleToday={handleToday}
|
||||
loaderTitle={loaderTitle}
|
||||
showToday={showToday}
|
||||
/>
|
||||
<GanttChartMainContent
|
||||
blockIds={blockIds}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
bottomSpacing={bottomSpacing}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableReorder={enableReorder}
|
||||
enableSelection={enableSelection}
|
||||
enableAddBlock={enableAddBlock}
|
||||
enableDependency={enableDependency}
|
||||
itemsContainerWidth={itemsContainerWidth}
|
||||
showAllBlocks={showAllBlocks}
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
|
||||
quickAdd={quickAdd}
|
||||
updateBlockDates={updateBlockDates}
|
||||
isEpic={isEpic}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return fullScreenMode && portalContainer ? createPortal(content, portalContainer) : content;
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { RefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useAutoScroller } from "@/hooks/use-auto-scroller";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
|
||||
|
||||
type Props = {
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
export const TimelineDragHelper = observer((props: Props) => {
|
||||
const { ganttContainerRef } = props;
|
||||
const { isDragging } = useTimeLineChartStore();
|
||||
|
||||
useAutoScroller(ganttContainerRef, isDragging, SIDEBAR_WIDTH, HEADER_HEIGHT);
|
||||
return <></>;
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./month";
|
||||
export * from "./quarter";
|
||||
export * from "./week";
|
||||
105
apps/web/core/components/gantt-chart/chart/views/month.tsx
Normal file
105
apps/web/core/components/gantt-chart/chart/views/month.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
// types
|
||||
import type { IMonthView } from "../../views";
|
||||
import { getNumberOfDaysBetweenTwoDates } from "../../views/helpers";
|
||||
|
||||
export const MonthChartView: FC<any> = observer(() => {
|
||||
// chart hook
|
||||
const { currentViewData, renderView } = useTimeLineChartStore();
|
||||
const monthView: IMonthView = renderView;
|
||||
|
||||
if (!monthView) return <></>;
|
||||
|
||||
const { months, weeks } = monthView;
|
||||
|
||||
const monthsStartDate = new Date(months[0].year, months[0].month, 1);
|
||||
const weeksStartDate = weeks[0].startDate;
|
||||
|
||||
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData && (
|
||||
<div className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200">
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="w-full sticky top-0 z-[5] bg-custom-background-100 flex-shrink-0"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{/** Main Month Title */}
|
||||
<div className="flex h-7" style={{ marginLeft: `${marginLeftDays * currentViewData.data.dayWidth}px` }}>
|
||||
{months?.map((monthBlock) => (
|
||||
<div
|
||||
key={`month-${monthBlock?.month}-${monthBlock?.year}`}
|
||||
className="flex outline-[0.5px] outline outline-custom-border-200"
|
||||
style={{ width: `${monthBlock.days * currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
<div
|
||||
className="sticky flex items-center font-normal z-[1] m-1 whitespace-nowrap px-3 py-1 text-base capitalize bg-custom-background-100 text-custom-text-200"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{monthBlock?.title}
|
||||
{monthBlock.today && (
|
||||
<span className={cn("rounded ml-2 font-medium bg-custom-primary-100 px-1 text-2xs text-white")}>
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/** Weeks Sub title */}
|
||||
<div className="h-5 w-full flex">
|
||||
{weeks?.map((weekBlock) => (
|
||||
<div
|
||||
key={`sub-title-${weekBlock.startDate}-${weekBlock.endDate}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 py-1 px-2 text-center capitalize justify-between outline-[0.25px] outline outline-custom-border-200",
|
||||
{
|
||||
"bg-custom-primary-100/20": weekBlock.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-xs font-medium text-custom-text-400">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded bg-custom-primary-100 px-1 text-white": weekBlock.today,
|
||||
})}
|
||||
>
|
||||
{weekBlock.startDate.getDate()}-{weekBlock.endDate.getDate()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-x-1 text-xs font-medium">{weekBlock.weekData.shortTitle}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/** Week Columns */}
|
||||
<div className="h-full w-full flex-grow flex">
|
||||
{weeks?.map((weekBlock) => (
|
||||
<div
|
||||
key={`column-${weekBlock.startDate}-${weekBlock.endDate}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
|
||||
"bg-custom-primary-100/20": weekBlock.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
94
apps/web/core/components/gantt-chart/chart/views/quarter.tsx
Normal file
94
apps/web/core/components/gantt-chart/chart/views/quarter.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../../constants";
|
||||
import type { IMonthBlock, IQuarterMonthBlock } from "../../views";
|
||||
import { groupMonthsToQuarters } from "../../views";
|
||||
|
||||
export const QuarterChartView: FC<any> = observer(() => {
|
||||
const { currentViewData, renderView } = useTimeLineChartStore();
|
||||
const monthBlocks: IMonthBlock[] = renderView;
|
||||
|
||||
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData &&
|
||||
quarterBlocks?.map((quarterBlock, rootIndex) => (
|
||||
<div
|
||||
key={`month-${quarterBlock.quarterNumber}-${quarterBlock.year}`}
|
||||
className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200"
|
||||
>
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="w-full sticky top-0 z-[5] bg-custom-background-100 flex-shrink-0 outline-[1px] outline outline-custom-border-200"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{/** Main Quarter Title */}
|
||||
<div className="w-full inline-flex h-7 justify-between">
|
||||
<div
|
||||
className="sticky flex items-center font-normal z-[1] my-1 whitespace-nowrap px-3 py-1 text-base capitalize bg-custom-background-100 text-custom-text-200"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{quarterBlock?.title}
|
||||
{quarterBlock.today && (
|
||||
<span className={cn("rounded ml-2 font-medium bg-custom-primary-100 px-1 text-2xs text-white")}>
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="sticky whitespace-nowrap px-3 py-2 text-xs capitalize text-custom-text-400">
|
||||
{quarterBlock.shortTitle}
|
||||
</div>
|
||||
</div>
|
||||
{/** Months Sub title */}
|
||||
<div className="h-5 w-full flex">
|
||||
{quarterBlock?.children?.map((monthBlock, index) => (
|
||||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 text-center capitalize justify-center outline-[0.25px] outline outline-custom-border-200",
|
||||
{
|
||||
"bg-custom-primary-100/20": monthBlock.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||
>
|
||||
<div className="space-x-1 flex items-center justify-center text-xs font-medium h-full">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded-lg bg-custom-primary-100 px-2 text-white": monthBlock.today,
|
||||
})}
|
||||
>
|
||||
{monthBlock.monthData.shortTitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/** Month Columns */}
|
||||
<div className="h-full w-full flex-grow flex">
|
||||
{quarterBlock?.children?.map((monthBlock, index) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
|
||||
"bg-custom-primary-100/20": monthBlock.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
93
apps/web/core/components/gantt-chart/chart/views/week.tsx
Normal file
93
apps/web/core/components/gantt-chart/chart/views/week.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../../constants";
|
||||
import type { IWeekBlock } from "../../views";
|
||||
|
||||
export const WeekChartView: FC<any> = observer(() => {
|
||||
const { currentViewData, renderView } = useTimeLineChartStore();
|
||||
const weekBlocks: IWeekBlock[] = renderView;
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData &&
|
||||
weekBlocks?.map((block, rootIndex) => (
|
||||
<div
|
||||
key={`month-${block?.startDate}-${block?.endDate}`}
|
||||
className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200"
|
||||
>
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="w-full sticky top-0 z-[5] bg-custom-background-100 flex-shrink-0 outline-[1px] outline outline-custom-border-200"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{/** Main Months Title */}
|
||||
<div className="w-full inline-flex h-7 justify-between">
|
||||
<div
|
||||
className="sticky flex items-center font-normal z-[1] m-1 whitespace-nowrap px-3 py-1 text-sm capitalize bg-custom-background-100 text-custom-text-200"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{block?.title}
|
||||
</div>
|
||||
<div className="sticky whitespace-nowrap px-3 py-2 text-xs capitalize text-custom-text-400">
|
||||
{block?.weekData?.title}
|
||||
</div>
|
||||
</div>
|
||||
{/** Days Sub title */}
|
||||
<div className="h-5 w-full flex">
|
||||
{block?.children?.map((weekDay, index) => (
|
||||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 p-1 text-center capitalize justify-between outline-[0.25px] outline outline-custom-border-200",
|
||||
{
|
||||
"bg-custom-primary-100/20": weekDay.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-xs font-medium text-custom-text-400">
|
||||
{weekDay.dayData.abbreviation}
|
||||
</div>
|
||||
<div className="space-x-1 text-xs font-medium">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded bg-custom-primary-100 px-1 text-white": weekDay.today,
|
||||
})}
|
||||
>
|
||||
{weekDay.date.getDate()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/** Day Columns */}
|
||||
<div className="h-full w-full flex-grow flex bg-custom-background-100">
|
||||
{block?.children?.map((weekDay, index) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
|
||||
"bg-custom-primary-100/20": weekDay.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
{["sat", "sun"].includes(weekDay?.dayData?.shortTitle) && (
|
||||
<div className="h-full bg-custom-background-90 outline-[0.25px] outline outline-custom-border-300" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user