feat: replace our home-grown gantt implementation with ganttastic #2180
|
@ -23,7 +23,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@infectoone/vue-ganttastic": "^2.0.4",
|
||||
"@infectoone/vue-ganttastic": "./vendor/infectoone-vue-ganttastic-2.1.1.tgz",
|
||||
"@kyvg/vue3-notification": "2.4.1",
|
||||
"@sentry/tracing": "7.17.0",
|
||||
"@sentry/vue": "7.17.0",
|
||||
|
|
3083
pnpm-lock.yaml
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<Loading class="gantt-container" v-if="taskService.loading || taskCollectionService.loading"/>
|
||||
konrad marked this conversation as resolved
Outdated
|
||||
<div class="gantt-container" v-else>
|
||||
<g-gantt-chart
|
||||
<GGanttChart
|
||||
:chart-start="`${dateFrom} 00:00`"
|
||||
:chart-end="`${dateTo} 23:59`"
|
||||
:precision="PRECISION"
|
||||
|
@ -10,7 +10,6 @@
|
|||
:grid="true"
|
||||
@dragend-bar="updateTask"
|
||||
@dblclick-bar="openTask"
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Is it possible to use here simply Is it possible to use here simply `inherit` as value?
konrad
commented
Seems to work, yes. Seems to work, yes.
dpschen
commented
Not necessary with lates release. Removed. Not necessary with lates release. Removed.
|
||||
font="inherit"
|
||||
:width="ganttChartWidth + 'px'"
|
||||
>
|
||||
<template #timeunit="{label, value}">
|
||||
|
@ -23,75 +22,45 @@
|
|||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<g-gantt-row
|
||||
<GGanttRow
|
||||
v-for="(bar, k) in ganttBars"
|
||||
:key="k"
|
||||
label=""
|
||||
:bars="bar"
|
||||
/>
|
||||
</g-gantt-chart>
|
||||
</GGanttChart>
|
||||
</div>
|
||||
<TaskForm v-if="canWrite" @create-task="createTask" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watchEffect, shallowReactive, type Ref, type PropType} from 'vue'
|
||||
import {computed, ref, watch, watchEffect, shallowReactive, type PropType} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {format, parse} from 'date-fns'
|
||||
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel from '@/models/task'
|
||||
import TaskModel, { getHexColor } from '@/models/task'
|
||||
|
||||
import type ListModel from '@/models/list'
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
import {
|
||||
extendDayjs,
|
||||
GGanttChart,
|
||||
GGanttRow,
|
||||
type GanttBarObject,
|
||||
} from '@infectoone/vue-ganttastic'
|
||||
|
||||
import Loading from '@/components/misc/loading.vue'
|
||||
import TaskForm from '@/components/tasks/TaskForm.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
// FIXME: these types should be exported from vue-ganttastic
|
||||
// see: https://github.com/InfectoOne/vue-ganttastic/blob/master/src/models/models.ts
|
||||
|
||||
export interface GanttBarConfig {
|
||||
id: string,
|
||||
label?: string
|
||||
hasHandles?: boolean
|
||||
immobile?: boolean
|
||||
bundle?: string
|
||||
pushOnOverlap?: boolean
|
||||
dragLimitLeft?: number
|
||||
dragLimitRight?: number
|
||||
style?: CSSStyleSheet
|
||||
}
|
||||
|
||||
export type GanttBarObject = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any,
|
||||
ganttBarConfig: GanttBarConfig
|
||||
}
|
||||
|
||||
export type GGanttChartPropsRefs = {
|
||||
chartStart: Ref<string>
|
||||
chartEnd: Ref<string>
|
||||
precision: Ref<'hour' | 'day' | 'month'>
|
||||
barStart: Ref<string>
|
||||
barEnd: Ref<string>
|
||||
rowHeight: Ref<number>
|
||||
dateFormat: Ref<string>
|
||||
width: Ref<string>
|
||||
hideTimeaxis: Ref<boolean>
|
||||
colorScheme: Ref<string>
|
||||
grid: Ref<boolean>
|
||||
pushOnOverlap: Ref<boolean>
|
||||
noOverlap: Ref<boolean>
|
||||
gGanttChart: Ref<HTMLElement | null>
|
||||
font: Ref<string>
|
||||
}
|
||||
|
||||
const PRECISION = 'day'
|
||||
extendDayjs()
|
||||
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Picky: Use Picky: Use `@/models...`
|
||||
const PRECISION = 'day' as const
|
||||
const DATE_FORMAT = 'yyyy-LL-dd HH:mm'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
picky: use picky: use `DATE_FORMAT` to make clear it's a 'config const'
dpschen
commented
But also: shouldn't this depend on the user setting / language? But also: shouldn't this depend on the user setting / language?
konrad
commented
It's only used to pass the date in the correct format to the gantt chart libaray so it will always be the same. Not sure why they only take strings as input instead of > shouldn't this depend on the user setting / language?
It's only used to pass the date in the correct format to the gantt chart libaray so it will always be the same. Not sure why they only take strings as input instead of `Date` objects but that's how it is.
|
||||
|
@ -111,7 +80,7 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
showTasksWithoutDates: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
@ -134,6 +103,18 @@ const canWrite = computed(() => baseStore.currentList.maxRight > RIGHTS.READ)
|
|||
const tasks = ref<Map<TaskModel['id'], TaskModel>>(new Map())
|
||||
const ganttBars = ref<GanttBarObject[][]>([])
|
||||
|
||||
watch(
|
||||
tasks,
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
User User `TaskModel['id']`
konrad
commented
Done (I think you did that one?) Done (I think you did that one?)
|
||||
// We need a "real" ref object for the gantt bars to instantly update the tasks when they are dragged on the chart.
|
||||
// A computed won't work directly.
|
||||
// function mapGanttBars() {
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
define types define types
konrad
commented
Done. Done.
|
||||
() => {
|
||||
ganttBars.value = []
|
||||
tasks.value.forEach(t => ganttBars.value.push(transformTaskToGanttBar(t)))
|
||||
},
|
||||
{deep: true}
|
||||
)
|
||||
|
||||
const defaultStartDate = format(new Date(), DATE_FORMAT)
|
||||
const defaultEndDate = format(new Date((new Date()).setDate((new Date()).getDate() + 7)), DATE_FORMAT)
|
||||
|
||||
|
@ -143,12 +124,12 @@ function transformTaskToGanttBar(t: TaskModel) {
|
|||
startDate: t.startDate ? format(t.startDate, DATE_FORMAT) : defaultStartDate,
|
||||
endDate: t.endDate ? format(t.endDate, DATE_FORMAT) : defaultEndDate,
|
||||
ganttBarConfig: {
|
||||
id: t.id,
|
||||
id: String(t.id),
|
||||
label: t.title,
|
||||
hasHandles: true,
|
||||
style: {
|
||||
color: t.startDate ? (colorIsDark(t.getHexColor(t.hexColor)) ? black : 'white') : black,
|
||||
backgroundColor: t.startDate ? t.getHexColor(t.hexColor) : 'var(--grey-100)',
|
||||
color: t.startDate ? (colorIsDark(getHexColor(t.hexColor)) ? black : 'white') : black,
|
||||
backgroundColor: t.startDate ? getHexColor(t.hexColor) : 'var(--grey-100)',
|
||||
border: t.startDate ? '' : '2px dashed var(--grey-300)',
|
||||
'text-decoration': t.done ? 'line-through' : null,
|
||||
},
|
||||
|
@ -156,13 +137,7 @@ function transformTaskToGanttBar(t: TaskModel) {
|
|||
} as GanttBarObject]
|
||||
}
|
||||
|
||||
// We need a "real" ref object for the gantt bars to instantly update the tasks when they are dragged on the chart.
|
||||
// A computed won't work directly.
|
||||
function mapGanttBars() {
|
||||
ganttBars.value = []
|
||||
|
||||
tasks.value.forEach(t => ganttBars.value.push(transformTaskToGanttBar(t)))
|
||||
}
|
||||
|
||||
// FIXME: unite with other filter params types
|
||||
interface GetAllTasksParams {
|
||||
|
@ -208,8 +183,6 @@ async function loadTasks({
|
|||
const loadedTasks = await getAllTasks(params)
|
||||
|
||||
loadedTasks.forEach(t => tasks.value.set(t.id, t))
|
||||
|
||||
mapGanttBars()
|
||||
}
|
||||
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Avoid Avoid `for ... in`, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#array_iteration_and_for...in
|
||||
watchEffect(() => loadTasks({
|
||||
|
@ -226,7 +199,6 @@ async function createTask(title: TaskModel['title']) {
|
|||
endDate: defaultEndDate,
|
||||
}))
|
||||
tasks.value.set(newTask.id, newTask)
|
||||
mapGanttBars()
|
||||
|
||||
return newTask
|
||||
}
|
||||
|
@ -268,14 +240,25 @@ function dayIsToday(label: string): boolean {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.gantt-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
// Not scoped because we need to style the elements inside the gantt chart component
|
||||
.g-gantt-chart {
|
||||
width: 2000px;
|
||||
}
|
||||
|
||||
.g-gantt-row-label {
|
||||
display: none !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.g-upper-timeunit, .g-timeunit {
|
||||
background: var(--white) !important;
|
||||
background: var(--white);
|
||||
font-family: $vikunja-font;
|
||||
}
|
||||
|
||||
|
@ -287,7 +270,7 @@ function dayIsToday(label: string): boolean {
|
|||
|
||||
.g-timeunit .timeunit-wrapper {
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
why do we need so many overwriting important styles here? why do we need so many overwriting important styles here?
konrad
commented
IIRC they're using inline styles for these :/ IIRC they're using inline styles for these :/
|
||||
padding: 0.5rem 0;
|
||||
font-size: 1rem !important;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -306,13 +289,13 @@ function dayIsToday(label: string): boolean {
|
|||
}
|
||||
|
||||
.g-timeaxis {
|
||||
height: auto !important;
|
||||
box-shadow: none !important;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.g-gantt-row > .g-gantt-row-bars-container {
|
||||
border-bottom: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.g-gantt-row:nth-child(odd) {
|
||||
|
@ -326,21 +309,11 @@ function dayIsToday(label: string): boolean {
|
|||
|
||||
&-handle-left,
|
||||
&-handle-right {
|
||||
width: 6px !important;
|
||||
height: 75% !important;
|
||||
opacity: .75 !important;
|
||||
border-radius: $radius !important;
|
||||
width: 6px;
|
||||
height: 75%;
|
||||
opacity: .75;
|
||||
border-radius: $radius;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.gantt-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#g-gantt-chart {
|
||||
width: 2000px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
|
@ -4,7 +4,7 @@
|
|||
* @param color
|
||||
* @returns {string}
|
||||
*/
|
||||
export function colorFromHex(color) {
|
||||
export function colorFromHex(color: string) {
|
||||
if (color.substring(0, 1) === '#') {
|
||||
color = color.substring(1, 7)
|
||||
}
|
||||
|
|
|
@ -59,7 +59,6 @@ import FontAwesomeIcon from '@/components/misc/Icon'
|
|||
import Button from '@/components/input/button.vue'
|
||||
import Modal from '@/components/misc/modal.vue'
|
||||
import Card from '@/components/misc/card.vue'
|
||||
import ganttastic from '@infectoone/vue-ganttastic'
|
||||
|
||||
app.component('icon', FontAwesomeIcon)
|
||||
app.component('x-button', Button)
|
||||
|
@ -103,8 +102,6 @@ if (window.SENTRY_ENABLED) {
|
|||
import('./sentry').then(sentry => sentry.default(app, router))
|
||||
}
|
||||
|
||||
|
||||
app.use(ganttastic)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
|
|
Use loading component. This way it's easier for us to refactor the
is-loading
styles from bulma later.Done.