feat(gantt): wire relation arrows into GanttChart with toggle

This commit is contained in:
kolaente
2026-03-02 10:15:37 +01:00
parent cd42406850
commit 5731ce9c76
2 changed files with 69 additions and 4 deletions

View File

@@ -62,6 +62,13 @@
</div>
</GanttRow>
</div>
<GanttRelationArrows
v-if="showRelationArrows && relationArrows.length > 0"
:arrows="relationArrows"
:width="totalWidth"
:height="totalHeight"
:row-height="ROW_HEIGHT"
/>
</template>
</GanttChartBody>
</div>
@@ -76,6 +83,7 @@ import {useDayjsLanguageSync} from '@/i18n/useDayjsLanguageSync'
import {getHexColor} from '@/models/task'
import {buildGanttTaskTree, type GanttTaskTreeNode} from '@/helpers/ganttTaskTree'
import {buildRelationArrows, type GanttBarPosition, type GanttArrow} from '@/helpers/ganttRelationArrows'
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
import type {DateISO} from '@/types/DateISO'
@@ -87,18 +95,22 @@ import GanttRow from '@/components/gantt/GanttRow.vue'
import GanttRowBars from '@/components/gantt/GanttRowBars.vue'
import GanttVerticalGridLines from '@/components/gantt/GanttVerticalGridLines.vue'
import GanttTimelineHeader from '@/components/gantt/GanttTimelineHeader.vue'
import GanttRelationArrows from '@/components/gantt/GanttRelationArrows.vue'
import Loading from '@/components/misc/Loading.vue'
import {MILLISECONDS_A_DAY} from '@/constants/date'
import {roundToNaturalDayBoundary} from '@/helpers/time/roundToNaturalDayBoundary'
const props = defineProps<{
const props = withDefaults(defineProps<{
isLoading: boolean,
filters: GanttFilters,
tasks: Map<ITask['id'], ITask>,
defaultTaskStartDate: DateISO
defaultTaskEndDate: DateISO
}>()
showRelationArrows?: boolean
}>(), {
showRelationArrows: false,
})
const emit = defineEmits<{
(e: 'update:task', task: ITaskPartialWithId): void
@@ -180,8 +192,8 @@ const visibleNodes = computed(() => {
return result
})
// Used in Task 8 for arrow re-routing when children are collapsed
const _hiddenToAncestor = computed(() => {
// Map hidden tasks to their visible ancestor for arrow re-routing
const hiddenToAncestor = computed(() => {
const map = new Map<number, number>()
const hiddenParents = new Set<number>()
@@ -323,6 +335,50 @@ watch(
{deep: true, immediate: true},
)
// Compute bar positions for arrow rendering
const ROW_HEIGHT = 40
const barPositions = computed(() => {
const positions = new Map<number, GanttBarPosition>()
ganttBars.value.forEach((rowBars, rowIndex) => {
for (const bar of rowBars) {
const taskId = Number(bar.id)
const x = computeBarX(bar.start)
const width = computeBarWidth(bar)
const y = rowIndex * ROW_HEIGHT + ROW_HEIGHT / 2
positions.set(taskId, {x, y, width, rowIndex})
}
})
return positions
})
function computeBarX(date: Date): number {
const diff = Math.ceil(
(roundToNaturalDayBoundary(date, true).getTime() - dateFromDate.value.getTime()) /
MILLISECONDS_A_DAY,
)
return diff * DAY_WIDTH_PIXELS
}
function computeBarWidth(bar: GanttBarModel): number {
const diff = Math.ceil(
(roundToNaturalDayBoundary(bar.end).getTime() - roundToNaturalDayBoundary(bar.start, true).getTime()) /
MILLISECONDS_A_DAY,
)
return diff * DAY_WIDTH_PIXELS
}
// Compute relation arrows
const relationArrows = computed<GanttArrow[]>(() => {
if (!props.showRelationArrows) return []
return buildRelationArrows(tasks.value, barPositions.value, hiddenToAncestor.value)
})
const totalHeight = computed(() => ganttRows.value.length * ROW_HEIGHT)
function updateGanttTask(id: string, newStart: Date, newEnd: Date) {
const task = tasks.value.get(Number(id))
if (!task) return

View File

@@ -38,6 +38,12 @@
>
{{ $t('task.show.noDates') }}
</FancyCheckbox>
<FancyCheckbox
v-model="showRelationArrows"
is-block
>
{{ $t('project.gantt.toggleRelationArrows') }}
</FancyCheckbox>
</div>
</Card>
@@ -53,6 +59,7 @@
:is-loading="isLoading"
:default-task-start-date="defaultTaskStartDate"
:default-task-end-date="defaultTaskEndDate"
:show-relation-arrows="showRelationArrows"
@update:task="updateTask"
/>
<TaskForm
@@ -111,6 +118,8 @@ const {
updateTask,
} = useGanttFilters(route, viewId)
const showRelationArrows = ref(false)
const DEFAULT_DATE_RANGE_DAYS = 7
const today = new Date()