feat(reminders): add proper time picker for relative dates
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
kolaente 2023-06-09 13:19:47 +02:00
parent 7b2a688b6e
commit 95487d7569
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
7 changed files with 156 additions and 221 deletions

View File

@ -151,7 +151,6 @@ function setDateValue(dateString: string | Date | null) {
function updateData() { function updateData() {
changed.value = true changed.value = true
console.log('emit', date.value)
emit('update:modelValue', date.value) emit('update:modelValue', date.value)
} }

View File

@ -3,29 +3,30 @@
{{ reminderText }} {{ reminderText }}
<div class="presets"> <div class="options" v-if="showFormSwitch === null">
<BaseButton <BaseButton
v-for="p in presets" v-for="p in presets"
> >
{{ formatReminder(p) }} {{ formatReminder(p) }}
</BaseButton> </BaseButton>
<BaseButton> <BaseButton @click="showFormSwitch = 'relative'">
Custom Custom
</BaseButton> </BaseButton>
<BaseButton @click="showFormSwitch = 'absolute'">
Date
</BaseButton>
</div> </div>
<ReminderPeriod <ReminderPeriod
v-if="showRelativeReminder" v-if="showFormSwitch === 'relative'"
v-model="reminder" v-model="reminder"
:disabled="disabled" @update:modelValue="emit('update:modelValue', reminder)"
@update:modelValue="emit('update:modelValue', reminder.value)"
/> />
<Datepicker <DatepickerInline
v-if="showAbsoluteReminder" v-if="showFormSwitch === 'absolute'"
v-model="reminderDate" v-model="reminderDate"
:disabled="disabled" @update:modelValue="setReminderDate"
@close-on-change="setReminderDate"
/> />
</div> </div>
</template> </template>
@ -34,16 +35,17 @@
import {computed, ref, watch, type PropType} from 'vue' import {computed, ref, watch, type PropType} from 'vue'
import {toRef} from '@vueuse/core' import {toRef} from '@vueuse/core'
import {SECONDS_A_DAY} from '@/constants/date' import {SECONDS_A_DAY} from '@/constants/date'
import {secondsToPeriod} from '@/helpers/time/period' import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import {secondsToPeriod} from '@/helpers/time/period'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder' import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import {formatDateShort} from '@/helpers/time/formatDate' import {formatDateShort} from '@/helpers/time/formatDate'
import Datepicker from '@/components/input/datepicker.vue'
import ReminderPeriod from '@/components/tasks/partials/reminder-period.vue'
import TaskReminderModel from '@/models/taskReminder'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo' import DatepickerInline from '@/components/input/datepickerInline.vue'
import ReminderPeriod from '@/components/tasks/partials/reminder-period.vue'
import TaskReminderModel from '@/models/taskReminder'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -65,20 +67,9 @@ const presets: TaskReminderModel[] = [
{relativePeriod: SECONDS_A_DAY * 7, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE}, {relativePeriod: SECONDS_A_DAY * 7, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE},
{relativePeriod: SECONDS_A_DAY * 30, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE}, {relativePeriod: SECONDS_A_DAY * 30, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE},
] ]
const reminderDate = computed({ const reminderDate = ref(null)
get() {
return reminder.value?.reminder
},
set(newReminderDate) {
if (!reminderDate.value) {
return
}
reminder.value.reminder = new Date(reminderDate.value)
},
})
const showAbsoluteReminder = computed(() => !reminder.value || !reminder.value?.relativeTo) const showFormSwitch = ref<null | 'relative' | 'absolute'>(null)
const showRelativeReminder = computed(() => !reminder.value || reminder.value?.relativeTo)
const reminderText = computed(() => { const reminderText = computed(() => {
@ -103,10 +94,9 @@ watch(
) )
function setReminderDate() { function setReminderDate() {
if (!reminderDate.value) { reminder.value.reminder = reminderDate.value === null
return ? null
} : new Date(reminderDate.value)
reminder.value.reminder = new Date(reminderDate.value)
emit('update:modelValue', reminder.value) emit('update:modelValue', reminder.value)
} }
@ -123,12 +113,12 @@ function formatReminder(reminder: TaskReminderModel) {
periodHuman = period.days + ' day' periodHuman = period.days + ' day'
} }
return periodHuman + ' ' + (reminder.relativePeriod > 0 ? 'before' : 'after') + ' ' + reminder.relativeTo return periodHuman + ' ' + (reminder.relativePeriod <= 0 ? 'before' : 'after') + ' ' + reminder.relativeTo
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.presets { .options {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;

View File

@ -1,92 +1,57 @@
<template> <template>
<div <div
v-if="!!reminder?.relativeTo" class="reminder-period control"
class="reminder-period"
> >
<Popup> <input
<template #trigger="{toggle}"> class="input"
<BaseButton v-model.number="period.duration"
@click="toggle" type="number"
:disabled="disabled" min="0"
class="show" @change="updateData"
> />
{{ formatDuration(reminder.relativePeriod) }} {{ reminder.relativePeriod <= 0 ? '&le;' : '&gt;' }}
{{ formatRelativeTo(reminder.relativeTo) }}
<span class="icon"><icon icon="chevron-down"/></span>
</BaseButton>
</template>
<template #content> <div class="select">
<div class="mt-2"> <select v-model="period.durationUnit" @change="updateData">
<div class="control is-flex is-align-items-center"> <option value="minutes">{{ $t('task.reminder.minutes') }}</option>
<label> <option value="hours">{{ $t('task.reminder.hours') }}</option>
<input <option value="days">{{ $t('task.reminder.days') }}</option>
:disabled="disabled" <option value="weeks">{{ $t('task.reminder.weeks') }}</option>
class="input" </select>
:placeholder="$t('task.reminder.daysShort')" </div>
v-model="periodInput.duration.days"
type="number"
min="0"
/> {{ $t('task.reminder.days') }}
</label>
<input
:disabled="disabled"
class="input"
:placeholder="$t('task.reminder.hoursShort')"
v-model="periodInput.duration.hours"
type="number"
min="0"
/>:
<input
:disabled="disabled"
class="input"
:placeholder="$t('task.reminder.minutesShort')"
v-model="periodInput.duration.minutes"
type="number"
min="0"
/>
<div class="select"> <div class="select">
<select :disabled="disabled" v-model.number="periodInput.sign"> <select v-model.number="period.sign" @change="updateData">
<option value="-1">&le;</option> <option value="-1">
<option value="1">&gt;</option> before
</select> </option>
</div> <option value="1">
after
</option>
</select>
</div>
<div class="select"> <div class="select">
<select :disabled="disabled" v-model="periodInput.relativeTo"> <select v-model="period.relativeTo" @change="updateData">
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE">{{ $t('task.attributes.dueDate') }}</option> <option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE">
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE">{{ $t('task.attributes.startDate')}}</option> {{ $t('task.attributes.dueDate') }}
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE">{{ $t('task.attributes.endDate') }}</option> </option>
</select> <option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE">
</div> {{ $t('task.attributes.startDate') }}
</div> </option>
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE">
<div class="control"> {{ $t('task.attributes.endDate') }}
<x-button </option>
:disabled="disabled" </select>
class="close-button" </div>
:shadow="false"
@click="submitForm"
>
{{ $t('misc.confirm') }}
</x-button>
</div>
</div>
</template>
</Popup>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {reactive, ref, watch, type PropType, computed} from 'vue' import {ref, watch, type PropType} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {toRef} from '@vueuse/core' import {toRef} from '@vueuse/core'
import BaseButton from '@/components/base/BaseButton.vue' import {periodToSeconds, PeriodUnit, secondsToPeriod} from '@/helpers/time/period'
import Popup from '@/components/misc/popup.vue'
import {periodToSeconds, secondsToPeriod} from '@/helpers/time/period'
import TaskReminderModel from '@/models/taskReminder' import TaskReminderModel from '@/models/taskReminder'
@ -108,106 +73,55 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const reminder = ref<ITaskReminder>() const reminder = ref<ITaskReminder>(new TaskReminderModel())
const showForm = ref(false) const showForm = ref(false)
const periodInput = reactive({ interface PeriodInput {
duration: { duration: number,
days: 0, durationUnit: PeriodUnit,
hours: 0, relativeTo: IReminderPeriodRelativeTo,
minutes: 0, sign: -1 | 1,
seconds: 0 }
},
const period = ref<PeriodInput>({
duration: 0,
durationUnit: 'hours',
relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE,
sign: -1, sign: -1,
}) })
const modelValue = toRef(props, 'modelValue') const modelValue = toRef(props, 'modelValue')
watch( watch(
modelValue, modelValue,
(value) => { (value) => {
reminder.value = value console.log({value})
if (value && value.relativeTo != null) { const p = secondsToPeriod(value?.relativePeriod)
Object.assign(periodInput.duration, secondsToPeriod(Math.abs(value.relativePeriod))) period.value.durationUnit = p.unit
periodInput.relativeTo = value.relativeTo period.value.duration = p.amount
periodInput.sign = value.relativePeriod <= 0 ? -1 : 1 period.value.relativeTo = value?.relativeTo
} else { },
reminder.value = new TaskReminderModel() {immediate: true},
showForm.value = true
}
},
{immediate: true},
) )
function updateData() { function updateData() {
changed.value = true reminder.value.relativePeriod = period.value.sign * periodToSeconds(period.value.duration, period.value.durationUnit)
if (reminder.value) { reminder.value.relativeTo = period.value.relativeTo
reminder.value.relativePeriod = periodInput.sign * periodToSeconds(periodInput.duration.days, periodInput.duration.hours, periodInput.duration.minutes, 0) reminder.value.reminder = null
reminder.value.relativeTo = periodInput.relativeTo
reminder.value.reminder = null
}
emit('update:modelValue', reminder.value) emit('update:modelValue', reminder.value)
} }
function submitForm() {
updateData()
close()
}
const changed = ref(false)
function close() {
setTimeout(() => {
showForm.value = false
if (changed.value) {
changed.value = false
}
}, 200)
}
function formatDuration(reminderPeriod: number): string {
if (Math.abs(reminderPeriod) < 60) {
return '00:00'
}
const duration = secondsToPeriod(Math.abs(reminderPeriod))
return (duration.days > 0 ? `${duration.days} ${t('task.reminder.days')} `: '') +
('' + duration.hours).padStart(2, '0') + ':' +
('' + duration.minutes).padStart(2, '0')
}
const relativeToOptions = {
[REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE]: t('task.attributes.dueDate'),
[REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE]: t('task.attributes.startDate'),
[REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE]: t('task.attributes.endDate'),
} as const
const relativeTo = computed(() => relativeToOptions[periodInput.relativeTo])
function formatRelativeTo(relativeTo: IReminderPeriodRelativeTo | null): string | null {
switch (relativeTo) {
case REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE:
return t('task.attributes.dueDate')
case REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE:
return t('task.attributes.startDate')
case REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE:
return t('task.attributes.endDate')
default:
return relativeTo
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.input { .reminder-period {
max-width: 5rem; display: flex;
width: 4rem; flex-direction: column;
} gap: .25rem;
.close-button { .input, .select select {
margin: 0.5rem; width: 100% !important;
width: calc(100% - 1rem); height: auto;
}
} }
</style> </style>

View File

@ -7,11 +7,14 @@
class="reminder-input" class="reminder-input"
> >
<div class="reminder-detail"> <div class="reminder-detail">
<ReminderDetail :disabled="disabled" v-model="reminders[index]" /> <ReminderDetail
:disabled="disabled"
v-model="reminders[index]"
@update:model-value="updateData"/>
</div> </div>
<div> <div>
<BaseButton v-if="!disabled" @click="removeReminderByIndex(index)" class="remove"> <BaseButton v-if="!disabled" @click="removeReminderByIndex(index)" class="remove">
<icon icon="times" /> <icon icon="times"/>
</BaseButton> </BaseButton>
</div> </div>
</div> </div>
@ -31,7 +34,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {reactive, ref, watch, type PropType} from 'vue' import {reactive, ref, watch, type PropType} from 'vue'
import type { ITaskReminder } from '@/modelTypes/ITaskReminder' import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ReminderDetail from '@/components/tasks/partials/reminder-detail.vue' import ReminderDetail from '@/components/tasks/partials/reminder-detail.vue'
@ -60,6 +63,7 @@ watch(
) )
const isAddReminder = ref(false) const isAddReminder = ref(false)
function toggleAddReminder() { function toggleAddReminder() {
isAddReminder.value = !isAddReminder.value isAddReminder.value = !isAddReminder.value
} }
@ -76,8 +80,9 @@ function editReminder(index: number) {
updateData() updateData()
} }
function addNewReminder(newReminder : ITaskReminder) { function addNewReminder(newReminder: ITaskReminder) {
if (newReminder == null) { console.log('add new reminder', newReminder)
if (newReminder === null) {
return return
} }
reminders.value.push(newReminder) reminders.value.push(newReminder)
@ -104,9 +109,11 @@ function removeReminderByIndex(index: number) {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
} }
.reminder-detail { .reminder-detail {
width: 100%; width: 100%;
} }
.remove { .remove {
color: var(--danger); color: var(--danger);
vertical-align: top; vertical-align: top;

View File

@ -1,20 +1,50 @@
import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_MINUTE} from '@/constants/date' import {
SECONDS_A_DAY,
SECONDS_A_HOUR,
SECONDS_A_MINUTE,
SECONDS_A_MONTH,
SECONDS_A_WEEK,
SECONDS_A_YEAR,
} from '@/constants/date'
export type PeriodUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years'
/** /**
* Convert time period given as seconds to days, hour, minutes, seconds * Convert time period given as seconds to days, hour, minutes, seconds
*/ */
export function secondsToPeriod(seconds: number): {days: number, hours: number, minutes: number, seconds: number} { export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: number } {
if (seconds % SECONDS_A_DAY === 0) {
if (seconds % SECONDS_A_WEEK === 0) {
return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK}
} else if (seconds % SECONDS_A_MONTH === 0) {
return {unit: 'months', amount: seconds / SECONDS_A_MONTH}
} else if (seconds % SECONDS_A_YEAR === 0) {
return {unit: 'years', amount: seconds / SECONDS_A_YEAR}
} else {
return {unit: 'days', amount: seconds / SECONDS_A_DAY}
}
}
return { return {
days: Math.floor(seconds / SECONDS_A_DAY), unit: 'hours',
hours: Math.floor(seconds % SECONDS_A_DAY / 3600), amount: seconds / SECONDS_A_HOUR,
minutes: Math.floor(seconds % SECONDS_A_HOUR / 60),
seconds: Math.floor(seconds % 60),
} }
} }
/** /**
* Convert time period of days, hour, minutes, seconds to duration in seconds * Convert time period of days, hour, minutes, seconds to duration in seconds
*/ */
export function periodToSeconds(days: number, hours: number, minutes: number, seconds: number): number { export function periodToSeconds(period: number, unit: PeriodUnit): number {
return days * SECONDS_A_DAY + hours * SECONDS_A_HOUR + minutes * SECONDS_A_MINUTE + seconds switch (unit) {
case 'minutes':
return period * SECONDS_A_MINUTE
case 'hours':
return period * SECONDS_A_HOUR
case 'days':
return period * SECONDS_A_DAY
case 'weeks':
return period * SECONDS_A_WEEK
}
return 0
} }

View File

@ -22,6 +22,7 @@ import AttachmentModel from './attachment'
import SubscriptionModel from './subscription' import SubscriptionModel from './subscription'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder' import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import TaskReminderModel from '@/models/taskReminder' import TaskReminderModel from '@/models/taskReminder'
import {periodToSeconds, secondsToPeriod} from '@/helpers/time/period'
export const TASK_DEFAULT_COLOR = '#1973ff' export const TASK_DEFAULT_COLOR = '#1973ff'
@ -37,21 +38,13 @@ export function getHexColor(hexColor: string): string {
* Parses `repeatAfterSeconds` into a usable js object. * Parses `repeatAfterSeconds` into a usable js object.
*/ */
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter { export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
const period = secondsToPeriod(repeatAfterSeconds)
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterSeconds % SECONDS_A_DAY === 0) { return {
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) { type: period.unit,
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK} amount: period.amount,
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
}
} }
return repeatAfter
} }
export default class TaskModel extends AbstractModel<ITask> implements ITask { export default class TaskModel extends AbstractModel<ITask> implements ITask {

View File

@ -1,4 +1,6 @@
export const REPEAT_TYPES = { export const REPEAT_TYPES = {
Seconds: 'seconds',
Minutes: 'minutes',
Hours: 'hours', Hours: 'hours',
Days: 'days', Days: 'days',
Weeks: 'weeks', Weeks: 'weeks',