forked from vikunja/frontend
feat(user): persist frontend settings in the api (#3594)
Implements saving of frontend settings for 04e2c51fac
.
Resolves https://github.com/go-vikunja/frontend/issues/105
Resolves vikunja/api#1250
Resolves vikunja/api#1452
Reviewed-on: vikunja/frontend#3594
This commit is contained in:
commit
169feaaf0f
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 === '') {
|
||||||
|
|
|
@ -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}) => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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')})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user