1
0

feat: edit relative reminders (#3248)

Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/3248
This commit is contained in:
konrad
2023-06-10 17:04:09 +00:00
18 changed files with 869 additions and 278 deletions

View 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>

View 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>

View 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>

View File

@ -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>