feat: port label store to pinia | pinia 1/9 (#2391)
Co-authored-by: Dominik Pschenitschni <mail@celement.de> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2391 Reviewed-by: konrad <k@knt.li> Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de> Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
parent
e91b5fde02
commit
d67e5e386d
17 changed files with 276 additions and 235 deletions
|
@ -46,6 +46,7 @@
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"marked": "4.1.0",
|
"marked": "4.1.0",
|
||||||
"minimist": "1.2.6",
|
"minimist": "1.2.6",
|
||||||
|
"pinia": "^2.0.21",
|
||||||
"register-service-worker": "1.7.2",
|
"register-service-worker": "1.7.2",
|
||||||
"snake-case": "3.0.4",
|
"snake-case": "3.0.4",
|
||||||
"ufo": "0.8.5",
|
"ufo": "0.8.5",
|
||||||
|
|
|
@ -66,6 +66,7 @@ import {useRoute, useRouter} from 'vue-router'
|
||||||
import {useEventListener} from '@vueuse/core'
|
import {useEventListener} from '@vueuse/core'
|
||||||
|
|
||||||
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
|
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
|
||||||
|
import {useLabelStore} from '@/stores/labels'
|
||||||
import Navigation from '@/components/home/navigation.vue'
|
import Navigation from '@/components/home/navigation.vue'
|
||||||
import QuickActions from '@/components/quick-actions/quick-actions.vue'
|
import QuickActions from '@/components/quick-actions/quick-actions.vue'
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
@ -197,7 +198,8 @@ function useRenewTokenOnFocus() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useRenewTokenOnFocus()
|
useRenewTokenOnFocus()
|
||||||
store.dispatch('labels/loadAllLabels')
|
const labelStore = useLabelStore()
|
||||||
|
labelStore.loadAllLabels()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -190,6 +190,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent} from 'vue'
|
import {defineComponent} from 'vue'
|
||||||
|
|
||||||
|
import {useLabelStore} from '@/stores/labels'
|
||||||
|
|
||||||
import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
|
import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
|
||||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||||
|
|
||||||
|
@ -307,8 +309,10 @@ export default defineComponent({
|
||||||
this.change()
|
this.change()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
foundLabels() {
|
foundLabels() {
|
||||||
return this.$store.getters['labels/filterLabelsByQuery'](this.labels, this.query)
|
const labelStore = useLabelStore()
|
||||||
|
return labelStore.filterLabelsByQuery(this.labels, this.labelQuery)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -336,7 +340,8 @@ export default defineComponent({
|
||||||
: ''
|
: ''
|
||||||
const labelIds = labels.split(',').map(i => parseInt(i))
|
const labelIds = labels.split(',').map(i => parseInt(i))
|
||||||
|
|
||||||
this.labels = this.$store.getters['labels/getLabelsByIds'](labelIds)
|
const labelStore = useLabelStore()
|
||||||
|
this.labels = labelStore.getLabelsByIds(labelIds)
|
||||||
},
|
},
|
||||||
removePropertyFromFilter(propertyName) {
|
removePropertyFromFilter(propertyName) {
|
||||||
// Because of the way arrays work, we can only ever remove one element at once.
|
// Because of the way arrays work, we can only ever remove one element at once.
|
||||||
|
|
|
@ -50,6 +50,7 @@ import {success} from '@/message'
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import Multiselect from '@/components/input/multiselect.vue'
|
import Multiselect from '@/components/input/multiselect.vue'
|
||||||
import type { ILabel } from '@/modelTypes/ILabel'
|
import type { ILabel } from '@/modelTypes/ILabel'
|
||||||
|
import { useLabelStore } from '@/stores/labels'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
@ -86,8 +87,10 @@ watch(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const foundLabels = computed(() => store.getters['labels/filterLabelsByQuery'](labels.value, query.value))
|
const labelStore = useLabelStore()
|
||||||
const loading = computed(() => labelTaskService.loading || (store.state.loading && store.state.loadingModule === 'labels'))
|
|
||||||
|
const foundLabels = computed(() => labelStore.filterLabelsByQuery(labels.value, query.value))
|
||||||
|
const loading = computed(() => labelTaskService.loading || labelStore.isLoading)
|
||||||
|
|
||||||
function findLabel(newQuery: string) {
|
function findLabel(newQuery: string) {
|
||||||
query.value = newQuery
|
query.value = newQuery
|
||||||
|
@ -129,7 +132,8 @@ async function createAndAddLabel(title: string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newLabel = await store.dispatch('labels/createLabel', new LabelModel({title}))
|
const labelStore = useLabelStore()
|
||||||
|
const newLabel = await labelStore.createLabel(new LabelModel({title}))
|
||||||
addLabel(newLabel, false)
|
addLabel(newLabel, false)
|
||||||
labels.value.push(newLabel)
|
labels.value.push(newLabel)
|
||||||
success({message: t('task.label.addCreateSuccess')})
|
success({message: t('task.label.addCreateSuccess')})
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import {describe, it, expect} from 'vitest'
|
|
||||||
|
|
||||||
import {filterLabelsByQuery} from './labels'
|
|
||||||
import {createNewIndexer} from '../indexes'
|
|
||||||
|
|
||||||
const {add} = createNewIndexer('labels', ['title', 'description'])
|
|
||||||
|
|
||||||
describe('filter labels', () => {
|
|
||||||
const state = {
|
|
||||||
labels: {
|
|
||||||
1: {id: 1, title: 'label1'},
|
|
||||||
2: {id: 2, title: 'label2'},
|
|
||||||
3: {id: 3, title: 'label3'},
|
|
||||||
4: {id: 4, title: 'label4'},
|
|
||||||
5: {id: 5, title: 'label5'},
|
|
||||||
6: {id: 6, title: 'label6'},
|
|
||||||
7: {id: 7, title: 'label7'},
|
|
||||||
8: {id: 8, title: 'label8'},
|
|
||||||
9: {id: 9, title: 'label9'},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.values(state.labels).forEach(add)
|
|
||||||
|
|
||||||
it('should return an empty array for an empty query', () => {
|
|
||||||
const labels = filterLabelsByQuery(state, [], '')
|
|
||||||
|
|
||||||
expect(labels).toHaveLength(0)
|
|
||||||
})
|
|
||||||
it('should return labels for a query', () => {
|
|
||||||
const labels = filterLabelsByQuery(state, [], 'label2')
|
|
||||||
|
|
||||||
expect(labels).toHaveLength(1)
|
|
||||||
expect(labels[0].title).toBe('label2')
|
|
||||||
})
|
|
||||||
it('should not return found but hidden labels', () => {
|
|
||||||
interface label {
|
|
||||||
id: number,
|
|
||||||
title: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelsToHide: label[] = [{id: 1, title: 'label1'}]
|
|
||||||
const labels = filterLabelsByQuery(state, labelsToHide, 'label1')
|
|
||||||
|
|
||||||
expect(labels).toHaveLength(0)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,33 +0,0 @@
|
||||||
import {createNewIndexer} from '../indexes'
|
|
||||||
|
|
||||||
import type {LabelState} from '@/store/types'
|
|
||||||
import type {ILabel} from '@/modelTypes/ILabel'
|
|
||||||
|
|
||||||
const {search} = createNewIndexer('labels', ['title', 'description'])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a list of labels is available in the store and filters them then query
|
|
||||||
* @param {Object} state
|
|
||||||
* @param {Array} labelsToHide
|
|
||||||
* @param {String} query
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
export function filterLabelsByQuery(state: LabelState, labelsToHide: ILabel[], query: string) {
|
|
||||||
const labelIdsToHide: number[] = labelsToHide.map(({id}) => id)
|
|
||||||
|
|
||||||
return search(query)
|
|
||||||
?.filter(value => !labelIdsToHide.includes(value))
|
|
||||||
.map(id => state.labels[id])
|
|
||||||
|| []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the labels by id if found
|
|
||||||
* @param {Object} state
|
|
||||||
* @param {Array} ids
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
export function getLabelsByIds(state: LabelState, ids: ILabel['id'][]) {
|
|
||||||
return Object.values(state.labels).filter(({id}) => ids.includes(id))
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ import {createApp} from 'vue'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
import {error, success} from './message'
|
import {error, success} from './message'
|
||||||
|
|
||||||
|
@ -104,6 +105,9 @@ if (window.SENTRY_ENABLED) {
|
||||||
import('./sentry').then(sentry => sentry.default(app, router))
|
import('./sentry').then(sentry => sentry.default(app, router))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
|
||||||
app.use(store, key) // pass the injection key
|
app.use(store, key) // pass the injection key
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { ActionContext } from 'vuex'
|
import type { ActionContext } from 'vuex'
|
||||||
|
import type { StoreDefinition } from 'pinia'
|
||||||
import {LOADING, LOADING_MODULE} from './mutation-types'
|
import {LOADING, LOADING_MODULE} from './mutation-types'
|
||||||
import type { RootStoreState } from './types'
|
import type { RootStoreState } from './types'
|
||||||
|
|
||||||
|
@ -32,3 +33,21 @@ export function setLoading<State>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const setLoadingPinia = (store: StoreDefinition, loadFunc : ((isLoading: boolean) => void) | null = null) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (loadFunc === null) {
|
||||||
|
store.isLoading = true
|
||||||
|
} else {
|
||||||
|
loadFunc(true)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
if (loadFunc === null) {
|
||||||
|
store.isLoading = false
|
||||||
|
} else {
|
||||||
|
loadFunc(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,6 @@ import kanban from './modules/kanban'
|
||||||
import tasks from './modules/tasks'
|
import tasks from './modules/tasks'
|
||||||
import lists from './modules/lists'
|
import lists from './modules/lists'
|
||||||
import attachments from './modules/attachments'
|
import attachments from './modules/attachments'
|
||||||
import labels from './modules/labels'
|
|
||||||
|
|
||||||
import ListModel from '@/models/list'
|
import ListModel from '@/models/list'
|
||||||
|
|
||||||
|
@ -46,7 +45,6 @@ export const store = createStore<RootStoreState>({
|
||||||
tasks,
|
tasks,
|
||||||
lists,
|
lists,
|
||||||
attachments,
|
attachments,
|
||||||
labels,
|
|
||||||
},
|
},
|
||||||
state: () => ({
|
state: () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
import type { Module } from 'vuex'
|
|
||||||
|
|
||||||
import {i18n} from '@/i18n'
|
|
||||||
import {success} from '@/message'
|
|
||||||
import LabelService from '@/services/label'
|
|
||||||
import {setLoading} from '@/store/helper'
|
|
||||||
import type { LabelState, RootStoreState } from '@/store/types'
|
|
||||||
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
|
|
||||||
import {createNewIndexer} from '@/indexes'
|
|
||||||
import type { ILabel } from '@/modelTypes/ILabel'
|
|
||||||
|
|
||||||
const {add, remove, update} = createNewIndexer('labels', ['title', 'description'])
|
|
||||||
|
|
||||||
async function getAllLabels(page = 1): Promise<ILabel[]> {
|
|
||||||
const labelService = new LabelService()
|
|
||||||
const labels = await labelService.getAll({}, {}, page) as ILabel[]
|
|
||||||
if (page < labelService.totalPages) {
|
|
||||||
const nextLabels = await getAllLabels(page + 1)
|
|
||||||
return labels.concat(nextLabels)
|
|
||||||
} else {
|
|
||||||
return labels
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const LabelStore : Module<LabelState, RootStoreState> = {
|
|
||||||
namespaced: true,
|
|
||||||
state: () => ({
|
|
||||||
labels: {},
|
|
||||||
loaded: false,
|
|
||||||
}),
|
|
||||||
mutations: {
|
|
||||||
setLabels(state, labels: ILabel[]) {
|
|
||||||
labels.forEach(l => {
|
|
||||||
state.labels[l.id] = l
|
|
||||||
add(l)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setLabel(state, label: ILabel) {
|
|
||||||
state.labels[label.id] = label
|
|
||||||
update(label)
|
|
||||||
},
|
|
||||||
removeLabelById(state, label: ILabel) {
|
|
||||||
remove(label)
|
|
||||||
delete state.labels[label.id]
|
|
||||||
},
|
|
||||||
setLoaded(state, loaded: boolean) {
|
|
||||||
state.loaded = loaded
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
getLabelsByIds(state) {
|
|
||||||
return (ids: ILabel['id'][]) => getLabelsByIds(state, ids)
|
|
||||||
},
|
|
||||||
filterLabelsByQuery(state) {
|
|
||||||
return (labelsToHide: ILabel[], query: string) => filterLabelsByQuery(state, labelsToHide, query)
|
|
||||||
},
|
|
||||||
getLabelsByExactTitles(state) {
|
|
||||||
return labelTitles => Object
|
|
||||||
.values(state.labels)
|
|
||||||
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
async loadAllLabels(ctx, {forceLoad} = {}) {
|
|
||||||
if (ctx.state.loaded && !forceLoad) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancel = setLoading(ctx, 'labels')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const labels = await getAllLabels()
|
|
||||||
ctx.commit('setLabels', labels)
|
|
||||||
ctx.commit('setLoaded', true)
|
|
||||||
return labels
|
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteLabel(ctx, label: ILabel) {
|
|
||||||
const cancel = setLoading(ctx, 'labels')
|
|
||||||
const labelService = new LabelService()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await labelService.delete(label)
|
|
||||||
ctx.commit('removeLabelById', label)
|
|
||||||
success({message: i18n.global.t('label.deleteSuccess')})
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async updateLabel(ctx, label: ILabel) {
|
|
||||||
const cancel = setLoading(ctx, 'labels')
|
|
||||||
const labelService = new LabelService()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newLabel = await labelService.update(label)
|
|
||||||
ctx.commit('setLabel', newLabel)
|
|
||||||
success({message: i18n.global.t('label.edit.success')})
|
|
||||||
return newLabel
|
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async createLabel(ctx, label: ILabel) {
|
|
||||||
const cancel = setLoading(ctx, 'labels')
|
|
||||||
const labelService = new LabelService()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newLabel = await labelService.create(label)
|
|
||||||
ctx.commit('setLabel', newLabel)
|
|
||||||
return newLabel
|
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LabelStore
|
|
|
@ -25,6 +25,7 @@ import type { IAttachment } from '@/modelTypes/IAttachment'
|
||||||
import type { IList } from '@/modelTypes/IList'
|
import type { IList } from '@/modelTypes/IList'
|
||||||
|
|
||||||
import type { RootStoreState, TaskState } from '@/store/types'
|
import type { RootStoreState, TaskState } from '@/store/types'
|
||||||
|
import { useLabelStore } from '@/stores/labels'
|
||||||
|
|
||||||
// IDEA: maybe use a small fuzzy search here to prevent errors
|
// IDEA: maybe use a small fuzzy search here to prevent errors
|
||||||
function findPropertyByValue(object, key, value) {
|
function findPropertyByValue(object, key, value) {
|
||||||
|
@ -268,22 +269,19 @@ const tasksStore : Module<TaskState, RootStoreState>= {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Do everything that is involved in finding, creating and adding the label to the task
|
// Do everything that is involved in finding, creating and adding the label to the task
|
||||||
async addLabelsToTask({rootState, dispatch}, {
|
async addLabelsToTask(_, { task, parsedLabels }) {
|
||||||
task,
|
|
||||||
parsedLabels,
|
|
||||||
}) {
|
|
||||||
if (parsedLabels.length <= 0) {
|
if (parsedLabels.length <= 0) {
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
const {labels} = rootState.labels
|
const labelStore = useLabelStore()
|
||||||
|
|
||||||
const labelAddsToWaitFor = parsedLabels.map(async labelTitle => {
|
const labelAddsToWaitFor = parsedLabels.map(async labelTitle => {
|
||||||
let label = validateLabel(labels, labelTitle)
|
let label = validateLabel(labelStore.labels, labelTitle)
|
||||||
if (typeof label === 'undefined') {
|
if (typeof label === 'undefined') {
|
||||||
// label not found, create it
|
// label not found, create it
|
||||||
const labelModel = new LabelModel({title: labelTitle})
|
const labelModel = new LabelModel({title: labelTitle})
|
||||||
label = await dispatch('labels/createLabel', labelModel, {root: true})
|
label = await labelStore.createLabel(labelModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return addLabelToTask(task, label)
|
return addLabelToTask(task, label)
|
||||||
|
|
|
@ -93,7 +93,7 @@ export interface LabelState {
|
||||||
labels: {
|
labels: {
|
||||||
[id: ILabel['id']]: ILabel
|
[id: ILabel['id']]: ILabel
|
||||||
},
|
},
|
||||||
loaded: boolean,
|
isLoading: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListState {
|
export interface ListState {
|
||||||
|
|
55
src/stores/labels.test.ts
Normal file
55
src/stores/labels.test.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import {setActivePinia, createPinia} from 'pinia'
|
||||||
|
import {describe, it, expect, beforeEach} from 'vitest'
|
||||||
|
|
||||||
|
import {useLabelStore} from './labels'
|
||||||
|
|
||||||
|
import type { ILabel } from '@/modelTypes/ILabel'
|
||||||
|
|
||||||
|
const MOCK_LABELS = {
|
||||||
|
1: {id: 1, title: 'label1'},
|
||||||
|
2: {id: 2, title: 'label2'},
|
||||||
|
3: {id: 3, title: 'label3'},
|
||||||
|
4: {id: 4, title: 'label4'},
|
||||||
|
5: {id: 5, title: 'label5'},
|
||||||
|
6: {id: 6, title: 'label6'},
|
||||||
|
7: {id: 7, title: 'label7'},
|
||||||
|
8: {id: 8, title: 'label8'},
|
||||||
|
9: {id: 9, title: 'label9'},
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupStore() {
|
||||||
|
const store = useLabelStore()
|
||||||
|
store.setLabels(Object.values(MOCK_LABELS) as ILabel[])
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('filter labels', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// creates a fresh pinia and make it active so it's automatically picked
|
||||||
|
// up by any useStore() call without having to pass it to it:
|
||||||
|
// `useStore(pinia)`
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty array for an empty query', () => {
|
||||||
|
const store = setupStore()
|
||||||
|
const labels = store.filterLabelsByQuery([], '')
|
||||||
|
|
||||||
|
expect(labels).toHaveLength(0)
|
||||||
|
})
|
||||||
|
it('should return labels for a query', () => {
|
||||||
|
const store = setupStore()
|
||||||
|
const labels = store.filterLabelsByQuery([], 'label2')
|
||||||
|
|
||||||
|
expect(labels).toHaveLength(1)
|
||||||
|
expect(labels[0].title).toBe('label2')
|
||||||
|
})
|
||||||
|
it('should not return found but hidden labels', () => {
|
||||||
|
const store = setupStore()
|
||||||
|
|
||||||
|
const labelsToHide = [{id: 1, title: 'label1'}] as ILabel[]
|
||||||
|
const labels = store.filterLabelsByQuery(labelsToHide, 'label1')
|
||||||
|
|
||||||
|
expect(labels).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
136
src/stores/labels.ts
Normal file
136
src/stores/labels.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
import LabelService from '@/services/label'
|
||||||
|
import {success} from '@/message'
|
||||||
|
import {i18n} from '@/i18n'
|
||||||
|
import {createNewIndexer} from '@/indexes'
|
||||||
|
import {setLoadingPinia} from '@/store/helper'
|
||||||
|
import type {ILabel} from '@/modelTypes/ILabel'
|
||||||
|
|
||||||
|
const {add, remove, update, search} = createNewIndexer('labels', ['title', 'description'])
|
||||||
|
|
||||||
|
async function getAllLabels(page = 1): Promise<ILabel[]> {
|
||||||
|
const labelService = new LabelService()
|
||||||
|
const labels = await labelService.getAll({}, {}, page) as ILabel[]
|
||||||
|
if (page < labelService.totalPages) {
|
||||||
|
const nextLabels = await getAllLabels(page + 1)
|
||||||
|
return labels.concat(nextLabels)
|
||||||
|
} else {
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import type {LabelState} from '@/store/types'
|
||||||
|
|
||||||
|
export const useLabelStore = defineStore('label', {
|
||||||
|
state: () : LabelState => ({
|
||||||
|
// The labels are stored as an object which has the label ids as keys.
|
||||||
|
labels: {},
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
getLabelsByIds(state) {
|
||||||
|
return (ids: ILabel['id'][]) => Object.values(state.labels).filter(({id}) => ids.includes(id))
|
||||||
|
},
|
||||||
|
// **
|
||||||
|
// * Checks if a list of labels is available in the store and filters them then query
|
||||||
|
// **
|
||||||
|
filterLabelsByQuery(state) {
|
||||||
|
return (labelsToHide: ILabel[], query: string) => {
|
||||||
|
const labelIdsToHide: number[] = labelsToHide.map(({id}) => id)
|
||||||
|
|
||||||
|
return search(query)
|
||||||
|
?.filter(value => !labelIdsToHide.includes(value))
|
||||||
|
.map(id => state.labels[id])
|
||||||
|
|| []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getLabelsByExactTitles(state) {
|
||||||
|
return (labelTitles: string[]) => Object
|
||||||
|
.values(state.labels)
|
||||||
|
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
setIsLoading(isLoading: boolean) {
|
||||||
|
this.isLoading = isLoading
|
||||||
|
},
|
||||||
|
|
||||||
|
setLabels(labels: ILabel[]) {
|
||||||
|
labels.forEach(l => {
|
||||||
|
this.labels[l.id] = l
|
||||||
|
add(l)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
setLabel(label: ILabel) {
|
||||||
|
this.labels[label.id] = label
|
||||||
|
update(label)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeLabelById(label: ILabel) {
|
||||||
|
remove(label)
|
||||||
|
delete this.labels[label.id]
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadAllLabels({forceLoad} : {forceLoad?: boolean} = {}) {
|
||||||
|
if (this.isLoading && !forceLoad) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = setLoadingPinia(useLabelStore)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const labels = await getAllLabels()
|
||||||
|
this.setLabels(labels)
|
||||||
|
this.setIsLoading(true)
|
||||||
|
return labels
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteLabel(label: ILabel) {
|
||||||
|
const cancel = setLoadingPinia(useLabelStore)
|
||||||
|
const labelService = new LabelService()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await labelService.delete(label)
|
||||||
|
this.removeLabelById(label)
|
||||||
|
success({message: i18n.global.t('label.deleteSuccess')})
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateLabel(label: ILabel) {
|
||||||
|
const cancel = setLoadingPinia(useLabelStore)
|
||||||
|
const labelService = new LabelService()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newLabel = await labelService.update(label)
|
||||||
|
this.setLabel(newLabel)
|
||||||
|
success({message: i18n.global.t('label.edit.success')})
|
||||||
|
return newLabel
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createLabel(label: ILabel) {
|
||||||
|
const cancel = setLoadingPinia(useLabelStore)
|
||||||
|
const labelService = new LabelService()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newLabel = await labelService.create(label)
|
||||||
|
this.setLabel(newLabel)
|
||||||
|
return newLabel
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -111,11 +111,12 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent} from 'vue'
|
import {defineComponent} from 'vue'
|
||||||
import {mapState} from 'vuex'
|
import {mapState as mapVuexState} from 'vuex'
|
||||||
|
import {mapState} from 'pinia'
|
||||||
|
|
||||||
import LabelModel from '@/models/label'
|
import LabelModel from '../../models/label'
|
||||||
import type {ILabel} from '@/modelTypes/ILabel'
|
import type {ILabel} from '@/modelTypes/ILabel'
|
||||||
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
|
import {useLabelStore} from '@/stores/labels'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import AsyncEditor from '@/components/input/AsyncEditor'
|
import AsyncEditor from '@/components/input/AsyncEditor'
|
||||||
|
@ -139,25 +140,32 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$store.dispatch('labels/loadAllLabels')
|
const labelStore = useLabelStore()
|
||||||
|
labelStore.loadAllLabels()
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
setTitle(this.$t('label.title'))
|
setTitle(this.$t('label.title'))
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: {
|
||||||
userInfo: state => state.auth.info,
|
...mapVuexState({
|
||||||
// Alphabetically sort the labels
|
userInfo: state => state.auth.info,
|
||||||
labels: state => Object.values(state.labels.labels).sort((f, s) => f.title > s.title ? 1 : -1),
|
}),
|
||||||
loading: state => state[LOADING] && state[LOADING_MODULE] === 'labels',
|
...mapState(useLabelStore, {
|
||||||
}),
|
// Alphabetically sort the labels
|
||||||
|
labels: state => Object.values(state.labels).sort((f, s) => f.title > s.title ? 1 : -1),
|
||||||
|
loading: state => state.isLoading,
|
||||||
|
}),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
deleteLabel(label: ILabel) {
|
deleteLabel(label: ILabel) {
|
||||||
this.showDeleteModal = false
|
this.showDeleteModal = false
|
||||||
this.isLabelEdit = false
|
this.isLabelEdit = false
|
||||||
return this.$store.dispatch('labels/deleteLabel', label)
|
const labelStore = useLabelStore()
|
||||||
|
return labelStore.deleteLabel(label)
|
||||||
},
|
},
|
||||||
editLabelSubmit() {
|
editLabelSubmit() {
|
||||||
return this.$store.dispatch('labels/updateLabel', this.labelEditLabel)
|
const labelStore = useLabelStore()
|
||||||
|
return labelStore.updateLabel(this.labelEditLabel)
|
||||||
},
|
},
|
||||||
editLabel(label: ILabel) {
|
editLabel(label: ILabel) {
|
||||||
if (label.createdBy.id !== this.userInfo.id) {
|
if (label.createdBy.id !== this.userInfo.id) {
|
||||||
|
|
|
@ -36,12 +36,13 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent} from 'vue'
|
import {defineComponent} from 'vue'
|
||||||
|
import {mapState} from 'pinia'
|
||||||
|
|
||||||
import LabelModel from '../../models/label'
|
import LabelModel from '../../models/label'
|
||||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||||
import ColorPicker from '../../components/input/colorPicker.vue'
|
import ColorPicker from '../../components/input/colorPicker.vue'
|
||||||
import {mapState} from 'vuex'
|
|
||||||
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
|
|
||||||
import { setTitle } from '@/helpers/setTitle'
|
import { setTitle } from '@/helpers/setTitle'
|
||||||
|
import { useLabelStore } from '@/stores/labels'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'NewLabel',
|
name: 'NewLabel',
|
||||||
|
@ -58,9 +59,11 @@ export default defineComponent({
|
||||||
mounted() {
|
mounted() {
|
||||||
setTitle(this.$t('label.create.title'))
|
setTitle(this.$t('label.create.title'))
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: {
|
||||||
loading: state => state[LOADING] && state[LOADING_MODULE] === 'labels',
|
...mapState(useLabelStore, {
|
||||||
}),
|
loading: state => state.isLoading,
|
||||||
|
}),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async newLabel() {
|
async newLabel() {
|
||||||
if (this.label.title === '') {
|
if (this.label.title === '') {
|
||||||
|
@ -69,7 +72,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
this.showError = false
|
this.showError = false
|
||||||
|
|
||||||
const label = this.$store.dispatch('labels/createLabel', this.label)
|
const labelStore = useLabelStore()
|
||||||
|
const label = labelStore.createLabel(this.label)
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'labels.index',
|
name: 'labels.index',
|
||||||
params: {id: label.id},
|
params: {id: label.id},
|
||||||
|
|
|
@ -10257,6 +10257,14 @@ pify@^4.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
|
||||||
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
|
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
|
||||||
|
|
||||||
|
pinia@^2.0.21:
|
||||||
|
version "2.0.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.21.tgz#2a6599ad3736fa71866f4b053ffb0073cd482270"
|
||||||
|
integrity sha512-6ol04PtL29O0Z6JHI47O3JUSoyOJ7Og0rstXrHVMZSP4zAldsQBXJCNF0i/H7m8vp/Hjd/CSmuPl7C5QAwpeWQ==
|
||||||
|
dependencies:
|
||||||
|
"@vue/devtools-api" "^6.2.1"
|
||||||
|
vue-demi "*"
|
||||||
|
|
||||||
pinkie-promise@^2.0.0:
|
pinkie-promise@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
|
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
|
||||||
|
|
Loading…
Reference in a new issue