feat: edit relative reminders (#3248)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/3248
This commit is contained in:
commit
3f8e457d52
26
src/components/input/SimpleButton.vue
Normal file
26
src/components/input/SimpleButton.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<BaseButton class="simple-button">
|
||||
<slot/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.simple-button {
|
||||
color: var(--text);
|
||||
padding: .25rem .5rem;
|
||||
transition: background-color $transition;
|
||||
border-radius: $radius;
|
||||
display: block;
|
||||
margin: .1rem 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,78 +1,15 @@
|
||||
<template>
|
||||
<div class="datepicker">
|
||||
<BaseButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
|
||||
<SimpleButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
|
||||
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
|
||||
</BaseButton>
|
||||
</SimpleButton>
|
||||
|
||||
<CustomTransition name="fade">
|
||||
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
||||
|
||||
<BaseButton
|
||||
v-if="(new Date()).getHours() < 21"
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('today')"
|
||||
>
|
||||
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.today') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('tomorrow')"
|
||||
>
|
||||
<span class="icon"><icon :icon="['far', 'sun']"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.tomorrow') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('nextMonday')"
|
||||
>
|
||||
<span class="icon"><icon icon="coffee"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.nextMonday') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('thisWeekend')"
|
||||
>
|
||||
<span class="icon"><icon icon="cocktail"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.thisWeekend') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('laterThisWeek')"
|
||||
>
|
||||
<span class="icon"><icon icon="chess-knight"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.laterThisWeek') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('nextWeek')"
|
||||
>
|
||||
<span class="icon"><icon icon="forward"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.nextWeek') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
|
||||
<flat-pickr
|
||||
:config="flatPickerConfig"
|
||||
class="input"
|
||||
v-model="flatPickrDate"
|
||||
<DatepickerInline
|
||||
v-model="date"
|
||||
@update:model-value="updateData"
|
||||
/>
|
||||
|
||||
<x-button
|
||||
@ -89,19 +26,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, onBeforeUnmount, toRef, watch, computed, type PropType} from 'vue'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import {ref, onMounted, onBeforeUnmount, toRef, watch, type PropType} from 'vue'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import DatepickerInline from '@/components/input/datepickerInline.vue'
|
||||
import SimpleButton from '@/components/input/SimpleButton.vue'
|
||||
|
||||
import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
|
||||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||
import {formatDateShort} from '@/helpers/time/formatDate'
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
@ -125,8 +58,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const date = ref<Date | null>()
|
||||
const show = ref(false)
|
||||
const changed = ref(false)
|
||||
@ -141,42 +72,6 @@ watch(
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const weekStart = computed(() => authStore.settings.weekStart)
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
locale: {
|
||||
firstDayOfWeek: weekStart.value,
|
||||
},
|
||||
}))
|
||||
|
||||
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
||||
// To make that work, we need a separate variable since flatpickr does not have a change event.
|
||||
const flatPickrDate = computed({
|
||||
set(newValue: string | Date | null) {
|
||||
if (newValue === null) {
|
||||
date.value = null
|
||||
return
|
||||
}
|
||||
|
||||
date.value = createDateFromString(newValue)
|
||||
updateData()
|
||||
},
|
||||
get() {
|
||||
if (!date.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return formatDate(date.value, 'yyy-LL-dd H:mm')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
function setDateValue(dateString: string | Date | null) {
|
||||
if (dateString === null) {
|
||||
date.value = null
|
||||
@ -217,29 +112,6 @@ function close() {
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function setDate(dateString: string) {
|
||||
if (date.value === null) {
|
||||
date.value = new Date()
|
||||
}
|
||||
|
||||
const interval = calculateDayInterval(dateString)
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
newDate.setHours(calculateNearestHours(newDate))
|
||||
newDate.setMinutes(0)
|
||||
newDate.setSeconds(0)
|
||||
date.value = newDate
|
||||
flatPickrDate.value = newDate
|
||||
updateData()
|
||||
}
|
||||
|
||||
function getWeekdayFromStringInterval(dateString: string) {
|
||||
const interval = calculateDayInterval(dateString)
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
return formatDate(newDate, 'E')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -262,42 +134,6 @@ function getWeekdayFromStringInterval(dateString: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker__quick-select-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 .5rem;
|
||||
width: 100%;
|
||||
height: 2.25rem;
|
||||
color: var(--text);
|
||||
transition: all $transition;
|
||||
|
||||
&:first-child {
|
||||
border-radius: $radius $radius 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--grey-100);
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 100%;
|
||||
font-size: .85rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: .25rem;
|
||||
|
||||
.weekday {
|
||||
color: var(--text-light);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker__close-button {
|
||||
margin: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
|
228
src/components/input/datepickerInline.vue
Normal file
228
src/components/input/datepickerInline.vue
Normal file
@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<BaseButton
|
||||
v-if="(new Date()).getHours() < 21"
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('today')"
|
||||
>
|
||||
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.today') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('tomorrow')"
|
||||
>
|
||||
<span class="icon"><icon :icon="['far', 'sun']"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.tomorrow') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('nextMonday')"
|
||||
>
|
||||
<span class="icon"><icon icon="coffee"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.nextMonday') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('thisWeekend')"
|
||||
>
|
||||
<span class="icon"><icon icon="cocktail"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.thisWeekend') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('laterThisWeek')"
|
||||
>
|
||||
<span class="icon"><icon icon="chess-knight"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.laterThisWeek') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('nextWeek')"
|
||||
>
|
||||
<span class="icon"><icon icon="forward"/></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.nextWeek') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
|
||||
<div class="flatpickr-container">
|
||||
<flat-pickr
|
||||
:config="flatPickerConfig"
|
||||
v-model="flatPickrDate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, toRef, watch, computed, type PropType} from 'vue'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {formatDate} from '@/helpers/time/formatDate'
|
||||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Date, null, String] as PropType<Date | null | string>,
|
||||
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close-on-change'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const date = ref<Date | null>()
|
||||
const changed = ref(false)
|
||||
|
||||
const modelValue = toRef(props, 'modelValue')
|
||||
watch(
|
||||
modelValue,
|
||||
setDateValue,
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const weekStart = computed(() => authStore.settings.weekStart)
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
locale: {
|
||||
firstDayOfWeek: weekStart.value,
|
||||
},
|
||||
}))
|
||||
|
||||
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
||||
// To make that work, we need a separate variable since flatpickr does not have a change event.
|
||||
const flatPickrDate = computed({
|
||||
set(newValue: string | Date | null) {
|
||||
if (newValue === null) {
|
||||
date.value = null
|
||||
return
|
||||
}
|
||||
|
||||
date.value = createDateFromString(newValue)
|
||||
updateData()
|
||||
},
|
||||
get() {
|
||||
if (!date.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return formatDate(date.value, 'yyy-LL-dd H:mm')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
function setDateValue(dateString: string | Date | null) {
|
||||
if (dateString === null) {
|
||||
date.value = null
|
||||
return
|
||||
}
|
||||
date.value = createDateFromString(dateString)
|
||||
}
|
||||
|
||||
function updateData() {
|
||||
changed.value = true
|
||||
emit('update:modelValue', date.value)
|
||||
}
|
||||
|
||||
function setDate(dateString: string) {
|
||||
if (date.value === null) {
|
||||
date.value = new Date()
|
||||
}
|
||||
|
||||
const interval = calculateDayInterval(dateString)
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
newDate.setHours(calculateNearestHours(newDate))
|
||||
newDate.setMinutes(0)
|
||||
newDate.setSeconds(0)
|
||||
date.value = newDate
|
||||
flatPickrDate.value = newDate
|
||||
updateData()
|
||||
}
|
||||
|
||||
function getWeekdayFromStringInterval(dateString: string) {
|
||||
const interval = calculateDayInterval(dateString)
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
return formatDate(newDate, 'E')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.datepicker__quick-select-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 .5rem;
|
||||
width: 100%;
|
||||
height: 2.25rem;
|
||||
color: var(--text);
|
||||
transition: all $transition;
|
||||
|
||||
&:first-child {
|
||||
border-radius: $radius $radius 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--grey-100);
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 100%;
|
||||
font-size: .85rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: .25rem;
|
||||
|
||||
.weekday {
|
||||
color: var(--text-light);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.flatpickr-container {
|
||||
:deep(.flatpickr-calendar) {
|
||||
margin: 0 auto 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.input) {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -8,7 +8,7 @@
|
||||
}"
|
||||
ref="popup"
|
||||
>
|
||||
<slot name="content" :isOpen="open"/>
|
||||
<slot name="content" :isOpen="open" :toggle="toggle"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -23,11 +23,14 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const open = ref(false)
|
||||
const popup = ref<HTMLElement | null>(null)
|
||||
|
||||
function close() {
|
||||
open.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
|
269
src/components/tasks/partials/reminder-detail.vue
Normal file
269
src/components/tasks/partials/reminder-detail.vue
Normal file
@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div>
|
||||
<Popup @close="showFormSwitch = null">
|
||||
<template #trigger="{toggle}">
|
||||
<SimpleButton
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ reminderText }}
|
||||
</SimpleButton>
|
||||
</template>
|
||||
<template #content="{isOpen, toggle}">
|
||||
<Card class="reminder-options-popup" :class="{'is-open': isOpen}" :padding="false">
|
||||
<div class="options" v-if="showFormSwitch === null">
|
||||
<SimpleButton
|
||||
v-for="(p, k) in presets"
|
||||
:key="k"
|
||||
class="option-button"
|
||||
:class="{'currently-active': p.relativePeriod === modelValue?.relativePeriod && modelValue?.relativeTo === p.relativeTo}"
|
||||
@click="setReminderFromPreset(p, toggle)"
|
||||
>
|
||||
{{ formatReminder(p) }}
|
||||
</SimpleButton>
|
||||
<SimpleButton
|
||||
@click="showFormSwitch = 'relative'"
|
||||
class="option-button"
|
||||
:class="{'currently-active': typeof modelValue !== 'undefined' && modelValue?.relativeTo !== null && presets.find(p => p.relativePeriod === modelValue?.relativePeriod && modelValue?.relativeTo === p.relativeTo) === undefined}"
|
||||
>
|
||||
{{ $t('task.reminder.custom') }}
|
||||
</SimpleButton>
|
||||
<SimpleButton
|
||||
@click="showFormSwitch = 'absolute'"
|
||||
class="option-button"
|
||||
:class="{'currently-active': modelValue?.relativeTo === null}"
|
||||
>
|
||||
{{ $t('task.reminder.dateAndTime') }}
|
||||
</SimpleButton>
|
||||
</div>
|
||||
|
||||
<ReminderPeriod
|
||||
v-if="showFormSwitch === 'relative'"
|
||||
v-model="reminder"
|
||||
@update:modelValue="updateDataAndMaybeClose(toggle)"
|
||||
/>
|
||||
|
||||
<DatepickerInline
|
||||
v-if="showFormSwitch === 'absolute'"
|
||||
v-model="reminderDate"
|
||||
@update:modelValue="setReminderDate"
|
||||
/>
|
||||
|
||||
<x-button
|
||||
v-if="showFormSwitch !== null"
|
||||
class="reminder__close-button"
|
||||
:shadow="false"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ $t('misc.confirm') }}
|
||||
</x-button>
|
||||
</Card>
|
||||
</template>
|
||||
</Popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch, type PropType} from 'vue'
|
||||
import {toRef} from '@vueuse/core'
|
||||
import {SECONDS_A_DAY} from '@/constants/date'
|
||||
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {PeriodUnit, secondsToPeriod} from '@/helpers/time/period'
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
import {formatDateShort} from '@/helpers/time/formatDate'
|
||||
|
||||
import DatepickerInline from '@/components/input/datepickerInline.vue'
|
||||
import ReminderPeriod from '@/components/tasks/partials/reminder-period.vue'
|
||||
import Popup from '@/components/misc/popup.vue'
|
||||
|
||||
import TaskReminderModel from '@/models/taskReminder'
|
||||
import Card from '@/components/misc/card.vue'
|
||||
import SimpleButton from '@/components/input/SimpleButton.vue'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<ITaskReminder>,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
clearAfterUpdate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const reminder = ref<ITaskReminder>(new TaskReminderModel())
|
||||
|
||||
const presets: TaskReminderModel[] = [
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 3, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 7, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 30, relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE},
|
||||
]
|
||||
const reminderDate = ref(null)
|
||||
|
||||
const showFormSwitch = ref<null | 'relative' | 'absolute'>(null)
|
||||
|
||||
const reminderText = computed(() => {
|
||||
|
||||
if (reminder.value.relativeTo !== null) {
|
||||
return formatReminder(reminder.value)
|
||||
}
|
||||
|
||||
if (reminder.value.reminder !== null) {
|
||||
return formatDateShort(reminder.value.reminder)
|
||||
}
|
||||
|
||||
return t('task.addReminder')
|
||||
})
|
||||
|
||||
const modelValue = toRef(props, 'modelValue')
|
||||
watch(
|
||||
modelValue,
|
||||
(newReminder) => {
|
||||
reminder.value = newReminder || new TaskReminderModel()
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function updateData() {
|
||||
emit('update:modelValue', reminder.value)
|
||||
|
||||
if (props.clearAfterUpdate) {
|
||||
reminder.value = new TaskReminderModel()
|
||||
}
|
||||
}
|
||||
|
||||
function setReminderDate() {
|
||||
reminder.value.reminder = reminderDate.value === null
|
||||
? null
|
||||
: new Date(reminderDate.value)
|
||||
reminder.value.relativeTo = null
|
||||
reminder.value.relativePeriod = 0
|
||||
updateData()
|
||||
}
|
||||
|
||||
function setReminderFromPreset(preset, toggle) {
|
||||
reminder.value = preset
|
||||
updateData()
|
||||
toggle()
|
||||
}
|
||||
|
||||
function updateDataAndMaybeClose(toggle) {
|
||||
updateData()
|
||||
if (props.clearAfterUpdate) {
|
||||
toggle()
|
||||
}
|
||||
}
|
||||
|
||||
function formatReminder(reminder: TaskReminderModel) {
|
||||
const period = secondsToPeriod(reminder.relativePeriod)
|
||||
|
||||
if (period.amount === 0) {
|
||||
switch (reminder.relativeTo) {
|
||||
case REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE:
|
||||
return t('task.reminder.onDueDate')
|
||||
case REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE:
|
||||
return t('task.reminder.onStartDate')
|
||||
case REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE:
|
||||
return t('task.reminder.onEndDate')
|
||||
}
|
||||
}
|
||||
|
||||
const amountAbs = Math.abs(period.amount)
|
||||
|
||||
let relativeTo = ''
|
||||
switch (reminder.relativeTo) {
|
||||
case REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE:
|
||||
relativeTo = t('task.attributes.dueDate')
|
||||
break
|
||||
case REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE:
|
||||
relativeTo = t('task.attributes.startDate')
|
||||
break
|
||||
case REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE:
|
||||
relativeTo = t('task.attributes.endDate')
|
||||
break
|
||||
}
|
||||
|
||||
if (reminder.relativePeriod <= 0) {
|
||||
return t('task.reminder.before', {
|
||||
amount: amountAbs,
|
||||
unit: translateUnit(amountAbs, period.unit),
|
||||
type: relativeTo,
|
||||
})
|
||||
}
|
||||
|
||||
return t('task.reminder.after', {
|
||||
amount: amountAbs,
|
||||
unit: translateUnit(amountAbs, period.unit),
|
||||
type: relativeTo,
|
||||
})
|
||||
}
|
||||
|
||||
function translateUnit(amount: number, unit: PeriodUnit): string {
|
||||
switch (unit) {
|
||||
case 'seconds':
|
||||
return t('time.units.seconds', amount)
|
||||
case 'minutes':
|
||||
return t('time.units.minutes', amount)
|
||||
case 'hours':
|
||||
return t('time.units.hours', amount)
|
||||
case 'days':
|
||||
return t('time.units.days', amount)
|
||||
case 'weeks':
|
||||
return t('time.units.weeks', amount)
|
||||
case 'months':
|
||||
return t('time.units.months', amount)
|
||||
case 'years':
|
||||
return t('time.units.years', amount)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:deep(.popup) {
|
||||
top: unset;
|
||||
}
|
||||
|
||||
.reminder-options-popup {
|
||||
width: 310px;
|
||||
z-index: 99;
|
||||
|
||||
@media screen and (max-width: ($tablet)) {
|
||||
width: calc(100vw - 5rem);
|
||||
}
|
||||
|
||||
.option-button {
|
||||
font-size: .85rem;
|
||||
border-radius: 0;
|
||||
padding: .5rem;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--grey-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reminder__close-button {
|
||||
margin: .5rem;
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.currently-active {
|
||||
color: var(--primary);
|
||||
}
|
||||
</style>
|
122
src/components/tasks/partials/reminder-period.vue
Normal file
122
src/components/tasks/partials/reminder-period.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div
|
||||
class="reminder-period control"
|
||||
>
|
||||
<input
|
||||
class="input"
|
||||
v-model.number="period.duration"
|
||||
type="number"
|
||||
min="0"
|
||||
@change="updateData"
|
||||
/>
|
||||
|
||||
<div class="select">
|
||||
<select v-model="period.durationUnit" @change="updateData">
|
||||
<option value="minutes">{{ $t('time.units.minutes', period.duration) }}</option>
|
||||
<option value="hours">{{ $t('time.units.hours', period.duration) }}</option>
|
||||
<option value="days">{{ $t('time.units.days', period.duration) }}</option>
|
||||
<option value="weeks">{{ $t('time.units.weeks', period.duration) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="select">
|
||||
<select v-model.number="period.sign" @change="updateData">
|
||||
<option value="-1">
|
||||
{{ $t('task.reminder.beforeShort') }}
|
||||
</option>
|
||||
<option value="1">
|
||||
{{ $t('task.reminder.afterShort') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="select">
|
||||
<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.STARTDATE">
|
||||
{{ $t('task.attributes.startDate') }}
|
||||
</option>
|
||||
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE">
|
||||
{{ $t('task.attributes.endDate') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch, type PropType} from 'vue'
|
||||
import {toRef} from '@vueuse/core'
|
||||
|
||||
import {periodToSeconds, PeriodUnit, secondsToPeriod} from '@/helpers/time/period'
|
||||
|
||||
import TaskReminderModel from '@/models/taskReminder'
|
||||
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
import {REMINDER_PERIOD_RELATIVE_TO_TYPES, type IReminderPeriodRelativeTo} from '@/types/IReminderPeriodRelativeTo'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<ITaskReminder>,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const reminder = ref<ITaskReminder>(new TaskReminderModel())
|
||||
|
||||
interface PeriodInput {
|
||||
duration: number,
|
||||
durationUnit: PeriodUnit,
|
||||
relativeTo: IReminderPeriodRelativeTo,
|
||||
sign: -1 | 1,
|
||||
}
|
||||
|
||||
const period = ref<PeriodInput>({
|
||||
duration: 0,
|
||||
durationUnit: 'hours',
|
||||
relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE,
|
||||
sign: -1,
|
||||
})
|
||||
|
||||
const modelValue = toRef(props, 'modelValue')
|
||||
watch(
|
||||
modelValue,
|
||||
(value) => {
|
||||
const p = secondsToPeriod(value?.relativePeriod)
|
||||
period.value.durationUnit = p.unit
|
||||
period.value.duration = p.amount
|
||||
period.value.relativeTo = value?.relativeTo || REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function updateData() {
|
||||
reminder.value.relativePeriod = period.value.sign * periodToSeconds(period.value.duration, period.value.durationUnit)
|
||||
reminder.value.relativeTo = period.value.relativeTo
|
||||
reminder.value.reminder = null
|
||||
|
||||
emit('update:modelValue', reminder.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reminder-period {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .25rem;
|
||||
padding: .5rem .5rem 0;
|
||||
|
||||
.input, .select select {
|
||||
width: 100% !important;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
26
src/components/tasks/partials/reminders.story.vue
Normal file
26
src/components/tasks/partials/reminders.story.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import reminders from './reminders.vue'
|
||||
import {ref} from 'vue'
|
||||
import ReminderDetail from '@/components/tasks/partials/reminder-detail.vue'
|
||||
|
||||
const reminderNow = ref({reminder: new Date(), relativePeriod: 0, relativeTo: null } )
|
||||
const relativeReminder = ref({reminder: null, relativePeriod: 1, relativeTo: 'due_date' } )
|
||||
const newReminder = ref(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story>
|
||||
<Variant title="Default">
|
||||
<reminders/>
|
||||
</Variant>
|
||||
<Variant title="Reminder Detail with fixed date">
|
||||
<reminder-detail v-model="reminderNow"/>
|
||||
</Variant>
|
||||
<Variant title="Reminder Detail with relative date">
|
||||
<reminder-detail v-model="relativeReminder"/>
|
||||
</Variant>
|
||||
<Variant title="New Reminder Detail">
|
||||
<reminder-detail v-model="newReminder"/>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
@ -3,104 +3,70 @@
|
||||
<div
|
||||
v-for="(r, index) in reminders"
|
||||
:key="index"
|
||||
:class="{ 'overdue': r < new Date()}"
|
||||
:class="{ 'overdue': r.reminder < new Date() }"
|
||||
class="reminder-input"
|
||||
>
|
||||
<Datepicker
|
||||
v-model="reminders[index]"
|
||||
<ReminderDetail
|
||||
class="reminder-detail"
|
||||
:disabled="disabled"
|
||||
@close-on-change="() => addReminderDate(index)"
|
||||
/>
|
||||
<BaseButton @click="removeReminderByIndex(index)" v-if="!disabled" class="remove">
|
||||
<icon icon="times"></icon>
|
||||
v-model="reminders[index]"
|
||||
@update:model-value="updateData"/>
|
||||
<BaseButton
|
||||
v-if="!disabled"
|
||||
@click="removeReminderByIndex(index)"
|
||||
class="remove"
|
||||
>
|
||||
<icon icon="times"/>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="reminder-input" v-if="!disabled">
|
||||
<Datepicker
|
||||
v-model="newReminder"
|
||||
@close-on-change="() => addReminderDate()"
|
||||
:choose-date-label="$t('task.addReminder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ReminderDetail
|
||||
:disabled="disabled"
|
||||
@update:modelValue="addNewReminder"
|
||||
:clear-after-update="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {type PropType, ref, onMounted, watch} from 'vue'
|
||||
import {ref, watch, type PropType} from 'vue'
|
||||
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Datepicker from '@/components/input/datepicker.vue'
|
||||
|
||||
type Reminder = Date | string
|
||||
|
||||
|
||||
import ReminderDetail from '@/components/tasks/partials/reminder-detail.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<Reminder[]>,
|
||||
type: Array as PropType<ITaskReminder[]>,
|
||||
default: () => [],
|
||||
validator(prop) {
|
||||
// This allows arrays of Dates and strings
|
||||
if (!(prop instanceof Array)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isDate = (e: unknown) => e instanceof Date
|
||||
const isString = (e: unknown) => typeof e === 'string'
|
||||
|
||||
for (const e of prop) {
|
||||
if (!isDate(e) && !isString(e)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const reminders = ref<Reminder[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
reminders.value = [...props.modelValue]
|
||||
})
|
||||
const reminders = ref<ITaskReminder[]>([])
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
props.modelValue,
|
||||
(newVal) => {
|
||||
for (const i in newVal) {
|
||||
if (typeof newVal[i] === 'string') {
|
||||
newVal[i] = new Date(newVal[i])
|
||||
}
|
||||
}
|
||||
reminders.value = newVal
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
|
||||
function updateData() {
|
||||
emit('update:modelValue', reminders.value)
|
||||
}
|
||||
|
||||
const newReminder = ref(null)
|
||||
function addReminderDate(index : number | null = null) {
|
||||
// New Date
|
||||
if (index === null) {
|
||||
if (newReminder.value === null) {
|
||||
return
|
||||
}
|
||||
reminders.value.push(new Date(newReminder.value))
|
||||
newReminder.value = null
|
||||
} else if(reminders.value[index] === null) {
|
||||
function addNewReminder(newReminder: ITaskReminder) {
|
||||
if (newReminder === null) {
|
||||
return
|
||||
}
|
||||
|
||||
reminders.value.push(newReminder)
|
||||
updateData()
|
||||
}
|
||||
|
||||
@ -111,23 +77,27 @@ function removeReminderByIndex(index: number) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reminders {
|
||||
.reminder-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.reminder-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.overdue :deep(.datepicker .show) {
|
||||
color: var(--danger);
|
||||
}
|
||||
&.overdue :deep(.datepicker .show) {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
&::last-child {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
padding-left: .5rem;
|
||||
}
|
||||
}
|
||||
.reminder-detail {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
vertical-align: top;
|
||||
padding-left: .5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
50
src/helpers/time/period.ts
Normal file
50
src/helpers/time/period.ts
Normal file
@ -0,0 +1,50 @@
|
||||
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
|
||||
*/
|
||||
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 {
|
||||
unit: 'hours',
|
||||
amount: seconds / SECONDS_A_HOUR,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert time period of days, hour, minutes, seconds to duration in seconds
|
||||
*/
|
||||
export function periodToSeconds(period: number, unit: PeriodUnit): number {
|
||||
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
|
||||
}
|
@ -720,6 +720,17 @@
|
||||
"copiedto": "Copied To | Copied To"
|
||||
}
|
||||
},
|
||||
"reminder": {
|
||||
"before": "{amount} {unit} before {type}",
|
||||
"after": "{amount} {unit} after {type}",
|
||||
"beforeShort": "before",
|
||||
"afterShort": "after",
|
||||
"onDueDate": "On the due date",
|
||||
"onStartDate": "On the start date",
|
||||
"onEndDate": "On the end date",
|
||||
"custom": "Custom",
|
||||
"dateAndTime": "Date and time"
|
||||
},
|
||||
"repeat": {
|
||||
"everyDay": "Every Day",
|
||||
"everyWeek": "Every Week",
|
||||
@ -983,5 +994,16 @@
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
"seconds": "second|seconds",
|
||||
"minutes": "minute|minutes",
|
||||
"hours": "hour|hours",
|
||||
"days": "day|days",
|
||||
"weeks": "week|weeks",
|
||||
"months": "month|months",
|
||||
"years": "year|years"
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import type {IRepeatAfter} from '@/types/IRepeatAfter'
|
||||
import type {IRepeatMode} from '@/types/IRepeatMode'
|
||||
|
||||
import type {PartialWithId} from '@/types/PartialWithId'
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
|
||||
export interface ITask extends IAbstract {
|
||||
id: number
|
||||
@ -30,7 +31,7 @@ export interface ITask extends IAbstract {
|
||||
repeatAfter: number | IRepeatAfter
|
||||
repeatFromCurrentDate: boolean
|
||||
repeatMode: IRepeatMode
|
||||
reminderDates: Date[]
|
||||
reminders: ITaskReminder[]
|
||||
parentTaskId: ITask['id']
|
||||
hexColor: string
|
||||
percentDone: number
|
||||
|
8
src/modelTypes/ITaskReminder.ts
Normal file
8
src/modelTypes/ITaskReminder.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { IAbstract } from './IAbstract'
|
||||
import type { IReminderPeriodRelativeTo } from '@/types/IReminderPeriodRelativeTo'
|
||||
|
||||
export interface ITaskReminder extends IAbstract {
|
||||
reminder: Date | null
|
||||
relativePeriod: number
|
||||
relativeTo: IReminderPeriodRelativeTo | null
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import {PRIORITIES, type Priority} from '@/constants/priorities'
|
||||
import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_MONTH, SECONDS_A_WEEK, SECONDS_A_YEAR} from '@/constants/date'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
@ -20,6 +19,9 @@ import LabelModel from './label'
|
||||
import UserModel from './user'
|
||||
import AttachmentModel from './attachment'
|
||||
import SubscriptionModel from './subscription'
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
import TaskReminderModel from '@/models/taskReminder'
|
||||
import {secondsToPeriod} from '@/helpers/time/period'
|
||||
|
||||
export const TASK_DEFAULT_COLOR = '#1973ff'
|
||||
|
||||
@ -35,21 +37,13 @@ export function getHexColor(hexColor: string): string {
|
||||
* Parses `repeatAfterSeconds` into a usable js object.
|
||||
*/
|
||||
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
|
||||
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
|
||||
|
||||
// if its dividable by 24, its something with days, otherwise hours
|
||||
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
|
||||
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
|
||||
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
|
||||
} 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}
|
||||
}
|
||||
const period = secondsToPeriod(repeatAfterSeconds)
|
||||
|
||||
return {
|
||||
type: period.unit,
|
||||
amount: period.amount,
|
||||
}
|
||||
return repeatAfter
|
||||
}
|
||||
|
||||
export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||
@ -68,7 +62,13 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||
repeatAfter: number | IRepeatAfter = 0
|
||||
repeatFromCurrentDate = false
|
||||
repeatMode: IRepeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||
reminderDates: Date[] = []
|
||||
/* Make sure to not return reminderDates to the server.
|
||||
The server currently supports both reminderDates (old API) and reminder (new API) and assumes the old logic
|
||||
if it still receives reminderDates.
|
||||
This line and reminderDates attributes will be removed after https://kolaente.dev/vikunja/api/pulls/1448 was merged.
|
||||
*/
|
||||
reminderDates = null
|
||||
reminders: ITaskReminder[] = []
|
||||
parentTaskId: ITask['id'] = 0
|
||||
hexColor = ''
|
||||
percentDone = 0
|
||||
@ -115,7 +115,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||
// Parse the repeat after into something usable
|
||||
this.repeatAfter = parseRepeatAfter(this.repeatAfter as number)
|
||||
|
||||
this.reminderDates = this.reminderDates.map(d => new Date(d))
|
||||
this.reminders = this.reminders.map(r => new TaskReminderModel(r))
|
||||
|
||||
if (this.hexColor !== '' && this.hexColor.substring(0, 1) !== '#') {
|
||||
this.hexColor = '#' + this.hexColor
|
||||
|
20
src/models/taskReminder.ts
Normal file
20
src/models/taskReminder.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import AbstractModel from './abstractModel'
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
import type {IReminderPeriodRelativeTo} from '@/types/IReminderPeriodRelativeTo'
|
||||
|
||||
export default class TaskReminderModel extends AbstractModel<ITaskReminder> implements ITaskReminder {
|
||||
reminder: Date | null
|
||||
relativePeriod = 0
|
||||
relativeTo: IReminderPeriodRelativeTo | null = null
|
||||
|
||||
constructor(data: Partial<ITaskReminder> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
this.reminder = parseDateOrNull(data.reminder)
|
||||
if (this.relativeTo === '') {
|
||||
this.relativeTo = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -54,17 +54,17 @@ export default class TaskService extends AbstractService<ITask> {
|
||||
model.created = new Date(model.created).toISOString()
|
||||
model.updated = new Date(model.updated).toISOString()
|
||||
|
||||
model.reminderDates = null
|
||||
// remove all nulls, these would create empty reminders
|
||||
for (const index in model.reminderDates) {
|
||||
if (model.reminderDates[index] === null) {
|
||||
model.reminderDates.splice(index, 1)
|
||||
for (const index in model.reminders) {
|
||||
if (model.reminders[index] === null) {
|
||||
model.reminders.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Make normal timestamps from js dates
|
||||
if (model.reminderDates.length > 0) {
|
||||
model.reminderDates = model.reminderDates.map(r => {
|
||||
return new Date(r).toISOString()
|
||||
if (model.reminders.length > 0) {
|
||||
model.reminders.forEach(r => {
|
||||
r.reminder = new Date(r.reminder).toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
|
8
src/types/IReminderPeriodRelativeTo.ts
Normal file
8
src/types/IReminderPeriodRelativeTo.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const REMINDER_PERIOD_RELATIVE_TO_TYPES = {
|
||||
DUEDATE: 'due_date',
|
||||
STARTDATE: 'start_date',
|
||||
ENDDATE: 'end_date',
|
||||
} as const
|
||||
|
||||
export type IReminderPeriodRelativeTo = typeof REMINDER_PERIOD_RELATIVE_TO_TYPES[keyof typeof REMINDER_PERIOD_RELATIVE_TO_TYPES]
|
||||
|
@ -1,4 +1,6 @@
|
||||
export const REPEAT_TYPES = {
|
||||
Seconds: 'seconds',
|
||||
Minutes: 'minutes',
|
||||
Hours: 'hours',
|
||||
Days: 'days',
|
||||
Weeks: 'weeks',
|
||||
|
@ -160,7 +160,7 @@
|
||||
<reminders
|
||||
:disabled="!canWrite"
|
||||
:ref="e => setFieldRef('reminders', e)"
|
||||
v-model="task.reminderDates"
|
||||
v-model="task.reminders"
|
||||
@update:model-value="saveTask"
|
||||
/>
|
||||
</div>
|
||||
@ -639,7 +639,7 @@ function setActiveFields() {
|
||||
activeFields.percentDone = task.percentDone > 0
|
||||
activeFields.priority = task.priority !== PRIORITIES.UNSET
|
||||
activeFields.relatedTasks = Object.keys(task.relatedTasks).length > 0
|
||||
activeFields.reminders = task.reminderDates.length > 0
|
||||
activeFields.reminders = task.reminders.length > 0
|
||||
activeFields.repeatAfter = task.repeatAfter.amount > 0
|
||||
activeFields.startDate = task.startDate !== null
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user