1
0

feat: improve store and model typing

This commit is contained in:
Dominik Pschenitschni
2022-07-21 00:42:36 +02:00
parent c9e85cb52b
commit 3766b5e51b
98 changed files with 1050 additions and 507 deletions

View File

@ -1,4 +1,6 @@
import type { ActionContext } from 'vuex'
import {LOADING, LOADING_MODULE} from './mutation-types'
import type { RootStoreState } from './types'
/**
* This helper sets the loading state with a 100ms delay to avoid flickering.
@ -7,7 +9,11 @@ import {LOADING, LOADING_MODULE} from './mutation-types'
* @param {null|String} module The module that is loading. This parameter allows components to listen for specific parts of the application loading.
* @param {null|function} loadFunc If not null, this function will be executed instead of the default setting loading.
*/
export const setLoading = (context, module = null, loadFunc = null) => {
export function setLoading<State>(
context : ActionContext<State, RootStoreState>,
module : string | null = null,
loadFunc : (() => void) | null = null,
) {
const timeout = setTimeout(() => {
if (loadFunc === null) {
context.commit(LOADING, true, {root: true})

View File

@ -25,7 +25,9 @@ import ListModel from '@/models/list'
import ListService from '../services/list'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
export const store = createStore({
import type { RootStoreState } from './types'
export const store = createStore<RootStoreState>({
strict: import.meta.env.DEV,
modules: {
config,
@ -37,7 +39,7 @@ export const store = createStore({
attachments,
labels,
},
state: {
state: () => ({
loading: false,
loadingModule: null,
// This is used to highlight the current list in menu for all list related views
@ -51,7 +53,7 @@ export const store = createStore({
menuActive: true,
keyboardShortcutsActive: false,
quickActionsActive: false,
},
}),
mutations: {
[LOADING](state, loading) {
state.loading = loading

View File

@ -1,21 +1,24 @@
import {findIndexById} from '@/helpers/utils'
import type { AttachmentState } from '@/store/types'
import type { IAttachment } from '@/models/attachment'
export default {
namespaced: true,
state: () => ({
state: (): AttachmentState => ({
attachments: [],
}),
mutations: {
set(state, attachments) {
set(state: AttachmentState, attachments: IAttachment[]) {
console.debug('Set attachments', attachments)
state.attachments = attachments
},
add(state, attachment) {
add(state: AttachmentState, attachment: IAttachment) {
console.debug('Add attachement', attachment)
state.attachments.push(attachment)
},
removeById(state, id) {
const attachmentIndex = findIndexById(state.attachments, id)
removeById(state: AttachmentState, id: IAttachment['id']) {
const attachmentIndex = findIndexById<IAttachment>(state.attachments, id)
state.attachments.splice(attachmentIndex, 1)
console.debug('Remove attachement', id)
},

View File

@ -1,3 +1,5 @@
import type { ActionContext } from 'vuex'
import {HTTPFactory, AuthenticatedHTTPFactory} from '@/http-common'
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
import {objectToSnakeCase} from '@/helpers/case'
@ -8,12 +10,10 @@ import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setLoading} from '@/store/helper'
import {success} from '@/message'
import {redirectToProvider} from '@/helpers/redirectToProvider'
import type { RootStoreState, AuthState, Info} from '@/store/types'
import {AUTH_TYPES} from '@/store/types'
import type { IUserSettings } from '@/models/userSettings'
const AUTH_TYPES = {
'UNKNOWN': 0,
'USER': 1,
'LINK_SHARE': 2,
}
const defaultSettings = settings => {
if (typeof settings.weekStart === 'undefined' || settings.weekStart === '') {
@ -24,7 +24,7 @@ const defaultSettings = settings => {
export default {
namespaced: true,
state: () => ({
state: (): AuthState => ({
authenticated: false,
isLinkShareAuth: false,
info: null,
@ -34,13 +34,13 @@ export default {
settings: {},
}),
getters: {
authUser(state) {
authUser(state: AuthState) {
return state.authenticated && (
state.info &&
state.info.type === AUTH_TYPES.USER
)
},
authLinkShare(state) {
authLinkShare(state: AuthState) {
return state.authenticated && (
state.info &&
state.info.type === AUTH_TYPES.LINK_SHARE
@ -48,7 +48,7 @@ export default {
},
},
mutations: {
info(state, info) {
info(state: AuthState, info: Info) {
state.info = info
if (info !== null) {
state.avatarUrl = info.getAvatarUrl()
@ -60,31 +60,32 @@ export default {
state.isLinkShareAuth = info.id < 0
}
},
setUserSettings(state, settings) {
setUserSettings(state: AuthState, settings: IUserSettings) {
state.settings = defaultSettings(settings)
const info = state.info !== null ? state.info : {}
const info = state.info !== null ? state.info : {} as Info
info.name = settings.name
state.info = info
},
authenticated(state, authenticated) {
authenticated(state: AuthState, authenticated: boolean) {
state.authenticated = authenticated
},
isLinkShareAuth(state, is) {
state.isLinkShareAuth = is
isLinkShareAuth(state: AuthState, isLinkShareAuth: boolean) {
state.isLinkShareAuth = isLinkShareAuth
},
needsTotpPasscode(state, needs) {
state.needsTotpPasscode = needs
needsTotpPasscode(state: AuthState, needsTotpPasscode: boolean) {
state.needsTotpPasscode = needsTotpPasscode
},
reloadAvatar(state) {
reloadAvatar(state: AuthState) {
if (!state.info) return
state.avatarUrl = `${state.info.getAvatarUrl()}&=${+new Date()}`
},
lastUserRefresh(state) {
lastUserRefresh(state: AuthState) {
state.lastUserInfoRefresh = new Date()
},
},
actions: {
// Logs a user in with a set of credentials.
async login(ctx, credentials) {
async login(ctx: ActionContext<AuthState, RootStoreState>, credentials) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
@ -115,7 +116,7 @@ export default {
// Registers a new user and logs them in.
// Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
async register(ctx, credentials) {
async register(ctx: ActionContext<AuthState, RootStoreState>, credentials) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
try {
@ -132,7 +133,7 @@ export default {
}
},
async openIdAuth(ctx, {provider, code}) {
async openIdAuth(ctx: ActionContext<AuthState, RootStoreState>, {provider, code}) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
@ -154,7 +155,7 @@ export default {
}
},
async linkShareAuth(ctx, {hash, password}) {
async linkShareAuth(ctx: ActionContext<AuthState, RootStoreState>, {hash, password}) {
const HTTP = HTTPFactory()
const response = await HTTP.post('/shares/' + hash + '/auth', {
password: password,
@ -165,7 +166,7 @@ export default {
},
// Populates user information from jwt token saved in local storage in store
checkAuth(ctx) {
checkAuth(ctx: ActionContext<AuthState, RootStoreState>) {
// This function can be called from multiple places at the same time and shortly after one another.
// To prevent hitting the api too frequently or race conditions, we check at most once per minute.
@ -197,7 +198,7 @@ export default {
}
},
redirectToProviderIfNothingElseIsEnabled({rootState}) {
redirectToProviderIfNothingElseIsEnabled({rootState}: ActionContext<AuthState, RootStoreState>) {
const {auth} = rootState.config
if (
auth.local.enabled === false &&
@ -209,7 +210,7 @@ export default {
}
},
async refreshUserInfo({state, commit, dispatch}) {
async refreshUserInfo({state, commit, dispatch}: ActionContext<AuthState, RootStoreState>) {
const jwt = getToken()
if (!jwt) {
return
@ -243,7 +244,7 @@ export default {
}
},
async saveUserSettings(ctx, payload) {
async saveUserSettings(ctx: ActionContext<AuthState, RootStoreState>, payload) {
const {settings} = payload
const showMessage = payload.showMessage ?? true
const userSettingsService = new UserSettingsService()
@ -264,7 +265,7 @@ export default {
},
// Renews the api token and saves it to local storage
renewToken(ctx) {
renewToken(ctx: ActionContext<AuthState, RootStoreState>) {
// FIXME: Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a
// link share in another tab. Without the timeout both the token renew and link share auth are executed at
// the same time and one might win over the other.
@ -285,7 +286,7 @@ export default {
}
}, 5000)
},
logout(ctx) {
logout(ctx: ActionContext<AuthState, RootStoreState>) {
removeToken()
window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
ctx.dispatch('checkAuth')

View File

@ -1,11 +1,14 @@
import type { ActionContext } from 'vuex'
import {parseURL} from 'ufo'
import {CONFIG} from '../mutation-types'
import {HTTPFactory} from '@/http-common'
import {objectToCamelCase} from '@/helpers/case'
import {parseURL} from 'ufo'
import type { RootStoreState, ConfigState } from '@/store/types'
export default {
namespaced: true,
state: () => ({
state: (): ConfigState => ({
// These are the api defaults.
version: '',
frontendUrl: '',
@ -36,19 +39,19 @@ export default {
},
}),
getters: {
migratorsEnabled: state => state.availableMigrators?.length > 0,
migratorsEnabled: (state: ConfigState) => state.availableMigrators?.length > 0,
apiBase() {
const {host, protocol} = parseURL(window.API_URL)
return protocol + '//' + host
},
},
mutations: {
[CONFIG](state, config) {
[CONFIG](state: ConfigState, config: ConfigState) {
Object.assign(state, config)
},
},
actions: {
async update(ctx) {
async update(ctx: ActionContext<ConfigState, RootStoreState>) {
const HTTP = HTTPFactory()
const {data: config} = await HTTP.get('info')
ctx.commit(CONFIG, objectToCamelCase(config))

View File

@ -7,10 +7,15 @@ import {success} from '@/message'
import BucketService from '../../services/bucket'
import {setLoading} from '../helper'
import TaskCollectionService from '@/services/taskCollection'
import type { ActionContext } from 'vuex'
import type { RootStoreState, KanbanState } from '@/store/types'
import type { ITask } from '@/models/task'
import type { IList } from '@/models/list'
import type { IBucket } from '@/models/bucket'
const TASKS_PER_BUCKET = 25
function getTaskIndicesById(state, taskId) {
function getTaskIndicesById(state: KanbanState, taskId: ITask['id']) {
let taskIndex
const bucketIndex = state.buckets.findIndex(({ tasks }) => {
taskIndex = findIndexById(tasks, taskId)
@ -23,7 +28,7 @@ function getTaskIndicesById(state, taskId) {
}
}
const addTaskToBucketAndSort = (state, task) => {
const addTaskToBucketAndSort = (state: KanbanState, task: ITask) => {
const bucketIndex = findIndexById(state.buckets, task.bucketId)
state.buckets[bucketIndex].tasks.push(task)
state.buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1)
@ -36,7 +41,7 @@ const addTaskToBucketAndSort = (state, task) => {
export default {
namespaced: true,
state: () => ({
state: (): KanbanState => ({
buckets: [],
listId: 0,
bucketLoading: {},
@ -45,11 +50,11 @@ export default {
}),
mutations: {
setListId(state, listId) {
setListId(state: KanbanState, listId: IList['id']) {
state.listId = parseInt(listId)
},
setBuckets(state, buckets) {
setBuckets(state: KanbanState, buckets: IBucket[]) {
state.buckets = buckets
buckets.forEach(b => {
state.taskPagesPerBucket[b.id] = 1
@ -57,31 +62,51 @@ export default {
})
},
addBucket(state, bucket) {
addBucket(state: KanbanState, bucket: IBucket) {
state.buckets.push(bucket)
},
removeBucket(state, bucket) {
removeBucket(state: KanbanState, bucket: IBucket) {
const bucketIndex = findIndexById(state.buckets, bucket.id)
state.buckets.splice(bucketIndex, 1)
},
setBucketById(state, bucket) {
setBucketById(state: KanbanState, bucket: IBucket) {
const bucketIndex = findIndexById(state.buckets, bucket.id)
state.buckets[bucketIndex] = bucket
},
setBucketByIndex(state, {bucketIndex, bucket}) {
setBucketByIndex(state: KanbanState, {
bucketIndex,
bucket,
} : {
bucketIndex: number,
bucket: IBucket
}) {
state.buckets[bucketIndex] = bucket
},
setTaskInBucketByIndex(state, {bucketIndex, taskIndex, task}) {
setTaskInBucketByIndex(state: KanbanState, {
bucketIndex,
taskIndex,
task,
} : {
bucketIndex: number,
taskIndex: number,
task: ITask
}) {
const bucket = state.buckets[bucketIndex]
bucket.tasks[taskIndex] = task
state.buckets[bucketIndex] = bucket
},
setTasksInBucketByBucketId(state, {bucketId, tasks}) {
setTasksInBucketByBucketId(state: KanbanState, {
bucketId,
tasks,
} : {
bucketId: IBucket['id'],
tasks: ITask[],
}) {
const bucketIndex = findIndexById(state.buckets, bucketId)
state.buckets[bucketIndex] = {
...state.buckets[bucketIndex],
@ -89,7 +114,7 @@ export default {
}
},
setTaskInBucket(state, task) {
setTaskInBucket(state: KanbanState, task: ITask) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (state.buckets.length === 0) {
return
@ -133,7 +158,7 @@ export default {
}
},
addTaskToBucket(state, task) {
addTaskToBucket(state: KanbanState, task: ITask) {
const bucketIndex = findIndexById(state.buckets, task.bucketId)
const oldBucket = state.buckets[bucketIndex]
const newBucket = {
@ -146,7 +171,10 @@ export default {
state.buckets[bucketIndex] = newBucket
},
addTasksToBucket(state, {tasks, bucketId}) {
addTasksToBucket(state: KanbanState, {tasks, bucketId}: {
tasks: ITask[];
bucketId: IBucket['id'];
}) {
const bucketIndex = findIndexById(state.buckets, bucketId)
const oldBucket = state.buckets[bucketIndex]
const newBucket = {
@ -159,7 +187,7 @@ export default {
state.buckets[bucketIndex] = newBucket
},
removeTaskInBucket(state, task) {
removeTaskInBucket(state: KanbanState, task: ITask) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (state.buckets.length === 0) {
return
@ -168,8 +196,10 @@ export default {
const { bucketIndex, taskIndex } = getTaskIndicesById(state, task.id)
if (
!bucketIndex ||
state.buckets[bucketIndex]?.id !== task.bucketId ||
state.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id
!taskIndex ||
(state.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id)
) {
return
}
@ -177,39 +207,40 @@ export default {
state.buckets[bucketIndex].tasks.splice(taskIndex, 1)
},
setBucketLoading(state, {bucketId, loading}) {
setBucketLoading(state: KanbanState, {bucketId, loading}) {
state.bucketLoading[bucketId] = loading
},
setTasksLoadedForBucketPage(state, {bucketId, page}) {
setTasksLoadedForBucketPage(state: KanbanState, {bucketId, page}) {
state.taskPagesPerBucket[bucketId] = page
},
setAllTasksLoadedForBucket(state, bucketId) {
setAllTasksLoadedForBucket(state: KanbanState, bucketId) {
state.allTasksLoadedForBucket[bucketId] = true
},
},
getters: {
getBucketById(state) {
getBucketById(state: KanbanState) {
return (bucketId) => findById(state.buckets, bucketId)
},
getTaskById(state) {
getTaskById(state: KanbanState) {
return (id) => {
const { bucketIndex, taskIndex } = getTaskIndicesById(state, id)
return {
bucketIndex,
taskIndex,
task: state.buckets[bucketIndex]?.tasks?.[taskIndex] || null,
task: bucketIndex && taskIndex && state.buckets[bucketIndex]?.tasks?.[taskIndex] || null,
}
}
},
},
actions: {
async loadBucketsForList(ctx, {listId, params}) {
async loadBucketsForList(ctx: ActionContext<KanbanState, RootStoreState>, {listId, params}) {
const cancel = setLoading(ctx, 'kanban')
// Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments
@ -219,7 +250,7 @@ export default {
const bucketService = new BucketService()
try {
const response = await bucketService.getAll({listId: listId}, params)
const response = await bucketService.getAll({listId}, params)
ctx.commit('setBuckets', response)
ctx.commit('setListId', listId)
return response
@ -228,7 +259,7 @@ export default {
}
},
async loadNextTasksForBucket(ctx, {listId, ps = {}, bucketId}) {
async loadNextTasksForBucket(ctx: ActionContext<KanbanState, RootStoreState>, {listId, ps = {}, bucketId}) {
const isLoading = ctx.state.bucketLoading[bucketId] ?? false
if (isLoading) {
return
@ -270,7 +301,7 @@ export default {
const taskService = new TaskCollectionService()
try {
const tasks = await taskService.getAll({listId: listId}, params, page)
const tasks = await taskService.getAll({listId}, params, page)
ctx.commit('addTasksToBucket', {tasks, bucketId: bucketId})
ctx.commit('setTasksLoadedForBucketPage', {bucketId, page})
if (taskService.totalPages <= page) {
@ -283,7 +314,7 @@ export default {
}
},
async createBucket(ctx, bucket) {
async createBucket(ctx: ActionContext<KanbanState, RootStoreState>, bucket: IBucket) {
const cancel = setLoading(ctx, 'kanban')
const bucketService = new BucketService()
@ -296,7 +327,7 @@ export default {
}
},
async deleteBucket(ctx, {bucket, params}) {
async deleteBucket(ctx: ActionContext<KanbanState, RootStoreState>, {bucket, params}) {
const cancel = setLoading(ctx, 'kanban')
const bucketService = new BucketService()
@ -304,14 +335,14 @@ export default {
const response = await bucketService.delete(bucket)
ctx.commit('removeBucket', bucket)
// We reload all buckets because tasks are being moved from the deleted bucket
ctx.dispatch('loadBucketsForList', {listId: bucket.listId, params: params})
ctx.dispatch('loadBucketsForList', {listId: bucket.listId, params})
return response
} finally {
cancel()
}
},
async updateBucket(ctx, updatedBucketData) {
async updateBucket(ctx: ActionContext<KanbanState, RootStoreState>, updatedBucketData) {
const cancel = setLoading(ctx, 'kanban')
const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id)
@ -339,10 +370,10 @@ export default {
}
},
async updateBucketTitle(ctx, { id, title }) {
async updateBucketTitle(ctx: ActionContext<KanbanState, RootStoreState>, { id, title }) {
const bucket = findById(ctx.state.buckets, id)
if (bucket.title === title) {
if (bucket?.title === title) {
// bucket title has not changed
return
}

View File

@ -1,15 +1,19 @@
import type { ActionContext } from 'vuex'
import {i18n} from '@/i18n'
import {success} from '@/message'
import LabelService from '@/services/label'
import {setLoading} from '@/store/helper'
import {success} from '@/message'
import {i18n} from '@/i18n'
import type { LabelState, RootStoreState } from '@/store/types'
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
import {createNewIndexer} from '@/indexes'
import type { ILabel } from '@/models/label'
const {add, remove, update} = createNewIndexer('labels', ['title', 'description'])
async function getAllLabels(page = 1) {
async function getAllLabels(page = 1): Promise<ILabel[]> {
const labelService = new LabelService()
const labels = await labelService.getAll({}, {}, page)
const labels = await labelService.getAll({}, {}, page) as ILabel[]
if (page < labelService.totalPages) {
const nextLabels = await getAllLabels(page + 1)
return labels.concat(nextLabels)
@ -20,45 +24,44 @@ async function getAllLabels(page = 1) {
export default {
namespaced: true,
state: () => ({
// The labels are stored as an object which has the label ids as keys.
state: (): LabelState => ({
labels: {},
loaded: false,
}),
mutations: {
setLabels(state, labels) {
setLabels(state: LabelState, labels: ILabel[]) {
labels.forEach(l => {
state.labels[l.id] = l
add(l)
})
},
setLabel(state, label) {
setLabel(state: LabelState, label: ILabel) {
state.labels[label.id] = label
update(label)
},
removeLabelById(state, label) {
removeLabelById(state: LabelState, label: ILabel) {
remove(label)
delete state.labels[label.id]
},
setLoaded(state, loaded) {
setLoaded(state: LabelState, loaded: boolean) {
state.loaded = loaded
},
},
getters: {
getLabelsByIds(state) {
return (ids) => getLabelsByIds(state, ids)
getLabelsByIds(state: LabelState) {
return (ids: ILabel['id'][]) => getLabelsByIds(state, ids)
},
filterLabelsByQuery(state) {
return (labelsToHide, query) => filterLabelsByQuery(state, labelsToHide, query)
filterLabelsByQuery(state: LabelState) {
return (labelsToHide: ILabel[], query: string) => filterLabelsByQuery(state, labelsToHide, query)
},
getLabelsByExactTitles(state) {
getLabelsByExactTitles(state: LabelState) {
return labelTitles => Object
.values(state.labels)
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()))
},
},
actions: {
async loadAllLabels(ctx, {forceLoad} = {}) {
async loadAllLabels(ctx: ActionContext<LabelState, RootStoreState>, {forceLoad} = {}) {
if (ctx.state.loaded && !forceLoad) {
return
}
@ -74,7 +77,7 @@ export default {
cancel()
}
},
async deleteLabel(ctx, label) {
async deleteLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService()
@ -87,7 +90,7 @@ export default {
cancel()
}
},
async updateLabel(ctx, label) {
async updateLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService()
@ -100,7 +103,7 @@ export default {
cancel()
}
},
async createLabel(ctx, label) {
async createLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService()

View File

@ -2,6 +2,9 @@ import ListService from '@/services/list'
import {setLoading} from '@/store/helper'
import {removeListFromHistory} from '@/modules/listHistory'
import {createNewIndexer} from '@/indexes'
import type {ListState, RootStoreState} from '@/store/types'
import type {ActionContext} from 'vuex'
import type {IList} from '@/models/list'
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
@ -10,37 +13,37 @@ const FavoriteListsNamespace = -2
export default {
namespaced: true,
// The state is an object which has the list ids as keys.
state: () => ({}),
state: (): ListState => ({}),
mutations: {
setList(state, list) {
setList(state: ListState, list: IList) {
state[list.id] = list
update(list)
},
setLists(state, lists) {
setLists(state: ListState, lists: IList[]) {
lists.forEach(l => {
state[l.id] = l
add(l)
})
},
removeListById(state, list) {
removeListById(state: ListState, list: IList) {
remove(list)
delete state[list.id]
},
},
getters: {
getListById: state => id => {
getListById: (state: ListState) => (id: IList['id']) => {
if (typeof state[id] !== 'undefined') {
return state[id]
}
return null
},
findListByExactname: state => name => {
findListByExactname: (state: ListState) => (name: string) => {
const list = Object.values(state).find(l => {
return l.title.toLowerCase() === name.toLowerCase()
})
return typeof list === 'undefined' ? null : list
},
searchList: state => (query, includeArchived = false) => {
searchList: (state: ListState) => (query: string, includeArchived = false) => {
return search(query)
?.filter(value => value > 0)
.map(id => state[id])
@ -49,14 +52,14 @@ export default {
},
},
actions: {
toggleListFavorite(ctx, list) {
toggleListFavorite(ctx: ActionContext<ListState, RootStoreState>, list: IList) {
return ctx.dispatch('updateList', {
...list,
isFavorite: !list.isFavorite,
})
},
async createList(ctx, list) {
async createList(ctx: ActionContext<ListState, RootStoreState>, list: IList) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()
@ -71,7 +74,7 @@ export default {
}
},
async updateList(ctx, list) {
async updateList(ctx: ActionContext<ListState, RootStoreState>, list: IList) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()
@ -106,7 +109,7 @@ export default {
}
},
async deleteList(ctx, list) {
async deleteList(ctx: ActionContext<ListState, RootStoreState>, list: IList) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()

View File

@ -1,22 +1,27 @@
import type {ActionContext} from 'vuex'
import NamespaceService from '../../services/namespace'
import {setLoading} from '@/store/helper'
import {createNewIndexer} from '@/indexes'
import type {NamespaceState, RootStoreState} from '@/store/types'
import type {INamespace} from '@/models/namespace'
import type {IList} from '@/models/list'
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
export default {
namespaced: true,
state: () => ({
state: (): NamespaceState => ({
namespaces: [],
}),
mutations: {
namespaces(state, namespaces) {
namespaces(state: NamespaceState, namespaces: INamespace[]) {
state.namespaces = namespaces
namespaces.forEach(n => {
add(n)
})
},
setNamespaceById(state, namespace) {
setNamespaceById(state: NamespaceState, namespace: INamespace) {
const namespaceIndex = state.namespaces.findIndex(n => n.id === namespace.id)
if (namespaceIndex === -1) {
@ -30,7 +35,7 @@ export default {
state.namespaces[namespaceIndex] = namespace
update(namespace)
},
setListInNamespaceById(state, list) {
setListInNamespaceById(state: NamespaceState, list: IList) {
for (const n in state.namespaces) {
// We don't have the namespace id on the list which means we need to loop over all lists until we find it.
// FIXME: Not ideal at all - we should fix that at the api level.
@ -46,11 +51,11 @@ export default {
}
}
},
addNamespace(state, namespace) {
addNamespace(state: NamespaceState, namespace: INamespace) {
state.namespaces.push(namespace)
add(namespace)
},
removeNamespaceById(state, namespaceId) {
removeNamespaceById(state: NamespaceState, namespaceId: INamespace['id']) {
for (const n in state.namespaces) {
if (state.namespaces[n].id === namespaceId) {
remove(state.namespaces[n])
@ -59,7 +64,7 @@ export default {
}
}
},
addListToNamespace(state, list) {
addListToNamespace(state: NamespaceState, list: IList) {
for (const n in state.namespaces) {
if (state.namespaces[n].id === list.namespaceId) {
state.namespaces[n].lists.push(list)
@ -67,7 +72,7 @@ export default {
}
}
},
removeListFromNamespaceById(state, list) {
removeListFromNamespaceById(state: NamespaceState, list: IList) {
for (const n in state.namespaces) {
// We don't have the namespace id on the list which means we need to loop over all lists until we find it.
// FIXME: Not ideal at all - we should fix that at the api level.
@ -83,7 +88,7 @@ export default {
},
},
getters: {
getListAndNamespaceById: state => (listId, ignorePseudoNamespaces = false) => {
getListAndNamespaceById: (state: NamespaceState) => (listId: IList['id'], ignorePseudoNamespaces = false) => {
for (const n in state.namespaces) {
if (ignorePseudoNamespaces && state.namespaces[n].id < 0) {
@ -101,10 +106,10 @@ export default {
}
return null
},
getNamespaceById: state => namespaceId => {
getNamespaceById: (state: NamespaceState) => (namespaceId: INamespace['id']) => {
return state.namespaces.find(({id}) => id == namespaceId) || null
},
searchNamespace: (state, getters) => query => {
searchNamespace: (state: NamespaceState, getters) => (query: string) => {
return search(query)
?.filter(value => value > 0)
.map(getters.getNamespaceById)
@ -113,7 +118,7 @@ export default {
},
},
actions: {
async loadNamespaces(ctx) {
async loadNamespaces(ctx: ActionContext<NamespaceState, RootStoreState>) {
const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService()
@ -133,20 +138,20 @@ export default {
}
},
loadNamespacesIfFavoritesDontExist(ctx) {
loadNamespacesIfFavoritesDontExist(ctx: ActionContext<NamespaceState, RootStoreState>) {
// The first or second namespace should be the one holding all favorites
if (ctx.state.namespaces[0].id !== -2 && ctx.state.namespaces[1]?.id !== -2) {
return ctx.dispatch('loadNamespaces')
}
},
removeFavoritesNamespaceIfEmpty(ctx) {
removeFavoritesNamespaceIfEmpty(ctx: ActionContext<NamespaceState, RootStoreState>) {
if (ctx.state.namespaces[0].id === -2 && ctx.state.namespaces[0].lists.length === 0) {
ctx.state.namespaces.splice(0, 1)
}
},
async deleteNamespace(ctx, namespace) {
async deleteNamespace(ctx: ActionContext<NamespaceState, RootStoreState>, namespace: INamespace) {
const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService()
@ -159,7 +164,7 @@ export default {
}
},
async createNamespace(ctx, namespace) {
async createNamespace(ctx: ActionContext<NamespaceState, RootStoreState>, namespace: INamespace) {
const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService()

View File

@ -1,20 +1,25 @@
import router from '@/router'
import type { ActionContext } from 'vuex'
import {formatISO} from 'date-fns'
import TaskService from '@/services/task'
import TaskAssigneeService from '@/services/taskAssignee'
import TaskAssigneeModel from '../../models/taskAssignee'
import LabelTaskModel from '../../models/labelTask'
import TaskAssigneeModel from '@/models/taskAssignee'
import LabelTaskModel from '@/models/labelTask'
import LabelTaskService from '@/services/labelTask'
import {HAS_TASKS} from '../mutation-types'
import {setLoading} from '../helper'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {parseTaskText} from '@/modules/parseTaskText'
import TaskModel from '@/models/task'
import {formatISO} from 'date-fns'
import TaskModel, { type ITask } from '@/models/task'
import LabelTask from '@/models/labelTask'
import LabelModel from '@/models/label'
import LabelModel, { type ILabel } from '@/models/label'
import UserService from '@/services/user'
import type { RootStoreState, TaskState } from '@/store/types'
import type { IUser } from '@/models/user'
import type { IAttachment } from '@/models/attachment'
import type { IList } from '@/models/list'
// IDEA: maybe use a small fuzzy search here to prevent errors
function findPropertyByValue(object, key, value) {
@ -24,16 +29,16 @@ function findPropertyByValue(object, key, value) {
}
// Check if the user exists
function validateUsername(users, username) {
function validateUsername(users: IUser[], username: IUser['username']) {
return findPropertyByValue(users, 'username', username)
}
// Check if the label exists
function validateLabel(labels, label) {
function validateLabel(labels: ILabel[], label: ILabel) {
return findPropertyByValue(labels, 'title', label)
}
async function addLabelToTask(task, label) {
async function addLabelToTask(task: ITask, label: ILabel) {
const labelTask = new LabelTask({
taskId: task.id,
labelId: label.id,
@ -62,9 +67,9 @@ async function findAssignees(parsedTaskAssignees) {
export default {
namespaced: true,
state: () => ({}),
state: (): TaskState => ({}),
actions: {
async loadTasks(ctx, params) {
async loadTasks(ctx: ActionContext<TaskState, RootStoreState>, params) {
const taskService = new TaskService()
const cancel = setLoading(ctx, 'tasks')
@ -77,7 +82,7 @@ export default {
}
},
async update(ctx, task) {
async update(ctx: ActionContext<TaskState, RootStoreState>, task: ITask) {
const cancel = setLoading(ctx, 'tasks')
const taskService = new TaskService()
@ -90,7 +95,7 @@ export default {
}
},
async delete(ctx, task) {
async delete(ctx: ActionContext<TaskState, RootStoreState>, task: ITask) {
const taskService = new TaskService()
const response = await taskService.delete(task)
ctx.commit('kanban/removeTaskInBucket', task, {root: true})
@ -99,7 +104,13 @@ export default {
// Adds a task attachment in store.
// This is an action to be able to commit other mutations
addTaskAttachment(ctx, {taskId, attachment}) {
addTaskAttachment(ctx: ActionContext<TaskState, RootStoreState>, {
taskId,
attachment,
}: {
taskId: ITask['id']
attachment: IAttachment
}) {
const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task !== null) {
const attachments = [
@ -119,7 +130,13 @@ export default {
ctx.commit('attachments/add', attachment, {root: true})
},
async addAssignee(ctx, {user, taskId}) {
async addAssignee(ctx: ActionContext<TaskState, RootStoreState>, {
user,
taskId,
}: {
user: IUser,
taskId: ITask['id']
}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService()
@ -148,7 +165,13 @@ export default {
return r
},
async removeAssignee(ctx, {user, taskId}) {
async removeAssignee(ctx: ActionContext<TaskState, RootStoreState>, {
user,
taskId,
}: {
user: IUser,
taskId: ITask['id']
}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService()
@ -175,8 +198,14 @@ export default {
},
async addLabel(ctx, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
async addLabel(ctx: ActionContext<TaskState, RootStoreState>, {
label,
taskId,
} : {
label: ILabel,
taskId: ITask['id']
}) {
const labelTask = new LabelTaskModel({taskId, labelId: label.id})
const labelTaskService = new LabelTaskService()
const r = await labelTaskService.create(labelTask)
@ -205,8 +234,8 @@ export default {
return r
},
async removeLabel(ctx, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
async removeLabel(ctx: ActionContext<TaskState, RootStoreState>, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId, labelId: label.id})
const labelTaskService = new LabelTaskService()
const response = await labelTaskService.delete(labelTask)
@ -234,7 +263,10 @@ export default {
},
// Do everything that is involved in finding, creating and adding the label to the task
async addLabelsToTask({rootState, dispatch}, { task, parsedLabels }) {
async addLabelsToTask({rootState, dispatch}: ActionContext<TaskState, RootStoreState>, {
task,
parsedLabels,
}) {
if (parsedLabels.length <= 0) {
return task
}
@ -257,7 +289,10 @@ export default {
return task
},
findListId({ rootGetters }, { list: listName, listId }) {
findListId({ rootGetters }: ActionContext<TaskState, RootStoreState>, { list: listName, listId }: {
list: string,
listId: IList['id']
}) {
let foundListId = null
// Uses the following ways to get the list id of the new task:
@ -285,12 +320,14 @@ export default {
return foundListId
},
async createNewTask({dispatch, commit}, {
async createNewTask({dispatch, commit}: ActionContext<TaskState, RootStoreState>, {
title,
bucketId,
listId,
position,
}) {
} :
Partial<ITask>,
) {
const cancel = setLoading({commit}, 'tasks')
const parsedTask = parseTaskText(title, getQuickAddMagicMode())

116
src/store/types.ts Normal file
View File

@ -0,0 +1,116 @@
import type { IBucket } from '@/models/bucket'
import type { IUserSettings } from '@/models/userSettings'
import type { IList } from '@/models/list'
import type { IAttachment } from '@/models/attachment'
import type { ILabel } from '@/models/label'
import type { INamespace } from '@/models/namespace'
export interface RootStoreState {
loading: boolean,
loadingModule: null,
currentList: IList,
background: string,
blurHash: string,
hasTasks: boolean,
menuActive: boolean,
keyboardShortcutsActive: boolean,
quickActionsActive: boolean,
// modules
attachments: AttachmentState,
auth: AuthState,
config: ConfigState,
kanban: KanbanState,
labels: LabelState,
lists: ListState,
namespaces: NamespaceState,
tasks: TaskState,
}
export interface AttachmentState {
attachments: IAttachment[],
}
export const AUTH_TYPES = {
'UNKNOWN': 0,
'USER': 1,
'LINK_SHARE': 2,
} as const
export interface Info {
id: number // what kind of id is this?
type: typeof AUTH_TYPES[keyof typeof AUTH_TYPES],
getAvatarUrl: () => string
settings: IUserSettings
name: string
email: string
exp: any
}
export interface AuthState {
authenticated: boolean,
isLinkShareAuth: boolean,
info: Info | null,
needsTotpPasscode: boolean,
avatarUrl: string,
lastUserInfoRefresh: Date | null,
settings: IUserSettings,
}
export interface ConfigState {
version: string,
frontendUrl: string,
motd: string,
linkSharingEnabled: boolean,
maxFileSize: '20MB',
registrationEnabled: boolean,
availableMigrators: [],
taskAttachmentsEnabled: boolean,
totpEnabled: boolean,
enabledBackgroundProviders: [],
legal: {
imprintUrl: string,
privacyPolicyUrl: string,
},
caldavEnabled: boolean,
userDeletionEnabled: boolean,
taskCommentsEnabled: boolean,
auth: {
local: {
enabled: boolean,
},
openidConnect: {
enabled: boolean,
redirectUrl: string,
providers: [],
},
},
}
export interface KanbanState {
buckets: IBucket[],
listId: IList['id'],
bucketLoading: {},
taskPagesPerBucket: {
[id: IBucket['id']]: number
},
allTasksLoadedForBucket: {
[id: IBucket['id']]: boolean
},
}
export interface LabelState {
labels: {
[id: ILabel['id']]: ILabel
},
loaded: boolean,
}
export interface ListState {
[id: IList['id']]: IList
}
export interface NamespaceState {
namespaces: INamespace[]
}
export interface TaskState {}