feat: port label store to pinia | pinia 1/9 #2391

Merged
konrad merged 2 commits from dpschen/frontend:feature/feat-pinia-label-store into main 2022-09-21 14:24:00 +00:00
17 changed files with 276 additions and 235 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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()
dpschen marked this conversation as resolved
Review

Shouldn't this use the setup function? (not script setup).

Shouldn't this use the setup function? (not `script setup`).
Review

We could introduce a setup block in this component.
But since this pull request was only about having the fastest way to integrate the store I didn't see that necessary.

Setup would only require the additional import of computed. Plus we would need to export it again from that function.
So overall more complex then this quick fix here.

We could introduce a setup block in this component. But since this pull request was only about having the fastest way to integrate the store I didn't see that necessary. Setup would only require the additional import of `computed`. Plus we would need to export it again from that function. So overall more complex then this quick fix here.
Review

Makes sense, especially considering this would be redundant once we'll have everything migrated over to script setup.

Makes sense, especially considering this would be redundant once we'll have everything migrated over to `script setup`.
Review

Exactly :)

Exactly :)
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.

View File

@ -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')})

View File

@ -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)
})
})

View File

@ -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))
}

View File

@ -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)

View File

@ -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'
@ -31,4 +32,22 @@ export function setLoading<State>(
loadFunc(false) loadFunc(false)
} }
} }
}
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)
}
}
} }

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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
View 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
View 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()
}
},
},
})

View File

@ -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) {

View File

@ -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},

View File

@ -10240,6 +10240,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"