From cbfdcd563860edc595e42b4d34a1711764e07009 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:33:24 +0530 Subject: [PATCH] [WEB-5390] refactor: gantt layout support in base layouts (#8089) --- apps/web/ce/hooks/use-timeline-chart.ts | 26 +++ .../core/components/base-layouts/constants.ts | 7 +- .../components/base-layouts/gantt/index.ts | 2 + .../components/base-layouts/gantt/layout.tsx | 140 +++++++++++++++++ .../components/base-layouts/gantt/sidebar.tsx | 148 ++++++++++++++++++ .../gantt-chart/chart/main-content.tsx | 1 + .../components/gantt-chart/contexts/index.tsx | 11 +- .../gantt-chart/sidebar/issues/sidebar.tsx | 4 +- .../gantt-chart/sidebar/modules/sidebar.tsx | 5 +- .../components/gantt-chart/sidebar/root.tsx | 3 + .../issue-layouts/gantt/base-gantt-root.tsx | 8 +- .../gantt-chart/modules-list-layout.tsx | 5 +- apps/web/core/hooks/use-timeline-chart.ts | 27 ++-- .../layouts/auth-layout/project-wrapper.tsx | 5 +- apps/web/ee/hooks/use-timeline-chart.ts | 3 + packages/types/src/base-layouts/base.ts | 2 +- packages/types/src/base-layouts/gantt/core.ts | 6 + .../types/src/base-layouts/gantt/extended.ts | 1 + .../types/src/base-layouts/gantt/index.ts | 78 +++++++++ packages/types/src/base-layouts/index.ts | 1 + 20 files changed, 442 insertions(+), 41 deletions(-) create mode 100644 apps/web/ce/hooks/use-timeline-chart.ts create mode 100644 apps/web/core/components/base-layouts/gantt/index.ts create mode 100644 apps/web/core/components/base-layouts/gantt/layout.tsx create mode 100644 apps/web/core/components/base-layouts/gantt/sidebar.tsx create mode 100644 apps/web/ee/hooks/use-timeline-chart.ts create mode 100644 packages/types/src/base-layouts/gantt/core.ts create mode 100644 packages/types/src/base-layouts/gantt/extended.ts create mode 100644 packages/types/src/base-layouts/gantt/index.ts diff --git a/apps/web/ce/hooks/use-timeline-chart.ts b/apps/web/ce/hooks/use-timeline-chart.ts new file mode 100644 index 000000000..afd329d76 --- /dev/null +++ b/apps/web/ce/hooks/use-timeline-chart.ts @@ -0,0 +1,26 @@ +// types +import type { TTimelineTypeCore } from "@plane/types"; +import { GANTT_TIMELINE_TYPE } from "@plane/types"; +// Plane-web + +import type { IBaseTimelineStore } from "@/plane-web/store/timeline/base-timeline.store"; +import type { ITimelineStore } from "../store/timeline"; + +export const getTimelineStore = ( + timelineStore: ITimelineStore, + timelineType: TTimelineTypeCore +): IBaseTimelineStore => { + if (timelineType === GANTT_TIMELINE_TYPE.ISSUE) { + return timelineStore.issuesTimeLineStore as IBaseTimelineStore; + } + if (timelineType === GANTT_TIMELINE_TYPE.MODULE) { + return timelineStore.modulesTimeLineStore as IBaseTimelineStore; + } + if (timelineType === GANTT_TIMELINE_TYPE.PROJECT) { + return timelineStore.projectTimeLineStore as IBaseTimelineStore; + } + if (timelineType === GANTT_TIMELINE_TYPE.GROUPED) { + return timelineStore.groupedTimeLineStore as IBaseTimelineStore; + } + throw new Error(`Unknown timeline type: ${timelineType}`); +}; diff --git a/apps/web/core/components/base-layouts/constants.ts b/apps/web/core/components/base-layouts/constants.ts index 60e45a372..943d23c7f 100644 --- a/apps/web/core/components/base-layouts/constants.ts +++ b/apps/web/core/components/base-layouts/constants.ts @@ -1,4 +1,4 @@ -import { BoardLayoutIcon, ListLayoutIcon } from "@plane/propel/icons"; +import { BoardLayoutIcon, ListLayoutIcon, TimelineLayoutIcon } from "@plane/propel/icons"; import type { IBaseLayoutConfig } from "@plane/types"; export const BASE_LAYOUTS: IBaseLayoutConfig[] = [ @@ -12,4 +12,9 @@ export const BASE_LAYOUTS: IBaseLayoutConfig[] = [ icon: BoardLayoutIcon, label: "Board Layout", }, + { + key: "gantt", + icon: TimelineLayoutIcon, + label: "Gantt Layout", + }, ]; diff --git a/apps/web/core/components/base-layouts/gantt/index.ts b/apps/web/core/components/base-layouts/gantt/index.ts new file mode 100644 index 000000000..9f4e2dafb --- /dev/null +++ b/apps/web/core/components/base-layouts/gantt/index.ts @@ -0,0 +1,2 @@ +export { BaseGanttLayout } from "./layout"; +export { BaseGanttSidebar } from "./sidebar"; diff --git a/apps/web/core/components/base-layouts/gantt/layout.tsx b/apps/web/core/components/base-layouts/gantt/layout.tsx new file mode 100644 index 000000000..0bc91fb9c --- /dev/null +++ b/apps/web/core/components/base-layouts/gantt/layout.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useCallback, useMemo } from "react"; +import { observer } from "mobx-react"; +import { GANTT_TIMELINE_TYPE } from "@plane/types"; +import type { + IBaseLayoutsGanttItem, + IBaseLayoutsGanttProps, + TGanttBlockUpdateData, + IBlockUpdateDependencyData, +} from "@plane/types"; +import { cn } from "@plane/utils"; +import { TimeLineTypeContext } from "@/components/gantt-chart/contexts"; +import { GanttChartRoot } from "@/components/gantt-chart/root"; +import { BaseGanttSidebar } from "./sidebar"; + +export const BaseGanttLayout = observer((props: IBaseLayoutsGanttProps) => { + const { + items, + groupedItemIds, + groups, + renderBlock, + renderSidebar, + onBlockUpdate, + onDateUpdate, + enableBlockLeftResize = false, + enableBlockRightResize = false, + enableBlockMove = false, + enableReorder = false, + enableAddBlock = false, + enableSelection = false, + enableDependency = false, + showAllBlocks = false, + showToday = true, + border = false, + title = "Items", + loaderTitle = "items", + quickAdd, + loadMoreItems, + isLoading: _isLoading, + className, + timelineType: timelineTypeKey = GANTT_TIMELINE_TYPE.ISSUE, + } = props; + + // Flatten all grouped item IDs into a single array for gantt + // Gantt doesn't typically show groups, it shows all items on a timeline + const blockIds = useMemo(() => { + const allIds: string[] = []; + groups.forEach((group) => { + const itemIds = groupedItemIds[group.id] || []; + allIds.push(...itemIds); + }); + return allIds; + }, [groups, groupedItemIds]); + + // Block update handler - transforms base layout item updates to gantt block updates + const handleBlockUpdate = useCallback( + (block: T, payload: TGanttBlockUpdateData) => { + if (onBlockUpdate) { + onBlockUpdate(block, payload); + } + }, + [onBlockUpdate] + ); + + // Block renderer - wraps the user's render function + const blockToRender = useCallback((item: T) => renderBlock(item), [renderBlock]); + + // Sidebar renderer - uses custom or default + const sidebarToRender = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (sidebarProps: any) => { + if (renderSidebar) { + // If custom sidebar renderer provided, use it + return ( + + ); + } + // Otherwise use default sidebar + return ( + + ); + }, + [renderSidebar, renderBlock, items, loadMoreItems] + ); + + const timelineType = GANTT_TIMELINE_TYPE[timelineTypeKey]; + + // Date update handler - transforms IBlockUpdateDependencyData to TGanttDateUpdate + const handleDateUpdate = useCallback( + async (updates: IBlockUpdateDependencyData[]) => { + if (onDateUpdate) { + // Transform IBlockUpdateDependencyData[] to TGanttDateUpdate[] + const transformedUpdates = updates.map((update) => ({ + id: update.id, + start_date: update.start_date, + target_date: update.target_date, + })); + await onDateUpdate(transformedUpdates); + } + }, + [onDateUpdate] + ); + + // Load more handler - wraps loadMoreItems to match expected signature + const handleLoadMore = useCallback(() => { + if (loadMoreItems) { + loadMoreItems(""); // Pass empty string as default group ID + } + }, [loadMoreItems]); + + return ( + +
+ +
+
+ ); +}); diff --git a/apps/web/core/components/base-layouts/gantt/sidebar.tsx b/apps/web/core/components/base-layouts/gantt/sidebar.tsx new file mode 100644 index 000000000..f26f59a41 --- /dev/null +++ b/apps/web/core/components/base-layouts/gantt/sidebar.tsx @@ -0,0 +1,148 @@ +"use client"; + +import type { RefObject } from "react"; +import { useState } from "react"; +import { observer } from "mobx-react"; +import type { IBaseLayoutsBaseItem, IBlockUpdateData } from "@plane/types"; +import { Loader, Row } from "@plane/ui"; +import { cn } from "@plane/utils"; +import RenderIfVisible from "@/components/core/render-if-visible-HOC"; +import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants"; +import { GanttDnDHOC } from "@/components/gantt-chart/sidebar/gantt-dnd-HOC"; +import { handleOrderChange } from "@/components/gantt-chart/sidebar/utils"; +import { GanttLayoutListItemLoader } from "@/components/ui/loader/layouts/gantt-layout-loader"; +import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; +import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; + +type Props = { + blockUpdateHandler: (block: T, payload: IBlockUpdateData) => void; + canLoadMoreBlocks?: boolean; + loadMoreItems?: (groupId: string) => void; + ganttContainerRef: RefObject; + blockIds: string[]; + enableReorder: boolean; + showAllBlocks?: boolean; + items: Record; + renderItem: (item: T) => React.ReactNode; +}; + +export const BaseGanttSidebar = observer((props: Props) => { + const { + blockUpdateHandler, + blockIds, + enableReorder, + loadMoreItems, + canLoadMoreBlocks, + ganttContainerRef, + showAllBlocks = false, + items, + renderItem, + } = props; + + const { getBlockById, updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore(); + + const [intersectionElement, setIntersectionElement] = useState(null); + + const isPaginating = false; // TODO: Add proper pagination state + + useIntersectionObserver( + ganttContainerRef, + isPaginating ? null : intersectionElement, + loadMoreItems ? () => loadMoreItems("") : undefined, + "100% 0% 100% 0%" + ); + + const handleOnDrop = ( + draggingBlockId: string | undefined, + droppedBlockId: string | undefined, + dropAtEndOfList: boolean + ) => { + handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blockIds, getBlockById, blockUpdateHandler); + }; + + return ( +
+ {blockIds ? ( + <> + {blockIds.map((blockId, index) => { + const block = getBlockById(blockId); + const item = items[blockId]; + const isBlockVisibleOnSidebar = block?.start_date && block?.target_date; + + // hide the block if it doesn't have start and target dates and showAllBlocks is false + if (!block || (!showAllBlocks && !isBlockVisibleOnSidebar)) return null; + + if (!item) return null; + + return ( + } + > + + {(isDragging: boolean) => { + const block = getBlockById(blockId); + const isBlockComplete = !!block?.start_date && !!block?.target_date; + const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined; + const isBlockHoveredOn = isBlockActive(blockId); + + return ( +
updateActiveBlockId(blockId)} + onMouseLeave={() => updateActiveBlockId(null)} + > + +
+
{renderItem(item)}
+ {duration && ( +
+ + {duration} day{duration > 1 ? "s" : ""} + +
+ )} +
+
+
+ ); + }} +
+
+ ); + })} + {canLoadMoreBlocks && ( +
+
+
+ )} + + ) : ( + + + + + + + )} +
+ ); +}); diff --git a/apps/web/core/components/gantt-chart/chart/main-content.tsx b/apps/web/core/components/gantt-chart/chart/main-content.tsx index 3e1ebafba..e95e4b0eb 100644 --- a/apps/web/core/components/gantt-chart/chart/main-content.tsx +++ b/apps/web/core/components/gantt-chart/chart/main-content.tsx @@ -187,6 +187,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { title={title} quickAdd={quickAdd} selectionHelpers={helpers} + showAllBlocks={showAllBlocks} isEpic={isEpic} />
diff --git a/apps/web/core/components/gantt-chart/contexts/index.tsx b/apps/web/core/components/gantt-chart/contexts/index.tsx index 6d268be7e..894a06d75 100644 --- a/apps/web/core/components/gantt-chart/contexts/index.tsx +++ b/apps/web/core/components/gantt-chart/contexts/index.tsx @@ -1,16 +1,9 @@ import { createContext, useContext } from "react"; +import type { TTimelineType } from "@plane/types"; -export enum ETimeLineTypeType { - ISSUE = "ISSUE", - MODULE = "MODULE", - PROJECT = "PROJECT", - GROUPED = "GROUPED", -} - -export const TimeLineTypeContext = createContext(undefined); +export const TimeLineTypeContext = createContext(undefined); export const useTimeLineType = () => { const timelineType = useContext(TimeLineTypeContext); - return timelineType; }; diff --git a/apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx b/apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx index a08651e4a..ca5686ffd 100644 --- a/apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -4,6 +4,7 @@ import type { RefObject } from "react"; import { useState } from "react"; import { observer } from "mobx-react"; // ui +import { GANTT_TIMELINE_TYPE } from "@plane/types"; import type { IBlockUpdateData } from "@plane/types"; import { Loader } from "@plane/ui"; // components @@ -15,7 +16,6 @@ import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import type { TSelectionHelper } from "@/hooks/use-multiple-select"; // local imports import { useTimeLineChart } from "../../../../hooks/use-timeline-chart"; -import { ETimeLineTypeType } from "../../contexts"; import { GanttDnDHOC } from "../gantt-dnd-HOC"; import { handleOrderChange } from "../utils"; import { IssuesSidebarBlock } from "./block"; @@ -47,7 +47,7 @@ export const IssueGanttSidebar: React.FC = observer((props) => { isEpic = false, } = props; - const { getBlockById } = useTimeLineChart(ETimeLineTypeType.ISSUE); + const { getBlockById } = useTimeLineChart(GANTT_TIMELINE_TYPE.ISSUE); const { issues: { getIssueLoader }, diff --git a/apps/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx b/apps/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx index 9c3715a76..e3adf776b 100644 --- a/apps/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx +++ b/apps/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx @@ -2,13 +2,12 @@ import { observer } from "mobx-react"; // ui +import { GANTT_TIMELINE_TYPE } from "@plane/types"; import type { IBlockUpdateData } from "@plane/types"; import { Loader } from "@plane/ui"; // components // hooks import { useTimeLineChart } from "@/hooks/use-timeline-chart"; -// -import { ETimeLineTypeType } from "../../contexts"; import { GanttDnDHOC } from "../gantt-dnd-HOC"; import { handleOrderChange } from "../utils"; import { ModulesSidebarBlock } from "./block"; @@ -24,7 +23,7 @@ type Props = { export const ModuleGanttSidebar: React.FC = observer((props) => { const { blockUpdateHandler, blockIds, enableReorder } = props; - const { getBlockById } = useTimeLineChart(ETimeLineTypeType.MODULE); + const { getBlockById } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE); const handleOnDrop = ( draggingBlockId: string | undefined, diff --git a/apps/web/core/components/gantt-chart/sidebar/root.tsx b/apps/web/core/components/gantt-chart/sidebar/root.tsx index acd777832..15af6287d 100644 --- a/apps/web/core/components/gantt-chart/sidebar/root.tsx +++ b/apps/web/core/components/gantt-chart/sidebar/root.tsx @@ -24,6 +24,7 @@ type Props = { title: string; quickAdd?: React.ReactNode | undefined; selectionHelpers: TSelectionHelper; + showAllBlocks?: boolean; isEpic?: boolean; }; @@ -41,6 +42,7 @@ export const GanttChartSidebar: React.FC = observer((props) => { title, quickAdd, selectionHelpers, + showAllBlocks = false, isEpic = false, } = props; @@ -94,6 +96,7 @@ export const GanttChartSidebar: React.FC = observer((props) => { ganttContainerRef, loadMoreBlocks, selectionHelpers, + showAllBlocks, isEpic, })} diff --git a/apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 2cd0ace94..58dda7d00 100644 --- a/apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -6,10 +6,10 @@ import { ALL_ISSUES, EUserPermissions, EUserPermissionsLevel } from "@plane/cons import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { EIssuesStoreType, IBlockUpdateData, TIssue } from "@plane/types"; -import { EIssueLayoutTypes } from "@plane/types"; +import { EIssueLayoutTypes, GANTT_TIMELINE_TYPE } from "@plane/types"; import { renderFormattedPayloadDate } from "@plane/utils"; // components -import { ETimeLineTypeType, TimeLineTypeContext } from "@/components/gantt-chart/contexts"; +import { TimeLineTypeContext } from "@/components/gantt-chart/contexts"; import { GanttChartRoot } from "@/components/gantt-chart/root"; import { IssueGanttSidebar } from "@/components/gantt-chart/sidebar/issues/sidebar"; // hooks @@ -47,7 +47,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan const storeType = useIssueStoreType() as GanttStoreType; const { issues, issuesFilter } = useIssues(storeType); const { fetchIssues, fetchNextIssues, updateIssue, quickAddIssue } = useIssuesActions(storeType); - const { initGantt } = useTimeLineChart(ETimeLineTypeType.ISSUE); + const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.ISSUE); // store hooks const { allowPermissions } = useUserPermissions(); @@ -120,7 +120,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan return ( - +
{ if (!filteredModuleIds) return null; return ( - + { +export const useTimeLineChart = (timelineType: TTimelineType): IBaseTimelineStore => { const context = useContext(StoreContext); - if (context === undefined) throw new Error("useTimeLineChart must be used within StoreProvider"); + if (!context) throw new Error("useTimeLineChart must be used within StoreProvider"); - switch (timeLineType) { - case ETimeLineTypeType.ISSUE: - return context.timelineStore.issuesTimeLineStore; - case ETimeLineTypeType.MODULE: - return context.timelineStore.modulesTimeLineStore as IBaseTimelineStore; - case ETimeLineTypeType.PROJECT: - return context.timelineStore.projectTimeLineStore as IBaseTimelineStore; - case ETimeLineTypeType.GROUPED: - return context.timelineStore.groupedTimeLineStore as IBaseTimelineStore; - } + return getTimelineStore(context.timelineStore, timelineType); }; -export const useTimeLineChartStore = () => { +export const useTimeLineChartStore = (): IBaseTimelineStore => { + const context = useContext(StoreContext); const timelineType = useTimeLineType(); + if (!context) throw new Error("useTimeLineChartStore must be used within StoreProvider"); if (!timelineType) throw new Error("useTimeLineChartStore must be used within TimeLineTypeContext"); - return useTimeLineChart(timelineType); + return getTimelineStore(context.timelineStore, timelineType); }; diff --git a/apps/web/core/layouts/auth-layout/project-wrapper.tsx b/apps/web/core/layouts/auth-layout/project-wrapper.tsx index f2371ffbd..e0a25c961 100644 --- a/apps/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/apps/web/core/layouts/auth-layout/project-wrapper.tsx @@ -8,11 +8,10 @@ import useSWR from "swr"; import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { EmptyStateDetailed } from "@plane/propel/empty-state"; -import { EProjectNetwork } from "@plane/types"; +import { EProjectNetwork, GANTT_TIMELINE_TYPE } from "@plane/types"; // components import { JoinProject } from "@/components/auth-screens/project/join-project"; import { LogoSpinner } from "@/components/common/logo-spinner"; -import { ETimeLineTypeType } from "@/components/gantt-chart/contexts"; import { PROJECT_DETAILS, PROJECT_ME_INFORMATION, @@ -57,7 +56,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { loader, getProjectById, fetchProjectDetails } = useProject(); const { fetchAllCycles } = useCycle(); const { fetchModulesSlim, fetchModules } = useModule(); - const { initGantt } = useTimeLineChart(ETimeLineTypeType.MODULE); + const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE); const { fetchViews } = useProjectView(); const { project: { fetchProjectMembers }, diff --git a/apps/web/ee/hooks/use-timeline-chart.ts b/apps/web/ee/hooks/use-timeline-chart.ts new file mode 100644 index 000000000..9e31d84e7 --- /dev/null +++ b/apps/web/ee/hooks/use-timeline-chart.ts @@ -0,0 +1,3 @@ +// For now, just re-export from CE +// In the future, you can extend the timeline store mapping here for EE-specific timeline types +export * from "ce/hooks/use-timeline-chart"; diff --git a/packages/types/src/base-layouts/base.ts b/packages/types/src/base-layouts/base.ts index 9848ca425..6bd7d441c 100644 --- a/packages/types/src/base-layouts/base.ts +++ b/packages/types/src/base-layouts/base.ts @@ -52,7 +52,7 @@ export interface IRenderProps extends IItemRende // Layout Configuration -export type TBaseLayoutType = "list" | "kanban"; +export type TBaseLayoutType = "list" | "kanban" | "gantt"; export interface IBaseLayoutConfig { key: TBaseLayoutType; diff --git a/packages/types/src/base-layouts/gantt/core.ts b/packages/types/src/base-layouts/gantt/core.ts new file mode 100644 index 000000000..5f27cb049 --- /dev/null +++ b/packages/types/src/base-layouts/gantt/core.ts @@ -0,0 +1,6 @@ +export const CORE_GANTT_TIMELINE_TYPE = { + ISSUE: "ISSUE", + MODULE: "MODULE", + PROJECT: "PROJECT", + GROUPED: "GROUPED", +} as const; diff --git a/packages/types/src/base-layouts/gantt/extended.ts b/packages/types/src/base-layouts/gantt/extended.ts new file mode 100644 index 000000000..daf690403 --- /dev/null +++ b/packages/types/src/base-layouts/gantt/extended.ts @@ -0,0 +1 @@ +export const EXTENDED_GANTT_TIMELINE_TYPE = {} as const; diff --git a/packages/types/src/base-layouts/gantt/index.ts b/packages/types/src/base-layouts/gantt/index.ts new file mode 100644 index 000000000..5787d9315 --- /dev/null +++ b/packages/types/src/base-layouts/gantt/index.ts @@ -0,0 +1,78 @@ +import type { ReactNode } from "react"; +import type { IBaseLayoutsBaseItem, IBaseLayoutsBaseProps } from "../base"; +import { CORE_GANTT_TIMELINE_TYPE } from "./core"; +import { EXTENDED_GANTT_TIMELINE_TYPE } from "./extended"; + +// Gantt-specific item with date fields +export interface IBaseLayoutsGanttItem extends IBaseLayoutsBaseItem { + start_date?: string | null; + target_date?: string | null; +} + +// Block update data (for drag/resize operations) +export type TGanttBlockUpdateData = { + start_date?: string; + target_date?: string; + sort_order?: { + destinationIndex: number; + newSortOrder: number; + }; +}; + +// Date update handler for bulk date changes (e.g., dependency updates) +export type TGanttDateUpdate = { + id: string; + start_date?: string; + target_date?: string; +}; + +// Render props specific to Gantt +export interface IGanttRenderProps { + renderBlock: (item: T) => ReactNode; + renderSidebar?: (item: T) => ReactNode; +} + +// Gantt-specific capabilities +export interface IGanttCapabilities { + enableBlockLeftResize?: boolean | ((itemId: string) => boolean); + enableBlockRightResize?: boolean | ((itemId: string) => boolean); + enableBlockMove?: boolean | ((itemId: string) => boolean); + enableReorder?: boolean | ((itemId: string) => boolean); + enableAddBlock?: boolean | ((itemId: string) => boolean); + enableSelection?: boolean | ((itemId: string) => boolean); + enableDependency?: boolean | ((itemId: string) => boolean); +} + +// Gantt display options +export type TGanttDisplayOptions = { + showAllBlocks?: boolean; // Show blocks even without dates + showToday?: boolean; // Highlight today on timeline + border?: boolean; + title?: string; + loaderTitle?: string; + quickAdd?: ReactNode; + timelineType?: TTimelineType; // Type of timeline to use for store +}; + +// Main Gantt Layout Props +export interface IBaseLayoutsGanttProps + extends Omit, "renderItem" | "enableDragDrop" | "onDrop" | "canDrag">, + IGanttRenderProps, + IGanttCapabilities, + TGanttDisplayOptions { + // Handler for block updates (position, dates, order) + onBlockUpdate?: (item: T, payload: TGanttBlockUpdateData) => void | Promise; + + // Handler for bulk date updates (dependencies, etc.) + onDateUpdate?: (updates: TGanttDateUpdate[]) => void | Promise; +} + +export const GANTT_TIMELINE_TYPE = { + ...CORE_GANTT_TIMELINE_TYPE, + ...EXTENDED_GANTT_TIMELINE_TYPE, +} as const; + +export type TTimelineTypeCore = (typeof CORE_GANTT_TIMELINE_TYPE)[keyof typeof CORE_GANTT_TIMELINE_TYPE]; +export type TTimelineType = + | TTimelineTypeCore + | (typeof EXTENDED_GANTT_TIMELINE_TYPE)[keyof typeof EXTENDED_GANTT_TIMELINE_TYPE]; diff --git a/packages/types/src/base-layouts/index.ts b/packages/types/src/base-layouts/index.ts index 2545d81e2..2fcb0abe7 100644 --- a/packages/types/src/base-layouts/index.ts +++ b/packages/types/src/base-layouts/index.ts @@ -1,3 +1,4 @@ export * from "./base"; export * from "./list"; export * from "./kanban"; +export * from "./gantt";