Skip to content

Commit 3d3a95a

Browse files
committed
enh(gantt): finalize gantt view
Signed-off-by: grnd-alt <git@belakkaf.net>
1 parent 11b2c54 commit 3d3a95a

3 files changed

Lines changed: 139 additions & 91 deletions

File tree

src/components/Controls.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,14 @@
226226

227227
<NcActions :aria-label="t('deck', 'View Modes')"
228228
:name="t('deck', 'Toggle View Modes')">
229-
<NcActionButton :class="{ 'action--active': viewMode === 'kanban' }"
229+
<NcActionButton :model-value="viewMode === 'kanban'"
230230
@click="setViewMode('kanban')">
231231
<template #icon>
232232
<ViewColumnIcon :size="20" decorative />
233233
</template>
234234
{{ t('deck', 'Kanban view') }}
235235
</NcActionButton>
236-
<NcActionButton :class="{ 'action--active': viewMode === 'gantt' }"
236+
<NcActionButton :model-value="viewMode === 'gantt'"
237237
@click="setViewMode('gantt')">
238238
<template #icon>
239239
<ChartGanttIcon :size="20" decorative />

src/components/board/GanttView.vue

Lines changed: 136 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
-->
55
<template>
66
<div class="gantt-wrapper">
7-
<div class="gantt-toolbar">
7+
<div v-if="ganttTasks.length" class="gantt-toolbar">
88
<NcButton v-for="mode in viewModes"
99
:key="mode.value"
1010
:type="currentViewMode === mode.value ? 'primary' : 'secondary'"
@@ -20,7 +20,7 @@
2020
<ChartGanttIcon />
2121
</template>
2222
<template #name>
23-
{{ t('deck', 'No cards with dates') }}
23+
{{ t('deck', 'No cards yet') }}
2424
</template>
2525
<template #description>
2626
{{ t('deck', 'Set a start date and due date on your cards to see them on the Gantt chart') }}
@@ -47,8 +47,8 @@
4747
</div>
4848
</div>
4949

50-
<div v-if="stacksByBoard.length" class="gantt-legend">
51-
<span v-for="stack in stacksByBoard"
50+
<div v-if="stacks.length" class="gantt-legend">
51+
<span v-for="stack in stacks"
5252
:key="stack.id"
5353
class="gantt-legend__item">
5454
<span class="gantt-legend__dot" :style="{ backgroundColor: getStackColor(stack.id) }" />
@@ -59,7 +59,7 @@
5959
</template>
6060

6161
<script>
62-
import { mapState } from 'vuex'
62+
import { mapGetters } from 'vuex'
6363
import Gantt from 'frappe-gantt'
6464
import 'frappe-gantt/dist/frappe-gantt.css' // eslint-disable-line
6565
import { NcButton, NcEmptyContent } from '@nextcloud/vue'
@@ -73,11 +73,40 @@ const STACK_COLORS = [
7373
'#3f51b5', '#8bc34a', '#ff5722', '#009688',
7474
]
7575
76+
// Mirrors frappe-gantt date_utils.convert_scales() constants.
77+
// days_per_unit[scale] = how many days one unit of that scale is.
78+
const DAYS_PER_UNIT = {
79+
millisecond: 1 / 86400000,
80+
second: 1 / 86400,
81+
minute: 1 / 1440,
82+
hour: 1 / 24,
83+
day: 1,
84+
month: 30,
85+
year: 365,
86+
}
87+
7688
const GANTT_VIEW_MODES = [
89+
{
90+
name: 'Hour',
91+
padding: '7d',
92+
step: '1h',
93+
snap_at: '1h',
94+
date_format: 'YYYY-MM-DD HH:',
95+
lower_text: 'HH',
96+
upper_text: (d, ld, lang) =>
97+
!ld || d.getDate() !== ld.getDate()
98+
? Intl.DateTimeFormat(lang || 'en', { month: 'short', day: 'numeric' }).format(d)
99+
: '',
100+
thick_line(date) {
101+
return date.getDay() === 1
102+
},
103+
upper_text_frequency: 24,
104+
},
77105
{
78106
name: 'Day',
79107
padding: '14d',
80-
step: '1d',
108+
step: '12h',
109+
snap_at: '12h',
81110
column_width: 38,
82111
date_format: 'YYYY-MM-DD',
83112
lower_text(date, last, lang) {
@@ -161,73 +190,47 @@ export default {
161190
isDragging: false,
162191
showUndated: false,
163192
viewModes: [
193+
{ value: 'Hour', label: t('deck', 'Hours') },
164194
{ value: 'Day', label: t('deck', 'Days') },
165195
{ value: 'Week', label: t('deck', 'Weeks') },
166196
{ value: 'Month', label: t('deck', 'Months') },
167197
],
168198
}
169199
},
170200
computed: {
171-
...mapState({
172-
filter: state => state.filter,
173-
}),
174-
stacksByBoard() {
175-
return this.stacks || []
176-
},
177-
allCards() {
178-
const cards = []
179-
for (const stack of this.stacksByBoard) {
180-
const stackCards = this.$store.getters.cardsByStack(stack.id)
181-
cards.push(...stackCards)
182-
}
183-
return cards
184-
},
185-
ganttTasks() {
186-
return this.allCards
187-
.filter(card => card.duedate || card.startdate)
188-
.map(card => {
189-
let start = card.startdate ? new Date(card.startdate) : null
190-
let end = card.duedate ? new Date(card.duedate) : null
191-
192-
if (!start && end) {
193-
start = new Date(end)
194-
start.setDate(start.getDate() - 1)
195-
}
196-
if (start && !end) {
197-
end = new Date(start)
198-
end.setDate(end.getDate() + 1)
199-
}
200-
201-
// Ensure start <= end
202-
if (start > end) {
203-
const tmp = start
204-
start = end
205-
end = tmp
206-
}
207-
208-
const stackIdx = this.stacksByBoard.findIndex(s => s.id === card.stackId)
209-
210-
return {
211-
id: String(card.id),
212-
name: card.title,
213-
start,
214-
end,
215-
progress: card.done ? 100 : 0,
216-
custom_class: 'gantt-bar--color-' + (stackIdx % STACK_COLORS.length),
217-
_card: card,
201+
...mapGetters(['cardsByStack']),
202+
partitionedCards() {
203+
const undatedCards = []
204+
const ganttTasks = []
205+
this.stacks.forEach((stack, index) => {
206+
this.cardsByStack(stack.id).forEach((card) => {
207+
if (!card.duedate && !card.startdate) {
208+
undatedCards.push(card)
209+
} else {
210+
ganttTasks.push(this.cardToGanttTask(card, index))
218211
}
219212
})
213+
})
214+
return { undatedCards, ganttTasks }
215+
},
216+
ganttTasks() {
217+
return this.partitionedCards.ganttTasks
220218
},
221219
undatedCards() {
222-
return this.allCards.filter(card => !card.duedate && !card.startdate)
220+
return this.partitionedCards.undatedCards
223221
},
224222
},
225223
watch: {
226224
ganttTasks: {
227225
deep: true,
228-
handler(tasks) {
226+
handler(tasks, oldTasks) {
227+
if (oldTasks.length === 0 && tasks.length > 0) {
228+
this.$nextTick(() => this.renderGantt())
229+
return
230+
}
229231
if (!this.isDragging && this.ganttInstance) {
230-
this.ganttInstance.refresh(JSON.parse(JSON.stringify(tasks)))
232+
const cloned = tasks.map(t => ({ ...t, start: new Date(t.start), end: new Date(t.end) }))
233+
this.ganttInstance.refresh(cloned)
231234
}
232235
},
233236
},
@@ -254,12 +257,42 @@ export default {
254257
this.ganttInstance = null
255258
},
256259
methods: {
260+
cardToGanttTask(card, stackIndex) {
261+
let start = card.startdate ? new Date(card.startdate) : null
262+
let end = card.duedate ? new Date(card.duedate) : null
263+
264+
if (!start && end) {
265+
start = new Date(end)
266+
start.setHours(start.getHours() - 1)
267+
}
268+
if (start && !end) {
269+
end = new Date(start)
270+
end.setHours(end.getHours() + 1)
271+
}
272+
273+
// Ensure start <= end
274+
if (start > end) {
275+
const tmp = start
276+
start = end
277+
end = tmp
278+
}
279+
280+
return {
281+
id: String(card.id),
282+
name: card.title,
283+
start,
284+
end,
285+
progress: card.done ? 100 : 0,
286+
custom_class: 'gantt-bar--color-' + (stackIndex % STACK_COLORS.length),
287+
_card: card,
288+
}
289+
},
257290
getStackColor(stackId) {
258-
const idx = this.stacksByBoard.findIndex(s => s.id === stackId)
291+
const idx = this.stacks.findIndex(s => s.id === stackId)
259292
return STACK_COLORS[idx % STACK_COLORS.length] || STACK_COLORS[0]
260293
},
261294
getStackTitle(stackId) {
262-
const stack = this.stacksByBoard.find(s => s.id === stackId)
295+
const stack = this.stacks.find(s => s.id === stackId)
263296
return stack ? stack.title : ''
264297
},
265298
changeViewMode(mode) {
@@ -281,26 +314,59 @@ export default {
281314
282315
this.$refs.ganttContainer.innerHTML = ''
283316
284-
this.ganttInstance = new Gantt(this.$refs.ganttContainer, JSON.parse(JSON.stringify(this.ganttTasks)), {
285-
view_mode: this.currentViewMode,
317+
this.ganttInstance = new Gantt(this.$refs.ganttContainer, this.ganttTasks, {
286318
view_modes: GANTT_VIEW_MODES,
287319
bar_height: 28,
288320
lower_header_height: 40,
289321
padding: 20,
290322
scroll_to: 'today',
291323
today_button: true,
292324
infinite_padding: false,
325+
readonly_progress: true,
326+
popup: false,
293327
on_click: (task) => {
294-
this.openCard(task._card)
328+
if (!this.isDragging) {
329+
this.openCard(task._card)
330+
}
295331
},
296332
on_date_change: (task, start, end) => {
297333
this.isDragging = true
298334
this._pendingChange = { task, start, end }
299335
},
300336
})
301337
338+
this._patchBarDuration()
339+
this.ganttInstance.change_view_mode(this.currentViewMode)
302340
this.fitColumnsToWidth()
303341
},
342+
_patchBarDuration() {
343+
const bars = this.ganttInstance?.bars
344+
if (!bars?.length) return
345+
const BarProto = Object.getPrototypeOf(bars[0])
346+
if (BarProto._deckDurationPatched) return
347+
BarProto._deckDurationPatched = true
348+
349+
// we overwrite the compute_duration function because it enforces a minimum of 1 day duration
350+
// for reference: https://github.com/frappe/gantt/issues/534
351+
BarProto.compute_duration = function() {
352+
const ms = this.task._end - this.task._start
353+
const unit = this.gantt.config.unit
354+
const step = this.gantt.config.step
355+
356+
// In Hour + Day view use full millisecond precision so sub-day tasks
357+
// render at their true duration. In all other views enforce a
358+
// minimum of 1 day so short tasks remain visible.
359+
const durationInDays = this.gantt.config.view_mode.name === 'Hour' || this.gantt.config.view_mode.name === 'Day'
360+
? ms / 86400000
361+
: Math.max(1, ms / 86400000)
362+
363+
this.task.actual_duration = Math.ceil(durationInDays)
364+
this.task.ignored_duration = 0
365+
this.duration = (durationInDays / DAYS_PER_UNIT[unit]) / step
366+
this.actual_duration_raw = this.duration
367+
this.ignored_duration_raw = 0
368+
}
369+
},
304370
fitColumnsToWidth() {
305371
const gantt = this.ganttInstance
306372
if (!gantt?.dates?.length) return
@@ -315,7 +381,6 @@ export default {
315381
async onDateChange(task, start, end) {
316382
const card = task._card
317383
if (!card) return
318-
319384
try {
320385
await this.$store.dispatch('updateCardStartDate', {
321386
...card,
@@ -352,6 +417,7 @@ export default {
352417
353418
.gantt-chart {
354419
flex: 1 1 0;
420+
width: 100%;
355421
min-height: 0;
356422
overflow-x: auto;
357423
overflow-y: hidden;
@@ -421,8 +487,11 @@ export default {
421487
</style>
422488
423489
<style lang="scss">
490+
@use 'sass:list';
491+
424492
/* Map frappe-gantt CSS variables to Nextcloud theme variables */
425493
.gantt-chart .gantt-container {
494+
max-height: 100%;
426495
/* Grid and background */
427496
--g-row-color: var(--color-main-background);
428497
--g-header-background: var(--color-background-dark);
@@ -449,11 +518,6 @@ export default {
449518
--g-progress-color: var(--color-primary-element);
450519
--g-handle-color: var(--color-primary-element);
451520
452-
/* Popup */
453-
--g-popup-actions: var(--color-main-background);
454-
455-
/* Prevent vertical scrollbar */
456-
overflow-y: hidden !important;
457521
}
458522
459523
/* Move the Today button up into the upper header area */
@@ -466,22 +530,6 @@ export default {
466530
}
467531
}
468532
469-
/* Popup theming */
470-
.gantt-chart .gantt-container .popup-wrapper {
471-
background: var(--color-main-background);
472-
border: 1px solid var(--color-border);
473-
474-
.pointer {
475-
border-color: var(--color-main-background) transparent transparent transparent;
476-
}
477-
.title-container .title {
478-
color: var(--color-main-text);
479-
}
480-
.title-container .subtitle {
481-
color: var(--color-text-maxcontrast);
482-
}
483-
}
484-
485533
/* Bar handle visibility */
486534
.gantt-chart .gantt {
487535
.handle {
@@ -492,12 +540,12 @@ export default {
492540
}
493541
494542
// Stack color classes — bar fill and progress colors
495-
@each $color in ('#0082c9', '#4caf50', '#ff9800', '#e91e63',
543+
$stack-colors: '#0082c9', '#4caf50', '#ff9800', '#e91e63',
496544
'#9c27b0', '#00bcd4', '#795548', '#607d8b',
497-
'#3f51b5', '#8bc34a', '#ff5722', '#009688') {
498-
$i: index(('#0082c9', '#4caf50', '#ff9800', '#e91e63',
499-
'#9c27b0', '#00bcd4', '#795548', '#607d8b',
500-
'#3f51b5', '#8bc34a', '#ff5722', '#009688'), $color) - 1;
545+
'#3f51b5', '#8bc34a', '#ff5722', '#009688';
546+
547+
@each $color in $stack-colors {
548+
$i: list.index($stack-colors, $color) - 1;
501549
502550
.bar-wrapper.gantt-bar--color-#{$i} {
503551
.bar {

src/components/board/SharingTabSidebar.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export default {
140140
displayName: item.displayname || item.name || item.label || item.id,
141141
user: item.id,
142142
subname: item.shareWithDisplayNameUnique || item.subline || item.id, // NcSelectUser does its own pattern matching to filter things out
143-
type: SOURCE_TO_SHARE_TYPE[item.source]
143+
type: SOURCE_TO_SHARE_TYPE[item.source],
144144
}
145145
return res
146146
}).slice(0, 10)

0 commit comments

Comments
 (0)