feat: add date math for filters #1342
|
@ -1,4 +1,4 @@
|
|||
export function parseDateOrString(rawValue: string, fallback: any) {
|
||||
export function parseDateOrString(rawValue: string, fallback: any): string | Date {
|
||||
konrad marked this conversation as resolved
Outdated
|
||||
if (typeof rawValue === 'undefined') {
|
||||
return fallback
|
||||
}
|
||||
|
|
|
@ -37,197 +37,183 @@
|
|||
<div v-else :class="{ 'is-loading': loading}" class="spinner"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {dateRanges} from '@/components/date/dateRanges'
|
||||
import SingleTaskInList from '@/components/tasks/partials/singleTaskInList'
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox'
|
||||
<script setup lang="ts">
|
||||
import {dateRanges} from '@/components/date/dateRanges'
|
||||
import SingleTaskInList from '@/components/tasks/partials/singleTaskInList.vue'
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
import {mapState, useStore} from 'vuex'
|
||||
import {computed, ref, watchEffect} from 'vue'
|
||||
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
|
||||
|
||||
import LlamaCool from '@/assets/llama-cool.svg?component'
|
||||
import DatepickerWithRange from '@/components/date/datepickerWithRange'
|
||||
import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
|
||||
import TaskModel from '@/models/task'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {formatDate} from '@/helpers/time/formatDate'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {setTitle} from './helpers/setTitle'
|
||||
|
||||
function getNextWeekDate() {
|
||||
return new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ShowTasks',
|
||||
components: {
|
||||
DatepickerWithRange,
|
||||
Fancycheckbox,
|
||||
SingleTaskInList,
|
||||
LlamaCool,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tasks: [],
|
||||
showNothingToDo: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
showAll: Boolean,
|
||||
},
|
||||
created() {
|
||||
this.loadPendingTasks()
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => this.showNothingToDo = true, 100)
|
||||
},
|
||||
watch: {
|
||||
'$route': {
|
||||
handler: 'loadPendingTasks',
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dateFrom() {
|
||||
return parseDateOrString(this.$route.query.from, new Date())
|
||||
},
|
||||
dateTo() {
|
||||
return parseDateOrString(this.$route.query.to, getNextWeekDate())
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Readd route props Readd route props
konrad
commented
Changed it to route props. However, we still have a dependency on the router: every time when seleting a date in ShowTasks it will push the change to the route. I don't know how to change that without massively overengeneering everything so I'd say we leave it at that. Changed it to route props.
However, we still have a dependency on the router: every time when seleting a date in ShowTasks it will push the change to the route. I don't know how to change that without massively overengeneering everything so I'd say we leave it at that.
dpschen
commented
I think it's fine for now. I thought a while about this but also don't have a better soltion for this at the moment (except the v-model version) I think it's fine for now. I thought a while about this but also don't have a better soltion for this at the moment (except the v-model version)
|
||||
},
|
||||
showNulls() {
|
||||
return this.$route.query.showNulls === 'true'
|
||||
},
|
||||
showOverdue() {
|
||||
return this.$route.query.showOverdue === 'true'
|
||||
},
|
||||
pageTitle() {
|
||||
let title = ''
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const {t} = useI18n()
|
||||
|
||||
// We need to define "key" because it is the first parameter in the array and we need the second
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const predefinedRange = Object.entries(dateRanges).find(([key, value]) => this.dateFrom === value[0] && this.dateTo === value[1])
|
||||
if (typeof predefinedRange !== 'undefined') {
|
||||
title = this.$t(`input.datepickerRange.ranges.${predefinedRange[0]}`)
|
||||
} else {
|
||||
title = this.showAll
|
||||
? this.$t('task.show.titleCurrent')
|
||||
: this.$t('task.show.fromuntil', {
|
||||
from: this.format(this.dateFrom, 'PPP'),
|
||||
until: this.format(this.dateTo, 'PPP'),
|
||||
})
|
||||
}
|
||||
const tasks = ref<TaskModel[]>([])
|
||||
const showNothingToDo = ref<boolean>(false)
|
||||
|
||||
this.setTitle(title)
|
||||
setTimeout(() => showNothingToDo.value = true, 100)
|
||||
|
||||
return title
|
||||
},
|
||||
tasksSorted() {
|
||||
// Sort all tasks to put those with a due date before the ones without a due date, the
|
||||
// soonest before the later ones.
|
||||
// We can't use the api sorting here because that sorts tasks with a due date after
|
||||
// ones without a due date.
|
||||
const props = defineProps({
|
||||
showAll: Boolean,
|
||||
})
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Wouldn't it be better to make showAll a computed that gets autoset when dateFrom and dateTo doesn't contain a value? Wouldn't it be better to make showAll a computed that gets autoset when dateFrom and dateTo doesn't contain a value?
konrad
commented
Excellent idea. Changed it! Excellent idea. Changed it!
|
||||
|
||||
const tasksWithDueDate = [...this.tasks]
|
||||
.filter(t => t.dueDate !== null)
|
||||
.sort((a, b) => {
|
||||
const sortByDueDate = a.dueDate - b.dueDate
|
||||
return sortByDueDate === 0
|
||||
? b.id - a.id
|
||||
: sortByDueDate
|
||||
})
|
||||
const tasksWithoutDueDate = [...this.tasks]
|
||||
.filter(t => t.dueDate === null)
|
||||
const dateFrom = computed<Date | string>(() => parseDateOrString(route.query.from as string, new Date()))
|
||||
const dateTo = computed<Date | string>(() => parseDateOrString(route.query.to as string, getNextWeekDate()))
|
||||
const showNulls = computed(() => route.query.showNulls === 'true')
|
||||
const showOverdue = computed(() => route.query.showOverdue === 'true')
|
||||
const pageTitle = computed(() => {
|
||||
let title = ''
|
||||
|
||||
return [
|
||||
...tasksWithDueDate,
|
||||
...tasksWithoutDueDate,
|
||||
]
|
||||
},
|
||||
hasTasks() {
|
||||
return this.tasks && this.tasks.length > 0
|
||||
},
|
||||
...mapState({
|
||||
userAuthenticated: state => state.auth.authenticated,
|
||||
loading: state => state[LOADING] && state[LOADING_MODULE] === 'tasks',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
setDate({dateFrom, dateTo}) {
|
||||
this.$router.push({
|
||||
name: this.$route.name,
|
||||
query: {
|
||||
from: dateFrom ?? this.dateFrom,
|
||||
to: dateTo ?? this.dateTo,
|
||||
showOverdue: this.showOverdue,
|
||||
showNulls: this.showNulls,
|
||||
},
|
||||
// We need to define "key" because it is the first parameter in the array and we need the second
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const predefinedRange = Object.entries(dateRanges).find(([key, value]) => dateFrom.value === value[0] && dateTo.value === value[1])
|
||||
if (typeof predefinedRange !== 'undefined') {
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
get value from props to remove dependency on route. Removing dependency from router makes the components easier reusable nested inside another view (which is what we do). get value from props to remove dependency on route. Removing dependency from router makes the components easier reusable nested inside another view (which is what we do).
|
||||
title = t(`input.datepickerRange.ranges.${predefinedRange[0]}`)
|
||||
} else {
|
||||
title = props.showAll
|
||||
? t('task.show.titleCurrent')
|
||||
: t('task.show.fromuntil', {
|
||||
from: formatDate(dateFrom.value, 'PPP'),
|
||||
until: formatDate(dateTo.value, 'PPP'),
|
||||
})
|
||||
},
|
||||
setShowOverdue(show) {
|
||||
this.$router.push({
|
||||
name: this.$route.name,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
showOverdue: show,
|
||||
},
|
||||
})
|
||||
},
|
||||
setShowNulls(show) {
|
||||
this.$router.push({
|
||||
name: this.$route.name,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
showNulls: show,
|
||||
},
|
||||
})
|
||||
},
|
||||
async loadPendingTasks() {
|
||||
// Since this route is authentication only, users would get an error message if they access the page unauthenticated.
|
||||
// Since this component is mounted as the home page before unauthenticated users get redirected
|
||||
// to the login page, they will almost always see the error message.
|
||||
if (!this.userAuthenticated) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Unsure: I think eslint doesn't complain here if you use underscore ( Unsure: I think eslint doesn't complain here if you use underscore (`_`) to spread the unused variable.
dpschen
commented
Add Add `?.[0]` so we return the key, which is what we actually want to use and adjust variable name.
konrad
commented
Nope, does not seem to have an effect. > Unsure: I think eslint doesn't complain here if you use underscore (_) to spread the unused variable.
Nope, does not seem to have an effect.
konrad
commented
Done. > Add ?.[0] so we return the key, which is what we actually want to use and adjust variable name.
Done.
dpschen
commented
Interesting! > Nope, does not seem to have an effect.
Interesting!
|
||||
const params = {
|
||||
sort_by: ['due_date', 'id'],
|
||||
order_by: ['desc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: [false],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
filter_include_nulls: this.showNulls,
|
||||
}
|
||||
return title
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Remove title var and return directly:
Remove title var and return directly:
```js
if (typeof predefinedRange !== 'undefined') {
return t(`input.datepickerRange.ranges.${predefinedRangeKey}`)
} else {
return showAll
? t('task.show.titleCurrent')
: t('task.show.fromuntil', {
from: formatDate(dateFrom, 'PPP'),
until: formatDate(dateTo, 'PPP'),
})
}
konrad
commented
Done. Done.
|
||||
})
|
||||
const tasksSorted = computed(() => {
|
||||
// Sort all tasks to put those with a due date before the ones without a due date, the
|
||||
// soonest before the later ones.
|
||||
// We can't use the api sorting here because that sorts tasks with a due date after
|
||||
// ones without a due date.
|
||||
|
||||
if (!this.showAll) {
|
||||
params.filter_by.push('due_date')
|
||||
params.filter_value.push(this.dateTo)
|
||||
params.filter_comparator.push('less')
|
||||
const tasksWithDueDate = [...tasks.value]
|
||||
.filter(t => t.dueDate !== null)
|
||||
.sort((a, b) => {
|
||||
const sortByDueDate = a.dueDate - b.dueDate
|
||||
return sortByDueDate === 0
|
||||
? b.id - a.id
|
||||
: sortByDueDate
|
||||
})
|
||||
const tasksWithoutDueDate = [...tasks.value]
|
||||
.filter(t => t.dueDate === null)
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
This is a sideeffect inside a computed. Remove this.
This is a sideeffect inside a computed. Remove this.
Better watch the computed and react to that:
```js
watchEffect(() => setTitle(pageTitle.value))
```
konrad
commented
Done. Done.
|
||||
|
||||
// NOTE: Ideally we could also show tasks with a start or end date in the specified range, but the api
|
||||
// is not capable (yet) of combining multiple filters with 'and' and 'or'.
|
||||
return [
|
||||
...tasksWithDueDate,
|
||||
...tasksWithoutDueDate,
|
||||
]
|
||||
})
|
||||
const hasTasks = computed(() => tasks && tasks.value.length > 0)
|
||||
const userAuthenticated = computed(() => store.state.auth.authenticated)
|
||||
const loading = computed(() => store.state[LOADING] && store.state[LOADING_MODULE] === 'tasks')
|
||||
|
||||
if (!this.showOverdue) {
|
||||
params.filter_by.push('due_date')
|
||||
params.filter_value.push(this.dateFrom)
|
||||
params.filter_comparator.push('greater')
|
||||
}
|
||||
}
|
||||
|
||||
this.tasks = await this.$store.dispatch('tasks/loadTasks', params)
|
||||
},
|
||||
|
||||
// FIXME: this modification should happen in the store
|
||||
updateTasks(updatedTask) {
|
||||
for (const t in this.tasks) {
|
||||
if (this.tasks[t].id === updatedTask.id) {
|
||||
this.tasks[t] = updatedTask
|
||||
// Move the task to the end of the done tasks if it is now done
|
||||
if (updatedTask.done) {
|
||||
this.tasks.splice(t, 1)
|
||||
this.tasks.push(updatedTask)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
interface dateStrings {
|
||||
from: string,
|
||||
to: string,
|
||||
}
|
||||
|
||||
function setDate({from, to}: dateStrings) {
|
||||
router.push({
|
||||
name: route.name as string,
|
||||
query: {
|
||||
from: from ?? dateFrom,
|
||||
to: to ?? dateTo,
|
||||
showOverdue: showOverdue.value ? 'true' : 'false',
|
||||
showNulls: showNulls.value ? 'true' : 'false',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function setShowOverdue(show: boolean) {
|
||||
router.push({
|
||||
name: route.name as string,
|
||||
query: {
|
||||
...route.query,
|
||||
showOverdue: show ? 'true' : 'false',
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Mhh that is a bit annoying that we have to convert to strings here :/ We might need to abstract this in the future, so let's keep that in the back of our head =) Mhh that is a bit annoying that we have to convert to strings here :/
But I also don't have a better idea for now.
Right now this might be still fine, but I remember similar usecases where the conversion from state object to the url representation was quite complex.
We might need to abstract this in the future, so let's keep that in the back of our head =)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function setShowNulls(show: boolean) {
|
||||
router.push({
|
||||
name: route.name as string,
|
||||
query: {
|
||||
...route.query,
|
||||
showNulls: show ? 'true' : 'false',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function loadPendingTasks(from:string, to:string) {
|
||||
// Since this route is authentication only, users would get an error message if they access the page unauthenticated.
|
||||
// Since this component is mounted as the home page before unauthenticated users get redirected
|
||||
// to the login page, they will almost always see the error message.
|
||||
if (!userAuthenticated) {
|
||||
return
|
||||
}
|
||||
|
||||
const params = {
|
||||
sort_by: ['due_date', 'id'],
|
||||
order_by: ['desc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
This should be marked as a Hack because it's something that shouldn't happen. This should be marked as a Hack because it's something that shouldn't happen.
When I tried to change the auth these days I tried to fix this, but got tangled.
We should try to solve this in the future because else we will just add more and more hacks =)
konrad
commented
Done. Done.
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
filter_include_nulls: showNulls.value,
|
||||
}
|
||||
|
||||
if (!props.showAll) {
|
||||
params.filter_by.push('due_date')
|
||||
params.filter_value.push(to)
|
||||
params.filter_comparator.push('less')
|
||||
|
||||
// NOTE: Ideally we could also show tasks with a start or end date in the specified range, but the api
|
||||
// is not capable (yet) of combining multiple filters with 'and' and 'or'.
|
||||
|
||||
if (!showOverdue.value) {
|
||||
params.filter_by.push('due_date')
|
||||
params.filter_value.push(from)
|
||||
params.filter_comparator.push('greater')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.value = await store.dispatch('tasks/loadTasks', params)
|
||||
}
|
||||
|
||||
// FIXME: this modification should happen in the store
|
||||
function updateTasks(updatedTask) {
|
||||
for (const t in tasks.value) {
|
||||
if (tasks.value[t].id === updatedTask.id) {
|
||||
tasks.value[t] = updatedTask
|
||||
// Move the task to the end of the done tasks if it is now done
|
||||
if (updatedTask.done) {
|
||||
tasks.value.splice(t, 1)
|
||||
tasks.value.push(updatedTask)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Instead of creating the params already in snake_case: Instead of creating the params already in snake_case:
Wait until the last moment (ideally abstracted away from a apiClient) until you convert params from camelCase to snake_case, needed by the api. This way we use consequently use camelCase in most areas of the frontend – which is what you usually do in JS.
konrad
commented
I agree. However, this whole filter thing is a mess right now, not only here. I'd like to fix this everywhere at once at some point, simplifying the filter handling in the process as well. That being said, I've changed there here for now. I agree. However, this whole filter thing is a mess right now, not only here. I'd like to fix this everywhere at once at some point, simplifying the filter handling in the process as well.
That being said, I've changed there here for now.
|
||||
|
||||
watchEffect(() => loadPendingTasks(dateFrom.value as string, dateTo.value as string))
|
||||
// loadPendingTasks()
|
||||
watchEffect(() => setTitle(pageTitle))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
Maybe we should just move this to the emits section of the date range component? To avoid callees requiring to import it every time?
Yes that makes sense!
Since we're now using route props where we need this function I think we'll have to leave it as is.