chore(gantt): wip daterange
This commit is contained in:

committed by
kolaente

parent
3b244dfdbe
commit
9f146c8c7f
@ -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'))
|
@ -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'
|
||||
|
243
src/components/misc/flatpickr/Flatpickr.vue
Normal file
243
src/components/misc/flatpickr/Flatpickr.vue
Normal 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>
|
@ -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%;
|
||||
|
@ -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>
|
||||
|
@ -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: {
|
||||
|
Reference in New Issue
Block a user