Compare commits

...
This repository has been archived on 2025-10-28. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

17 commits

Author SHA1 Message Date
kolaente
ad2644edf8
chore: remove unused comment 2022-09-30 13:32:01 +02:00
kolaente
aacd0a1331
chore: clarify comment 2022-09-30 13:31:04 +02:00
kolaente
e623954351
chore: better typing 2022-09-30 13:27:14 +02:00
kolaente
d5bc1cd1d6
chore: make amounts const 2022-09-30 13:22:46 +02:00
kolaente
a341dbd5d2
fix: combine related css classes 2022-09-29 18:32:51 +02:00
kolaente
5c68643892
fix: directly populate user settings with default reminder amount 2022-09-29 18:31:56 +02:00
kolaente
2aee048f61
fix: use vue-i18n pluralization 2022-09-29 18:30:40 +02:00
kolaente
429b8a1ec4
chore: use amount const in tests 2022-09-29 18:23:07 +02:00
kolaente
7725de7483
feat: move amount second calculation to mapping const 2022-09-29 18:20:43 +02:00
kolaente
e65c286730
fix: lint 2022-09-29 18:16:55 +02:00
kolaente
5aafbd9a72
feat: re-populate default reminder from saved settings 2022-09-29 18:16:55 +02:00
kolaente
28312081ae
feat: re-populate default reminder enabled state when loading settings 2022-09-29 18:16:55 +02:00
kolaente
8baafab456
fix: show reminder field when changing a due date and a reminder was set 2022-09-29 18:16:55 +02:00
kolaente
80cc58a45d
feat: automatically add a reminder to a task with due date but no reminders 2022-09-29 18:16:55 +02:00
kolaente
5b4fe9176e
feat: unify time units and use the same ones everywhere 2022-09-29 18:16:55 +02:00
kolaente
3d5f50ccd4
fix: improve the reminder hint 2022-09-29 18:16:54 +02:00
kolaente
9d2990a23b
feat: allow saving a default reminder amount 2022-09-29 18:16:53 +02:00
12 changed files with 252 additions and 27 deletions

View file

@ -36,35 +36,35 @@
<tbody> <tbody>
<tr> <tr>
<td><code>s</code></td> <td><code>s</code></td>
<td>{{ $t('input.datemathHelp.units.seconds') }}</td> <td>{{ $tc('time.seconds', 2) }}</td>
</tr> </tr>
<tr> <tr>
<td><code>m</code></td> <td><code>m</code></td>
<td>{{ $t('input.datemathHelp.units.minutes') }}</td> <td>{{ $tc('time.minutes', 2) }}</td>
</tr> </tr>
<tr> <tr>
<td><code>h</code></td> <td><code>h</code></td>
<td>{{ $t('input.datemathHelp.units.hours') }}</td> <td>{{ $tc('time.hours', 2) }}</td>
</tr> </tr>
<tr> <tr>
<td><code>H</code></td> <td><code>H</code></td>
<td>{{ $t('input.datemathHelp.units.hours') }}</td> <td>{{ $tc('time.hours', 2) }}</td>
</tr> </tr>
<tr> <tr>
<td><code>d</code></td> <td><code>d</code></td>
<td>{{ $t('input.datemathHelp.units.days') }}</td> <td>{{ $tc('time.days', 2) }}</td>
</tr> </tr>
<tr> <tr>
<td><code>w</code></td> <td><code>w</code></td>
<td>{{ $t('input.datemathHelp.units.weeks') }}</td> <td>{{ $tc('time.weeks', 2) }}</td>
</tr> </tr>
<tr> <tr>
<td><code>M</code></td> <td><code>M</code></td>
<td>{{ $t('input.datemathHelp.units.months') }}</td> <td>{{ $tc('time.months', 2) }}</td>
</tr> </tr>
<tr> <tr>
<td><code>y</code></td> <td><code>y</code></td>
<td>{{ $t('input.datemathHelp.units.years') }}</td> <td>{{ $tc('time.years', 2) }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -48,11 +48,11 @@
@change="updateData" @change="updateData"
:disabled="disabled || undefined" :disabled="disabled || undefined"
> >
<option value="hours">{{ $t('task.repeat.hours') }}</option> <option value="hours">{{ $tc('time.hours', 2) }}</option>
<option value="days">{{ $t('task.repeat.days') }}</option> <option value="days">{{ $tc('time.days', 2) }}</option>
<option value="weeks">{{ $t('task.repeat.weeks') }}</option> <option value="weeks">{{ $tc('time.weeks', 2) }}</option>
<option value="months">{{ $t('task.repeat.months') }}</option> <option value="months">{{ $tc('time.months', 2) }}</option>
<option value="years">{{ $t('task.repeat.years') }}</option> <option value="years">{{ $tc('time.years', 2) }}</option>
</select> </select>
</div> </div>
</div> </div>

View file

@ -0,0 +1,63 @@
import {describe, it, expect, vi, afterEach, beforeEach} from 'vitest'
import {
AMOUNTS_IN_SECONDS,
getDefaultReminderSettings,
getSavedReminderSettings,
parseSavedReminderAmount,
saveDefaultReminder,
} from '@/helpers/defaultReminder'
import * as exports from '@/helpers/defaultReminder'
describe('Default Reminder Save', () => {
it('Should save a default reminder with minutes', () => {
const spy = vi.spyOn(window.localStorage, 'setItem')
saveDefaultReminder(true, 'minutes', 5)
expect(spy).toHaveBeenCalledWith('defaultReminder', '{"enabled":true,"amount":300}')
})
it('Should save a default reminder with hours', () => {
const spy = vi.spyOn(window.localStorage, 'setItem')
saveDefaultReminder(true, 'hours', 5)
expect(spy).toHaveBeenCalledWith('defaultReminder', '{"enabled":true,"amount":18000}')
})
it('Should save a default reminder with days', () => {
const spy = vi.spyOn(window.localStorage, 'setItem')
saveDefaultReminder(true, 'days', 5)
expect(spy).toHaveBeenCalledWith('defaultReminder', '{"enabled":true,"amount":432000}')
})
it('Should save a default reminder with months', () => {
const spy = vi.spyOn(window.localStorage, 'setItem')
saveDefaultReminder(true, 'months', 5)
expect(spy).toHaveBeenCalledWith('defaultReminder', '{"enabled":true,"amount":12960000}')
})
})
describe('Default Reminder Load', () => {
it('Should parse minutes', () => {
const settings = parseSavedReminderAmount(5 * AMOUNTS_IN_SECONDS.minutes)
expect(settings.amount).toBe(5)
expect(settings.type).toBe('minutes')
})
it('Should parse hours', () => {
const settings = parseSavedReminderAmount(5 * AMOUNTS_IN_SECONDS.hours)
expect(settings.amount).toBe(5)
expect(settings.type).toBe('hours')
})
it('Should parse days', () => {
const settings = parseSavedReminderAmount(5 * AMOUNTS_IN_SECONDS.days)
expect(settings.amount).toBe(5)
expect(settings.type).toBe('days')
})
it('Should parse months', () => {
const settings = parseSavedReminderAmount(5 * AMOUNTS_IN_SECONDS.months)
expect(settings.amount).toBe(5)
expect(settings.type).toBe('months')
})
})

View file

@ -0,0 +1,89 @@
const DEFAULT_REMINDER_KEY = 'defaultReminder'
export const AMOUNTS_IN_SECONDS: {
[type in SavedReminderSettings['type']]: number
} = {
minutes: 60,
hours: 60 * 60,
days: 60 * 60 * 24,
months: 60 * 60 * 24 * 30,
} as const
interface DefaultReminderSettings {
enabled: boolean,
amount: number,
}
interface SavedReminderSettings {
enabled: boolean,
amount: number,
type: 'minutes' | 'hours' | 'days' | 'months',
}
function calculateDefaultReminderSeconds(type: SavedReminderSettings['type'], amount: number): number {
return amount * (AMOUNTS_IN_SECONDS[type] || 0)
}
export function saveDefaultReminder(enabled: boolean, type: SavedReminderSettings['type'], amount: number) {
const defaultReminderSeconds = calculateDefaultReminderSeconds(type, amount)
localStorage.setItem(DEFAULT_REMINDER_KEY, JSON.stringify(<DefaultReminderSettings>{
enabled,
amount: defaultReminderSeconds,
}))
}
export function getDefaultReminderAmount(): number | null {
const settings = getDefaultReminderSettings()
return settings?.enabled
? settings.amount
: null
}
export function getDefaultReminderSettings(): DefaultReminderSettings | null {
const s: string | null = window.localStorage.getItem(DEFAULT_REMINDER_KEY)
if (s === null) {
return null
}
return JSON.parse(s)
}
export function parseSavedReminderAmount(amountSeconds: number): SavedReminderSettings {
const amountMinutes = amountSeconds / 60
const settings: SavedReminderSettings = {
enabled: true, // We're assuming the caller to have checked this properly
amount: amountMinutes,
type: 'minutes',
}
if ((amountMinutes / 60 / 24) % 30 === 0) {
settings.amount = amountMinutes / 60 / 24 / 30
settings.type = 'months'
} else if ((amountMinutes / 60) % 24 === 0) {
settings.amount = amountMinutes / 60 / 24
settings.type = 'days'
} else if (amountMinutes % 60 === 0) {
settings.amount = amountMinutes / 60
settings.type = 'hours'
}
return settings
}
export function getSavedReminderSettings(): SavedReminderSettings | null {
const s = getDefaultReminderSettings()
if (s === null) {
return null
}
if (!s.enabled) {
return {
enabled: false,
type: 'minutes',
amount: 0,
}
}
return parseSavedReminderAmount(s.amount)
}

View file

@ -87,7 +87,11 @@
"language": "Language", "language": "Language",
"defaultList": "Default List", "defaultList": "Default List",
"timezone": "Time Zone", "timezone": "Time Zone",
"overdueTasksRemindersTime": "Overdue tasks reminder email time" "overdueTasksRemindersTime": "Overdue tasks reminder email time",
"defaultReminder": "Set a default task reminder",
"defaultReminderHint": "If enabled, Vikunja will automatically create a reminder for a task if you set a due date and the task does not have any reminders yet.",
"defaultReminderAmount": "Default task reminder amount",
"defaultReminderAmountBefore": "before the due date of a task"
}, },
"totp": { "totp": {
"title": "Two Factor Authentication", "title": "Two Factor Authentication",
@ -547,19 +551,16 @@
"fromto": "{from} to {to}", "fromto": "{from} to {to}",
"ranges": { "ranges": {
"today": "Today", "today": "Today",
"thisWeek": "This Week", "thisWeek": "This Week",
"restOfThisWeek": "The Rest of This Week", "restOfThisWeek": "The Rest of This Week",
"nextWeek": "Next Week", "nextWeek": "Next Week",
"next7Days": "Next 7 Days", "next7Days": "Next 7 Days",
"lastWeek": "Last Week", "lastWeek": "Last Week",
"thisMonth": "This Month", "thisMonth": "This Month",
"restOfThisMonth": "The Rest of This Month", "restOfThisMonth": "The Rest of This Month",
"nextMonth": "Next Month", "nextMonth": "Next Month",
"next30Days": "Next 30 Days", "next30Days": "Next 30 Days",
"lastMonth": "Last Month", "lastMonth": "Last Month",
"thisYear": "This Year", "thisYear": "This Year",
"restOfThisYear": "The Rest of This Year" "restOfThisYear": "The Rest of This Year"
} }
@ -576,15 +577,6 @@
"roundDay": "Round down to the nearest day", "roundDay": "Round down to the nearest day",
"supportedUnits": "Supported time units are:", "supportedUnits": "Supported time units are:",
"someExamples": "Some examples of time expressions:", "someExamples": "Some examples of time expressions:",
"units": {
"seconds": "Seconds",
"minutes": "Minutes",
"hours": "Hours",
"days": "Days",
"weeks": "Weeks",
"months": "Months",
"years": "Years"
},
"examples": { "examples": {
"now": "Right now", "now": "Right now",
"in24h": "In 24h", "in24h": "In 24h",
@ -596,6 +588,15 @@
} }
} }
}, },
"time": {
"seconds": "Second | Seconds",
"minutes": "Minute | Minutes",
"hours": "Hour | Hours",
"days": "Day | Days",
"weeks": "Week | Weeks",
"months": "Month | Months",
"years": "Year | Years"
},
"task": { "task": {
"task": "Task", "task": "Task",
"new": "Create a new task", "new": "Create a new task",

View file

@ -12,4 +12,6 @@ export interface IUserSettings extends IAbstract {
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6 weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string timezone: string
language: string language: string
defaultReminder: boolean
defaultReminderAmount: number // The amount of seconds a reminder should be set before a given due date
} }

View file

@ -2,6 +2,7 @@ import AbstractModel from './abstractModel'
import type {IUserSettings} from '@/modelTypes/IUserSettings' import type {IUserSettings} from '@/modelTypes/IUserSettings'
import {getCurrentLanguage} from '@/i18n' import {getCurrentLanguage} from '@/i18n'
import {getDefaultReminderAmount} from '@/helpers/defaultReminder'
export default class UserSettingsModel extends AbstractModel<IUserSettings> implements IUserSettings { export default class UserSettingsModel extends AbstractModel<IUserSettings> implements IUserSettings {
name = '' name = ''
@ -13,6 +14,8 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
weekStart = 0 as IUserSettings['weekStart'] weekStart = 0 as IUserSettings['weekStart']
timezone = '' timezone = ''
language = getCurrentLanguage() language = getCurrentLanguage()
defaultReminder = false
defaultReminderAmount = getDefaultReminderAmount() || 0
constructor(data: Partial<IUserSettings> = {}) { constructor(data: Partial<IUserSettings> = {}) {
super() super()

View file

@ -6,6 +6,7 @@ import LabelService from './label'
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
import {colorFromHex} from '@/helpers/color/colorFromHex' import {colorFromHex} from '@/helpers/color/colorFromHex'
import {getDefaultReminderAmount} from '@/helpers/defaultReminder'
const parseDate = date => { const parseDate = date => {
if (date) { if (date) {
@ -39,7 +40,7 @@ export default class TaskService extends AbstractService<ITask> {
} }
processModel(updatedModel) { processModel(updatedModel) {
const model = { ...updatedModel } const model = {...updatedModel}
model.title = model.title?.trim() model.title = model.title?.trim()
@ -68,6 +69,15 @@ export default class TaskService extends AbstractService<ITask> {
}) })
} }
if (model.dueDate !== null && model.reminderDates.length === 0) {
const defaultReminder = getDefaultReminderAmount()
if (defaultReminder !== null) {
const dueDate = +new Date(model.dueDate)
const reminderDate = new Date(dueDate - (defaultReminder * 1000))
model.reminderDates.push(formatISO(reminderDate))
}
}
// Make the repeating amount to seconds // Make the repeating amount to seconds
let repeatAfterSeconds = 0 let repeatAfterSeconds = 0
if (model.repeatAfter !== null && (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0)) { if (model.repeatAfter !== null && (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0)) {

View file

@ -56,6 +56,7 @@
} }
} }
.field.has-addons .select select,
.field.has-addons .control .select select { .field.has-addons .control .select select {
height: 100%; height: 100%;
} }

View file

@ -697,6 +697,9 @@ export default defineComponent({
} }
this.task = await this.$store.dispatch('tasks/update', task) this.task = await this.$store.dispatch('tasks/update', task)
// Show new fields set from the api or a newly set default reminder
this.$nextTick(() => this.setActiveFields())
if (!showNotification) { if (!showNotification) {
return return

View file

@ -18,6 +18,50 @@
</label> </label>
<list-search v-model="defaultList"/> <list-search v-model="defaultList"/>
</div> </div>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="defaultReminderEnabled"/>
{{ $t('user.settings.general.defaultReminder') }}
</label>
<p class="is-size-7">
{{ $t('user.settings.general.defaultReminderHint') }}
</p>
</div>
<div class="field" v-if="defaultReminderEnabled">
<label class="label" for="defaultReminderAmount">
{{ $t('user.settings.general.defaultReminderAmount') }}
</label>
<div class="field has-addons is-align-items-center">
<div class="control">
<input
@keyup.enter="updateSettings"
class="input"
id="defaultReminderAmount"
type="number"
min="0"
v-model="defaultReminderAmount"/>
</div>
<div class="control select">
<select v-model="defaultReminderAmountType">
<option value="minutes">
{{ $tc('time.minutes', defaultReminderAmount) }}
</option>
<option value="hours">
{{ $tc('time.hours', defaultReminderAmount) }}
</option>
<option value="days">
{{ $tc('time.days', defaultReminderAmount) }}
</option>
<option value="months">
{{ $tc('time.months', defaultReminderAmount) }}
</option>
</select>
</div>
<p class="pl-2">
{{ $t('user.settings.general.defaultReminderAmountBefore') }}
</p>
</div>
</div>
<div class="field"> <div class="field">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/> <input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
@ -175,6 +219,7 @@ import {AuthenticatedHTTPFactory} from '@/http-common'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {objectIsEmpty} from '@/helpers/objectIsEmpty' import {objectIsEmpty} from '@/helpers/objectIsEmpty'
import {getSavedReminderSettings, saveDefaultReminder} from '@/helpers/defaultReminder'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')}`) useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')}`)
@ -266,9 +311,16 @@ watch(
async function updateSettings() { async function updateSettings() {
localStorage.setItem(playSoundWhenDoneKey, playSoundWhenDone.value ? 'true' : 'false') localStorage.setItem(playSoundWhenDoneKey, playSoundWhenDone.value ? 'true' : 'false')
setQuickAddMagicMode(quickAddMagicMode.value) setQuickAddMagicMode(quickAddMagicMode.value)
saveDefaultReminder(defaultReminderEnabled.value, defaultReminderAmountType.value, defaultReminderAmount.value)
await authStore.saveUserSettings({ await authStore.saveUserSettings({
settings: {...settings.value}, settings: {...settings.value},
}) })
} }
const reminderSettings = getSavedReminderSettings()
const defaultReminderEnabled = ref<boolean>(reminderSettings?.enabled || false)
const defaultReminderAmount = ref(reminderSettings?.amount || 1)
const defaultReminderAmountType = ref(reminderSettings?.type || 'days')
</script> </script>

View file

@ -32,6 +32,7 @@ export default defineConfig({
// https://vitest.dev/config/ // https://vitest.dev/config/
test: { test: {
environment: 'happy-dom', environment: 'happy-dom',
mockReset: true,
}, },
css: { css: {
preprocessorOptions: { preprocessorOptions: {