From 23d640d083162cf8073557302c8bb88dd71444d1 Mon Sep 17 00:00:00 2001 From: Eugenio Topa Date: Sat, 25 Jan 2025 16:40:16 +0100 Subject: [PATCH 1/3] feat: Gantt connections with bar groups --- docs/guide/chart-configuration.md | 2 -- src/components/GGanttRow.vue | 10 ++++++++-- src/composables/useRows.ts | 9 ++++++--- src/types/chart.ts | 3 ++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/guide/chart-configuration.md b/docs/guide/chart-configuration.md index a1ed744..715475a 100644 --- a/docs/guide/chart-configuration.md +++ b/docs/guide/chart-configuration.md @@ -14,7 +14,6 @@ The GGanttChart component accepts several key configuration properties that defi :bar-start="'start'" :bar-end="'end'" :row-height="40" - :width="'100%'" :color-scheme="'default'" :grid="true" :push-on-overlap="true" @@ -29,7 +28,6 @@ The GGanttChart component accepts several key configuration properties that defi - `precision`: Sets the time unit ('hour', 'day', 'week', 'month') - `bar-start` and `bar-end`: Specify data properties for dates - `row-height`: Controls row height in pixels -- `width`: Chart width (percentage or pixels) ### Advanced Configuration diff --git a/src/components/GGanttRow.vue b/src/components/GGanttRow.vue index 15bbe52..89c2a94 100644 --- a/src/components/GGanttRow.vue +++ b/src/components/GGanttRow.vue @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome" import { faChevronRight, faChevronDown } from "@fortawesome/free-solid-svg-icons" import useTimePositionMapping from "../composables/useTimePositionMapping" import provideConfig from "../provider/provideConfig" -import type { GanttBarObject } from "../types" +import type { GanttBarConnection, GanttBarObject } from "../types" import GGanttBar from "./GGanttBar.vue" import { BAR_CONTAINER_KEY } from "../provider/symbols" import type { UseRowsReturn } from "../composables/useRows" @@ -20,7 +20,13 @@ const props = defineProps<{ bars: GanttBarObject[] highlightOnHover?: boolean id?: string | number - children?: { id: string | number; label: string; bars: GanttBarObject[] }[] + children?: { + id: string | number + label: string + bars: GanttBarObject[] + connections?: GanttBarConnection[] + }[] + connections?: GanttBarConnection[] }>() const emit = defineEmits<{ diff --git a/src/composables/useRows.ts b/src/composables/useRows.ts index 3b72fcc..9a9d6e3 100644 --- a/src/composables/useRows.ts +++ b/src/composables/useRows.ts @@ -83,24 +83,26 @@ export function useRows( defaultSlot.forEach((child) => { if (child.props?.bars || child.props?.children) { - const { label, bars = [], children = [], id } = child.props + const { label, bars = [], children = [], id, connections = [] } = child.props rows.push({ id, label, bars, children, + connections, _originalNode: child }) } else if (Array.isArray(child.children)) { child.children.forEach((grandchild) => { const grandchildNode = grandchild as { props?: ChartRow } if (grandchildNode?.props?.bars || grandchildNode?.props?.children) { - const { label, bars = [], children = [], id } = grandchildNode.props + const { label, bars = [], children = [], id, connections = [] } = grandchildNode.props rows.push({ id, label, bars, children, + connections, _originalNode: grandchildNode }) } @@ -173,7 +175,8 @@ export function useRows( label: row.label, style: { background: "transparent" - } + }, + connections: row.connections || [] } } ] diff --git a/src/types/chart.ts b/src/types/chart.ts index 9378dc7..1e3b46c 100644 --- a/src/types/chart.ts +++ b/src/types/chart.ts @@ -1,10 +1,11 @@ -import type { GanttBarObject } from "./bar" +import type { GanttBarConnection, GanttBarObject } from "./bar" export interface ChartRow { id?: string | number label: string bars: GanttBarObject[] children?: ChartRow[] + connections?: GanttBarConnection[] _originalNode?: any } From c939b9046e309f87adb0b07c7a6ec2223b42ef5e Mon Sep 17 00:00:00 2001 From: Eugenio Topa Date: Sat, 25 Jan 2025 17:15:05 +0100 Subject: [PATCH 2/3] fix: pushOnOverlap and pushOnConnect for bars in group --- src/composables/useBarMovement.ts | 40 +++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/composables/useBarMovement.ts b/src/composables/useBarMovement.ts index 037bca9..e28d983 100644 --- a/src/composables/useBarMovement.ts +++ b/src/composables/useBarMovement.ts @@ -75,6 +75,24 @@ export function useBarMovement( return endDate.isSameOrBefore(milestoneDate) } + /** + * Gets all bars from all rows including nested groups + * @returns Array of all bars in the chart + */ + const getAllBars = (): GanttBarObject[] => { + const extractBarsFromRow = (row: any): GanttBarObject[] => { + let bars: GanttBarObject[] = [...row.bars] + if (row.children?.length) { + row.children.forEach((child: any) => { + bars = [...bars, ...extractBarsFromRow(child)] + }) + } + return bars + } + + return rowManager.rows.value.flatMap((row) => extractBarsFromRow(row)) + } + /** * Moves a bar to new start and end positions * Handles validation and affected bar movement @@ -221,11 +239,23 @@ export function useBarMovement( * @returns Array of overlapping bars */ const findOverlappingBars = (bar: GanttBarObject): GanttBarObject[] => { - const currentRow = rowManager.rows.value.find((row) => row.bars.includes(bar)) - if (!currentRow) return [] + const findRowForBar = (searchBar: GanttBarObject, rows: any[]): any | null => { + for (const row of rows) { + if (row.bars.includes(searchBar)) return row + if (row.children?.length) { + const foundInChildren = findRowForBar(searchBar, row.children) + if (foundInChildren) return foundInChildren + } + } + return null + } + + const barRow = findRowForBar(bar, rowManager.rows.value) + if (!barRow) return [] - return currentRow.bars.filter((otherBar) => { + return barRow.bars.filter((otherBar: GanttBarObject) => { if (otherBar === bar || otherBar.ganttBarConfig.pushOnOverlap === false) return false + if (otherBar.ganttBarConfig.id.startsWith("group-")) return false const start1 = dayjsHelper.toDayjs(bar[barStart.value]) const end1 = dayjsHelper.toDayjs(bar[barEnd.value]) @@ -245,7 +275,7 @@ export function useBarMovement( * @returns Array of connected bars */ const findConnectedBars = (bar: GanttBarObject): GanttBarObject[] => { - const allBars = rowManager.rows.value.flatMap((row) => row.bars) + const allBars = getAllBars() const connectedBars: GanttBarObject[] = [] bar.ganttBarConfig.connections?.forEach((conn) => { @@ -266,7 +296,7 @@ export function useBarMovement( }) }) - return connectedBars + return connectedBars.filter((b) => !b.ganttBarConfig.id.startsWith("group-")) } return { From ba335fb87c539123a4a5938220ab4b064d7753df Mon Sep 17 00:00:00 2001 From: Eugenio Topa Date: Sat, 25 Jan 2025 17:35:56 +0100 Subject: [PATCH 3/3] fix: Bundle movement for bars in groups --- src/composables/useBarDragManagement.ts | 23 ++++++++++++++--- src/composables/useBarMovement.ts | 33 ++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/composables/useBarDragManagement.ts b/src/composables/useBarDragManagement.ts index e2fe7ec..ddc7fdd 100644 --- a/src/composables/useBarDragManagement.ts +++ b/src/composables/useBarDragManagement.ts @@ -38,12 +38,30 @@ const useBarDragManagement = () => { isDragging: false } + const getBundleBars = (bundle?: string) => { + const res: GanttBarObject[] = [] + if (bundle != null) { + const allBars = movement.getAllBars() + allBars.forEach((bar) => { + if (bar.ganttBarConfig.bundle === bundle) { + res.push(bar) + } + }) + } + return res + } + /** * Initializes drag operation for a single bar * @param bar - Bar to initialize drag for * @param e - Mouse event that triggered the drag */ const initDragOfBar = (bar: GanttBarObject, e: MouseEvent) => { + if (bar.ganttBarConfig.bundle) { + initDragOfBundle(bar, e) + return + } + const dragHandler = createDragHandler(bar) dragHandler.initiateDrag(e) addBarToMovedBars(bar) @@ -60,10 +78,7 @@ const useBarDragManagement = () => { const bundle = mainBar.ganttBarConfig.bundle if (!bundle) return - const bundleBars = rowManager.rows.value - .flatMap((row) => row.bars) - .filter((bar) => bar.ganttBarConfig.bundle === bundle) - + const bundleBars = getBundleBars(bundle) bundleBars.forEach((bar) => { const isMainBar = bar === mainBar const dragHandler = createDragHandler(bar, isMainBar) diff --git a/src/composables/useBarMovement.ts b/src/composables/useBarMovement.ts index e28d983..3fc46a5 100644 --- a/src/composables/useBarMovement.ts +++ b/src/composables/useBarMovement.ts @@ -18,6 +18,7 @@ export interface MovementAPI { moveBar: (bar: GanttBarObject, newStart: string, newEnd: string) => MovementResult findOverlappingBars: (bar: GanttBarObject) => GanttBarObject[] findConnectedBars: (bar: GanttBarObject) => GanttBarObject[] + getAllBars: () => GanttBarObject[] } /** @@ -125,6 +126,34 @@ export function useBarMovement( bar[barStart.value] = newStart bar[barEnd.value] = newEnd + if (bar.ganttBarConfig.bundle && initialMove) { + const bundleBars = getAllBars().filter( + (b) => b.ganttBarConfig.bundle === bar.ganttBarConfig.bundle && b !== bar + ) + + const timeDiff = dayjsHelper + .toDayjs(newStart) + .diff(dayjsHelper.toDayjs(originalStart), "minutes") + + for (const bundleBar of bundleBars) { + const bundleBarNewStart = formatDate( + dayjsHelper.toDayjs(bundleBar[barStart.value]).add(timeDiff, "minutes") + ) + const bundleBarNewEnd = formatDate( + dayjsHelper.toDayjs(bundleBar[barEnd.value]).add(timeDiff, "minutes") + ) + + const bundleResult = moveBar(bundleBar, bundleBarNewStart, bundleBarNewEnd, false) + if (!bundleResult.success) { + bar[barStart.value] = originalStart + bar[barEnd.value] = originalEnd + processedBars.delete(bar.ganttBarConfig.id) + return { success: false, affectedBars: new Set() } + } + bundleResult.affectedBars.forEach((b) => affectedBars.add(b)) + } + } + const result = handleBarInteractions(bar, affectedBars) if (!result.success) { @@ -138,6 +167,7 @@ export function useBarMovement( processedBars.clear() } + affectedBars.add(bar) return { success: true, affectedBars } } @@ -302,6 +332,7 @@ export function useBarMovement( return { moveBar, findOverlappingBars, - findConnectedBars + findConnectedBars, + getAllBars } }