feat(user): persist frontend settings in the api #3594

Merged
konrad merged 12 commits from feature/persist-frontend-settings into main 2023-06-12 16:22:53 +00:00
17 changed files with 117 additions and 157 deletions

View File

@ -92,7 +92,7 @@ watch(userEmailConfirm, (userEmailConfirm) => {
router.push({name: 'user.login'}) router.push({name: 'user.login'})
}, { immediate: true }) }, { immediate: true })
setLanguage() setLanguage(authStore.settings.language)
useColorScheme() useColorScheme()
</script> </script>

View File

@ -71,10 +71,10 @@ import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {useAuthStore} from '@/stores/auth'
import {getHistory} from '@/modules/projectHistory' import {getHistory} from '@/modules/projectHistory'
import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText' import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {success} from '@/message' import {success} from '@/message'
import type {ITeam} from '@/modelTypes/ITeam' import type {ITeam} from '@/modelTypes/ITeam'
@ -88,6 +88,7 @@ const baseStore = useBaseStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const labelStore = useLabelStore() const labelStore = useLabelStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const authStore = useAuthStore()
type DoAction<Type = any> = { type: ACTION_TYPE } & Type type DoAction<Type = any> = { type: ACTION_TYPE } & Type
@ -242,7 +243,7 @@ const hintText = computed(() => {
} }
} }
const prefixes = const prefixes =
PREFIXES[getQuickAddMagicMode()] ?? PREFIXES[PrefixMode.Default] PREFIXES[authStore.settings.frontendSettings.quickAddMagicMode] ?? PREFIXES[PrefixMode.Default]
return t('quickActions.hint', prefixes) return t('quickActions.hint', prefixes)
}) })
@ -255,7 +256,7 @@ const availableCmds = computed(() => {
return cmds return cmds
}) })
const parsedQuery = computed(() => parseTaskText(query.value, getQuickAddMagicMode())) const parsedQuery = computed(() => parseTaskText(query.value, authStore.settings.frontendSettings.quickAddMagicMode))
const searchMode = computed(() => { const searchMode = computed(() => {
if (query.value === '') { if (query.value === '') {

View File

@ -116,12 +116,12 @@ async function addTask() {
// This allows us to find the tasks with the title they had before being parsed // This allows us to find the tasks with the title they had before being parsed
// by quick add magic. // by quick add magic.
const createdTasks: { [key: ITask['title']]: ITask } = {} const createdTasks: { [key: ITask['title']]: ITask } = {}
const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value) const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value, authStore.settings.frontendSettings.quickAddMagicMode)
// We ensure all labels exist prior to passing them down to the create task method // We ensure all labels exist prior to passing them down to the create task method
// In the store it will only ever see one task at a time so there's no way to reliably // In the store it will only ever see one task at a time so there's no way to reliably
// check if a new label was created before (because everything happens async). // check if a new label was created before (because everything happens async).
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title) ?? []) const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title, authStore.settings.frontendSettings.quickAddMagicMode) ?? [])
await taskStore.ensureLabelsExist(allLabels.flat()) await taskStore.ensureLabelsExist(allLabels.flat())
const newTasks = tasksToCreate.map(async ({title, project}) => { const newTasks = tasksToCreate.map(async ({title, project}) => {

View File

@ -99,11 +99,13 @@ import {ref, computed} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {PREFIXES} from '@/modules/parseTaskText' import {PREFIXES} from '@/modules/parseTaskText'
import {useAuthStore} from '@/stores/auth'
const authStore = useAuthStore()
const visible = ref(false) const visible = ref(false)
const mode = ref(getQuickAddMagicMode()) const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
defineProps<{ defineProps<{
highlightHintIcon: boolean, highlightHintIcon: boolean,

View File

@ -1,8 +1,7 @@
import {computed, watch, readonly} from 'vue' import {computed, watch, readonly} from 'vue'
import {useStorage, createSharedComposable, usePreferredColorScheme, tryOnMounted} from '@vueuse/core' import {createSharedComposable, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
import type {BasicColorSchema} from '@vueuse/core' import type {BasicColorSchema} from '@vueuse/core'
import {useAuthStore} from '@/stores/auth'
const STORAGE_KEY = 'color-scheme'
const DEFAULT_COLOR_SCHEME_SETTING: BasicColorSchema = 'light' const DEFAULT_COLOR_SCHEME_SETTING: BasicColorSchema = 'light'
@ -17,7 +16,8 @@ const CLASS_LIGHT = 'light'
// - value is synced via `createSharedComposable` // - value is synced via `createSharedComposable`
// https://github.com/vueuse/vueuse/blob/main/packages/core/useDark/index.ts // https://github.com/vueuse/vueuse/blob/main/packages/core/useDark/index.ts
export const useColorScheme = createSharedComposable(() => { export const useColorScheme = createSharedComposable(() => {
const store = useStorage<BasicColorSchema>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING) const authStore = useAuthStore()
const store = computed(() => authStore.settings.frontendSettings.colorSchema)
const preferredColorScheme = usePreferredColorScheme() const preferredColorScheme = usePreferredColorScheme()

View File

@ -1,5 +1,6 @@
import {describe, it, expect} from 'vitest' import {describe, expect, it} from 'vitest'
import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention' import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention'
import {PrefixMode} from '@/modules/parseTaskText'
describe('Parse Subtasks via Relation', () => { describe('Parse Subtasks via Relation', () => {
it('Should not return a parent for a single task', () => { it('Should not return a parent for a single task', () => {
@ -10,7 +11,7 @@ describe('Parse Subtasks via Relation', () => {
}) })
it('Should not return a parent for multiple tasks without indention', () => { it('Should not return a parent for multiple tasks without indention', () => {
const tasks = parseSubtasksViaIndention(`task one const tasks = parseSubtasksViaIndention(`task one
task two`) task two`, PrefixMode.Default)
expect(tasks).to.have.length(2) expect(tasks).to.have.length(2)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -18,7 +19,7 @@ task two`)
}) })
it('Should return a parent for two tasks with indention', () => { it('Should return a parent for two tasks with indention', () => {
const tasks = parseSubtasksViaIndention(`parent task const tasks = parseSubtasksViaIndention(`parent task
sub task`) sub task`, PrefixMode.Default)
expect(tasks).to.have.length(2) expect(tasks).to.have.length(2)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -29,7 +30,7 @@ task two`)
it('Should return a parent for multiple subtasks', () => { it('Should return a parent for multiple subtasks', () => {
const tasks = parseSubtasksViaIndention(`parent task const tasks = parseSubtasksViaIndention(`parent task
sub task one sub task one
sub task two`) sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(3) expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -42,7 +43,7 @@ task two`)
it('Should work with multiple indention levels', () => { it('Should work with multiple indention levels', () => {
const tasks = parseSubtasksViaIndention(`parent task const tasks = parseSubtasksViaIndention(`parent task
sub task sub task
sub sub task`) sub sub task`, PrefixMode.Default)
expect(tasks).to.have.length(3) expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -56,7 +57,7 @@ task two`)
const tasks = parseSubtasksViaIndention(`parent task const tasks = parseSubtasksViaIndention(`parent task
sub task sub task
sub sub task one sub sub task one
sub sub task two`) sub sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(4) expect(tasks).to.have.length(4)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -73,7 +74,7 @@ task two`)
sub task sub task
sub sub task one sub sub task one
sub sub sub task sub sub sub task
sub sub task two`) sub sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(5) expect(tasks).to.have.length(5)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -90,7 +91,7 @@ task two`)
it('Should return a parent for multiple subtasks with special stuff', () => { it('Should return a parent for multiple subtasks with special stuff', () => {
const tasks = parseSubtasksViaIndention(`* parent task const tasks = parseSubtasksViaIndention(`* parent task
* sub task one * sub task one
sub task two`) sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(3) expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -101,7 +102,7 @@ task two`)
expect(tasks[2].parent).to.eq('parent task') expect(tasks[2].parent).to.eq('parent task')
}) })
it('Should not break when the first line is indented', () => { it('Should not break when the first line is indented', () => {
const tasks = parseSubtasksViaIndention(' single task') const tasks = parseSubtasksViaIndention(' single task', PrefixMode.Default)
expect(tasks).to.have.length(1) expect(tasks).to.have.length(1)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -110,7 +111,7 @@ task two`)
const tasks = parseSubtasksViaIndention( const tasks = parseSubtasksViaIndention(
`parent task +list `parent task +list
sub task 1 sub task 1
sub task 2`) sub task 2`, PrefixMode.Default)
expect(tasks).to.have.length(3) expect(tasks).to.have.length(3)
expect(tasks[0].project).to.eq('list') expect(tasks[0].project).to.eq('list')

View File

@ -1,4 +1,4 @@
import {getProjectFromPrefix} from '@/modules/parseTaskText' import {getProjectFromPrefix, PrefixMode} from '@/modules/parseTaskText'
export interface TaskWithParent { export interface TaskWithParent {
title: string, title: string,
@ -16,7 +16,7 @@ const spaceRegex = /^ */
* @param taskTitles should be multiple lines of task tiles with indention to declare their parent/subtask * @param taskTitles should be multiple lines of task tiles with indention to declare their parent/subtask
* relation between each other. * relation between each other.
*/ */
export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[] { export function parseSubtasksViaIndention(taskTitles: string, prefixMode: PrefixMode): TaskWithParent[] {
const titles = taskTitles.split(/[\r\n]+/) const titles = taskTitles.split(/[\r\n]+/)
return titles.map((title, index) => { return titles.map((title, index) => {
@ -26,7 +26,7 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
project: null, project: null,
} }
task.project = getProjectFromPrefix(task.title) task.project = getProjectFromPrefix(task.title, prefixMode)
if (index === 0) { if (index === 0) {
return task return task
@ -49,7 +49,7 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
task.parent = task.parent.replace(spaceRegex, '') task.parent = task.parent.replace(spaceRegex, '')
if (task.project === null) { if (task.project === null) {
// This allows to specify a project once for the parent task and inherit it to all subtasks // This allows to specify a project once for the parent task and inherit it to all subtasks
task.project = getProjectFromPrefix(task.parent) task.project = getProjectFromPrefix(task.parent, prefixMode)
} }
} }

View File

@ -2,15 +2,6 @@ import popSoundFile from '@/assets/audio/pop.mp3'
export const playSoundWhenDoneKey = 'playSoundWhenTaskDone' export const playSoundWhenDoneKey = 'playSoundWhenTaskDone'
export function playPop() {
const enabled = localStorage.getItem(playSoundWhenDoneKey) === 'true'
if (!enabled) {
return
}
playPopSound()
}
export function playPopSound() { export function playPopSound() {
const popSound = new Audio(popSoundFile) const popSound = new Audio(popSoundFile)
popSound.play() popSound.play()

View File

@ -1,21 +0,0 @@
import {PrefixMode} from '@/modules/parseTaskText'
const key = 'quickAddMagicMode'
export const setQuickAddMagicMode = (mode: PrefixMode) => {
localStorage.setItem(key, mode)
}
export const getQuickAddMagicMode = (): PrefixMode => {
const mode = localStorage.getItem(key)
switch (mode) {
case null:
case PrefixMode.Default:
return PrefixMode.Default
case PrefixMode.Todoist:
return PrefixMode.Todoist
}
return PrefixMode.Disabled
}

View File

@ -32,7 +32,7 @@ export const i18n = createI18n({
} as Record<SupportedLocale, any>, } as Record<SupportedLocale, any>,
}) })
export async function setLanguage(lang: SupportedLocale = getCurrentLanguage()): Promise<SupportedLocale | undefined> { export async function setLanguage(lang: SupportedLocale): Promise<SupportedLocale | undefined> {
if (!lang) { if (!lang) {
throw new Error() throw new Error()
} }
@ -53,12 +53,7 @@ export async function setLanguage(lang: SupportedLocale = getCurrentLanguage()):
return lang return lang
} }
export function getCurrentLanguage(): SupportedLocale { export function getBrowserLanguage(): SupportedLocale {
const savedLanguage = localStorage.getItem('language') as SupportedLocale | null
if (savedLanguage !== null) {
return savedLanguage
}
const browserLanguage = navigator.language const browserLanguage = navigator.language
const language = Object.keys(SUPPORTED_LOCALES).find(langKey => { const language = Object.keys(SUPPORTED_LOCALES).find(langKey => {
@ -67,8 +62,3 @@ export function getCurrentLanguage(): SupportedLocale {
return language || DEFAULT_LANGUAGE return language || DEFAULT_LANGUAGE
} }
export async function saveLanguage(lang: SupportedLocale) {
localStorage.setItem('language', lang)
await setLanguage()
}

View File

@ -16,7 +16,7 @@ import Notifications from '@kyvg/vue3-notification'
import './registerServiceWorker' import './registerServiceWorker'
// i18n // i18n
import {i18n, setLanguage} from './i18n' import {getBrowserLanguage, i18n, setLanguage} from './i18n'
declare global { declare global {
interface Window { interface Window {
@ -56,7 +56,8 @@ import Card from '@/components/misc/card.vue'
// We're loading the language before creating the app so that it won't fail to load when the user's // We're loading the language before creating the app so that it won't fail to load when the user's
// language file is not yet loaded. // language file is not yet loaded.
setLanguage().then(() => { const browserLanguage = getBrowserLanguage()
setLanguage(browserLanguage).then(() => {
const app = createApp(App) const app = createApp(App)
app.use(Notifications) app.use(Notifications)

View File

@ -1,6 +1,15 @@
import type {IAbstract} from './IAbstract' import type {IAbstract} from './IAbstract'
import type {IProject} from './IProject' import type {IProject} from './IProject'
import type {PrefixMode} from '@/modules/parseTaskText'
import type {BasicColorSchema} from '@vueuse/core'
import type {SupportedLocale} from '@/i18n'
export interface IFrontendSettings {
playSoundWhenDone: boolean
quickAddMagicMode: PrefixMode
colorSchema: BasicColorSchema
}
export interface IUserSettings extends IAbstract { export interface IUserSettings extends IAbstract {
name: string name: string
@ -12,5 +21,6 @@ export interface IUserSettings extends IAbstract {
defaultProjectId: undefined | IProject['id'] defaultProjectId: undefined | IProject['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6 weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string timezone: string
language: string language: SupportedLocale
frontendSettings: IFrontendSettings
} }

View File

@ -1,7 +1,8 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import type {IUserSettings} from '@/modelTypes/IUserSettings' import type {IFrontendSettings, IUserSettings} from '@/modelTypes/IUserSettings'
import {getCurrentLanguage} from '@/i18n' import {getBrowserLanguage} from '@/i18n'
import {PrefixMode} from '@/modules/parseTaskText'
export default class UserSettingsModel extends AbstractModel<IUserSettings> implements IUserSettings { export default class UserSettingsModel extends AbstractModel<IUserSettings> implements IUserSettings {
name = '' name = ''
@ -13,7 +14,12 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
defaultProjectId = undefined defaultProjectId = undefined
weekStart = 0 as IUserSettings['weekStart'] weekStart = 0 as IUserSettings['weekStart']
timezone = '' timezone = ''
language = getCurrentLanguage() language = getBrowserLanguage()
frontendSettings: IFrontendSettings = {
playSoundWhenDone: true,
quickAddMagicMode: PrefixMode.Default,
colorSchema: 'auto',
}
constructor(data: Partial<IUserSettings> = {}) { constructor(data: Partial<IUserSettings> = {}) {
super() super()

View File

@ -1,7 +1,6 @@
import {parseDate} from '../helpers/time/parseDate' import {parseDate} from '../helpers/time/parseDate'
import {PRIORITIES} from '@/constants/priorities' import {PRIORITIES} from '@/constants/priorities'
import {REPEAT_TYPES, type IRepeatAfter, type IRepeatType} from '@/types/IRepeatAfter' import {REPEAT_TYPES, type IRepeatAfter, type IRepeatType} from '@/types/IRepeatAfter'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
const VIKUNJA_PREFIXES: Prefixes = { const VIKUNJA_PREFIXES: Prefixes = {
label: '*', label: '*',
@ -72,10 +71,10 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
return result return result
} }
result.labels = getLabelsFromPrefix(text, prefixes.label) ?? [] result.labels = getLabelsFromPrefix(text, prefixesMode) ?? []
result.text = cleanupItemText(result.text, result.labels, prefixes.label) result.text = cleanupItemText(result.text, result.labels, prefixes.label)
result.project = getProjectFromPrefix(result.text, prefixes.project) result.project = getProjectFromPrefix(result.text, prefixesMode)
result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
result.priority = getPriority(result.text, prefixes.priority) result.priority = getPriority(result.text, prefixes.priority)
@ -131,27 +130,21 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
return Array.from(new Set(items)) return Array.from(new Set(items))
} }
export const getProjectFromPrefix = (text: string, projectPrefix: string | null = null): string | null => { export const getProjectFromPrefix = (text: string, prefixMode: PrefixMode): string | null => {
if (projectPrefix === null) { const projectPrefix = PREFIXES[prefixMode]?.project
const prefixes = PREFIXES[getQuickAddMagicMode()] if(typeof projectPrefix === 'undefined') {
if (prefixes === undefined) { return null
return null
}
projectPrefix = prefixes.project
} }
const projects: string[] = getItemsFromPrefix(text, projectPrefix) const projects: string[] = getItemsFromPrefix(text, projectPrefix)
return projects.length > 0 ? projects[0] : null return projects.length > 0 ? projects[0] : null
} }
export const getLabelsFromPrefix = (text: string, projectPrefix: string | null = null): string[] | null => { export const getLabelsFromPrefix = (text: string, prefixMode: PrefixMode): string[] | null => {
if (projectPrefix === null) { const labelsPrefix = PREFIXES[prefixMode]?.label
const prefixes = PREFIXES[getQuickAddMagicMode()] if(typeof labelsPrefix === 'undefined') {
if (prefixes === undefined) { return null
return null
}
projectPrefix = prefixes.label
} }
return getItemsFromPrefix(text, projectPrefix) return getItemsFromPrefix(text, labelsPrefix)
} }
const getPriority = (text: string, prefix: string): number | null => { const getPriority = (text: string, prefix: string): number | null => {

View File

@ -1,10 +1,10 @@
import {computed, readonly, ref} from 'vue' import {computed, readonly, ref} from 'vue'
import {defineStore, acceptHMRUpdate} from 'pinia' import {acceptHMRUpdate, defineStore} from 'pinia'
import {HTTPFactory, AuthenticatedHTTPFactory} from '@/helpers/fetcher' import {AuthenticatedHTTPFactory, HTTPFactory} from '@/helpers/fetcher'
import {i18n, getCurrentLanguage, saveLanguage, setLanguage} from '@/i18n' import {getBrowserLanguage, i18n, setLanguage} from '@/i18n'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import UserModel, { getAvatarUrl, getDisplayName } from '@/models/user' import UserModel, {getAvatarUrl, getDisplayName} from '@/models/user'
import UserSettingsService from '@/services/userSettings' import UserSettingsService from '@/services/userSettings'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth' import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setModuleLoading} from '@/stores/helper' import {setModuleLoading} from '@/stores/helper'
@ -16,6 +16,7 @@ import router from '@/router'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import UserSettingsModel from '@/models/userSettings' import UserSettingsModel from '@/models/userSettings'
import {MILLISECONDS_A_SECOND} from '@/constants/date' import {MILLISECONDS_A_SECOND} from '@/constants/date'
import {PrefixMode} from '@/modules/parseTaskText'
function redirectToProviderIfNothingElseIsEnabled() { function redirectToProviderIfNothingElseIsEnabled() {
const {auth} = useConfigStore() const {auth} = useConfigStore()
@ -68,13 +69,13 @@ export const useAuthStore = defineStore('auth', () => {
isLoadingGeneralSettings.value = isLoading isLoadingGeneralSettings.value = isLoading
} }
function setUser(newUser: IUser | null) { function setUser(newUser: IUser | null, saveSettings = true) {
info.value = newUser info.value = newUser
if (newUser !== null) { if (newUser !== null) {
reloadAvatar() reloadAvatar()
if (newUser.settings) { if (saveSettings && newUser.settings) {
settings.value = new UserSettingsModel(newUser.settings) loadSettings(newUser.settings)
} }
isLinkShareAuth.value = newUser.id < 0 isLinkShareAuth.value = newUser.id < 0
@ -82,12 +83,26 @@ export const useAuthStore = defineStore('auth', () => {
} }
function setUserSettings(newSettings: IUserSettings) { function setUserSettings(newSettings: IUserSettings) {
settings.value = new UserSettingsModel(newSettings) loadSettings(newSettings)
info.value = new UserModel({ info.value = new UserModel({
...info.value !== null ? info.value : {}, ...info.value !== null ? info.value : {},
name: newSettings.name, name: newSettings.name,
}) })
} }
function loadSettings(newSettings: IUserSettings) {
settings.value = new UserSettingsModel({
...newSettings,
frontendSettings: {
// Need to set default settings here in case the user does not have any saved in the api already
playSoundWhenDone: true,
quickAddMagicMode: PrefixMode.Default,
colorSchema: 'auto',
...newSettings.frontendSettings,
},
})
// console.log('settings from auth store', {...settings.value.frontendSettings})
}
function setAuthenticated(newAuthenticated: boolean) { function setAuthenticated(newAuthenticated: boolean) {
authenticated.value = newAuthenticated authenticated.value = newAuthenticated
@ -218,7 +233,8 @@ export const useAuthStore = defineStore('auth', () => {
const info = new UserModel(JSON.parse(atob(base64))) const info = new UserModel(JSON.parse(atob(base64)))
const ts = Math.round((new Date()).getTime() / MILLISECONDS_A_SECOND) const ts = Math.round((new Date()).getTime() / MILLISECONDS_A_SECOND)
isAuthenticated = info.exp >= ts isAuthenticated = info.exp >= ts
setUser(info) // Settings should only be loaded from the api request, not via the jwt
setUser(info, false)
if (isAuthenticated) { if (isAuthenticated) {
await refreshUserInfo() await refreshUserInfo()
@ -268,7 +284,7 @@ export const useAuthStore = defineStore('auth', () => {
await saveUserSettings({ await saveUserSettings({
settings: { settings: {
...settings.value, ...settings.value,
language: getCurrentLanguage(), language: settings.value.language ? settings.value.language : getBrowserLanguage(),
}, },
showMessage: false, showMessage: false,
}) })
@ -316,10 +332,9 @@ export const useAuthStore = defineStore('auth', () => {
const cancel = setModuleLoading(setIsLoadingGeneralSettings) const cancel = setModuleLoading(setIsLoadingGeneralSettings)
try { try {
const updateSettingsPromise = userSettingsService.update(settings) const updateSettingsPromise = userSettingsService.update(settings)
const saveLanguagePromise = saveLanguage(settings.language)
await updateSettingsPromise
setUserSettings({...settings}) setUserSettings({...settings})
await saveLanguagePromise await setLanguage(settings.language)
await updateSettingsPromise
if (showMessage) { if (showMessage) {
success({message: i18n.global.t('user.settings.general.savedSuccess')}) success({message: i18n.global.t('user.settings.general.savedSuccess')})
} }

View File

@ -6,8 +6,7 @@ import TaskService from '@/services/task'
import TaskAssigneeService from '@/services/taskAssignee' import TaskAssigneeService from '@/services/taskAssignee'
import LabelTaskService from '@/services/labelTask' import LabelTaskService from '@/services/labelTask'
import {playPop} from '@/helpers/playPop' import {playPopSound} from '@/helpers/playPop'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {cleanupItemText, parseTaskText, PREFIXES} from '@/modules/parseTaskText' import {cleanupItemText, parseTaskText, PREFIXES} from '@/modules/parseTaskText'
import TaskAssigneeModel from '@/models/taskAssignee' import TaskAssigneeModel from '@/models/taskAssignee'
@ -29,6 +28,7 @@ import {useAttachmentStore} from '@/stores/attachments'
import {useKanbanStore} from '@/stores/kanban' import {useKanbanStore} from '@/stores/kanban'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import ProjectUserService from '@/services/projectUsers' import ProjectUserService from '@/services/projectUsers'
import {useAuthStore} from '@/stores/auth'
interface MatchedAssignee extends IUser { interface MatchedAssignee extends IUser {
match: string, match: string,
@ -106,6 +106,7 @@ export const useTaskStore = defineStore('task', () => {
const attachmentStore = useAttachmentStore() const attachmentStore = useAttachmentStore()
const labelStore = useLabelStore() const labelStore = useLabelStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const authStore = useAuthStore()
const tasks = ref<{ [id: ITask['id']]: ITask }>({}) // TODO: or is this ITask[] const tasks = ref<{ [id: ITask['id']]: ITask }>({}) // TODO: or is this ITask[]
const isLoading = ref(false) const isLoading = ref(false)
@ -142,8 +143,8 @@ export const useTaskStore = defineStore('task', () => {
try { try {
const updatedTask = await taskService.update(task) const updatedTask = await taskService.update(task)
kanbanStore.setTaskInBucket(updatedTask) kanbanStore.setTaskInBucket(updatedTask)
if (task.done) { if (task.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPop() playPopSound()
} }
return updatedTask return updatedTask
} finally { } finally {
@ -398,7 +399,7 @@ export const useTaskStore = defineStore('task', () => {
Partial<ITask>, Partial<ITask>,
) { ) {
const cancel = setModuleLoading(setIsLoading) const cancel = setModuleLoading(setIsLoading)
const quickAddMagicMode = getQuickAddMagicMode() const quickAddMagicMode = authStore.settings.frontendSettings.quickAddMagicMode
const parsedTask = parseTaskText(title, quickAddMagicMode) const parsedTask = parseTaskText(title, quickAddMagicMode)
const foundProjectId = await findProjectId({ const foundProjectId = await findProjectId({

View File

@ -57,7 +57,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="playSoundWhenDone"/> <input type="checkbox" v-model="settings.frontendSettings.playSoundWhenDone"/>
{{ $t('user.settings.general.playSoundWhenDone') }} {{ $t('user.settings.general.playSoundWhenDone') }}
</label> </label>
</div> </div>
@ -97,7 +97,7 @@
{{ $t('user.settings.quickAddMagic.title') }} {{ $t('user.settings.quickAddMagic.title') }}
</span> </span>
<div class="select ml-2"> <div class="select ml-2">
<select v-model="quickAddMagicMode"> <select v-model="settings.frontendSettings.quickAddMagicMode">
<option v-for="set in PrefixMode" :key="set" :value="set"> <option v-for="set in PrefixMode" :key="set" :value="set">
{{ $t(`user.settings.quickAddMagic.${set}`) }} {{ $t(`user.settings.quickAddMagic.${set}`) }}
</option> </option>
@ -111,7 +111,7 @@
{{ $t('user.settings.appearance.title') }} {{ $t('user.settings.appearance.title') }}
</span> </span>
<div class="select ml-2"> <div class="select ml-2">
<select v-model="activeColorSchemeSetting"> <select v-model="settings.frontendSettings.colorSchema">
<!-- TODO: use the Vikunja logo in color scheme as option buttons --> <!-- TODO: use the Vikunja logo in color scheme as option buttons -->
<option v-for="(title, schemeId) in colorSchemeSettings" :key="schemeId" :value="schemeId"> <option v-for="(title, schemeId) in colorSchemeSettings" :key="schemeId" :value="schemeId">
{{ title }} {{ title }}
@ -159,47 +159,25 @@ import {PrefixMode} from '@/modules/parseTaskText'
import ProjectSearch from '@/components/tasks/partials/projectSearch.vue' import ProjectSearch from '@/components/tasks/partials/projectSearch.vue'
import {SUPPORTED_LOCALES} from '@/i18n' import {SUPPORTED_LOCALES} from '@/i18n'
import {playSoundWhenDoneKey, playPopSound} from '@/helpers/playPop'
import {getQuickAddMagicMode, setQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
import {success} from '@/message'
import {AuthenticatedHTTPFactory} from '@/helpers/fetcher' import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
import {useColorScheme} from '@/composables/useColorScheme'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import type {IUserSettings} from '@/modelTypes/IUserSettings'
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')}`)
const DEFAULT_PROJECT_ID = 0 const DEFAULT_PROJECT_ID = 0
function useColorSchemeSetting() { const colorSchemeSettings = computed(() => ({
const {t} = useI18n({useScope: 'global'}) light: t('user.settings.appearance.colorScheme.light'),
const colorSchemeSettings = computed(() => ({ auto: t('user.settings.appearance.colorScheme.system'),
light: t('user.settings.appearance.colorScheme.light'), dark: t('user.settings.appearance.colorScheme.dark'),
auto: t('user.settings.appearance.colorScheme.system'), }))
dark: t('user.settings.appearance.colorScheme.dark'),
}))
const {store} = useColorScheme()
watch(store, (schemeId) => {
success({
message: t('user.settings.appearance.setSuccess', {
colorScheme: colorSchemeSettings.value[schemeId],
}),
})
})
return {
colorSchemeSettings,
activeColorSchemeSetting: store,
}
}
const {colorSchemeSettings, activeColorSchemeSetting} = useColorSchemeSetting()
function useAvailableTimezones() { function useAvailableTimezones() {
const availableTimezones = ref([]) const availableTimezones = ref([])
@ -215,15 +193,15 @@ function useAvailableTimezones() {
const availableTimezones = useAvailableTimezones() const availableTimezones = useAvailableTimezones()
function getPlaySoundWhenDoneSetting() {
return localStorage.getItem(playSoundWhenDoneKey) === 'true' || localStorage.getItem(playSoundWhenDoneKey) === null
}
const playSoundWhenDone = ref(getPlaySoundWhenDoneSetting())
const quickAddMagicMode = ref(getQuickAddMagicMode())
const authStore = useAuthStore() const authStore = useAuthStore()
const settings = ref({...authStore.settings}) const settings = ref<IUserSettings>({
...authStore.settings,
frontendSettings: {
// Sub objects get exported as read only as well, so we need to
// explicitly spread the object here to allow modification
...authStore.settings.frontendSettings,
},
})
const id = ref(createRandomID()) const id = ref(createRandomID())
const availableLanguageOptions = ref( const availableLanguageOptions = ref(
Object.entries(SUPPORTED_LOCALES) Object.entries(SUPPORTED_LOCALES)
@ -252,15 +230,7 @@ const defaultProject = computed({
}) })
const loading = computed(() => authStore.isLoadingGeneralSettings) const loading = computed(() => authStore.isLoadingGeneralSettings)
watch(
playSoundWhenDone,
(play) => play && playPopSound(),
)
async function updateSettings() { async function updateSettings() {
localStorage.setItem(playSoundWhenDoneKey, playSoundWhenDone.value ? 'true' : 'false')
setQuickAddMagicMode(quickAddMagicMode.value)
await authStore.saveUserSettings({ await authStore.saveUserSettings({
settings: {...settings.value}, settings: {...settings.value},
}) })