1
0

feat: default view setting (#2306)

This PR adds configuration of default project view in settings, which is used when the user has not visited the project and thus last view hasn't yet been saved in projects. Updates old settings and adds "First View" option with fallback.

Resolves https://kolaente.dev/vikunja/vikunja/issues/2153

Co-authored-by: Elscrux <nickposer2102@gmail.com>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2306
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Elscrux <elscrux@gmail.com>
Co-committed-by: Elscrux <elscrux@gmail.com>
This commit is contained in:
Elscrux 2024-06-02 08:15:53 +00:00 committed by konrad
parent a175214aa0
commit aac01c7a35
6 changed files with 53 additions and 6 deletions

View File

@ -95,6 +95,7 @@
"weekStartMonday": "Monday", "weekStartMonday": "Monday",
"language": "Language", "language": "Language",
"defaultProject": "Default Project", "defaultProject": "Default Project",
"defaultView": "Default View",
"timezone": "Time Zone", "timezone": "Time Zone",
"overdueTasksRemindersTime": "Overdue tasks reminder email time", "overdueTasksRemindersTime": "Overdue tasks reminder email time",
"filterUsedOnOverview": "Saved filter used on the overview page" "filterUsedOnOverview": "Saved filter used on the overview page"
@ -314,6 +315,9 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"first": {
"title": "First View"
},
"list": { "list": {
"title": "List", "title": "List",
"add": "Add", "add": "Add",

View File

@ -9,6 +9,12 @@ export const PROJECT_VIEW_KINDS = {
} as const } as const
export type ProjectViewKind = typeof PROJECT_VIEW_KINDS[keyof typeof PROJECT_VIEW_KINDS] export type ProjectViewKind = typeof PROJECT_VIEW_KINDS[keyof typeof PROJECT_VIEW_KINDS]
export const DEFAULT_PROJECT_VIEW_SETTINGS = {
FIRST: 'first',
...PROJECT_VIEW_KINDS,
} as const
export type DefaultProjectViewKind = typeof DEFAULT_PROJECT_VIEW_SETTINGS[keyof typeof DEFAULT_PROJECT_VIEW_SETTINGS]
export const PROJECT_VIEW_BUCKET_CONFIGURATION_MODES = ['none', 'manual', 'filter'] export const PROJECT_VIEW_BUCKET_CONFIGURATION_MODES = ['none', 'manual', 'filter']
export type ProjectViewBucketConfigurationMode = typeof PROJECT_VIEW_BUCKET_CONFIGURATION_MODES[number] export type ProjectViewBucketConfigurationMode = typeof PROJECT_VIEW_BUCKET_CONFIGURATION_MODES[number]

View File

@ -4,12 +4,14 @@ import type {IProject} from './IProject'
import type {PrefixMode} from '@/modules/parseTaskText' import type {PrefixMode} from '@/modules/parseTaskText'
import type {BasicColorSchema} from '@vueuse/core' import type {BasicColorSchema} from '@vueuse/core'
import type {SupportedLocale} from '@/i18n' import type {SupportedLocale} from '@/i18n'
import type {DefaultProjectViewKind} from '@/modelTypes/IProjectView'
export interface IFrontendSettings { export interface IFrontendSettings {
playSoundWhenDone: boolean playSoundWhenDone: boolean
quickAddMagicMode: PrefixMode quickAddMagicMode: PrefixMode
colorSchema: BasicColorSchema colorSchema: BasicColorSchema
filterIdUsedOnOverview: IProject['id'] | null filterIdUsedOnOverview: IProject['id'] | null
defaultView?: DefaultProjectViewKind
} }
export interface IUserSettings extends IAbstract { export interface IUserSettings extends IAbstract {

View File

@ -3,6 +3,7 @@ import AbstractModel from './abstractModel'
import type {IFrontendSettings, IUserSettings} from '@/modelTypes/IUserSettings' import type {IFrontendSettings, IUserSettings} from '@/modelTypes/IUserSettings'
import {getBrowserLanguage} from '@/i18n' import {getBrowserLanguage} from '@/i18n'
import {PrefixMode} from '@/modules/parseTaskText' import {PrefixMode} from '@/modules/parseTaskText'
import {DEFAULT_PROJECT_VIEW_SETTINGS} from '@/modelTypes/IProjectView'
export default class UserSettingsModel extends AbstractModel<IUserSettings> implements IUserSettings { export default class UserSettingsModel extends AbstractModel<IUserSettings> implements IUserSettings {
name = '' name = ''
@ -19,6 +20,7 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
playSoundWhenDone: true, playSoundWhenDone: true,
quickAddMagicMode: PrefixMode.Default, quickAddMagicMode: PrefixMode.Default,
colorSchema: 'auto', colorSchema: 'auto',
defaultView: DEFAULT_PROJECT_VIEW_SETTINGS.FIRST,
} }
constructor(data: Partial<IUserSettings> = {}) { constructor(data: Partial<IUserSettings> = {}) {

View File

@ -8,6 +8,8 @@ import ProjectList from '@/components/project/views/ProjectList.vue'
import ProjectGantt from '@/components/project/views/ProjectGantt.vue' import ProjectGantt from '@/components/project/views/ProjectGantt.vue'
import ProjectTable from '@/components/project/views/ProjectTable.vue' import ProjectTable from '@/components/project/views/ProjectTable.vue'
import ProjectKanban from '@/components/project/views/ProjectKanban.vue' import ProjectKanban from '@/components/project/views/ProjectKanban.vue'
import {useAuthStore} from '@/stores/auth'
import {DEFAULT_PROJECT_VIEW_SETTINGS} from '@/modelTypes/IProjectView'
const { const {
projectId, projectId,
@ -19,6 +21,7 @@ const {
const router = useRouter() const router = useRouter()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const authStore = useAuthStore()
const currentView = computed(() => { const currentView = computed(() => {
const project = projectStore.projects[projectId] const project = projectStore.projects[projectId]
@ -26,17 +29,27 @@ const currentView = computed(() => {
return project?.views.find(v => v.id === viewId) return project?.views.find(v => v.id === viewId)
}) })
function redirectToFirstViewIfNecessary() { function redirectToDefaultViewIfNecessary() {
if (viewId === 0 || !projectStore.projects[projectId]?.views.find(v => v.id === viewId)) { if (viewId === 0 || !projectStore.projects[projectId]?.views.find(v => v.id === viewId)) {
// Ideally, we would do that in the router redirect, but the projects (and therefore, the views) // Ideally, we would do that in the router redirect, but the projects (and therefore, the views)
// are not always loaded then. // are not always loaded then.
const firstViewId = projectStore.projects[projectId]?.views[0].id
if (firstViewId) { let view
if (authStore.settings.frontendSettings.defaultView !== DEFAULT_PROJECT_VIEW_SETTINGS.FIRST) {
view = projectStore.projects[projectId]?.views.find(v => v.viewKind === authStore.settings.frontendSettings.defaultView)
}
// Use the first view as fallback if the default view is not available
if (view === undefined && projectStore.projects[projectId]?.views?.length > 0) {
view = projectStore.projects[projectId]?.views[0]
}
if (view) {
router.replace({ router.replace({
name: 'project.view', name: 'project.view',
params: { params: {
projectId, projectId,
viewId: firstViewId, viewId: view.id,
}, },
}) })
} }
@ -45,13 +58,13 @@ function redirectToFirstViewIfNecessary() {
watch( watch(
() => viewId, () => viewId,
redirectToFirstViewIfNecessary, redirectToDefaultViewIfNecessary,
{immediate: true}, {immediate: true},
) )
watch( watch(
() => projectStore.projects[projectId], () => projectStore.projects[projectId],
redirectToFirstViewIfNecessary, redirectToDefaultViewIfNecessary,
) )
// using a watcher instead of beforeEnter because beforeEnter is not called when only the viewId changes // using a watcher instead of beforeEnter because beforeEnter is not called when only the viewId changes

View File

@ -26,6 +26,22 @@
</label> </label>
<ProjectSearch v-model="defaultProject" /> <ProjectSearch v-model="defaultProject" />
</div> </div>
<div class="field">
<label class="label">
{{ $t('user.settings.general.defaultView') }}
</label>
<div class="select">
<select v-model="settings.frontendSettings.defaultView">
<option
v-for="view in DEFAULT_PROJECT_VIEW_SETTINGS"
:key="view"
:value="view"
>
{{ $t(`project.${view}.title`) }}
</option>
</select>
</div>
</div>
<div <div
v-if="hasFilters" v-if="hasFilters"
class="field" class="field"
@ -221,6 +237,7 @@ import {useProjectStore} from '@/stores/projects'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import type {IUserSettings} from '@/modelTypes/IUserSettings' import type {IUserSettings} from '@/modelTypes/IUserSettings'
import {isSavedFilter} from '@/services/savedFilter' import {isSavedFilter} from '@/services/savedFilter'
import {DEFAULT_PROJECT_VIEW_SETTINGS} from '@/modelTypes/IProjectView'
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')}`)
@ -253,12 +270,15 @@ function useAvailableTimezones() {
const availableTimezones = useAvailableTimezones() const availableTimezones = useAvailableTimezones()
const authStore = useAuthStore() const authStore = useAuthStore()
const settings = ref<IUserSettings>({ const settings = ref<IUserSettings>({
...authStore.settings, ...authStore.settings,
frontendSettings: { frontendSettings: {
// Sub objects get exported as read only as well, so we need to // Sub objects get exported as read only as well, so we need to
// explicitly spread the object here to allow modification // explicitly spread the object here to allow modification
...authStore.settings.frontendSettings, ...authStore.settings.frontendSettings,
// Add fallback for old settings that don't have the default view set
defaultView: authStore.settings.frontendSettings.defaultView ?? DEFAULT_PROJECT_VIEW_SETTINGS.FIRST,
}, },
}) })
const id = ref(createRandomID()) const id = ref(createRandomID())