1
0

chore(gantt): wip daterange

This commit is contained in:
Dominik Pschenitschni
2022-10-10 21:44:59 +02:00
committed by kolaente
parent 3b244dfdbe
commit 9f146c8c7f
17 changed files with 659 additions and 158 deletions

View File

@ -1,12 +1,3 @@
import { defineAsyncComponent } from 'vue'
import ErrorComponent from '@/components/misc/error.vue'
import LoadingComponent from '@/components/misc/loading.vue'
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
const Editor = () => import('@/components/input/editor.vue')
export default defineAsyncComponent({
loader: Editor,
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
timeout: 60000,
})
export default createAsyncComponent(() => import('@/components/input/editor.vue'))

View File

@ -7,6 +7,12 @@
</message>
</template>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import Message from '@/components/misc/message.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'

View File

@ -0,0 +1,243 @@
<template>
<input
type="text"
data-input
:disabled="disabled"
v-bind="attrs"
ref="root"
/>
</template>
<script lang="ts">
import Flatpickr from 'flatpickr';
import 'flatpickr/dist/flatpickr.css'
// FIXME: Not sure how to alias these correctly
// import Options = Flatpickr.Options doesn't work
type Hook = Flatpickr.Options.Hook
type HookKey = Flatpickr.Options.HookKey
type Options = Flatpickr.Options.Options
type DateOption = Flatpickr.Options.DateOption
function camelToKebab(string: string) {
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
};
function arrayify<T extends unknown>(obj: T) {
return obj instanceof Array ? obj : [obj];
}
function nullify<T extends unknown>(value: T) {
return (value && (value as unknown[]).length) ? value : null;
}
function cloneObject<T extends {}>(obj: T): T {
return Object.assign({}, obj)
}
// Events to emit, copied from flatpickr source
const includedEvents = [
'onChange',
'onClose',
'onDestroy',
'onMonthChange',
'onOpen',
'onYearChange',
] as HookKey[]
// Let's not emit these events by default
const excludedEvents = [
'onValueUpdate',
'onDayCreate',
'onParseConfig',
'onReady',
'onPreCalendarPosition',
'onKeyDown',
] as HookKey[]
// Keep a copy of all events for later use
const allEvents = includedEvents.concat(excludedEvents);
export default {inheritAttrs: false}
</script>
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect, type PropType} from 'vue';
const props = defineProps({
modelValue: {
type: [String, Number, Date, Array] as PropType<DateOption | DateOption[]>,
default: null,
required: true,
// validator(value) {
// return (
// value === null ||
// value instanceof Date ||
// typeof value === 'string' ||
// value instanceof String ||
// value instanceof Array ||
// typeof value === 'number'
// );
// }
},
// https://chmln.github.io/flatpickr/options/
config: {
type: Object as PropType<Options>,
default: () => ({
wrap: false,
defaultDate: null
})
},
events: {
type: Array as PropType<HookKey[]>,
default: () => includedEvents
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits([
'blur',
'update:modelValue',
...allEvents.map(camelToKebab)
])
const {modelValue, config, disabled} = toRefs(props)
// bind listener like onBlur
const attrs = useAttrs()
const root = ref<HTMLInputElement | null>(null)
let fp = ref<Flatpickr.Instance | null>(null)
const safeConfig = ref<Options>(cloneObject(props.config))
onMounted(() => {
// Don't mutate original object on parent component
let newConfig = cloneObject(props.config);
if (
fp.value || // Return early if flatpickr is already loaded
!root.value // our input needs to be mounted
) {
return
}
props.events.forEach((hook) => {
// Respect global callbacks registered via setDefault() method
const globalCallbacks = Flatpickr.defaultConfig[hook] || [];
// Inject our own method along with user callback
const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
// Overwrite with merged array
newConfig[hook] = arrayify(newConfig[hook] || []).concat(
globalCallbacks,
localCallback
);
});
// Watch for value changed by date-picker itself and notify parent component
const onChange: Hook = (dates) => emit('update:modelValue', dates)
newConfig['onChange'] = arrayify(newConfig['onChange'] || []).concat(onChange)
// Flatpickr does not emit input event in some cases
// const onClose: Hook = (_selectedDates, dateStr) => emit('update:modelValue', dateStr)
// newConfig['onClose'] = arrayify(newConfig['onClose'] || []).concat(onClose)
// Set initial date without emitting any event
newConfig.defaultDate = props.modelValue || newConfig.defaultDate;
safeConfig.value = newConfig
/**
* Get the HTML node where flatpickr to be attached
* Bind on parent element if wrap is true
*/
const element = props.config.wrap
? root.value.parentNode as ParentNode
: root.value
// Init flatpickr
fp.value = Flatpickr(element, safeConfig.value);
})
onBeforeUnmount(() => fp.value?.destroy())
watch(config, () => {
if (!fp.value) return
// Workaround: Don't pass hooks to configs again otherwise
// previously registered hooks will stop working
// Notice: we are looping through all events
// This also means that new callbacks can not be passed once component has been initialized
allEvents.forEach((hook) => {
delete safeConfig.value?.[hook];
});
fp.value.set(safeConfig.value);
// Passing these properties in `set()` method will cause flatpickr to trigger some callbacks
const configCallbacks = ['locale', 'showMonths'] as (keyof Options)[]
// Workaround: Allow to change locale dynamically
configCallbacks.forEach(name => {
if (typeof safeConfig.value?.[name] !== 'undefined' && fp.value) {
fp.value.set(name, safeConfig.value[name]);
}
});
}, {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(() => {
if (!fp.value) return
return fp.value.altInput || fp.value.input;
})
/**
* init blur event
* (is required by many validation libraries)
*/
function onBlur(event: Event) {
emit('blur', nullify((event.target as HTMLInputElement).value));
}
watchEffect(() => fpInput.value?.addEventListener('blur', onBlur))
onBeforeUnmount(() => fpInput.value?.removeEventListener('blur', onBlur))
/**
* Watch for the disabled property and sets the value to the real input.
*/
watchEffect(() => {
if (disabled.value) {
fpInput.value?.setAttribute('disabled', '');
} else {
fpInput.value?.removeAttribute('disabled');
}
})
/**
* Watch for changes from parent component and update DOM
*/
watch(
modelValue,
newValue => {
// Prevent updates if v-model value is same as input's current value
if (!root.value || newValue === nullify(root.value.value)) return;
// Make sure we have a flatpickr instanceand
// Notify flatpickr instance that there is a change in value
fp.value?.setDate(newValue, true);
},
{deep: true}
)
</script>

View File

@ -2,6 +2,12 @@
<div class="loader-container is-loading"></div>
</template>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<style scoped lang="scss">
.loader-container {
height: 100%;

View File

@ -22,7 +22,7 @@
<script setup lang="ts">
import {nextTick, ref} from 'vue'
import type {ITask} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
const emit = defineEmits<{
(e: 'create-task', title: string): Promise<ITask>

View File

@ -1,10 +1,14 @@
<template>
<Loading class="gantt-container" v-if="taskService.loading || taskCollectionService.loading"/>
<Loading
v-if="taskService.loading || taskCollectionService.loading || dayjsLanguageLoading"
class="gantt-container"
/>
<div class="gantt-container" v-else>
<GGanttChart
dateFormat="YYYY-MM-DDTHH:mm:ssZ[Z]"
:chart-start="`${dateFrom} 00:00`"
:chart-end="`${dateTo} 23:59`"
:precision="PRECISION"
precision="day"
bar-start="startDate"
bar-end="endDate"
:grid="true"
@ -37,15 +41,19 @@
import {computed, ref, watch, watchEffect, shallowReactive, type PropType} from 'vue'
import {useRouter} from 'vue-router'
import {format, parse} from 'date-fns'
import dayjs from 'dayjs'
import {useDayjsLanguageSync} from '@/i18n'
import TaskCollectionService from '@/services/taskCollection'
import TaskService from '@/services/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 type {ITask} from '@/modelTypes/ITask'
import type {IList} from '@/modelTypes/IList'
import {
extendDayjs,
GGanttChart,
@ -58,33 +66,30 @@ import TaskForm from '@/components/tasks/TaskForm.vue'
import {useBaseStore} from '@/stores/base'
extendDayjs()
export type DateRange = {
dateFrom: string,
dateTo: string,
}
const PRECISION = 'day' as const
const DATE_FORMAT = 'yyyy-LL-dd HH:mm'
export interface GanttChartProps {
listId: IList['id']
dateRange: DateRange
showTasksWithoutDates: boolean
}
export const DATE_FORMAT = 'yyyy-LL-dd HH:mm'
const props = withDefaults(defineProps<GanttChartProps>(), {
showTasksWithoutDates: false,
})
// setup dayjs for vue-ganttastic
const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
extendDayjs()
const baseStore = useBaseStore()
const router = useRouter()
const props = defineProps({
listId: {
type: Number as PropType<ListModel['id']>,
required: true,
},
dateFrom: {
type: String as PropType<any>,
required: true,
},
dateTo: {
type: String as PropType<any>,
required: true,
},
showTasksWithoutDates: {
type: Boolean,
default: false,
},
})
const taskCollectionService = shallowReactive(new TaskCollectionService())
const taskService = shallowReactive(new TaskService())
@ -100,14 +105,11 @@ const ganttChartWidth = computed(() => {
const canWrite = computed(() => baseStore.currentList.maxRight > RIGHTS.READ)
const tasks = ref<Map<TaskModel['id'], TaskModel>>(new Map())
const tasks = ref<Map<ITask['id'], ITask>>(new Map())
const ganttBars = ref<GanttBarObject[][]>([])
watch(
tasks,
// 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)))
@ -115,14 +117,15 @@ watch(
{deep: true}
)
const defaultStartDate = format(new Date(), DATE_FORMAT)
const defaultEndDate = format(new Date((new Date()).setDate((new Date()).getDate() + 7)), DATE_FORMAT)
const defaultStartDate = new Date().toISOString()
const defaultEndDate = new Date((new Date()).setDate((new Date()).getDate() + 7)).toISOString()
function transformTaskToGanttBar(t: TaskModel) {
function transformTaskToGanttBar(t: ITask) {
const black = 'var(--grey-800)'
console.log(t)
return [{
startDate: t.startDate ? format(t.startDate, DATE_FORMAT) : defaultStartDate,
endDate: t.endDate ? format(t.endDate, DATE_FORMAT) : defaultEndDate,
startDate: t.startDate ? new Date(t.startDate).toISOString() : defaultStartDate,
endDate: t.endDate ? new Date(t.endDate).toISOString() : defaultEndDate,
ganttBarConfig: {
id: String(t.id),
label: t.title,
@ -137,8 +140,6 @@ function transformTaskToGanttBar(t: TaskModel) {
} as GanttBarObject]
}
// FIXME: unite with other filter params types
interface GetAllTasksParams {
sort_by: ('start_date' | 'done' | 'id')[],
@ -150,8 +151,8 @@ interface GetAllTasksParams {
filter_include_nulls: boolean,
}
async function getAllTasks(params: GetAllTasksParams, page = 1): Promise<TaskModel[]> {
const tasks = await taskCollectionService.getAll({listId: props.listId}, params, page) as TaskModel[]
async function getAllTasks(params: GetAllTasksParams, page = 1): Promise<ITask[]> {
const tasks = await taskCollectionService.getAll({listId: props.listId}, params, page) as ITask[]
if (page < taskCollectionService.totalPages) {
const nextTasks = await getAllTasks(params, page + 1)
return tasks.concat(nextTasks)
@ -170,7 +171,7 @@ async function loadTasks({
}) {
tasks.value = new Map()
const params = {
const params: GetAllTasksParams = {
sort_by: ['start_date', 'done', 'id'],
order_by: ['asc', 'asc', 'desc'],
filter_by: ['start_date', 'start_date'],
@ -191,7 +192,7 @@ watchEffect(() => loadTasks({
showTasksWithoutDates: props.showTasksWithoutDates,
}))
async function createTask(title: TaskModel['title']) {
async function createTask(title: ITask['title']) {
const newTask = await taskService.create(new TaskModel({
title,
listId: props.listId,
@ -212,14 +213,26 @@ async function updateTask(e: {
if (!task) return
task.startDate = e.bar.startDate
task.endDate = e.bar.endDate
console.log(task.startDate.toISOString())
console.log(dayjs(task.startDate), "YYYY-MM-DD HH:mm").toISOString()
console.log(dayjs(task.startDate, "YYYY-MM-DDTHH:mm:ssZ[Z]").toISOString())
console.log(task.startDate)
console.log(dayjs(e.bar.startDate).toDate())
console.log(e.bar.startDate)
console.log(task.endDate.toISOString())
console.log(task.endDate)
console.log(dayjs(e.bar.startDate).toDate())
console.log(e.bar.endDate)
// task.startDate = e.bar.startDate
// task.endDate = e.bar.endDate
const updatedTask = await taskService.update(task)
ganttBars.value.map(gantBar => {
return Number(gantBar[0].ganttBarConfig.id) === task.id
? transformTaskToGanttBar(updatedTask)
: gantBar
})
// ganttBars.value.map(gantBar => {
// return Number(gantBar[0].ganttBarConfig.id) === task.id
// ? transformTaskToGanttBar(updatedTask)
// : gantBar
// })
}
function openTask(e: {