feat(settings): restructure general settings view

This commit is contained in:
kolaente 2025-08-01 13:44:24 +02:00
parent e608f987f6
commit 715c28736f
No known key found for this signature in database
GPG Key ID: F40E70337AB24C9B
3 changed files with 343 additions and 257 deletions

1
.gitignore vendored
View File

@ -43,3 +43,4 @@ devenv.local.nix
# AI Tools
/.claude/
PLAN.md
/.crush/

View File

@ -115,6 +115,13 @@
},
"externalUserNameChange": "Your name is managed by your login provider ({provider}). To change it, please update it there instead."
},
"sections": {
"personalInformation": "Personal Information",
"taskAndNotifications": "Projects & Tasks",
"privacy": "Privacy",
"localization": "Localization",
"appearance": "Appearance & Behavior"
},
"totp": {
"title": "Two Factor Authentication",
"enroll": "Enroll",

View File

@ -1,275 +1,326 @@
<template>
<Card
:title="$t('user.settings.general.title')"
:title="$t('user.settings.sections.personalInformation')"
class="general-settings"
:loading="loading"
>
<div class="field">
<label
class="label"
:for="`newName${id}`"
>
{{ $t('user.settings.general.name') }}
</label>
<div class="control">
<input
:id="`newName${id}`"
v-model="settings.name"
:disabled="isExternalUser"
class="input"
:placeholder="$t('user.settings.general.newName')"
type="text"
@keyup.enter="updateSettings"
>
</div>
<p
v-if="isExternalUser"
class="help"
>
{{ $t('user.settings.general.externalUserNameChange', {provider: authStore.info.authProvider}) }}
</p>
</div>
<div class="field">
<label class="label">
{{ $t('user.settings.general.defaultProject') }}
</label>
<ProjectSearch v-model="defaultProject" />
</div>
<div class="field">
<label class="label">
{{ $t('user.settings.general.defaultView') }}
</label>
<div class="select">
<select v-model="settings.frontendSettings.defaultView">
<option
v-for="view in DEFAULT_PROJECT_VIEW_SETTINGS"
:key="view"
:value="view"
>
{{ $t(`project.${view}.title`) }}
</option>
</select>
</div>
</div>
<div class="field">
<label class="label">
{{ $t('user.settings.general.minimumPriority') }}
</label>
<div class="select">
<select v-model="settings.frontendSettings.minimumPriority">
<option :value="PRIORITIES.LOW">
{{ $t('task.priority.low') }}
</option>
<option :value="PRIORITIES.MEDIUM">
{{ $t('task.priority.medium') }}
</option>
<option :value="PRIORITIES.HIGH">
{{ $t('task.priority.high') }}
</option>
<option :value="PRIORITIES.URGENT">
{{ $t('task.priority.urgent') }}
</option>
<option :value="PRIORITIES.DO_NOW">
{{ $t('task.priority.doNow') }}
</option>
</select>
</div>
</div>
<div
v-if="hasFilters"
class="field"
>
<label class="label">
{{ $t('user.settings.general.filterUsedOnOverview') }}
</label>
<ProjectSearch
v-model="filterUsedInOverview"
:saved-filters-only="true"
/>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.emailRemindersEnabled"
type="checkbox"
>
{{ $t('user.settings.general.emailReminders') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.discoverableByName"
type="checkbox"
>
{{ $t('user.settings.general.discoverableByName') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.discoverableByEmail"
type="checkbox"
>
{{ $t('user.settings.general.discoverableByEmail') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.frontendSettings.playSoundWhenDone"
type="checkbox"
>
{{ $t('user.settings.general.playSoundWhenDone') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.frontendSettings.allowIconChanges"
type="checkbox"
>
{{ $t('user.settings.general.allowIconChanges') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.overdueTasksRemindersEnabled"
type="checkbox"
>
{{ $t('user.settings.general.overdueReminders') }}
</label>
</div>
<div
v-if="settings.overdueTasksRemindersEnabled"
class="field"
>
<label
class="label"
for="overdueTasksReminderTime"
>
{{ $t('user.settings.general.overdueTasksRemindersTime') }}
</label>
<div class="control">
<input
id="overdueTasksReminderTime"
v-model="settings.overdueTasksRemindersTime"
class="input"
type="time"
@keyup.enter="updateSettings"
>
</div>
</div>
<div class="field">
<label class="is-flex is-align-items-center">
<span>
{{ $t('user.settings.general.weekStart') }}
</span>
<div class="select ml-2">
<select v-model.number="settings.weekStart">
<option value="0">{{ $t('user.settings.general.weekStartSunday') }}</option>
<option value="1">{{ $t('user.settings.general.weekStartMonday') }}</option>
</select>
</div>
</label>
</div>
<div class="field">
<label class="is-flex is-align-items-center">
<span>
{{ $t('user.settings.general.language') }}
</span>
<div class="select ml-2">
<select v-model="settings.language">
<option
v-for="lang in availableLanguageOptions"
:key="lang.code"
:value="lang.code"
>{{ lang.title }}
</option>
</select>
</div>
</label>
</div>
<div class="field">
<label class="is-flex is-align-items-center">
<span>
{{ $t('user.settings.quickAddMagic.title') }}
</span>
<div class="select ml-2">
<select v-model="settings.frontendSettings.quickAddMagicMode">
<option
v-for="set in PrefixMode"
:key="set"
:value="set"
<div class="field-group">
<div class="field">
<label :for="`newName${id}`">
<span>
{{ $t('user.settings.general.name') }}
</span>
<div class="ml-auto two-col">
<input
:id="`newName${id}`"
v-model="settings.name"
:disabled="isExternalUser"
class="input"
:placeholder="$t('user.settings.general.newName')"
type="text"
@keyup.enter="updateSettings"
>
{{ $t(`user.settings.quickAddMagic.${set}`) }}
</option>
</select>
</div>
</label>
</div>
<div class="field">
<label class="is-flex is-align-items-center">
<span>
{{ $t('user.settings.appearance.title') }}
</span>
<div class="select ml-2">
<select v-model="settings.frontendSettings.colorSchema">
<!-- TODO: use the Vikunja logo in color scheme as option buttons -->
<option
v-for="(title, schemeId) in colorSchemeSettings"
:key="schemeId"
:value="schemeId"
>
{{ title }}
</option>
</select>
</div>
</label>
</div>
<div class="field">
<label class="is-flex is-align-items-center">
<span>
{{ $t('user.settings.general.dateDisplay') }}
</span>
<div class="select ml-2">
<select v-model="settings.frontendSettings.dateDisplay">
<option
v-for="(label, value) in dateDisplaySettings"
:key="value"
:value="value"
>{{ label }}</option>
</select>
</div>
</label>
</div>
<div class="field">
<label class="is-flex is-align-items-center">
<span>
{{ $t('user.settings.general.timezone') }}
</span>
<Multiselect
v-model="timezoneObject"
:placeholder="$t('user.settings.general.timezone')"
:search-results="timezoneSearchResults"
:show-empty="true"
class="ml-2 timezone-select"
label="label"
@search="searchTimezones"
/>
</label>
</div>
</label>
<p
v-if="isExternalUser"
class="help"
>
{{ $t('user.settings.general.externalUserNameChange', {provider: authStore.info.authProvider}) }}
</p>
</div>
<div class="field">
<label>
<span>
{{ $t('user.settings.general.defaultProject') }}
</span>
<div class="ml-auto two-col">
<ProjectSearch v-model="defaultProject" />
</div>
</label>
</div>
</div>
</Card>
<Card
:title="$t('user.settings.sections.taskAndNotifications')"
class="general-settings section-block"
:loading="loading"
>
<div class="field-group">
<div class="field">
<label>
<span>
{{ $t('user.settings.general.defaultView') }}
</span>
<div class="select ml-auto two-col">
<select v-model="settings.frontendSettings.defaultView">
<option
v-for="view in DEFAULT_PROJECT_VIEW_SETTINGS"
:key="view"
:value="view"
>
{{ $t(`project.${view}.title`) }}
</option>
</select>
</div>
</label>
</div>
<div class="field">
<label>
<span>
{{ $t('user.settings.general.minimumPriority') }}
</span>
<div class="select ml-auto two-col">
<select v-model="settings.frontendSettings.minimumPriority">
<option :value="PRIORITIES.LOW">
{{ $t('task.priority.low') }}
</option>
<option :value="PRIORITIES.MEDIUM">
{{ $t('task.priority.medium') }}
</option>
<option :value="PRIORITIES.HIGH">
{{ $t('task.priority.high') }}
</option>
<option :value="PRIORITIES.URGENT">
{{ $t('task.priority.urgent') }}
</option>
<option :value="PRIORITIES.DO_NOW">
{{ $t('task.priority.doNow') }}
</option>
</select>
</div>
</label>
</div>
<div
v-if="hasFilters"
class="field"
>
<label>
<span>
{{ $t('user.settings.general.filterUsedOnOverview') }}
</span>
<div class="ml-auto two-col">
<ProjectSearch
v-model="filterUsedInOverview"
:saved-filters-only="true"
/>
</div>
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.emailRemindersEnabled"
type="checkbox"
>
{{ $t('user.settings.general.emailReminders') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.overdueTasksRemindersEnabled"
type="checkbox"
>
{{ $t('user.settings.general.overdueReminders') }}
</label>
</div>
<div
v-if="settings.overdueTasksRemindersEnabled"
class="field"
>
<label for="overdueTasksReminderTime">
<span>
{{ $t('user.settings.general.overdueTasksRemindersTime') }}
</span>
<div class="ml-auto two-col">
<input
id="overdueTasksReminderTime"
v-model="settings.overdueTasksRemindersTime"
class="input"
type="time"
@keyup.enter="updateSettings"
>
</div>
</label>
</div>
</div>
</Card>
<Card
:title="$t('user.settings.sections.localization')"
class="general-settings section-block"
:loading="loading"
>
<div class="field-group">
<div class="field">
<label>
<span>
{{ $t('user.settings.general.language') }}
</span>
<div class="select ml-auto two-col">
<select v-model="settings.language">
<option
v-for="lang in availableLanguageOptions"
:key="lang.code"
:value="lang.code"
>{{ lang.title }}
</option>
</select>
</div>
</label>
</div>
<div class="field">
<label>
<span>
{{ $t('user.settings.general.timezone') }}
</span>
<div class="ml-auto two-col">
<Multiselect
v-model="timezoneObject"
:placeholder="$t('user.settings.general.timezone')"
:search-results="timezoneSearchResults"
:show-empty="true"
class="timezone-select"
label="label"
@search="searchTimezones"
/>
</div>
</label>
</div>
<div class="field">
<label>
<span>
{{ $t('user.settings.general.weekStart') }}
</span>
<div class="select ml-auto two-col">
<select v-model.number="settings.weekStart">
<option value="0">{{ $t('user.settings.general.weekStartSunday') }}</option>
<option value="1">{{ $t('user.settings.general.weekStartMonday') }}</option>
</select>
</div>
</label>
</div>
<div class="field">
<label>
<span>
{{ $t('user.settings.general.dateDisplay') }}
</span>
<div class="select ml-auto two-col">
<select v-model="settings.frontendSettings.dateDisplay">
<option
v-for="(label, value) in dateDisplaySettings"
:key="value"
:value="value"
>{{ label }}</option>
</select>
</div>
</label>
</div>
</div>
</Card>
<Card
:title="$t('user.settings.sections.appearance')"
class="general-settings section-block"
:loading="loading"
>
<div class="field-group">
<div class="field">
<label>
<span>
{{ $t('user.settings.appearance.title') }}
</span>
<div class="select ml-auto two-col">
<select v-model="settings.frontendSettings.colorSchema">
<option
v-for="(title, schemeId) in colorSchemeSettings"
:key="schemeId"
:value="schemeId"
>
{{ title }}
</option>
</select>
</div>
</label>
</div>
<div class="field">
<label>
<span>
{{ $t('user.settings.quickAddMagic.title') }}
</span>
<div class="select ml-auto two-col">
<select v-model="settings.frontendSettings.quickAddMagicMode">
<option
v-for="set in PrefixMode"
:key="set"
:value="set"
>
{{ $t(`user.settings.quickAddMagic.${set}`) }}
</option>
</select>
</div>
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.frontendSettings.playSoundWhenDone"
type="checkbox"
>
{{ $t('user.settings.general.playSoundWhenDone') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.frontendSettings.allowIconChanges"
type="checkbox"
>
{{ $t('user.settings.general.allowIconChanges') }}
</label>
</div>
</div>
</Card>
<Card
:title="$t('user.settings.sections.privacy')"
class="general-settings section-block"
:loading="loading"
>
<div class="field-group">
<div class="field">
<label class="checkbox">
<input
v-model="settings.discoverableByName"
type="checkbox"
>
{{ $t('user.settings.general.discoverableByName') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.discoverableByEmail"
type="checkbox"
>
{{ $t('user.settings.general.discoverableByEmail') }}
</label>
</div>
</div>
</Card>
<div class="sticky-save">
<XButton
v-cy="'saveGeneralSettings'"
:loading="loading"
class="is-fullwidth mt-4"
class="is-fullwidth"
@click="updateSettings()"
>
{{ $t('misc.save') }}
</XButton>
</Card>
</div>
</template>
@ -456,4 +507,31 @@ async function updateSettings() {
min-width: 200px;
flex-grow: 1;
}
.section-block + .section-block {
margin-top: 1.5rem;
}
.field-group {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
}
.field > label {
display: flex;
align-items: center;
gap: 0.5rem;
}
.two-col {
flex: 0 0 50%;
margin-left: .5rem;
}
.sticky-save {
position: sticky;
bottom: 0;
padding: .25rem 1rem 1rem;
}
</style>