feat: replace our home-grown gantt implementation with ganttastic #2180
|
@ -186,19 +186,6 @@ watch(config, () => {
|
||||||
});
|
});
|
||||||
}, {deep:true})
|
}, {deep:true})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// watch(root, () => {
|
|
||||||
// if (
|
|
||||||
// fp.value || // Return early if flatpickr is already loaded
|
|
||||||
// !root.value // our input needs to be mounted
|
|
||||||
// ) {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// })
|
|
||||||
|
|
||||||
const fpInput = computed(() => {
|
const fpInput = computed(() => {
|
||||||
if (!fp.value) return
|
if (!fp.value) return
|
||||||
return fp.value.altInput || fp.value.input;
|
return fp.value.altInput || fp.value.input;
|
||||||
|
|
|
@ -71,13 +71,12 @@ export type DateRange = {
|
||||||
dateTo: string,
|
dateTo: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GanttChartProps {
|
export interface GanttChartProps extends DateRange {
|
||||||
listId: IList['id']
|
listId: IList['id']
|
||||||
dateRange: DateRange
|
|
||||||
showTasksWithoutDates: boolean
|
showTasksWithoutDates: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DATE_FORMAT = 'yyyy-LL-dd HH:mm'
|
// export const DATE_FORMAT = 'yyyy-LL-dd HH:mm'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<GanttChartProps>(), {
|
const props = withDefaults(defineProps<GanttChartProps>(), {
|
||||||
showTasksWithoutDates: false,
|
showTasksWithoutDates: false,
|
||||||
|
|
|
@ -384,6 +384,7 @@ const router = createRouter({
|
||||||
dateFrom: route.query.dateFrom as string,
|
dateFrom: route.query.dateFrom as string,
|
||||||
dateTo: route.query.dateTo as string,
|
dateTo: route.query.dateTo as string,
|
||||||
showTasksWithoutDates: Boolean(route.query.showTasksWithoutDates),
|
showTasksWithoutDates: Boolean(route.query.showTasksWithoutDates),
|
||||||
|
route,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<ListWrapper class="list-gantt" :list-id="props.listId" viewName="gantt">
|
<ListWrapper class="list-gantt" :list-id="filters.listId" viewName="gantt">
|
||||||
<template #header>
|
<template #header>
|
||||||
<card>
|
<card>
|
||||||
<div class="gantt-options">
|
<div class="gantt-options">
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fancycheckbox class="is-block" v-model="showTasksWithoutDates">
|
<fancycheckbox class="is-block" v-model="filters.showTasksWithoutDates">
|
||||||
{{ $t('list.gantt.showTasksWithoutDates') }}
|
{{ $t('list.gantt.showTasksWithoutDates') }}
|
||||||
</fancycheckbox>
|
</fancycheckbox>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,15 +27,14 @@
|
||||||
<div class="gantt-chart-container">
|
<div class="gantt-chart-container">
|
||||||
<card :padding="false" class="has-overflow">
|
<card :padding="false" class="has-overflow">
|
||||||
<pre>{{dateRange}}</pre>
|
<pre>{{dateRange}}</pre>
|
||||||
<pre>{{new Date(dateRange.dateFrom).toLocaleDateString()}}</pre>
|
<pre>{{new Date(dateRange.dateFrom).toISOString()}}</pre>
|
||||||
<pre>{{new Date(dateRange.dateTo).toLocaleDateString()}}</pre>
|
<pre>{{new Date(dateRange.dateTo).toISOString()}}</pre>
|
||||||
<!-- <gantt-chart
|
<!-- <gantt-chart
|
||||||
v-if="false"
|
:list-id="filters.listId"
|
||||||
:date-range="dateRange"
|
:date-from="filters.dateFrom"
|
||||||
:list-id="props.listId"
|
:date-to="filters.dateTo"
|
||||||
:show-tasks-without-dates="showTasksWithoutDates"
|
:show-tasks-without-dates="showTasksWithoutDates"
|
||||||
/> -->
|
/> -->
|
||||||
|
|
||||||
</card>
|
</card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -43,12 +42,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, ref, type PropType} from 'vue'
|
import {computed, reactive, ref, watch, type PropType} from 'vue'
|
||||||
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
|
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
|
||||||
// import type FlatPickr from 'vue-flatpickr-component'
|
// import type FlatPickr from 'vue-flatpickr-component'
|
||||||
|
import type Flatpickr from 'flatpickr'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {format} from 'date-fns'
|
import {format} from 'date-fns'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute, useRouter, type LocationQuery, type RouteLocationNormalized, type RouteLocationRaw} from 'vue-router'
|
||||||
|
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
|
||||||
|
@ -56,40 +56,40 @@ import ListWrapper from './ListWrapper.vue'
|
||||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||||
|
|
||||||
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
|
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
|
||||||
|
import type { IList } from '@/modelTypes/IList'
|
||||||
|
|
||||||
type DateKebab = `${string}-${string}-${string}`
|
export type DateKebab = `${string}-${string}-${string}`
|
||||||
|
export type DateISO = string
|
||||||
|
export type DateRange = {
|
||||||
|
dateFrom: string
|
||||||
|
dateTo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GanttParams {
|
||||||
|
listId: IList['id']
|
||||||
|
dateFrom: DateKebab
|
||||||
|
dateTo: DateKebab
|
||||||
|
showTasksWithoutDates: boolean
|
||||||
|
route: RouteLocationNormalized,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GanttFilter {
|
||||||
|
listId: IList['id']
|
||||||
|
dateFrom: DateISO
|
||||||
konrad marked this conversation as resolved
Outdated
|
|||||||
|
dateTo: DateISO
|
||||||
|
showTasksWithoutDates: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options = Flatpickr.Options.Options
|
||||||
dpschen
commented
Can you explain in a different way? Can you explain in a different way?
konrad
commented
The problem is the gantt chart already updates when only one date (the start or end date) is selected. Ideally, they would only update the prop when both of these dates are available to avoid these partial updates. The problem is the gantt chart already updates when only one date (the start or end date) is selected. Ideally, they would only update the prop when both of these dates are available to avoid these partial updates.
dpschen
commented
Maybe I'm still not getting this correctly, but can't we just update the value when both (start and end) are set? Maybe I'm still not getting this correctly, but can't we just update the value when both (start and end) are set?
konrad
commented
Currently the from and to dates get passed as individual props. That means if one changes, it changes directly in the chart. I think the way to go here would be to pass a single object with both dates instead? Currently the from and to dates get passed as individual props. That means if one changes, it changes directly in the chart.
I think the way to go here would be to pass a single object with both dates instead?
dpschen
commented
That seems like the right approach That seems like the right approach
dpschen
commented
Will check this out again. Shouldn't be too hard. Will check this out again. Shouldn't be too hard.
|
|||||||
|
|
||||||
const GanttChart = createAsyncComponent(() => import('@/components/tasks/gantt-chart.vue'))
|
const GanttChart = createAsyncComponent(() => import('@/components/tasks/gantt-chart.vue'))
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps<GanttParams>()
|
||||||
listId: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dateFrom: {
|
|
||||||
type: String as PropType<DateKebab>,
|
|
||||||
},
|
|
||||||
dateTo: {
|
|
||||||
type: String as PropType<DateKebab>,
|
|
||||||
},
|
|
||||||
showTasksWithoutDates: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const showTasksWithoutDates = computed({
|
function parseDateProp(kebabDate: DateKebab | undefined): string | undefined {
|
||||||
get: () => props.showTasksWithoutDates,
|
|
||||||
set: (value) => router.push({ query: {
|
|
||||||
...route.query,
|
|
||||||
showTasksWithoutDates: String(value),
|
|
||||||
}}),
|
|
||||||
})
|
|
||||||
|
|
||||||
function parseKebabDate(kebabDate: DateKebab | undefined, fallback: () => Date): Date {
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!kebabDate) {
|
if (!kebabDate) {
|
||||||
|
@ -110,134 +110,120 @@ function parseKebabDate(kebabDate: DateKebab | undefined, fallback: () => Date):
|
||||||
if (!dateValuesAreValid) {
|
if (!dateValuesAreValid) {
|
||||||
throw new Error('Invalid date values')
|
throw new Error('Invalid date values')
|
||||||
}
|
}
|
||||||
return new Date(year, month, date)
|
return new Date(year, month, date).toISOString()
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// ignore nonsense route queries
|
// ignore nonsense route queries
|
||||||
return fallback()
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBooleanProp(booleanProp: string) {
|
||||||
|
return (booleanProp === 'false' || booleanProp === '0')
|
||||||
|
? false
|
||||||
|
: Boolean(booleanProp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SHOW_TASKS_WITHOUT_DATES = false
|
||||||
|
|
||||||
const DEFAULT_DATEFROM_DAY_OFFSET = 0
|
const DEFAULT_DATEFROM_DAY_OFFSET = 0
|
||||||
// const DEFAULT_DATEFROM_DAY_OFFSET = -15
|
// const DEFAULT_DATEFROM_DAY_OFFSET = -15
|
||||||
const DEFAULT_DATETO_DAY_OFFSET = +55
|
const DEFAULT_DATETO_DAY_OFFSET = +55
|
||||||
// const DEFAULT_DATETO_DAY_OFFSET = +55
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
function getDefaultDateFrom() {
|
function getDefaultDateFrom() {
|
||||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + DEFAULT_DATEFROM_DAY_OFFSET)
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + DEFAULT_DATEFROM_DAY_OFFSET).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultDateTo() {
|
function getDefaultDateTo() {
|
||||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + DEFAULT_DATETO_DAY_OFFSET)
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + DEFAULT_DATETO_DAY_OFFSET).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
let isChangingRoute = ref<ReturnType<typeof router.push> | false>(false)
|
function routeToFilter(route: RouteLocationNormalized): GanttFilter {
|
||||||
|
return {
|
||||||
|
listId: Number(route.params.listId as string),
|
||||||
|
dateFrom: parseDateProp(route.query.dateFrom as DateKebab) || getDefaultDateFrom(),
|
||||||
|
dateTo: parseDateProp(route.query.dateTo as DateKebab) || getDefaultDateTo(),
|
||||||
|
showTasksWithoutDates: parseBooleanProp(route.query.showTasksWithoutDates as string) || DEFAULT_SHOW_TASKS_WITHOUT_DATES,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const count = ref(0)
|
function filterToRoute(filters: GanttFilter): RouteLocationRaw {
|
||||||
|
let query: Record<string, string> = {}
|
||||||
const dateRange = computed<{
|
if (
|
||||||
dateFrom: string
|
filters.dateFrom !== getDefaultDateFrom() ||
|
||||||
dateTo: string
|
filters.dateTo !== getDefaultDateTo()
|
||||||
}>({
|
) {
|
||||||
get: () => ({
|
query = {
|
||||||
dateFrom: parseKebabDate(props.dateFrom, getDefaultDateFrom).toISOString(),
|
dateFrom: format(new Date(filters.dateFrom), 'yyyy-LL-dd'),
|
||||||
dateTo: parseKebabDate(props.dateTo, getDefaultDateTo).toISOString(),
|
dateTo: format(new Date(filters.dateTo), 'yyyy-LL-dd'),
|
||||||
}),
|
|
||||||
async set(range: {
|
|
||||||
dateFrom: string
|
|
||||||
dateTo: string
|
|
||||||
} | null) {
|
|
||||||
if (range === null) {
|
|
||||||
const query = {...route.query}
|
|
||||||
delete query?.dateFrom
|
|
||||||
delete query?.dateTo
|
|
||||||
console.log('set range to null. query is: ', query)
|
|
||||||
router.push(query)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
dateFrom,
|
|
||||||
dateTo,
|
|
||||||
} = range
|
|
||||||
count.value = count.value + 1
|
|
||||||
if (count.value >= 4) {
|
|
||||||
console.log('triggered ', count, ' times, stopping.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isChangingRoute.value !== false) {
|
|
||||||
console.log('called again while changing route')
|
|
||||||
await isChangingRoute.value
|
|
||||||
console.log('changing route finished, continuing...')
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const queryDateFrom = format(new Date(dateFrom || getDefaultDateFrom()), 'yyyy-LL-dd')
|
if (filters.showTasksWithoutDates) {
|
||||||
const queryDateTo = format(new Date(dateTo || getDefaultDateTo()), 'yyyy-LL-dd')
|
query.showTasksWithoutDates = String(filters.showTasksWithoutDates)
|
||||||
|
}
|
||||||
|
|
||||||
console.log(dateFrom, 'dateFrom')
|
return {
|
||||||
console.log(dateRange.value.dateFrom, 'dateRange.value.dateFrom')
|
name: 'list.gantt',
|
||||||
console.log(dateTo, 'dateTo')
|
params: {listId: filters.listId},
|
||||||
console.log(dateRange.value.dateTo, 'dateRange.value.dateTo')
|
query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (queryDateFrom === route.query.dateFrom || queryDateTo === route.query.dateTo) {
|
const filters: GanttFilter = reactive(routeToFilter(route))
|
||||||
console.log('is same date')
|
|
||||||
// only set if the value has changed
|
watch(() => JSON.parse(JSON.stringify(props.route)) as RouteLocationNormalized, (route, oldRoute) => {
|
||||||
return
|
if (route.name !== oldRoute.name) {
|
||||||
}
|
return
|
||||||
console.log('change url to', {
|
}
|
||||||
query: {
|
const filterFullPath = router.resolve(filterToRoute(filters)).fullPath
|
||||||
...route.query,
|
if (filterFullPath === route.fullPath) {
|
||||||
dateFrom: format(new Date(dateFrom), 'yyyy-LL-dd'),
|
return
|
||||||
dateTo: format(new Date(dateTo), 'yyyy-LL-dd'),
|
}
|
||||||
}
|
|
||||||
})
|
Object.assign(filters, routeToFilter(route))
|
||||||
isChangingRoute.value = router.push({
|
|
||||||
query: {
|
|
||||||
...route.query,
|
|
||||||
dateFrom: format(new Date(dateFrom), 'yyyy-LL-dd'),
|
|
||||||
dateTo: format(new Date(dateTo), 'yyyy-LL-dd'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialDateRange = [dateRange.value.dateFrom, dateRange.value.dateTo]
|
watch(
|
||||||
|
filters,
|
||||||
|
async () => {
|
||||||
|
const newRouteFullPath = router.resolve(filterToRoute(filters)).fullPath
|
||||||
|
if (newRouteFullPath !== route.fullPath) {
|
||||||
|
await router.push(newRouteFullPath)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{flush: "post"}
|
||||||
|
)
|
||||||
|
|
||||||
function getCurrentDateRangeFromFlatpicker() {
|
const dateRange = computed(() => ({
|
||||||
return flatPickerEl.value.fp.selectedDates.map((date: Date) => date?.toISOString())
|
dateFrom: filters.dateFrom,
|
||||||
}
|
dateTo: filters.dateTo,
|
||||||
|
}))
|
||||||
|
|
||||||
const flatPickerEl = ref<typeof FlatPickr | null>(null)
|
const flatPickerEl = ref<typeof FlatPickr | null>(null)
|
||||||
const flatPickerDateRange = computed({
|
const flatPickerDateRange = computed({
|
||||||
get: () => ([
|
get: () => ([
|
||||||
dateRange.value.dateFrom,
|
filters.dateFrom,
|
||||||
dateRange.value.dateTo
|
filters.dateTo
|
||||||
]),
|
]),
|
||||||
set(newVal) {
|
set(newVal) {
|
||||||
// set([dateFrom, dateTo]) {
|
const [dateFrom, dateTo] = newVal.map((date) => date?.toISOString())
|
||||||
// newVal from event does only contain the wrong format
|
|
||||||
console.log(newVal)
|
|
||||||
const [dateFrom, dateTo] = newVal
|
|
||||||
// const [dateFrom, dateTo] = getCurrentDateRangeFromFlatpicker()
|
|
||||||
|
|
||||||
if (
|
// only set after whole range has been selected
|
||||||
// only set after whole range has been selected
|
if (!dateTo) return
|
||||||
!dateTo ||
|
|
||||||
// only set if the value has changed
|
Object.assign(filters, {dateFrom, dateTo})
|
||||||
dateRange.value.dateFrom === dateFrom &&
|
|
||||||
dateRange.value.dateTo === dateTo
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// dateRange.value = {dateFrom, dateTo}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ssZ[Z]"
|
const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ssZ[Z]"
|
||||||
|
|
||||||
|
const initialDateRange = [filters.dateFrom, filters.dateTo]
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const flatPickerConfig = computed(() => ({
|
const flatPickerConfig = computed<Options>(() => ({
|
||||||
altFormat: t('date.altFormatShort'),
|
altFormat: t('date.altFormatShort'),
|
||||||
altInput: true,
|
altInput: true,
|
||||||
// dateFornat: ISO_DATE_FORMAT,
|
// dateFornat: ISO_DATE_FORMAT,
|
||||||
|
|
Reference in New Issue
Should this update?
No, doesn't even need to be ref.