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 # AI Tools
/.claude/ /.claude/
PLAN.md 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." "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": { "totp": {
"title": "Two Factor Authentication", "title": "Two Factor Authentication",
"enroll": "Enroll", "enroll": "Enroll",

View File

@ -1,275 +1,326 @@
<template> <template>
<Card <Card
:title="$t('user.settings.general.title')" :title="$t('user.settings.sections.personalInformation')"
class="general-settings" class="general-settings"
:loading="loading" :loading="loading"
> >
<div class="field"> <div class="field-group">
<label <div class="field">
class="label" <label :for="`newName${id}`">
:for="`newName${id}`" <span>
> {{ $t('user.settings.general.name') }}
{{ $t('user.settings.general.name') }} </span>
</label> <div class="ml-auto two-col">
<div class="control"> <input
<input :id="`newName${id}`"
:id="`newName${id}`" v-model="settings.name"
v-model="settings.name" :disabled="isExternalUser"
:disabled="isExternalUser" class="input"
class="input" :placeholder="$t('user.settings.general.newName')"
:placeholder="$t('user.settings.general.newName')" type="text"
type="text" @keyup.enter="updateSettings"
@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"
> >
{{ $t(`user.settings.quickAddMagic.${set}`) }} </div>
</option> </label>
</select> <p
</div> v-if="isExternalUser"
</label> class="help"
</div> >
<div class="field"> {{ $t('user.settings.general.externalUserNameChange', {provider: authStore.info.authProvider}) }}
<label class="is-flex is-align-items-center"> </p>
<span> </div>
{{ $t('user.settings.appearance.title') }} <div class="field">
</span> <label>
<div class="select ml-2"> <span>
<select v-model="settings.frontendSettings.colorSchema"> {{ $t('user.settings.general.defaultProject') }}
<!-- TODO: use the Vikunja logo in color scheme as option buttons --> </span>
<option <div class="ml-auto two-col">
v-for="(title, schemeId) in colorSchemeSettings" <ProjectSearch v-model="defaultProject" />
:key="schemeId" </div>
:value="schemeId" </label>
> </div>
{{ 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> </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 <XButton
v-cy="'saveGeneralSettings'" v-cy="'saveGeneralSettings'"
:loading="loading" :loading="loading"
class="is-fullwidth mt-4" class="is-fullwidth"
@click="updateSettings()" @click="updateSettings()"
> >
{{ $t('misc.save') }} {{ $t('misc.save') }}
</XButton> </XButton>
</Card> </div>
</template> </template>
@ -456,4 +507,31 @@ async function updateSettings() {
min-width: 200px; min-width: 200px;
flex-grow: 1; 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> </style>