refactor(frontend): use form primitives in user settings General view

This commit is contained in:
kolaente 2026-04-17 14:36:34 +02:00 committed by kolaente
parent a6811a922a
commit cab970b59b
1 changed files with 254 additions and 395 deletions

View File

@ -5,39 +5,30 @@
:loading="loading"
>
<div class="field-group">
<div class="field">
<label
:for="`newName${id}`"
class="two-col"
>
<span>
{{ $t('user.settings.general.name') }}
</span>
<input
:id="`newName${id}`"
v-model="settings.name"
:disabled="isExternalUser"
class="input"
:placeholder="$t('user.settings.general.newName')"
type="text"
@keyup.enter="updateSettings"
>
</label>
<p
v-if="isExternalUser"
class="help"
>
{{ $t('user.settings.general.externalUserNameChange', {provider: authStore.info.authProvider}) }}
</p>
</div>
<div class="field">
<label class="two-col">
<span>
{{ $t('user.settings.general.defaultProject') }}
</span>
<ProjectSearch v-model="defaultProject" />
</label>
</div>
<FormField
:label="$t('user.settings.general.name')"
layout="two-col"
>
<FormInput
v-model="settings.name"
:disabled="isExternalUser"
:placeholder="$t('user.settings.general.newName')"
type="text"
@keyup.enter="updateSettings"
/>
</FormField>
<p
v-if="isExternalUser"
class="help"
>
{{ $t('user.settings.general.externalUserNameChange', {provider: authStore.info.authProvider}) }}
</p>
<FormField
:label="$t('user.settings.general.defaultProject')"
layout="two-col"
>
<ProjectSearch v-model="defaultProject" />
</FormField>
</div>
</Card>
@ -47,111 +38,57 @@
:loading="loading"
>
<div class="field-group">
<div class="field">
<label class="two-col">
<span>
{{ $t('user.settings.general.defaultView') }}
</span>
<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>
</label>
</div>
<div class="field">
<label class="two-col">
<span>
{{ $t('user.settings.general.minimumPriority') }}
</span>
<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>
</label>
</div>
<div
<FormField
:label="$t('user.settings.general.defaultView')"
layout="two-col"
>
<FormSelect
v-model="settings.frontendSettings.defaultView"
:options="defaultViewOptions"
/>
</FormField>
<FormField
:label="$t('user.settings.general.minimumPriority')"
layout="two-col"
>
<FormSelect
v-model="settings.frontendSettings.minimumPriority"
:options="minimumPriorityOptions"
/>
</FormField>
<FormField
v-if="hasFilters"
class="field"
:label="$t('user.settings.general.filterUsedOnOverview')"
layout="two-col"
>
<label class="two-col">
<span>
{{ $t('user.settings.general.filterUsedOnOverview') }}
</span>
<ProjectSearch
v-model="filterUsedInOverview"
:saved-filters-only="true"
/>
</label>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="settings.frontendSettings.showLastViewed"
type="checkbox"
>
{{ $t('user.settings.general.showLastViewed') }}
</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
<ProjectSearch
v-model="filterUsedInOverview"
:saved-filters-only="true"
/>
</FormField>
<FormCheckbox
v-model="settings.frontendSettings.showLastViewed"
:label="$t('user.settings.general.showLastViewed')"
/>
<FormCheckbox
v-model="settings.emailRemindersEnabled"
:label="$t('user.settings.general.emailReminders')"
/>
<FormCheckbox
v-model="settings.overdueTasksRemindersEnabled"
:label="$t('user.settings.general.overdueReminders')"
/>
<FormField
v-if="settings.overdueTasksRemindersEnabled"
class="field"
:label="$t('user.settings.general.overdueTasksRemindersTime')"
layout="two-col"
>
<label
for="overdueTasksReminderTime"
class="two-col"
>
<span>
{{ $t('user.settings.general.overdueTasksRemindersTime') }}
</span>
<input
id="overdueTasksReminderTime"
v-model="settings.overdueTasksRemindersTime"
class="input"
type="time"
@keyup.enter="updateSettings"
>
</label>
</div>
<FormInput
v-model="settings.overdueTasksRemindersTime"
type="time"
@keyup.enter="updateSettings"
/>
</FormField>
</div>
</Card>
@ -161,88 +98,58 @@
:loading="loading"
>
<div class="field-group">
<div class="field">
<label class="two-col">
<span>
{{ $t('user.settings.general.language') }}
</span>
<div class="select">
<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="two-col">
<span>
{{ $t('user.settings.general.timezone') }}
</span>
<Multiselect
v-model="timezoneObject"
:placeholder="$t('user.settings.general.timezone')"
:search-results="timezoneSearchResults"
:show-empty="true"
class="timezone-select"
label="label"
select-placeholder=""
@search="searchTimezones"
/>
</label>
</div>
<div class="field">
<label class="two-col">
<span>
{{ $t('user.settings.general.weekStart') }}
</span>
<div class="select">
<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="two-col">
<span>
{{ $t('user.settings.general.dateDisplay') }}
</span>
<div class="select">
<select v-model="settings.frontendSettings.dateDisplay">
<option
v-for="(label, value) in dateDisplaySettings"
:key="value"
:value="value"
>{{ label }}</option>
</select>
</div>
</label>
</div>
<div
v-if="settings.frontendSettings.dateDisplay !== 'relative'"
class="field"
<FormField
:label="$t('user.settings.general.language')"
layout="two-col"
>
<label class="two-col">
<span>
{{ $t('user.settings.general.timeFormat') }}
</span>
<div class="select">
<select v-model="settings.frontendSettings.timeFormat">
<option
v-for="(label, value) in timeFormatSettings"
:key="value"
:value="value"
>{{ label }}</option>
</select>
</div>
</label>
</div>
<FormSelect
v-model="settings.language"
:options="languageOptions"
/>
</FormField>
<FormField
:label="$t('user.settings.general.timezone')"
layout="two-col"
>
<Multiselect
v-model="timezoneObject"
:placeholder="$t('user.settings.general.timezone')"
:search-results="timezoneSearchResults"
:show-empty="true"
class="timezone-select"
label="label"
select-placeholder=""
@search="searchTimezones"
/>
</FormField>
<FormField
:label="$t('user.settings.general.weekStart')"
layout="two-col"
>
<FormSelect
v-model.number="settings.weekStart"
:options="weekStartOptions"
/>
</FormField>
<FormField
:label="$t('user.settings.general.dateDisplay')"
layout="two-col"
>
<FormSelect
v-model="settings.frontendSettings.dateDisplay"
:options="dateDisplayOptions"
/>
</FormField>
<FormField
v-if="settings.frontendSettings.dateDisplay !== 'relative'"
:label="$t('user.settings.general.timeFormat')"
layout="two-col"
>
<FormSelect
v-model="settings.frontendSettings.timeFormat"
:options="timeFormatOptions"
/>
</FormField>
</div>
</Card>
@ -252,42 +159,24 @@
:loading="loading"
>
<div class="field-group">
<div class="field">
<label class="two-col">
<span>
{{ $t('user.settings.appearance.title') }}
</span>
<div class="select">
<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 class="two-col">
<span>
{{ $t('user.settings.quickAddMagic.title') }}
</span>
<div class="select">
<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>
<FormField
:label="$t('user.settings.appearance.title')"
layout="two-col"
>
<FormSelect
v-model="settings.frontendSettings.colorSchema"
:options="colorSchemeOptions"
/>
</FormField>
<FormField
:label="$t('user.settings.quickAddMagic.title')"
layout="two-col"
>
<FormSelect
v-model="settings.frontendSettings.quickAddMagicMode"
:options="quickAddMagicModeOptions"
/>
</FormField>
<div
v-if="settings.frontendSettings.quickAddMagicMode !== PrefixMode.Disabled"
class="field"
@ -305,66 +194,39 @@
:allow-absolute="false"
/>
</div>
<div class="field">
<label class="two-col">
<span>
{{ $t('user.settings.general.defaultTaskRelationType') }}
</span>
<div class="select">
<select v-model="settings.frontendSettings.defaultTaskRelationType">
<option
v-for="relationKind in RELATION_KINDS"
:key="relationKind"
:value="relationKind"
>
{{ $t(`task.relation.kinds.${relationKind}`, 1) }}
</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 class="field">
<label class="checkbox">
<input
v-model="settings.frontendSettings.alwaysShowBucketTaskCount"
type="checkbox"
>
{{ $t('user.settings.general.alwaysShowBucketTaskCount') }}
</label>
</div>
<div class="field">
<label class="two-col">
<span>
{{ $t('user.settings.backgroundBrightness.title') }}
</span>
<input
v-model.number="settings.frontendSettings.backgroundBrightness"
class="input"
type="number"
min="0"
max="100"
@blur="enforceBackgroundBrightnessBounds"
>
</label>
</div>
<FormField
:label="$t('user.settings.general.defaultTaskRelationType')"
layout="two-col"
>
<FormSelect
v-model="settings.frontendSettings.defaultTaskRelationType"
:options="defaultTaskRelationTypeOptions"
/>
</FormField>
<FormCheckbox
v-model="settings.frontendSettings.playSoundWhenDone"
:label="$t('user.settings.general.playSoundWhenDone')"
/>
<FormCheckbox
v-model="settings.frontendSettings.allowIconChanges"
:label="$t('user.settings.general.allowIconChanges')"
/>
<FormCheckbox
v-model="settings.frontendSettings.alwaysShowBucketTaskCount"
:label="$t('user.settings.general.alwaysShowBucketTaskCount')"
/>
<FormField
:label="$t('user.settings.backgroundBrightness.title')"
layout="two-col"
>
<FormInput
v-model.number="settings.frontendSettings.backgroundBrightness"
type="number"
min="0"
max="100"
@blur="enforceBackgroundBrightnessBounds"
/>
</FormField>
</div>
</Card>
@ -375,20 +237,15 @@
:loading="loading"
>
<div class="field-group">
<div class="field">
<label
:for="`quickEntryShortcut${id}`"
class="two-col"
>
<span>
{{ $t('user.settings.desktop.quickEntryShortcut') }}
</span>
<ShortcutRecorder
v-model="settings.frontendSettings.desktopQuickEntryShortcut"
@update:modelValue="updateSettings"
/>
</label>
</div>
<FormField
:label="$t('user.settings.desktop.quickEntryShortcut')"
layout="two-col"
>
<ShortcutRecorder
v-model="settings.frontendSettings.desktopQuickEntryShortcut"
@update:modelValue="updateSettings"
/>
</FormField>
</div>
</Card>
@ -398,24 +255,14 @@
: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>
<FormCheckbox
v-model="settings.discoverableByName"
:label="$t('user.settings.general.discoverableByName')"
/>
<FormCheckbox
v-model="settings.discoverableByEmail"
:label="$t('user.settings.general.discoverableByEmail')"
/>
</div>
</Card>
@ -445,9 +292,12 @@ import {PrefixMode} from '@/modules/quickAddMagic'
import ProjectSearch from '@/components/tasks/partials/ProjectSearch.vue'
import Multiselect from '@/components/input/Multiselect.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import FormField from '@/components/input/FormField.vue'
import FormInput from '@/components/input/FormInput.vue'
import FormSelect from '@/components/input/FormSelect.vue'
import FormCheckbox from '@/components/input/FormCheckbox.vue'
import {SUPPORTED_LOCALES} from '@/i18n'
import {createRandomID} from '@/helpers/randomId'
import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
import {formatDisplayDateFormat} from '@/helpers/time/formatDate'
@ -476,28 +326,68 @@ useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')
const DEFAULT_PROJECT_ID = 0
const colorSchemeSettings = computed(() => ({
light: t('user.settings.appearance.colorScheme.light'),
auto: t('user.settings.appearance.colorScheme.system'),
dark: t('user.settings.appearance.colorScheme.dark'),
}))
const defaultViewOptions = computed(() =>
Object.values(DEFAULT_PROJECT_VIEW_SETTINGS).map(view => ({
value: view,
label: t(`project.${view}.title`),
})),
)
const dateDisplaySettings = computed(() => ({
[DATE_DISPLAY.RELATIVE]: t('user.settings.general.dateDisplayOptions.relative'),
[DATE_DISPLAY.MM_DD_YYYY]: t('user.settings.general.dateDisplayOptions.mm-dd-yyyy'),
[DATE_DISPLAY.DD_MM_YYYY]: t('user.settings.general.dateDisplayOptions.dd-mm-yyyy'),
[DATE_DISPLAY.YYYY_MM_DD]: t('user.settings.general.dateDisplayOptions.yyyy-mm-dd'),
[DATE_DISPLAY.MM_SLASH_DD_YYYY]: t('user.settings.general.dateDisplayOptions.mm/dd/yyyy'),
[DATE_DISPLAY.DD_SLASH_MM_YYYY]: t('user.settings.general.dateDisplayOptions.dd/mm/yyyy'),
[DATE_DISPLAY.YYYY_SLASH_MM_DD]: t('user.settings.general.dateDisplayOptions.yyyy/mm/dd'),
[DATE_DISPLAY.DAY_MONTH_YEAR]: formatDisplayDateFormat(new Date(), DATE_DISPLAY.DAY_MONTH_YEAR, settings.value?.frontendSettings?.timeFormat),
[DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR]: formatDisplayDateFormat(new Date(), DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR, settings.value?.frontendSettings?.timeFormat),
}))
const minimumPriorityOptions = computed(() => [
{value: PRIORITIES.LOW, label: t('task.priority.low')},
{value: PRIORITIES.MEDIUM, label: t('task.priority.medium')},
{value: PRIORITIES.HIGH, label: t('task.priority.high')},
{value: PRIORITIES.URGENT, label: t('task.priority.urgent')},
{value: PRIORITIES.DO_NOW, label: t('task.priority.doNow')},
])
const timeFormatSettings = computed(() => ({
[TIME_FORMAT.HOURS_12]: t('user.settings.general.timeFormatOptions.12h'),
[TIME_FORMAT.HOURS_24]: t('user.settings.general.timeFormatOptions.24h'),
}))
const weekStartOptions = computed(() => [
{value: 0, label: t('user.settings.general.weekStartSunday')},
{value: 1, label: t('user.settings.general.weekStartMonday')},
])
const dateDisplayOptions = computed(() => [
{value: DATE_DISPLAY.RELATIVE, label: t('user.settings.general.dateDisplayOptions.relative')},
{value: DATE_DISPLAY.MM_DD_YYYY, label: t('user.settings.general.dateDisplayOptions.mm-dd-yyyy')},
{value: DATE_DISPLAY.DD_MM_YYYY, label: t('user.settings.general.dateDisplayOptions.dd-mm-yyyy')},
{value: DATE_DISPLAY.YYYY_MM_DD, label: t('user.settings.general.dateDisplayOptions.yyyy-mm-dd')},
{value: DATE_DISPLAY.MM_SLASH_DD_YYYY, label: t('user.settings.general.dateDisplayOptions.mm/dd/yyyy')},
{value: DATE_DISPLAY.DD_SLASH_MM_YYYY, label: t('user.settings.general.dateDisplayOptions.dd/mm/yyyy')},
{value: DATE_DISPLAY.YYYY_SLASH_MM_DD, label: t('user.settings.general.dateDisplayOptions.yyyy/mm/dd')},
{value: DATE_DISPLAY.DAY_MONTH_YEAR, label: formatDisplayDateFormat(new Date(), DATE_DISPLAY.DAY_MONTH_YEAR, settings.value?.frontendSettings?.timeFormat)},
{value: DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR, label: formatDisplayDateFormat(new Date(), DATE_DISPLAY.WEEKDAY_DAY_MONTH_YEAR, settings.value?.frontendSettings?.timeFormat)},
])
const timeFormatOptions = computed(() => [
{value: TIME_FORMAT.HOURS_12, label: t('user.settings.general.timeFormatOptions.12h')},
{value: TIME_FORMAT.HOURS_24, label: t('user.settings.general.timeFormatOptions.24h')},
])
const colorSchemeOptions = computed(() => [
{value: 'light', label: t('user.settings.appearance.colorScheme.light')},
{value: 'auto', label: t('user.settings.appearance.colorScheme.system')},
{value: 'dark', label: t('user.settings.appearance.colorScheme.dark')},
])
const quickAddMagicModeOptions = computed(() =>
(Object.values(PrefixMode) as PrefixMode[]).map(mode => ({
value: mode,
label: t(`user.settings.quickAddMagic.${mode}`),
})),
)
const defaultTaskRelationTypeOptions = computed(() =>
RELATION_KINDS.map(kind => ({
value: kind,
label: t(`task.relation.kinds.${kind}`, 1),
})),
)
const languageOptions = computed(() =>
Object.entries(SUPPORTED_LOCALES)
.map(([code, title]) => ({value: code, label: title}))
.sort((a, b) => a.label.localeCompare(b.label)),
)
const authStore = useAuthStore()
@ -635,13 +525,6 @@ const {
timezoneObject,
} = useAvailableTimezones(settings)
const id = ref(createRandomID())
const availableLanguageOptions = ref(
Object.entries(SUPPORTED_LOCALES)
.map(l => ({code: l[0], title: l[1]}))
.sort((a, b) => a.title.localeCompare(b.title)),
)
const isExternalUser = computed(() => !authStore.info.isLocalUser)
watch(
@ -688,10 +571,6 @@ async function updateSettings() {
</script>
<style lang="scss" scoped>
.select select {
inline-size: 100%;
}
.timezone-select {
min-inline-size: 200px;
flex-grow: 1;
@ -710,26 +589,6 @@ async function updateSettings() {
grid-template-columns: 1fr;
}
.field > label.two-col {
display: flex;
align-items: center;
gap: .5rem;
> span {
flex: 0 0 50%;
}
input, .input, .select, .timezone-select, :deep(.multiselect) {
flex: 0 0 50%;
box-sizing: border-box;
}
}
label.checkbox {
display: flex;
gap: .5rem;
}
.sticky-save {
position: sticky;
inset-block-end: 0;