chore(gantt): wip daterange
This commit is contained in:
parent
3b244dfdbe
commit
9f146c8c7f
@ -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",
|
||||
|
79
pnpm-lock.yaml
generated
79
pnpm-lock.yaml
generated
@ -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'
|
||||
|
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: {
|
||||
|
21
src/helpers/createAsyncComponent.ts
Normal file
21
src/helpers/createAsyncComponent.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { defineAsyncComponent, type AsyncComponentLoader, type AsyncComponentOptions, type Component, type ComponentPublicInstance } from 'vue'
|
||||
|
||||
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 => {
|
||||
const messages = await import(`./lang/${lang}.json`)
|
||||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
loadedLanguages.push(lang)
|
||||
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`)
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
const showTasksWithoutDates = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user