feat(projects): allow setting a saved filter for tasks shown on the overview page
Resolves https://kolaente.dev/vikunja/api/issues/1545 Resolves https://community.vikunja.io/t/customizable-overview-page/685
This commit is contained in:
parent
ac6c4cf2bc
commit
2a14325f62
@ -37,6 +37,10 @@ const props = defineProps({
|
|||||||
type: Object as PropType<IProject>,
|
type: Object as PropType<IProject>,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
savedFiltersOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
@ -57,6 +61,12 @@ function findProjects(query: string) {
|
|||||||
if (query === '') {
|
if (query === '') {
|
||||||
select(null)
|
select(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.savedFiltersOnly) {
|
||||||
|
foundProjects.value = projectStore.searchSavedFilter(query)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
foundProjects.value = projectStore.searchProject(query)
|
foundProjects.value = projectStore.searchProject(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,8 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"defaultProject": "Default Project",
|
"defaultProject": "Default Project",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"totp": {
|
"totp": {
|
||||||
"title": "Two Factor Authentication",
|
"title": "Two Factor Authentication",
|
||||||
|
@ -9,6 +9,7 @@ export interface IFrontendSettings {
|
|||||||
playSoundWhenDone: boolean
|
playSoundWhenDone: boolean
|
||||||
quickAddMagicMode: PrefixMode
|
quickAddMagicMode: PrefixMode
|
||||||
colorSchema: BasicColorSchema
|
colorSchema: BasicColorSchema
|
||||||
|
filterIdUsedOnOverview: IProject['id'] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserSettings extends IAbstract {
|
export interface IUserSettings extends IAbstract {
|
||||||
|
@ -17,6 +17,7 @@ import type {MaybeRef} from '@vueuse/core'
|
|||||||
import ProjectModel from '@/models/project'
|
import ProjectModel from '@/models/project'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
|
||||||
|
|
||||||
const {remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
const {remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
||||||
|
|
||||||
@ -63,6 +64,16 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const searchSavedFilter = computed(() => {
|
||||||
|
return (query: string, includeArchived = false) => {
|
||||||
|
return search(query)
|
||||||
|
?.filter(value => getSavedFilterIdFromProjectId(value) > 0)
|
||||||
|
.map(id => projects.value[id])
|
||||||
|
.filter(project => project.isArchived === includeArchived)
|
||||||
|
|| []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function setIsLoading(newIsLoading: boolean) {
|
function setIsLoading(newIsLoading: boolean) {
|
||||||
isLoading.value = newIsLoading
|
isLoading.value = newIsLoading
|
||||||
}
|
}
|
||||||
@ -191,6 +202,7 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
getChildProjects,
|
getChildProjects,
|
||||||
findProjectByExactname,
|
findProjectByExactname,
|
||||||
searchProject,
|
searchProject,
|
||||||
|
searchSavedFilter,
|
||||||
|
|
||||||
setProject,
|
setProject,
|
||||||
setProjects,
|
setProjects,
|
||||||
|
@ -29,6 +29,7 @@ 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'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import TaskCollectionService from '@/services/taskCollection'
|
||||||
|
|
||||||
interface MatchedAssignee extends IUser {
|
interface MatchedAssignee extends IUser {
|
||||||
match: string,
|
match: string,
|
||||||
@ -123,12 +124,17 @@ export const useTaskStore = defineStore('task', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTasks(params) {
|
async function loadTasks(params, projectId: IProject['id'] | null = null) {
|
||||||
const taskService = new TaskService()
|
|
||||||
|
|
||||||
const cancel = setModuleLoading(setIsLoading)
|
const cancel = setModuleLoading(setIsLoading)
|
||||||
try {
|
try {
|
||||||
|
if (projectId === null) {
|
||||||
|
const taskService = new TaskService()
|
||||||
tasks.value = await taskService.getAll({}, params)
|
tasks.value = await taskService.getAll({}, params)
|
||||||
|
} else {
|
||||||
|
const taskCollectionService = new TaskCollectionService()
|
||||||
|
tasks.value = await taskCollectionService.getAll({projectId}, params)
|
||||||
|
}
|
||||||
baseStore.setHasTasks(tasks.value.length > 0)
|
baseStore.setHasTasks(tasks.value.length > 0)
|
||||||
return tasks.value
|
return tasks.value
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -59,6 +59,7 @@ import LlamaCool from '@/assets/llama-cool.svg?component'
|
|||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
import {useTaskStore} from '@/stores/tasks'
|
import {useTaskStore} from '@/stores/tasks'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const taskStore = useTaskStore()
|
const taskStore = useTaskStore()
|
||||||
@ -69,6 +70,8 @@ const {t} = useI18n({useScope: 'global'})
|
|||||||
const tasks = ref<ITask[]>([])
|
const tasks = ref<ITask[]>([])
|
||||||
const showNothingToDo = ref<boolean>(false)
|
const showNothingToDo = ref<boolean>(false)
|
||||||
|
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
|
||||||
setTimeout(() => showNothingToDo.value = true, 100)
|
setTimeout(() => showNothingToDo.value = true, 100)
|
||||||
|
|
||||||
// Linting disabled because we explicitely enabled destructuring in vite's config, this will work.
|
// Linting disabled because we explicitely enabled destructuring in vite's config, this will work.
|
||||||
@ -179,6 +182,11 @@ async function loadPendingTasks(from: string, to: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authStore.settings.frontendSettings.filterIdUsedOnOverview && typeof projectStore.projects[authStore.settings.frontendSettings.filterIdUsedOnOverview] !== 'undefined') {
|
||||||
|
tasks.value = await taskStore.loadTasks(params, authStore.settings.frontendSettings.filterIdUsedOnOverview)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tasks.value = await taskStore.loadTasks(params)
|
tasks.value = await taskStore.loadTasks(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,12 @@
|
|||||||
</label>
|
</label>
|
||||||
<project-search v-model="defaultProject"/>
|
<project-search v-model="defaultProject"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field" v-if="hasFilters">
|
||||||
|
<label class="label">
|
||||||
|
{{ $t('user.settings.general.filterUsedOnOverview') }}
|
||||||
|
</label>
|
||||||
|
<project-search v-model="filterUsedInOverview" :saved-filters-only="true"/>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
|
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
|
||||||
@ -167,6 +173,7 @@ 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'
|
import type {IUserSettings} from '@/modelTypes/IUserSettings'
|
||||||
|
import {isSavedFilter} from '@/services/savedFilter'
|
||||||
|
|
||||||
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')}`)
|
||||||
@ -233,6 +240,13 @@ const defaultProject = computed({
|
|||||||
settings.value.defaultProjectId = l ? l.id : DEFAULT_PROJECT_ID
|
settings.value.defaultProjectId = l ? l.id : DEFAULT_PROJECT_ID
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
const filterUsedInOverview = computed({
|
||||||
|
get: () => projectStore.projects[settings.value.frontendSettings.filterIdUsedOnOverview],
|
||||||
|
set(l) {
|
||||||
|
settings.value.frontendSettings.filterIdUsedOnOverview = l ? l.id : null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const hasFilters = computed(() => typeof projectStore.projectsArray.find(p => isSavedFilter(p)) !== 'undefined')
|
||||||
const loading = computed(() => authStore.isLoadingGeneralSettings)
|
const loading = computed(() => authStore.isLoadingGeneralSettings)
|
||||||
|
|
||||||
async function updateSettings() {
|
async function updateSettings() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user