Skip to content
Snippets Groups Projects
Commit 7182db4b authored by Theo Lesage's avatar Theo Lesage Committed by ml bonhomme
Browse files

Migrate repository module to Pinia

parent d09102f6
No related branches found
No related tags found
1 merge request!1664Migrate repository module to Pinia
......@@ -40,25 +40,29 @@
</Modal>
</template>
<script>
import { mapMutations, mapActions } from 'vuex'
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { mapActions } from 'pinia'
import { errorParser } from '@/helpers'
import { useRepositoryStore, useNotificationStore } from '@/stores'
import { Repository } from '@/types/worker'
import Modal from '@/components/Modal.vue'
export default {
export default defineComponent({
components: {
Modal
},
emits: [
'update:modal',
'delete'
],
emits: {
'update:modal': (value: boolean) => typeof value === 'boolean',
delete: (value: boolean) => typeof value === 'boolean'
},
props: {
modal: {
type: Boolean,
required: true
},
repo: {
type: Object,
type: Object as PropType<Repository>,
required: true
}
},
......@@ -66,14 +70,14 @@ export default {
loading: false
}),
methods: {
...mapActions('repos', ['delete']),
...mapMutations('notifications', ['notify']),
...mapActions(useRepositoryStore, ['deleteRepository']),
...mapActions(useNotificationStore, ['notify']),
async remove () {
this.loading = true
try {
await this.delete(this.repo.id)
await this.deleteRepository(this.repo.id)
// Redirect the user to the repositories list
this.$emit('delete')
this.$emit('delete', true)
} catch (err) {
this.notify({ type: 'error', text: `An error occurred deleting the repository: ${errorParser(err)}` })
} finally {
......@@ -87,7 +91,7 @@ export default {
return this.repo.workers.map(w => w.name).join(', ')
}
}
}
})
</script>
<style scoped>
......
......@@ -34,18 +34,25 @@
</tr>
</template>
<script>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { mapGetters } from 'vuex'
export default {
emits: ['remove'],
import { Repository } from '@/types/worker'
export default defineComponent({
emits: {
remove (value: Repository) {
return value.id !== undefined
}
},
props: {
repo: {
type: Object,
type: Object as PropType<Repository>,
required: true
}
},
computed: {
...mapGetters('auth', ['isVerified', 'hasFeature'])
}
}
})
</script>
......@@ -13,7 +13,8 @@ import {
usePonosStore,
useRightsStore,
useSearchStore,
useWorkerStore
useWorkerStore,
useRepositoryStore
} from '@/stores'
/**
......@@ -30,7 +31,6 @@ const moduleNames = [
'navigation',
'notifications',
'process',
'repos',
'selection',
'tree'
]
......@@ -52,7 +52,8 @@ export const piniaStores = [
usePonosStore,
useRightsStore,
useSearchStore,
useWorkerStore
useWorkerStore,
useRepositoryStore
]
export const actions = {
......
import { assign } from 'lodash'
import * as api from '@/api'
import { useWorkerStore } from '@/stores'
export const initialState = () => ({
// { [repoId]: repo }
repositories: {}
})
export const mutations = {
setRepos (state, repos) {
state.repositories = {
...state.repositories,
...Object.fromEntries(repos.map(repo => [repo.id, repo]))
}
},
removeRepo (state, id) {
delete state.repositories[id]
},
reset (state) {
assign(state, initialState())
}
}
export const actions = {
async retrieve ({ commit }, id) {
const repo = await api.retrieveRepository(id)
commit('setRepos', [repo])
},
async list ({ commit }, { page = 1 }) {
// List repositories imported on Arkindex
const data = await api.listRepositories({ page })
commit('setRepos', data.results)
// Set workers in the appropriate store to retrieve them following a link later
const workerStore = useWorkerStore()
const workers = data.results.flatMap(({ workers }) => workers)
workerStore.workers = {
...workerStore.workers,
...Object.fromEntries(workers.map(worker => [worker.id, worker]))
}
return data
},
async delete ({ commit }, id) {
const data = await api.deleteRepository(id)
commit('removeRepo', id)
return data
}
}
export default {
namespaced: true,
state: initialState(),
mutations,
actions
}
......@@ -15,3 +15,4 @@ export { useDatasetStore } from './dataset'
export { useExportStore } from './exports'
export { useEntityStore } from './entity'
export { useTranscriptionStore } from './transcription'
export { useRepositoryStore } from './repos'
import { defineStore } from 'pinia'
import * as api from '@/api'
import { useWorkerStore } from '@/stores'
import { UUID } from '@/types'
import { Repository, WorkerLight } from '@/types/worker'
interface State {
/**
* Repository details mapped by ID
*/
repositories: {
[repositoryId: UUID]: Repository
}
}
export const useRepositoryStore = defineStore('repos', {
state: (): State => ({
repositories: {}
}),
actions: {
async retrieveRepository (id: UUID) {
const repo = await api.retrieveRepository(id)
this.repositories[repo.id] = repo
},
async listRepository (page = 1) {
// List repositories imported on Arkindex
const data = await api.listRepositories({ page })
this.repositories = {
...this.repositories,
...Object.fromEntries(data.results.map((repo: Repository) => [repo.id, repo]))
}
// Set workers in the appropriate store to retrieve them following a link later
const workerStore = useWorkerStore()
const workers = data.results.flatMap(({ workers }) => workers)
workerStore.workers = {
...workerStore.workers,
...Object.fromEntries(workers.map((worker: WorkerLight) => [worker.id, worker]))
}
return data
},
async deleteRepository (id: UUID) {
const data = await api.deleteRepository(id)
delete this.repositories[id]
return data
}
}
})
......@@ -154,14 +154,13 @@ import { isEmpty } from 'lodash'
import { mapState, mapActions } from 'pinia'
import MarkdownIt from 'markdown-it'
import {
mapState as mapVuexState,
mapGetters as mapVuexGetters
} from 'vuex'
import { errorParser } from '@/helpers'
import { truncateMixin } from '@/mixins'
import { useWorkerStore, useNotificationStore } from '@/stores'
import { useWorkerStore, useNotificationStore, useRepositoryStore } from '@/stores'
import VersionList from '@/components/Process/Workers/Versions/List'
import Paginator from '@/components/Paginator.vue'
import ListMembers from '@/components/Memberships/ListMembers.vue'
......@@ -234,8 +233,8 @@ export default {
expandDescription: false
}),
computed: {
...mapVuexState('repos', ['reposPage']),
...mapState(useWorkerStore, ['workerTypes']),
...mapState(useRepositoryStore, ['repos']),
...mapVuexGetters('auth', ['hasFeature']),
readMoreText () {
if (this.expandDescription) return 'collapse description'
......
......@@ -21,7 +21,7 @@
v-for="repo in results"
:key="repo.id"
:repo="repo"
v-on:remove="repo => repoDeletion = repo"
v-on:remove="repoDeletion = repo"
/>
</tbody>
</table>
......@@ -35,13 +35,19 @@
</main>
</template>
<script>
import { mapActions, mapState, mapGetters, mapMutations } from 'vuex'
import Paginator from '@/components/Paginator.vue'
import Row from '@/components/Repos/Row'
import DeleteModal from '@/components/Repos/DeleteModal'
<script lang="ts">
import { defineComponent } from 'vue'
import { useRepositoryStore, useNotificationStore } from '@/stores'
import { mapActions, mapState } from 'pinia'
import { mapGetters } from 'vuex'
import { errorParser } from '@/helpers'
export default {
import Paginator from '@/components/Paginator.vue'
import Row from '@/components/Repos/Row.vue'
import DeleteModal from '@/components/Repos/DeleteModal.vue'
import { PageNumberPagination } from '@/types'
import { Repository } from '@/types/worker'
const Component = defineComponent({
components: {
Paginator,
Row,
......@@ -50,27 +56,31 @@ export default {
data: () => ({
loading: false,
// Stores a repository to delete. Automatically prompt the deletion modal
repoDeletion: null,
repoDeletion: null as Repository | null,
deleteModal: false,
reposPage: null
reposPage: null as PageNumberPagination<Repository> | null
}),
beforeRouteEnter (to, from, next) {
next(vm => vm.updatePage(to.query.page))
beforeRouteEnter ({ query }, from, next) {
/*
* In order to not get typing error related to 'vm', we name the component which allos us to tell TypeScript that vm
* is of the same type as that component.
*/
next(vm => (vm as InstanceType<typeof Component>).updatePage(parseInt(`${query.page ?? 1}`)))
},
beforeRouteUpdate ({ query }) {
this.updatePage(query.page ?? 1)
this.updatePage(parseInt(`${query.page ?? 1}`))
},
computed: {
...mapGetters('auth', ['isVerified', 'hasFeature']),
...mapState('repos', ['repositories'])
...mapState(useRepositoryStore, ['repositories'])
},
methods: {
...mapMutations('notifications', ['notify']),
...mapActions('repos', ['list']),
async updatePage (page) {
...mapActions(useNotificationStore, ['notify']),
...mapActions(useRepositoryStore, ['listRepository']),
async updatePage (page: number) {
this.loading = true
try {
this.reposPage = await this.list({ page })
this.reposPage = await this.listRepository(page)
} catch (err) {
this.notify({ type: 'error', text: `An error occurred listing repositories: ${errorParser(err)}` })
} finally {
......@@ -80,7 +90,7 @@ export default {
handleDeletion () {
// Reload repositories page
this.repoDeletion = null
this.updatePage(this.$route.query.page)
this.updatePage(parseInt(`${this.$route.query.page ?? 1}`))
}
},
watch: {
......@@ -91,5 +101,7 @@ export default {
if (!open) this.repoDeletion = null
}
}
}
})
export default Component
</script>
......@@ -29,19 +29,24 @@
</main>
</template>
<script>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { UUID } from '@/types'
import { useNotificationStore, useRepositoryStore } from '@/stores'
import { mapState, mapActions } from 'pinia'
import { errorParser } from '@/helpers'
import { mapState, mapActions, mapMutations } from 'vuex'
import ListMembers from '@/components/Memberships/ListMembers.vue'
import DeleteModal from '@/components/Repos/DeleteModal'
export default {
import DeleteModal from '@/components/Repos/DeleteModal.vue'
import { Repository } from '@/types/worker'
export default defineComponent({
components: {
ListMembers,
DeleteModal
},
props: {
repoId: {
type: String,
type: String as PropType<UUID>,
required: true
}
},
......@@ -53,18 +58,18 @@ export default {
if (!this.repo) this.load()
},
computed: {
...mapState('repos', ['repositories']),
repo () {
...mapState(useRepositoryStore, ['repositories']),
repo (): Repository {
return this.repositories[this.repoId]
}
},
methods: {
...mapActions('repos', ['retrieve']),
...mapMutations('notifications', ['notify']),
...mapActions(useRepositoryStore, ['retrieveRepository']),
...mapActions(useNotificationStore, ['notify']),
async load () {
try {
this.loading = true
await this.retrieve(this.repoId)
await this.retrieveRepository(this.repoId)
} catch (err) {
this.notify({ type: 'error', text: `An error occurred retrieving the repository: ${errorParser(err)}` })
} finally {
......@@ -76,5 +81,5 @@ export default {
this.$router.replace({ name: 'repos-list' })
}
}
}
})
</script>
......@@ -120,7 +120,6 @@ describe('auth', () => {
{ mutation: 'navigation/reset' },
{ mutation: 'notifications/reset' },
{ mutation: 'process/reset' },
{ mutation: 'repos/reset' },
{ mutation: 'selection/reset' },
{ mutation: 'tree/reset' },
{ action: 'corpora/list', payload: null },
......@@ -151,7 +150,6 @@ describe('auth', () => {
{ mutation: 'navigation/reset' },
{ mutation: 'notifications/reset' },
{ mutation: 'process/reset' },
{ mutation: 'repos/reset' },
{ mutation: 'selection/reset' },
{ mutation: 'tree/reset' },
{ action: 'corpora/list', payload: null },
......@@ -180,7 +178,6 @@ describe('auth', () => {
{ mutation: 'navigation/reset' },
{ mutation: 'notifications/reset' },
{ mutation: 'process/reset' },
{ mutation: 'repos/reset' },
{ mutation: 'selection/reset' },
{ mutation: 'tree/reset' },
{ action: 'corpora/list', payload: null },
......@@ -211,7 +208,6 @@ describe('auth', () => {
{ mutation: 'navigation/reset' },
{ mutation: 'notifications/reset' },
{ mutation: 'process/reset' },
{ mutation: 'repos/reset' },
{ mutation: 'selection/reset' },
{ mutation: 'tree/reset' },
{ action: 'corpora/list', payload: null },
......@@ -238,7 +234,6 @@ describe('auth', () => {
{ mutation: 'navigation/reset' },
{ mutation: 'notifications/reset' },
{ mutation: 'process/reset' },
{ mutation: 'repos/reset' },
{ mutation: 'selection/reset' },
{ mutation: 'tree/reset' },
{ action: 'corpora/list', payload: null },
......
......@@ -46,7 +46,6 @@ describe('store', () => {
{ mutation: 'navigation/reset' },
{ mutation: 'notifications/reset' },
{ mutation: 'process/reset' },
{ mutation: 'repos/reset' },
{ mutation: 'selection/reset' },
{ mutation: 'tree/reset' }
])
......@@ -61,7 +60,6 @@ describe('store', () => {
require('./files.spec.js')
require('./navigation.spec.js')
require('./process.spec.js')
require('./repos.spec.js')
require('./selection.spec.js')
require('./tree.spec.js')
})
import { assert } from 'chai'
import axios from 'axios'
import { initialState, mutations } from '@/store/repos.js'
import store from './index.spec.js'
import { useWorkerStore } from '@/stores'
import { createPinia, setActivePinia } from 'pinia'
import { repoSample, reposSample } from '../samples.js'
import { FakeAxios } from '../testhelpers.js'
import { assert } from 'chai'
import { pick } from 'lodash'
import { setActivePinia, createPinia } from 'pinia'
import { useWorkerStore, useRepositoryStore } from '@/stores'
import { reposSample, repoSample } from '../samples'
import { FakeAxios } from '../testhelpers'
describe('repos', () => {
describe('mutations', () => {
it('setRepos', () => {
const state = initialState()
mutations.setRepos(state, [
repoSample,
{ id: 'second_repo', name: 'repo2' }
])
assert.deepStrictEqual(state.repositories, {
repoid: repoSample,
second_repo: { id: 'second_repo', name: 'repo2' }
})
})
it('removeRepo', () => {
const state = {
repositories: { repoid: repoSample }
}
mutations.removeRepo(state, 'repoid')
assert.deepStrictEqual(state.repositories, {})
})
it('reset', () => {
const state = {
repositories: { repoid: repoSample }
}
mutations.reset(state)
assert.deepStrictEqual(state, initialState())
})
})
describe('actions', () => {
let mock, workerStore
let mock, store, workerStore
before('Setting up Axios mock', () => {
mock = new FakeAxios(axios)
setActivePinia(createPinia())
store = useRepositoryStore()
workerStore = useWorkerStore()
})
afterEach(() => {
// Remove any handlers, but leave mocking in place
mock.reset()
store.reset()
store.$reset()
workerStore.$reset()
})
......@@ -59,7 +29,7 @@ describe('repos', () => {
mock.restore()
})
describe('list', () => {
describe('listRepository', () => {
it('lists existing repositories', async () => {
const sample = {
...reposSample,
......@@ -78,37 +48,45 @@ describe('repos', () => {
}
]
}
mock.onGet('/process/repos/').reply(200, sample)
const page = await store.dispatch('repos/list', {})
mock.onGet('/process/repos/').reply(200, sample)
assert.deepStrictEqual(store.history, [
{ action: 'repos/list', payload: {} },
{ mutation: 'repos/setRepos', payload: sample.results }
const page = await store.listRepository()
assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
{
method: 'get',
url: '/process/repos/'
}
])
assert.deepStrictEqual(page, sample)
assert.deepStrictEqual(Object.keys(workerStore.workers), ['worker 1', 'worker 2', 'worker 3'])
})
})
describe('delete', () => {
describe('deleteRepository', () => {
it('deletes a repo', async () => {
mock.onDelete('/process/repos/repoid/').reply(204)
store.state.repositories = { repoid: {}, secondId: {} }
await store.dispatch('repos/delete', 'repoid')
store.repositories = { repoid: {}, secondId: {} }
await store.deleteRepository('repoid')
assert.deepStrictEqual(store.history, [
assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
{
action: 'repos/delete',
payload: 'repoid'
},
{
mutation: 'repos/removeRepo',
payload: 'repoid'
method: 'delete',
url: '/process/repos/repoid/'
}
])
})
})
describe('retrieveRepository', () => {
it('retrieves a repo', async () => {
mock.onGet('/process/repos/repoid/').reply(200, repoSample)
await store.retrieveRepository('repoid')
assert.deepStrictEqual(store.repositories, {
repoid: repoSample
})
})
})
})
})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment