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' "
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') }}
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) }" />
5959</template >
6060
6161<script >
62- import { mapState } from ' vuex'
62+ import { mapGetters } from ' vuex'
6363import Gantt from ' frappe-gantt'
6464import ' frappe-gantt/dist/frappe-gantt.css' // eslint-disable-line
6565import { 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+
7688const 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 {
0 commit comments