feat: replace our home-grown gantt implementation with ganttastic #2180
|
@ -38,6 +38,7 @@
|
|||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.65.9",
|
||||
"date-fns": "2.29.3",
|
||||
"dayjs": "^1.11.5",
|
||||
"dompurify": "2.4.0",
|
||||
"easymde": "2.18.0",
|
||||
"flatpickr": "4.6.13",
|
||||
|
|
|
@ -42,6 +42,7 @@ specifiers:
|
|||
codemirror: 5.65.9
|
||||
cypress: 10.9.0
|
||||
date-fns: 2.29.3
|
||||
dayjs: ^1.11.5
|
||||
dompurify: 2.4.0
|
||||
easymde: 2.18.0
|
||||
esbuild: 0.15.10
|
||||
|
@ -80,7 +81,7 @@ specifiers:
|
|||
vue-flatpickr-component: 9.0.6
|
||||
vue-i18n: 9.2.2
|
||||
vue-router: 4.1.5
|
||||
vue-tsc: 0.40.13
|
||||
vue-tsc: 1.0.2
|
||||
wait-on: 6.0.1
|
||||
workbox-cli: 6.5.4
|
||||
workbox-precaching: 6.5.4
|
||||
|
@ -107,6 +108,7 @@ dependencies:
|
|||
camel-case: 4.1.2
|
||||
codemirror: 5.65.9
|
||||
date-fns: 2.29.3
|
||||
dayjs: 1.11.5
|
||||
dompurify: 2.4.0
|
||||
easymde: 2.18.0
|
||||
flatpickr: 4.6.13
|
||||
|
@ -170,7 +172,7 @@ devDependencies:
|
|||
vite-plugin-pwa: 0.13.1_bhe5iaipiq3lmbaxwdxgnnn2gq
|
||||
vite-svg-loader: 3.6.0
|
||||
vitest: 0.23.4_k3tqzgv3vrnpmbq3o6ks4hg4vi
|
||||
vue-tsc: 0.40.13_typescript@4.8.4
|
||||
vue-tsc: 1.0.2_typescript@4.8.4
|
||||
wait-on: 6.0.1
|
||||
workbox-cli: 6.5.4
|
||||
|
||||
|
@ -3385,42 +3387,44 @@ packages:
|
|||
vue: 3.2.40
|
||||
dev: true
|
||||
|
||||
/@volar/code-gen/0.40.13:
|
||||
resolution: {integrity: sha512-4gShBWuMce868OVvgyA1cU5WxHbjfEme18Tw6uVMfweZCF5fB2KECG0iPrA9D54vHk3FeHarODNwgIaaFfUBlA==}
|
||||
/@volar/language-core/1.0.2:
|
||||
resolution: {integrity: sha512-kNzrnZYxNWcIgU8b1Yzm4pDb4fEM/LNicWLyVOxaNRvemU5Sh4ORDzKihI5AkzGGkvy0F/67xVe7/5/o9U1oPg==}
|
||||
dependencies:
|
||||
'@volar/source-map': 0.40.13
|
||||
'@volar/source-map': 1.0.2
|
||||
'@vue/reactivity': 3.2.40
|
||||
muggle-string: 0.1.0
|
||||
dev: true
|
||||
|
||||
/@volar/source-map/0.40.13:
|
||||
resolution: {integrity: sha512-dbdkAB2Nxb0wLjAY5O64o3ywVWlAGONnBIoKAkXSf6qkGZM+nJxcizsoiI66K+RHQG0XqlyvjDizfnTxr+6PWg==}
|
||||
/@volar/source-map/1.0.2:
|
||||
resolution: {integrity: sha512-MizyyDw+Eg704bSVkS/8MeP7G4t5U4ZmaXdA2SW3cYAncQKn10P2r6eSyI/8vno+y8wgpM3k9aRfineXB1fMkQ==}
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.2.38
|
||||
muggle-string: 0.1.0
|
||||
dev: true
|
||||
|
||||
/@volar/typescript-faster/0.40.13:
|
||||
resolution: {integrity: sha512-uy+TlcFkKoNlKEnxA4x5acxdxLyVDIXGSc8cYDNXpPKjBKXrQaetzCzlO3kVBqu1VLMxKNGJMTKn35mo+ILQmw==}
|
||||
/@volar/typescript/1.0.2:
|
||||
resolution: {integrity: sha512-yQ6ze+gmf+jYTT/Y+IIH9cBE2YF3I92N6vGfdrVgwnDOj1jnulvJ5viwdTYcAIeEQRB0qtwYfRpSHOrGG2FL2w==}
|
||||
dependencies:
|
||||
semver: 7.3.8
|
||||
'@volar/language-core': 1.0.2
|
||||
dev: true
|
||||
|
||||
/@volar/vue-language-core/0.40.13:
|
||||
resolution: {integrity: sha512-QkCb8msi2KUitTdM6Y4kAb7/ZlEvuLcbBFOC2PLBlFuoZwyxvSP7c/dBGmKGtJlEvMX0LdCyrg5V2aBYxD38/Q==}
|
||||
/@volar/vue-language-core/1.0.2:
|
||||
resolution: {integrity: sha512-SiFHeXu0yWl9N+sIrvQ+iIh/P2oMCBkUT6XFhhh6RCY01KNKc7/iO/xB/OJlO9yl/6mAiFwIVdtPcp6GNOkb3g==}
|
||||
dependencies:
|
||||
'@volar/code-gen': 0.40.13
|
||||
'@volar/source-map': 0.40.13
|
||||
'@vue/compiler-core': 3.2.40
|
||||
'@volar/language-core': 1.0.2
|
||||
'@volar/source-map': 1.0.2
|
||||
'@vue/compiler-dom': 3.2.40
|
||||
'@vue/compiler-sfc': 3.2.40
|
||||
'@vue/reactivity': 3.2.40
|
||||
'@vue/shared': 3.2.40
|
||||
minimatch: 5.1.0
|
||||
vue-template-compiler: 2.7.10
|
||||
dev: true
|
||||
|
||||
/@volar/vue-typescript/0.40.13:
|
||||
resolution: {integrity: sha512-o7bNztwjs8JmbQjVkrnbZUOfm7q4B8ZYssETISN1tRaBdun6cfNqgpkvDYd+VUBh1O4CdksvN+5BUNnwAz4oCQ==}
|
||||
/@volar/vue-typescript/1.0.2:
|
||||
resolution: {integrity: sha512-wOUebVHS8ENF0uMGvFdnkjVdVnf80SHUKbCoCkgngxeVFQBvAlEShcm/XT8bSX3QKoNPl1ZGqYN4UQRgZFyaow==}
|
||||
dependencies:
|
||||
'@volar/code-gen': 0.40.13
|
||||
'@volar/typescript-faster': 0.40.13
|
||||
'@volar/vue-language-core': 0.40.13
|
||||
'@volar/typescript': 1.0.2
|
||||
'@volar/vue-language-core': 1.0.2
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-core/3.2.40:
|
||||
|
@ -3491,12 +3495,6 @@ packages:
|
|||
estree-walker: 2.0.2
|
||||
magic-string: 0.25.9
|
||||
|
||||
/@vue/reactivity/3.2.38:
|
||||
resolution: {integrity: sha512-6L4myYcH9HG2M25co7/BSo0skKFHpAN8PhkNPM4xRVkyGl1K5M3Jx4rp5bsYhvYze2K4+l+pioN4e6ZwFLUVtw==}
|
||||
dependencies:
|
||||
'@vue/shared': 3.2.38
|
||||
dev: true
|
||||
|
||||
/@vue/reactivity/3.2.40:
|
||||
resolution: {integrity: sha512-N9qgGLlZmtUBMHF9xDT4EkD9RdXde1Xbveb+niWMXuHVWQP5BzgRmE3SFyUBBcyayG4y1lhoz+lphGRRxxK4RA==}
|
||||
dependencies:
|
||||
|
@ -3524,10 +3522,6 @@ packages:
|
|||
'@vue/shared': 3.2.40
|
||||
vue: 3.2.40
|
||||
|
||||
/@vue/shared/3.2.38:
|
||||
resolution: {integrity: sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==}
|
||||
dev: true
|
||||
|
||||
/@vue/shared/3.2.40:
|
||||
resolution: {integrity: sha512-0PLQ6RUtZM0vO3teRfzGi4ltLUO5aO+kLgwh4Um3THSR03rpQWLTuRCkuO5A41ITzwdWeKdPHtSARuPkoo5pCQ==}
|
||||
|
||||
|
@ -5189,6 +5183,10 @@ packages:
|
|||
/dayjs/1.11.5:
|
||||
resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==}
|
||||
|
||||
/de-indent/1.0.2:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
dev: true
|
||||
|
||||
/debounce/1.2.1:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
dev: false
|
||||
|
@ -9158,6 +9156,10 @@ packages:
|
|||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/muggle-string/0.1.0:
|
||||
resolution: {integrity: sha512-Tr1knR3d2mKvvWthlk7202rywKbiOm4rVFLsfAaSIhJ6dt9o47W4S+JMtWhd/PW9Wrdew2/S2fSvhz3E2gkfEg==}
|
||||
dev: true
|
||||
|
||||
/multiparty/4.2.3:
|
||||
resolution: {integrity: sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
@ -12761,14 +12763,21 @@ packages:
|
|||
vue: 3.2.40
|
||||
dev: false
|
||||
|
||||
/vue-tsc/0.40.13_typescript@4.8.4:
|
||||
resolution: {integrity: sha512-xzuN3g5PnKfJcNrLv4+mAjteMd5wLm5fRhW0034OfNJZY4WhB07vhngea/XeGn7wNYt16r7syonzvW/54dcNiA==}
|
||||
/vue-template-compiler/2.7.10:
|
||||
resolution: {integrity: sha512-QO+8R9YRq1Gudm8ZMdo/lImZLJVUIAM8c07Vp84ojdDAf8HmPJc7XB556PcXV218k2AkKznsRz6xB5uOjAC4EQ==}
|
||||
dependencies:
|
||||
de-indent: 1.0.2
|
||||
he: 1.2.0
|
||||
dev: true
|
||||
|
||||
/vue-tsc/1.0.2_typescript@4.8.4:
|
||||
resolution: {integrity: sha512-Atl8vBo1ThxZ8OjnCcquRCjRYczmQVF2nAjXVOqHA19Q4lvPX+bpbbDUhk8SDww6U10sttGc4Vjb+1xBwA4yow==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
dependencies:
|
||||
'@volar/vue-language-core': 0.40.13
|
||||
'@volar/vue-typescript': 0.40.13
|
||||
'@volar/vue-language-core': 1.0.2
|
||||
'@volar/vue-typescript': 1.0.2
|
||||
typescript: 4.8.4
|
||||
dev: true
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Use loading component. This way it's easier for us to refactor the Use loading component. This way it's easier for us to refactor the `is-loading` styles from bulma later.
|
||||
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"
|
||||
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.
|
||||
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'
|
|||
|
||||
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.
|
||||
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()
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
picky: If you use a default value there is no need to define picky: If you use a default value there is no need to define `required`
|
||||
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)
|
||||
|
||||
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?)
|
||||
const tasks = ref<Map<TaskModel['id'], TaskModel>>(new Map())
|
||||
const tasks = ref<Map<ITask['id'], ITask>>(new Map())
|
||||
const ganttBars = ref<GanttBarObject[][]>([])
|
||||
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
define types define types
konrad
commented
Done. Done.
|
||||
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 {
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
define types define types
|
||||
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'],
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
This and the three lines below should be combined to one watcher that trigger immediately.
Something like:
This and the three lines below should be combined to one watcher that trigger immediately.
`loadTasks` should accept these three as params, so that it's clear that these are needed to reload.
Something like:
```
watchEffect(() => loadTasks({
dateTo: props.dateTo,
dateFrom: props.dateFrom,
showTasksWithoutDates: props.showTasksWithoutDates,
})
```
|
||||
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']) {
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Add type: https://vuejs.org/guide/typescript/composition-api.html#typing-template-refs
|
||||
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: {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { defineAsyncComponent, type AsyncComponentLoader, type AsyncComponentOptions, type Component, type ComponentPublicInstance } from 'vue'
|
||||
konrad marked this conversation as resolved
Outdated
konrad
commented
Is this new helper relevant for the gantt chart? Is this new helper relevant for the gantt chart?
dpschen
commented
I load the GanttChart async. This way the chart library can be loaded when necessary. I load the GanttChart async. This way the chart library can be loaded when necessary.
konrad
commented
Okay I think this could be improved/simplified further by just loading the whole gantt view async as that's the only place where the chart is loaded. But that's something I'd leave for another PR. Okay I think this could be improved/simplified further by just loading the whole gantt view async as that's the only place where the chart is loaded. But that's something I'd leave for another PR.
dpschen
commented
I thought GanttChart makes more sense, since it's our custom wrapper where we also load some other stuff :) This way all additional load gets loaded async while the page load is really fast, since lightweight. I thought GanttChart makes more sense, since it's our custom wrapper where we also load some other stuff :) This way all additional load gets loaded async while the page load is really fast, since lightweight.
|
||||
|
||||
import ErrorComponent from '@/components/misc/error.vue'
|
||||
import LoadingComponent from '@/components/misc/loading.vue'
|
||||
|
||||
const DEFAULT_TIMEOUT = 60000
|
||||
|
||||
export function createAsyncComponent<T extends Component = {
|
||||
new (): ComponentPublicInstance;
|
||||
}>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
|
||||
if (typeof source === 'function') {
|
||||
source = { loader: source }
|
||||
}
|
||||
|
||||
return defineAsyncComponent({
|
||||
...source,
|
||||
loadingComponent: LoadingComponent,
|
||||
errorComponent: ErrorComponent,
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {format, formatDistanceToNow, formatISO as formatISOfns} from 'date-fns'
|
||||
|
||||
// FIXME: support all locales and load dynamically
|
||||
import {enGB, de, fr, ru} from 'date-fns/locale'
|
||||
|
||||
import {i18n} from '@/i18n'
|
||||
|
|
|
@ -1,19 +1,9 @@
|
|||
import {computed, ref, watch} from 'vue'
|
||||
import {createI18n} from 'vue-i18n'
|
||||
import langEN from './lang/en.json'
|
||||
|
||||
export const i18n = createI18n({
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en',
|
||||
legacy: true,
|
||||
globalInjection: true,
|
||||
allowComposition: true,
|
||||
messages: {
|
||||
en: langEN,
|
||||
},
|
||||
})
|
||||
|
||||
export const availableLanguages = {
|
||||
en: 'English',
|
||||
export const SUPPORTED_LOCALES = {
|
||||
'en': 'English',
|
||||
'de-DE': 'Deutsch',
|
||||
'de-swiss': 'Schwizertütsch',
|
||||
'ru-RU': 'Русский',
|
||||
|
@ -24,41 +14,58 @@ export const availableLanguages = {
|
|||
'pl-PL': 'Polski',
|
||||
'nl-NL': 'Nederlands',
|
||||
'pt-PT': 'Português',
|
||||
}
|
||||
'zh-CN': 'Chinese',
|
||||
} as Record<string, string>
|
||||
|
||||
const loadedLanguages = ['en'] // our default language that is preloaded
|
||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
||||
|
||||
const setI18nLanguage = (lang: string) => {
|
||||
export const DEFAULT_LANGUAGE: SupportedLocale= 'en'
|
||||
|
||||
export type ISOLanguage = string
|
||||
|
||||
export const DAYJS_LOCALE_MAPPING = {
|
||||
'de-swiss': 'de-AT',
|
||||
} as Record<SupportedLocale, ISOLanguage>
|
||||
|
||||
export const i18n = createI18n({
|
||||
locale: DEFAULT_LANGUAGE, // set locale
|
||||
fallbackLocale: DEFAULT_LANGUAGE,
|
||||
legacy: true,
|
||||
globalInjection: true,
|
||||
allowComposition: true,
|
||||
inheritLocale: true,
|
||||
messages: {
|
||||
en: langEN,
|
||||
} as Record<SupportedLocale, any>,
|
||||
})
|
||||
|
||||
function setI18nLanguage(lang: SupportedLocale): SupportedLocale {
|
||||
i18n.global.locale = lang
|
||||
document.documentElement.lang =lang
|
||||
document.documentElement.lang = lang
|
||||
return lang
|
||||
}
|
||||
|
||||
export const loadLanguageAsync = lang => {
|
||||
export async function loadLanguageAsync(lang: SupportedLocale) {
|
||||
if (!lang) {
|
||||
return
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (
|
||||
// If the same language
|
||||
i18n.global.locale === lang ||
|
||||
// If the language was already loaded
|
||||
loadedLanguages.includes(lang)
|
||||
i18n.global.availableLocales.includes(lang)
|
||||
) {
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
// If the language hasn't been loaded yet
|
||||
return import(`./lang/${lang}.json`).then(
|
||||
messages => {
|
||||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
loadedLanguages.push(lang)
|
||||
return setI18nLanguage(lang)
|
||||
},
|
||||
)
|
||||
const messages = await import(`./lang/${lang}.json`)
|
||||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
export const getCurrentLanguage = () => {
|
||||
export function getCurrentLanguage(): SupportedLocale {
|
||||
const savedLanguage = localStorage.getItem('language')
|
||||
if (savedLanguage !== null) {
|
||||
return savedLanguage
|
||||
|
@ -66,20 +73,48 @@ export const getCurrentLanguage = () => {
|
|||
|
||||
const browserLanguage = navigator.language || navigator.userLanguage
|
||||
|
||||
for (const k in availableLanguages) {
|
||||
if (browserLanguage[k] === browserLanguage || k.startsWith(browserLanguage + '-')) {
|
||||
return k
|
||||
}
|
||||
}
|
||||
const language: SupportedLocale | undefined = Object.keys(SUPPORTED_LOCALES).find(langKey => {
|
||||
return langKey === browserLanguage || langKey.startsWith(browserLanguage + '-')
|
||||
})
|
||||
|
||||
return 'en'
|
||||
return language || DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
export const saveLanguage = (lang: string) => {
|
||||
export function saveLanguage(lang: SupportedLocale) {
|
||||
localStorage.setItem('language', lang)
|
||||
setLanguage()
|
||||
}
|
||||
|
||||
export const setLanguage = () => {
|
||||
loadLanguageAsync(getCurrentLanguage())
|
||||
export function setLanguage() {
|
||||
return loadLanguageAsync(getCurrentLanguage())
|
||||
}
|
||||
|
||||
import type dayjs from 'dayjs'
|
||||
|
||||
export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) {
|
||||
const dayjsLanguageLoaded = ref(false)
|
||||
watch(
|
||||
() => i18n.global.locale,
|
||||
async (currentLanguage: string) => {
|
||||
if (!dayjsGlobal) {
|
||||
return
|
||||
}
|
||||
const dayjsLanguageCode = DAYJS_LOCALE_MAPPING[currentLanguage.toLowerCase()] || currentLanguage.toLowerCase()
|
||||
dayjsLanguageLoaded.value = dayjsGlobal.locale() === dayjsLanguageCode
|
||||
if (dayjsLanguageLoaded.value) {
|
||||
return
|
||||
}
|
||||
console.log('foo')
|
||||
await import(`../../node_modules/dayjs/locale/${dayjsLanguageCode}.js`)
|
||||
konrad
commented
This statement did not work in my tests. IMHO we could merge the gantt chart without proper locale support and rework that later. This PR is already a lot bigger than it should be. This statement did not work in my tests. IMHO we could merge the gantt chart without proper locale support and rework that later. This PR is already a lot bigger than it should be.
dpschen
commented
Agree! I found out how to make this work. Turned out that more work is needed since after the locale support works the Agree!
I found out how to make this work. Turned out that more work is needed since after the locale support works the `weekdayFromTimeLabel` doesn't anymore. So better to add this at a later point.
|
||||
console.log('bar')
|
||||
dayjsGlobal.locale(dayjsLanguageCode)
|
||||
dayjsLanguageLoaded.value = true
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
// we export the loading state since that's easier to work with
|
||||
const isLoading = computed(() => !dayjsLanguageLoaded.value)
|
||||
|
||||
return isLoading
|
||||
}
|
|
@ -35,7 +35,7 @@ import MigrationComponent from '../views/migrator/Migrate.vue'
|
|||
import MigrateServiceComponent from '../views/migrator/MigrateService.vue'
|
||||
// List Views
|
||||
import ListList from '../views/list/ListList.vue'
|
||||
import ListGantt from '../views/list/ListGantt.vue'
|
||||
const ListGantt = () => import('../views/list/ListGantt.vue')
|
||||
import ListTable from '../views/list/ListTable.vue'
|
||||
import ListKanban from '../views/list/ListKanban.vue'
|
||||
const ListInfo = () => import('../views/list/ListInfo.vue')
|
||||
|
@ -379,7 +379,12 @@ const router = createRouter({
|
|||
name: 'list.gantt',
|
||||
component: ListGantt,
|
||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
props: route => ({
|
||||
listId: Number(route.params.listId as string),
|
||||
dateFrom: route.query.dateFrom as string,
|
||||
dateTo: route.query.dateTo as string,
|
||||
showTasksWithoutDates: Boolean(route.query.showTasksWithoutDates),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/table',
|
||||
|
|
|
@ -2,8 +2,9 @@ import {AuthenticatedHTTPFactory} from '@/http-common'
|
|||
import type {Method} from 'axios'
|
||||
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
import AbstractModel, { type IAbstract } from '@/models/abstractModel'
|
||||
import type { Right } from '@/constants/rights'
|
||||
import AbstractModel from '@/models/abstractModel'
|
||||
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||
import type {Right} from '@/constants/rights'
|
||||
import type {IFile} from '@/modelTypes/IFile'
|
||||
|
||||
interface Paths {
|
||||
|
|
|
@ -2,7 +2,7 @@ import AbstractService from './abstractService'
|
|||
import TaskModel from '../models/task'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export default class TaskCollectionService extends AbstractService {
|
||||
export default class TaskCollectionService extends AbstractService<ITask> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/lists/{listId}/tasks',
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
<div class="field">
|
||||
<label class="label" for="range">{{ $t('list.gantt.range') }}</label>
|
||||
<div class="control">
|
||||
<flat-pickr
|
||||
<Foo
|
||||
ref="flatPickerEl"
|
||||
:config="flatPickerConfig"
|
||||
class="input"
|
||||
id="range"
|
||||
:placeholder="$t('list.gantt.range')"
|
||||
v-model="range"
|
||||
v-model="flatPickerDateRange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,13 +26,15 @@
|
|||
<template #default>
|
||||
<div class="gantt-chart-container">
|
||||
<card :padding="false" class="has-overflow">
|
||||
|
||||
<gantt-chart
|
||||
:date-from="dateFrom"
|
||||
:date-to="dateTo"
|
||||
<pre>{{dateRange}}</pre>
|
||||
<pre>{{new Date(dateRange.dateFrom).toLocaleDateString()}}</pre>
|
||||
<pre>{{new Date(dateRange.dateTo).toLocaleDateString()}}</pre>
|
||||
<!-- <gantt-chart
|
||||
v-if="false"
|
||||
:date-range="dateRange"
|
||||
:list-id="props.listId"
|
||||
:show-tasks-without-dates="showTasksWithoutDates"
|
||||
/>
|
||||
/> -->
|
||||
|
||||
</card>
|
||||
</div>
|
||||
|
@ -40,41 +43,206 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed} from 'vue'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import {computed, ref, type PropType} from 'vue'
|
||||
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
|
||||
// import type FlatPickr from 'vue-flatpickr-component'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {format} from 'date-fns'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
import ListWrapper from './ListWrapper.vue'
|
||||
import GanttChart from '@/components/tasks/gantt-chart.vue'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import {format} from 'date-fns'
|
||||
|
||||
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
|
||||
|
||||
type DateKebab = `${string}-${string}-${string}`
|
||||
|
||||
const GanttChart = createAsyncComponent(() => import('@/components/tasks/gantt-chart.vue'))
|
||||
|
||||
const props = defineProps({
|
||||
listId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dateFrom: {
|
||||
type: String as PropType<DateKebab>,
|
||||
},
|
||||
dateTo: {
|
||||
type: String as PropType<DateKebab>,
|
||||
},
|
||||
showTasksWithoutDates: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Should this update? Should this update?
konrad
commented
No, doesn't even need to be ref. No, doesn't even need to be ref.
|
||||
})
|
||||
|
||||
const showTasksWithoutDates = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
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 showTasksWithoutDates = computed({
|
||||
get: () => props.showTasksWithoutDates,
|
||||
set: (value) => router.push({ query: {
|
||||
...route.query,
|
||||
showTasksWithoutDates: String(value),
|
||||
}}),
|
||||
})
|
||||
|
||||
function parseKebabDate(kebabDate: DateKebab | undefined, fallback: () => Date): Date {
|
||||
try {
|
||||
|
||||
if (!kebabDate) {
|
||||
throw new Error('No value')
|
||||
}
|
||||
const dateValues = kebabDate.split('-')
|
||||
const [, monthString, dateString] = dateValues
|
||||
const [year, month, date] = dateValues.map(val => Number(val))
|
||||
const dateValuesAreValid = (
|
||||
!Number.isNaN(year) &&
|
||||
monthString.length >= 1 && monthString.length <= 2 &&
|
||||
!Number.isNaN(month) &&
|
||||
month >= 1 && month <= 12 &&
|
||||
dateString.length >= 1 && dateString.length <= 31 &&
|
||||
!Number.isNaN(date) &&
|
||||
date >= 1 && date <= 31
|
||||
)
|
||||
if (!dateValuesAreValid) {
|
||||
throw new Error('Invalid date values')
|
||||
}
|
||||
return new Date(year, month, date)
|
||||
} catch(e) {
|
||||
// ignore nonsense route queries
|
||||
return fallback()
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DATEFROM_DAY_OFFSET = 0
|
||||
// const DEFAULT_DATEFROM_DAY_OFFSET = -15
|
||||
const DEFAULT_DATETO_DAY_OFFSET = +55
|
||||
// const DEFAULT_DATETO_DAY_OFFSET = +55
|
||||
|
||||
const now = new Date()
|
||||
const defaultFrom = format(new Date((new Date()).setDate(now.getDate() - 15)), 'yyyy-LL-dd')
|
||||
const defaultTo = format(new Date((new Date()).setDate(now.getDate() + 55)), 'yyyy-LL-dd')
|
||||
const range = ref(`${defaultFrom} to ${defaultTo}`)
|
||||
|
||||
// TODO: only update once both dates are available (maybe use a watcher + refs instead?)
|
||||
const dateFrom = computed(() => range.value?.split(' to ')[0] ?? defaultFrom)
|
||||
const dateTo = computed(() => range.value?.split(' to ')[1] ?? defaultTo)
|
||||
function getDefaultDateFrom() {
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + DEFAULT_DATEFROM_DAY_OFFSET)
|
||||
}
|
||||
|
||||
function getDefaultDateTo() {
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + DEFAULT_DATETO_DAY_OFFSET)
|
||||
}
|
||||
|
||||
let isChangingRoute = ref<ReturnType<typeof router.push> | false>(false)
|
||||
|
||||
const count = ref(0)
|
||||
|
||||
const dateRange = computed<{
|
||||
dateFrom: string
|
||||
dateTo: string
|
||||
}>({
|
||||
get: () => ({
|
||||
dateFrom: parseKebabDate(props.dateFrom, getDefaultDateFrom).toISOString(),
|
||||
dateTo: parseKebabDate(props.dateTo, getDefaultDateTo).toISOString(),
|
||||
}),
|
||||
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')
|
||||
const queryDateTo = format(new Date(dateTo || getDefaultDateTo()), 'yyyy-LL-dd')
|
||||
|
||||
console.log(dateFrom, 'dateFrom')
|
||||
console.log(dateRange.value.dateFrom, 'dateRange.value.dateFrom')
|
||||
console.log(dateTo, 'dateTo')
|
||||
console.log(dateRange.value.dateTo, 'dateRange.value.dateTo')
|
||||
|
||||
if (queryDateFrom === route.query.dateFrom || queryDateTo === route.query.dateTo) {
|
||||
console.log('is same date')
|
||||
// only set if the value has changed
|
||||
return
|
||||
}
|
||||
console.log('change url to', {
|
||||
query: {
|
||||
...route.query,
|
||||
dateFrom: format(new Date(dateFrom), 'yyyy-LL-dd'),
|
||||
dateTo: format(new Date(dateTo), 'yyyy-LL-dd'),
|
||||
}
|
||||
})
|
||||
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]
|
||||
|
||||
function getCurrentDateRangeFromFlatpicker() {
|
||||
return flatPickerEl.value.fp.selectedDates.map((date: Date) => date?.toISOString())
|
||||
}
|
||||
|
||||
const flatPickerEl = ref<typeof FlatPickr | null>(null)
|
||||
const flatPickerDateRange = computed({
|
||||
get: () => ([
|
||||
dateRange.value.dateFrom,
|
||||
dateRange.value.dateTo
|
||||
]),
|
||||
set(newVal) {
|
||||
// set([dateFrom, dateTo]) {
|
||||
// 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
|
||||
!dateTo ||
|
||||
// only set if the value has changed
|
||||
dateRange.value.dateFrom === dateFrom &&
|
||||
dateRange.value.dateTo === dateTo
|
||||
) {
|
||||
return
|
||||
}
|
||||
// dateRange.value = {dateFrom, dateTo}
|
||||
}
|
||||
})
|
||||
|
||||
const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ssZ[Z]"
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatShort'),
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d',
|
||||
// dateFornat: ISO_DATE_FORMAT,
|
||||
// dateFormat: 'Y-m-d',
|
||||
defaultDate: initialDateRange,
|
||||
enableTime: false,
|
||||
mode: 'range',
|
||||
locale: {
|
||||
|
|
|
@ -158,7 +158,7 @@ import {PrefixMode} from '@/modules/parseTaskText'
|
|||
|
||||
import ListSearch from '@/components/tasks/partials/listSearch.vue'
|
||||
|
||||
import {availableLanguages} from '@/i18n'
|
||||
import {SUPPORTED_LOCALES} from '@/i18n'
|
||||
import {playSoundWhenDoneKey, playPopSound} from '@/helpers/playPop'
|
||||
import {getQuickAddMagicMode, setQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
|
@ -227,7 +227,7 @@ const authStore = useAuthStore()
|
|||
const settings = ref({...authStore.settings})
|
||||
const id = ref(createRandomID())
|
||||
const availableLanguageOptions = ref(
|
||||
Object.entries(availableLanguages)
|
||||
Object.entries(SUPPORTED_LOCALES)
|
||||
.map(l => ({code: l[0], title: l[1]}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title)),
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/i18n/lang/*.json"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
|
What are the flatpickr changes doing in this PR?