feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,3 @@
export * from "./month";
export * from "./quarter";
export * from "./week";

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

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

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