feat: Edit relative reminders #3248
|
@ -1,22 +1,29 @@
|
|||
<template>
|
||||
<div>
|
||||
<ReminderPeriod v-if="showRelativeReminder" v-model="reminder" :disabled="disabled"
|
||||
@update:modelValue="() => updateData()"></ReminderPeriod>
|
||||
<ReminderPeriod
|
||||
dpschen marked this conversation as resolved
Outdated
|
||||
v-if="showRelativeReminder"
|
||||
v-model="reminder"
|
||||
:disabled="disabled"
|
||||
@update:modelValue="emit('update:modelValue', reminder.value)"
|
||||
/>
|
||||
|
||||
<Datepicker
|
||||
v-if="showAbsoluteReminder"
|
||||
v-model="reminderDate"
|
||||
:disabled="disabled"
|
||||
@close-on-change="() => setReminderDate()"
|
||||
@close-on-change="setReminderDate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch, type PropType} from 'vue'
|
||||
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
|
||||
import Datepicker from '@/components/input/datepicker.vue'
|
||||
import ReminderPeriod from '@/components/tasks/partials/reminder-period.vue'
|
||||
import TaskReminderModel from '@/models/taskReminder'
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
import {computed, ref, watch, type PropType} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -30,35 +37,35 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const reminder = ref<ITaskReminder>()
|
||||
const reminderDate = ref()
|
||||
const reminder = ref<ITaskReminder>(new TaskReminderModel())
|
||||
const reminderDate = computed({
|
||||
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 showRelativeReminder = computed(() => !reminder.value || reminder.value?.relativeTo)
|
||||
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
Please use a Please use a `computed` for both `show...` functions instead.
ce72
commented
ok ok
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
reminder.value = value
|
||||
if (reminder.value && reminder.value.reminder) {
|
||||
reminderDate.value = new Date(reminder.value.reminder)
|
||||
}
|
||||
props.modelValue,
|
||||
(newReminder) => {
|
||||
reminder.value = newReminder || new TaskReminderModel()
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function updateData() {
|
||||
emit('update:modelValue', reminder.value)
|
||||
}
|
||||
|
||||
function setReminderDate() {
|
||||
if (!reminderDate.value) {
|
||||
return
|
||||
}
|
||||
if (!reminder.value) {
|
||||
reminder.value = new TaskReminderModel()
|
||||
}
|
||||
reminder.value.reminder = new Date(reminderDate.value)
|
||||
updateData()
|
||||
emit('update:modelValue', reminder.value)
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
<template>
|
||||
<div class="datepicker">
|
||||
<BaseButton :disabled="disabled" class="show" v-if="!!reminder?.relativeTo" @click.stop="togglePeriodPopup">
|
||||
<div
|
||||
v-if="!!reminder?.relativeTo"
|
||||
class="reminder-period"
|
||||
ce72 marked this conversation as resolved
Outdated
konrad
commented
What about inlining What about inlining `formatBeforeAfter` to something like `{{ reminder.relativePeriod <= 0 ? '≤' : '>' }}`
ce72
commented
ok ok
|
||||
>
|
||||
<Popup>
|
||||
<template #trigger="{toggle}">
|
||||
<BaseButton
|
||||
@click="toggle"
|
||||
:disabled="disabled"
|
||||
class="show"
|
||||
>
|
||||
{{ formatDuration(reminder.relativePeriod) }} {{ reminder.relativePeriod <= 0 ? '≤' : '>' }}
|
||||
{{ formatRelativeTo(reminder.relativeTo) }}
|
||||
<span class="icon"><icon icon="chevron-down"/></span>
|
||||
</BaseButton>
|
||||
<CustomTransition name="fade">
|
||||
<div v-if="isShowForm" class="mt-2" ref="periodPopup">
|
||||
</template>
|
||||
ce72 marked this conversation as resolved
Outdated
konrad
commented
Please add a translation string for the Please add a translation string for the `d`. (It does not look like much, but still)
konrad
commented
Same for the placeholders. Same for the placeholders.
ce72
commented
ok ok
dpschen
commented
To be honest I was confused by the To be honest I was confused by the `d` when I first saw the interface.
Why don't we write it out?
ce72
commented
ok ok
|
||||
|
||||
<template #content>
|
||||
<div class="mt-2">
|
||||
<div class="control is-flex is-align-items-center">
|
||||
<label>
|
||||
<input
|
||||
:disabled="disabled"
|
||||
class="input"
|
||||
|
@ -15,6 +28,7 @@
|
|||
type="number"
|
||||
min="0"
|
||||
/> {{ $t('task.reminder.days') }}
|
||||
</label>
|
||||
<input
|
||||
:disabled="disabled"
|
||||
class="input"
|
||||
|
@ -31,23 +45,23 @@
|
|||
type="number"
|
||||
min="0"
|
||||
/>
|
||||
|
||||
<div class="select">
|
||||
<select :disabled="disabled" v-model="periodInput.sign" id="sign">
|
||||
<select :disabled="disabled" v-model.number="periodInput.sign">
|
||||
<option value="-1">≤</option>
|
||||
<option value="1">></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="select">
|
||||
<select :disabled="disabled" v-model="periodInput.relativeTo" id="relativeTo">
|
||||
<select :disabled="disabled" v-model="periodInput.relativeTo">
|
||||
<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.STARTDATE">{{ $t('task.attributes.startDate')}}</option>
|
||||
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE">{{ $t('task.attributes.endDate') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<x-button
|
||||
:disabled="disabled"
|
||||
|
@ -59,20 +73,24 @@
|
|||
</x-button>
|
||||
</div>
|
||||
</div>
|
||||
</CustomTransition>
|
||||
</template>
|
||||
</Popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {reactive, ref, watch, type PropType, computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import Popup from '@/components/misc/popup.vue'
|
||||
|
||||
import {periodToSeconds, 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'
|
||||
import {onMounted, onBeforeUnmount, reactive, ref, watch, type PropType} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
|
@ -87,22 +105,23 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const reminder = ref<ITaskReminder>()
|
||||
const isShowForm = ref(false)
|
||||
|
||||
const periodInput = reactive({
|
||||
duration: {days: 0, hours: 0, minutes: 0, seconds: 0},
|
||||
duration: {
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0
|
||||
},
|
||||
relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE,
|
||||
sign: -1,
|
||||
})
|
||||
|
||||
onMounted(() => document.addEventListener('click', hidePeriodPopup))
|
||||
onBeforeUnmount(() => document.removeEventListener('click', hidePeriodPopup))
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
props.modelValue,
|
||||
(value) => {
|
||||
reminder.value = value
|
||||
if (value && value.relativeTo != null) {
|
||||
|
@ -121,28 +140,13 @@ watch(
|
|||
function updateData() {
|
||||
changed.value = true
|
||||
if (reminder.value) {
|
||||
reminder.value.relativePeriod = parseInt(periodInput.sign) * periodToSeconds(periodInput.duration.days, periodInput.duration.hours, periodInput.duration.minutes, 0)
|
||||
reminder.value.relativePeriod = periodInput.sign * periodToSeconds(periodInput.duration.days, periodInput.duration.hours, periodInput.duration.minutes, 0)
|
||||
reminder.value.relativeTo = periodInput.relativeTo
|
||||
reminder.value.reminder = null
|
||||
}
|
||||
emit('update:modelValue', reminder.value)
|
||||
}
|
||||
|
||||
function togglePeriodPopup() {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
isShowForm.value = !isShowForm.value
|
||||
}
|
||||
|
||||
const periodPopup = ref<HTMLElement | null>(null)
|
||||
|
||||
function hidePeriodPopup(e: MouseEvent) {
|
||||
if (isShowForm.value) {
|
||||
closeWhenClickedOutside(e, periodPopup.value, close)
|
||||
}
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
updateData()
|
||||
close()
|
||||
|
@ -153,10 +157,8 @@ const changed = ref(false)
|
|||
function close() {
|
||||
setTimeout(() => {
|
||||
isShowForm.value = false
|
||||
emit('close', changed.value)
|
||||
if (changed.value) {
|
||||
changed.value = false
|
||||
emit('close-on-change', changed.value)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
@ -171,6 +173,16 @@ function formatDuration(reminderPeriod: number): string {
|
|||
('' + 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:
|
||||
|
|
11
src/components/tasks/partials/reminders.story.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import reminders from './reminders.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story>
|
||||
<Variant title="Default">
|
||||
<reminders />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
|
@ -1,32 +1,41 @@
|
|||
<template>
|
||||
<div class="reminders">
|
||||
<div v-for="(r, index) in reminders" :key="index" :class="{ 'overdue': r.reminder < new Date() }" class="reminder-input">
|
||||
<div
|
||||
v-for="(r, index) in reminders"
|
||||
:key="index"
|
||||
:class="{ 'overdue': r.reminder < new Date() }"
|
||||
class="reminder-input"
|
||||
>
|
||||
<div class="reminder-detail">
|
||||
<ReminderDetail :disabled="disabled" v-model="reminders[index]" @update:modelValue="() => editReminder(index)"/>
|
||||
<ReminderDetail :disabled="disabled" v-model="reminders[index]" />
|
||||
</div>
|
||||
<div>
|
||||
<BaseButton @click="removeReminderByIndex(index)" v-if="!disabled" class="remove">
|
||||
<icon icon="times"></icon>
|
||||
<BaseButton v-if="!disabled" @click="removeReminderByIndex(index)" class="remove">
|
||||
<icon icon="times" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reminder-input">
|
||||
<BaseButton @click.stop="toggleAddReminder" v-if="!disabled">
|
||||
|
||||
<div v-if="!disabled" class="reminder-input">
|
||||
<BaseButton @click="toggleAddReminder">
|
||||
{{ $t('task.addReminder') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="reminder-input">
|
||||
<ReminderDetail v-if="isAddReminder" :disabled="disabled" @update:modelValue="addNewReminder"/>
|
||||
|
||||
<div v-if="isAddReminder" class="reminder-input">
|
||||
<ReminderDetail :disabled="disabled" @update:modelValue="addNewReminder"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {reactive, ref, watch, type PropType} from 'vue'
|
||||
|
||||
import type { ITaskReminder } from '@/modelTypes/ITaskReminder'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
konrad marked this conversation as resolved
Outdated
konrad
commented
Why remove the type? Why remove the type?
ce72
commented
The type has changed from The type has changed from `Date | String` to a complex type (`Array as PropType<ITaskReminder[]>`). I don't see why a validation is required. What do you suggest?
dpschen
commented
@ce72: Sry I don't understand your answer here :) @ce72: Sry I don't understand your answer here :)
What has the validation of the modelValue to do with the type of disabled?
ce72
commented
Can you please make a concrete suggestion what you expect here? I can find no other place in the code where you have a validation on a complex props type. And am not enough typescript native to understand what you want to have added here. Can you please make a concrete suggestion what you expect here? I can find no other place in the code where you have a validation on a complex props type. And am not enough typescript native to understand what you want to have added here.
dpschen
commented
I think I misunderstood @konrads first question, because the lines changed. I assumed he is refering to the removed type from disabled and not the removed validation of the modelValue. I think I misunderstood @konrads first question, because the lines changed. I assumed he is refering to the removed type from disabled and not the removed validation of the modelValue.
ce72
commented
Then my question goes to @konrad Then my question goes to @konrad
konrad
commented
Okay that actually makes sense. > The type has changed from Date | String to a complex type (Array as PropType<ITaskReminder[]>). I don't see why a validation is required. What do you suggest?
Okay that actually makes sense.
|
||||
import ReminderDetail from '@/components/tasks/partials/reminder-detail.vue'
|
||||
import TaskReminderModel from '@/models/taskReminder'
|
||||
import type { ITaskReminder } from '@/modelTypes/ITaskReminder'
|
||||
import { onMounted, reactive, ref, watch, type PropType } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -42,15 +51,12 @@ const emit = defineEmits(['update:modelValue'])
|
|||
|
||||
const reminders = ref<ITaskReminder[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
reminders.value = [...props.modelValue]
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
props.modelValue,
|
||||
(newVal) => {
|
||||
reminders.value = newVal
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const isAddReminder = ref(false)
|
||||
|
@ -86,8 +92,7 @@ function removeReminderByIndex(index: number) {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reminders {
|
||||
.reminder-input {
|
||||
.reminder-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
@ -95,19 +100,17 @@ function removeReminderByIndex(index: number) {
|
|||
color: var(--danger);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::last-child {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.reminder-detail {
|
||||
}
|
||||
.reminder-detail {
|
||||
width: 100%;
|
||||
}
|
||||
.remove {
|
||||
}
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
vertical-align: top;
|
||||
padding-left: .5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Nitpicking, but please format this like this:
That way it's more readable.
Fixed in my changes, see #3248 (comment)