snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
86
apps/web/core/components/gantt-chart/chart/header.tsx
Normal file
86
apps/web/core/components/gantt-chart/chart/header.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { ArrowCollapseOutline, FullScreenOutline } from "@makeplane/propel/icons";
|
||||
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 = observer(function GanttChartHeader(props: 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 bg-surface-1 py-2 whitespace-nowrap"
|
||||
style={{ height: `${GANTT_BREADCRUMBS_HEIGHT}px` }}
|
||||
>
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto text-11 font-medium text-tertiary">
|
||||
{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-md bg-layer-transparent p-1 px-2 text-11 hover:bg-layer-transparent-hover",
|
||||
{
|
||||
"bg-layer-transparent-selected": currentView === chartView?.key,
|
||||
}
|
||||
)}
|
||||
onClick={() => handleChartView(chartView?.key)}
|
||||
>
|
||||
{t(chartView?.i18n_title)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showToday && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md bg-layer-transparent p-1 px-2 text-11 hover:bg-layer-transparent-hover"
|
||||
onClick={handleToday}
|
||||
>
|
||||
{t("common.today")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center rounded-md border border-subtle bg-layer-transparent p-1 transition-all hover:bg-layer-transparent-hover"
|
||||
onClick={toggleFullScreenMode}
|
||||
>
|
||||
{fullScreenMode ? <ArrowCollapseOutline className="h-4 w-4" /> : <FullScreenOutline className="h-4 w-4" />}
|
||||
</button>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
10
apps/web/core/components/gantt-chart/chart/index.ts
Normal file
10
apps/web/core/components/gantt-chart/chart/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./views";
|
||||
export * from "./header";
|
||||
export * from "./main-content";
|
||||
export * from "./root";
|
||||
238
apps/web/core/components/gantt-chart/chart/main-content.tsx
Normal file
238
apps/web/core/components/gantt-chart/chart/main-content.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
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";
|
||||
import { GanttChartRowList } from "@/components/gantt-chart/blocks/block-row-list";
|
||||
import { GanttChartBlocksList } from "@/components/gantt-chart/blocks/blocks-list";
|
||||
import { IssueBulkOperationsRoot } from "@/components/issues/bulk-operations";
|
||||
import { useBulkOperationStatus } from "@/hooks/use-bulk-operation-status";
|
||||
// local imports
|
||||
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 = observer(function GanttChartMainContent(props: 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",
|
||||
})
|
||||
);
|
||||
// oxlint-disable-next-line eslint-plugin-react-hooks/exhaustive-deps
|
||||
}, [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(
|
||||
"vertical-scrollbar horizontal-scrollbar scrollbar-lg flex h-full w-full overflow-auto border-t-[0.5px] border-subtle",
|
||||
{
|
||||
"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}
|
||||
selectionHelpers={helpers}
|
||||
showAllBlocks={showAllBlocks}
|
||||
isEpic={isEpic}
|
||||
/>
|
||||
<div className="relative h-max min-h-full 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}
|
||||
/>
|
||||
<GanttChartBlocksList
|
||||
blockIds={blockIds}
|
||||
blockToRender={blockToRender}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
enableDependency={enableDependency}
|
||||
showAllBlocks={showAllBlocks}
|
||||
updateBlockDates={updateBlockDates}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{quickAdd ? quickAdd : null}
|
||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||
</>
|
||||
)}
|
||||
</MultipleSelectGroup>
|
||||
</>
|
||||
);
|
||||
});
|
||||
224
apps/web/core/components/gantt-chart/chart/root.tsx
Normal file
224
apps/web/core/components/gantt-chart/chart/root.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
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 = observer(function ChartViewRoot(props: ChartViewRootProps) {
|
||||
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("shadow relative flex h-full flex-col rounded-xs bg-surface-1 select-none", {
|
||||
"inset-0 z-[25] bg-surface-1": fullScreenMode,
|
||||
"border-[0.5px] border-subtle": 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,24 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
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 | null>;
|
||||
};
|
||||
export const TimelineDragHelper = observer(function TimelineDragHelper(props: Props) {
|
||||
const { ganttContainerRef } = props;
|
||||
const { isDragging } = useTimeLineChartStore();
|
||||
|
||||
useAutoScroller(ganttContainerRef, isDragging, SIDEBAR_WIDTH, HEADER_HEIGHT);
|
||||
return <></>;
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./month";
|
||||
export * from "./quarter";
|
||||
export * from "./week";
|
||||
110
apps/web/core/components/gantt-chart/chart/views/month.tsx
Normal file
110
apps/web/core/components/gantt-chart/chart/views/month.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
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 = observer(function MonthChartView(_props: any) {
|
||||
// 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 flex h-max min-h-full w-max">
|
||||
{currentViewData && (
|
||||
<div className="relative flex flex-col outline-[0.25px] outline-subtle-1">
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1"
|
||||
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-subtle-1"
|
||||
style={{ width: `${monthBlock.days * currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
<div
|
||||
className="sticky z-[1] m-1 flex items-center bg-surface-1 px-3 py-1 text-14 font-regular whitespace-nowrap text-secondary capitalize"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{monthBlock?.title}
|
||||
{monthBlock.today && (
|
||||
<span className={cn("ml-2 rounded-sm bg-accent-primary px-1 text-9 font-medium text-on-color")}>
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/** Weeks Sub title */}
|
||||
<div className="flex h-5 w-full">
|
||||
{weeks?.map((weekBlock) => (
|
||||
<div
|
||||
key={`sub-title-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 justify-between px-2 py-1 text-center capitalize outline-[0.25px] outline-subtle-1",
|
||||
{
|
||||
"bg-accent-primary/20": weekBlock.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-11 font-medium text-placeholder">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded-sm bg-accent-primary px-1 text-on-color": weekBlock.today,
|
||||
})}
|
||||
>
|
||||
{weekBlock.startDate.getDate()}-{weekBlock.endDate.getDate()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-x-1 text-11 font-medium">{weekBlock.weekData.shortTitle}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/** Week Columns */}
|
||||
<div className="flex h-full w-full flex-grow">
|
||||
{weeks?.map((weekBlock) => (
|
||||
<div
|
||||
key={`column-${weekBlock.startDate.toString()}-${weekBlock.endDate.toString()}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
||||
"bg-accent-primary/20": weekBlock.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
99
apps/web/core/components/gantt-chart/chart/views/quarter.tsx
Normal file
99
apps/web/core/components/gantt-chart/chart/views/quarter.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
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 = observer(function QuarterChartView(_props: any) {
|
||||
const { currentViewData, renderView } = useTimeLineChartStore();
|
||||
const monthBlocks: IMonthBlock[] = renderView;
|
||||
|
||||
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 flex h-max min-h-full w-max`}>
|
||||
{currentViewData &&
|
||||
quarterBlocks?.map((quarterBlock, rootIndex) => (
|
||||
<div
|
||||
key={`month-${quarterBlock.quarterNumber}-${quarterBlock.year}`}
|
||||
className="relative flex flex-col outline-[0.25px] outline-subtle-1"
|
||||
>
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1 outline-[1px] outline-subtle-1"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{/** Main Quarter Title */}
|
||||
<div className="inline-flex h-7 w-full justify-between">
|
||||
<div
|
||||
className="sticky z-[1] my-1 flex items-center bg-surface-1 px-3 py-1 text-14 font-regular whitespace-nowrap text-secondary capitalize"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{quarterBlock?.title}
|
||||
{quarterBlock.today && (
|
||||
<span className={cn("ml-2 rounded-sm bg-accent-primary px-1 text-9 font-medium text-on-color")}>
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize">
|
||||
{quarterBlock.shortTitle}
|
||||
</div>
|
||||
</div>
|
||||
{/** Months Sub title */}
|
||||
<div className="flex h-5 w-full">
|
||||
{quarterBlock?.children?.map((monthBlock, index) => (
|
||||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 justify-center text-center capitalize outline-[0.25px] outline-subtle-1",
|
||||
{
|
||||
"bg-accent-primary/20": monthBlock.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||
>
|
||||
<div className="flex h-full items-center justify-center space-x-1 text-11 font-medium">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded-lg bg-accent-primary px-2 text-on-color": monthBlock.today,
|
||||
})}
|
||||
>
|
||||
{monthBlock.monthData.shortTitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/** Month Columns */}
|
||||
<div className="flex h-full w-full flex-grow">
|
||||
{quarterBlock?.children?.map((monthBlock, index) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
||||
"bg-accent-primary/20": monthBlock.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
96
apps/web/core/components/gantt-chart/chart/views/week.tsx
Normal file
96
apps/web/core/components/gantt-chart/chart/views/week.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
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 = observer(function WeekChartView(_props: any) {
|
||||
const { currentViewData, renderView } = useTimeLineChartStore();
|
||||
const weekBlocks: IWeekBlock[] = renderView;
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 flex h-max min-h-full w-max`}>
|
||||
{currentViewData &&
|
||||
weekBlocks?.map((block, rootIndex) => (
|
||||
<div
|
||||
key={`month-${block?.startDate.toString()}-${block?.endDate.toString()}`}
|
||||
className="relative flex flex-col outline-[0.25px] outline-subtle-1"
|
||||
>
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="sticky top-0 z-[5] w-full flex-shrink-0 bg-surface-1 outline-[1px] outline-subtle-1"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{/** Main Months Title */}
|
||||
<div className="inline-flex h-7 w-full justify-between">
|
||||
<div
|
||||
className="sticky z-[1] m-1 flex items-center bg-surface-1 px-3 py-1 text-13 font-regular whitespace-nowrap text-secondary capitalize"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{block?.title}
|
||||
</div>
|
||||
<div className="sticky px-3 py-2 text-11 whitespace-nowrap text-placeholder capitalize">
|
||||
{block?.weekData?.title}
|
||||
</div>
|
||||
</div>
|
||||
{/** Days Sub title */}
|
||||
<div className="flex h-5 w-full">
|
||||
{block?.children?.map((weekDay, index) => (
|
||||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 justify-between p-1 text-center capitalize outline-[0.25px] outline-subtle-1",
|
||||
{
|
||||
"bg-accent-primary/20": weekDay.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-11 font-medium text-placeholder">{weekDay.dayData.abbreviation}</div>
|
||||
<div className="space-x-1 text-11 font-medium">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded-sm bg-accent-primary px-1 text-on-color": weekDay.today,
|
||||
})}
|
||||
>
|
||||
{weekDay.date.getDate()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/** Day Columns */}
|
||||
<div className="flex h-full w-full flex-grow bg-surface-1">
|
||||
{block?.children?.map((weekDay, index) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline-subtle", {
|
||||
"bg-accent-primary/20": weekDay.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
{["sat", "sun"].includes(weekDay?.dayData?.shortTitle) && (
|
||||
<div className="h-full bg-surface-2 outline-[0.25px] outline-strong" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user