From 7182db4b1d8557b291d4d188552b54d0dd36d3b0 Mon Sep 17 00:00:00 2001 From: Theo Lesage <tlesage@teklia.com> Date: Mon, 8 Apr 2024 08:38:45 +0000 Subject: [PATCH] Migrate repository module to Pinia --- src/components/Repos/DeleteModal.vue | 30 ++++--- src/components/Repos/Row.vue | 17 ++-- src/store/index.js | 7 +- src/store/repos.js | 58 -------------- src/stores/index.ts | 1 + src/stores/repos.ts | 47 +++++++++++ src/views/Process/Workers/List.vue | 5 +- src/views/Repos/List.vue | 50 +++++++----- src/views/Repos/Rights.vue | 27 ++++--- tests/unit/store/auth.spec.js | 5 -- tests/unit/store/index.spec.js | 2 - tests/unit/store/repos.spec.js | 114 --------------------------- tests/unit/stores/repos.spec.js | 92 +++++++++++++++++++++ 13 files changed, 222 insertions(+), 233 deletions(-) delete mode 100644 src/store/repos.js create mode 100644 src/stores/repos.ts delete mode 100644 tests/unit/store/repos.spec.js create mode 100644 tests/unit/stores/repos.spec.js diff --git a/src/components/Repos/DeleteModal.vue b/src/components/Repos/DeleteModal.vue index f2b75622c..315275011 100644 --- a/src/components/Repos/DeleteModal.vue +++ b/src/components/Repos/DeleteModal.vue @@ -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> diff --git a/src/components/Repos/Row.vue b/src/components/Repos/Row.vue index c917408d7..4a3ca0c8b 100644 --- a/src/components/Repos/Row.vue +++ b/src/components/Repos/Row.vue @@ -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> diff --git a/src/store/index.js b/src/store/index.js index 6383f0c51..41c78a304 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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 = { diff --git a/src/store/repos.js b/src/store/repos.js deleted file mode 100644 index e24317eef..000000000 --- a/src/store/repos.js +++ /dev/null @@ -1,58 +0,0 @@ -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 -} diff --git a/src/stores/index.ts b/src/stores/index.ts index ec73e8171..c55f48605 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -15,3 +15,4 @@ export { useDatasetStore } from './dataset' export { useExportStore } from './exports' export { useEntityStore } from './entity' export { useTranscriptionStore } from './transcription' +export { useRepositoryStore } from './repos' diff --git a/src/stores/repos.ts b/src/stores/repos.ts new file mode 100644 index 000000000..d760b9253 --- /dev/null +++ b/src/stores/repos.ts @@ -0,0 +1,47 @@ +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 + } + } +}) diff --git a/src/views/Process/Workers/List.vue b/src/views/Process/Workers/List.vue index 9383bdf38..896b793c1 100644 --- a/src/views/Process/Workers/List.vue +++ b/src/views/Process/Workers/List.vue @@ -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' diff --git a/src/views/Repos/List.vue b/src/views/Repos/List.vue index 092dce9ff..1129e2eba 100644 --- a/src/views/Repos/List.vue +++ b/src/views/Repos/List.vue @@ -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> diff --git a/src/views/Repos/Rights.vue b/src/views/Repos/Rights.vue index 5ada8aa5a..447d4ed41 100644 --- a/src/views/Repos/Rights.vue +++ b/src/views/Repos/Rights.vue @@ -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> diff --git a/tests/unit/store/auth.spec.js b/tests/unit/store/auth.spec.js index be6ce1349..4216c5483 100644 --- a/tests/unit/store/auth.spec.js +++ b/tests/unit/store/auth.spec.js @@ -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 }, diff --git a/tests/unit/store/index.spec.js b/tests/unit/store/index.spec.js index 7c9fb8dfb..751572e8e 100644 --- a/tests/unit/store/index.spec.js +++ b/tests/unit/store/index.spec.js @@ -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') }) diff --git a/tests/unit/store/repos.spec.js b/tests/unit/store/repos.spec.js deleted file mode 100644 index 6061a4f01..000000000 --- a/tests/unit/store/repos.spec.js +++ /dev/null @@ -1,114 +0,0 @@ -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' - -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 - - before('Setting up Axios mock', () => { - mock = new FakeAxios(axios) - setActivePinia(createPinia()) - workerStore = useWorkerStore() - }) - - afterEach(() => { - // Remove any handlers, but leave mocking in place - mock.reset() - store.reset() - workerStore.$reset() - }) - - after('Removing Axios mock', () => { - // Remove mocking entirely - mock.restore() - }) - - describe('list', () => { - it('lists existing repositories', async () => { - const sample = { - ...reposSample, - count: 2, - results: [ - ...reposSample.results, - { - id: 'other_repo', - workers: [ - { - id: 'worker 2' - }, { - id: 'worker 3' - } - ] - } - ] - } - mock.onGet('/process/repos/').reply(200, sample) - - const page = await store.dispatch('repos/list', {}) - - assert.deepStrictEqual(store.history, [ - { action: 'repos/list', payload: {} }, - { mutation: 'repos/setRepos', payload: sample.results } - ]) - assert.deepStrictEqual(page, sample) - assert.deepStrictEqual(Object.keys(workerStore.workers), ['worker 1', 'worker 2', 'worker 3']) - }) - }) - - describe('delete', () => { - it('deletes a repo', async () => { - mock.onDelete('/process/repos/repoid/').reply(204) - - store.state.repositories = { repoid: {}, secondId: {} } - await store.dispatch('repos/delete', 'repoid') - - assert.deepStrictEqual(store.history, [ - { - action: 'repos/delete', - payload: 'repoid' - }, - { - mutation: 'repos/removeRepo', - payload: 'repoid' - } - ]) - }) - }) - }) -}) diff --git a/tests/unit/stores/repos.spec.js b/tests/unit/stores/repos.spec.js new file mode 100644 index 000000000..5b5cb0343 --- /dev/null +++ b/tests/unit/stores/repos.spec.js @@ -0,0 +1,92 @@ +import axios from 'axios' +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('actions', () => { + 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() + workerStore.$reset() + }) + + after('Removing Axios mock', () => { + // Remove mocking entirely + mock.restore() + }) + + describe('listRepository', () => { + it('lists existing repositories', async () => { + const sample = { + ...reposSample, + count: 2, + results: [ + ...reposSample.results, + { + id: 'other_repo', + workers: [ + { + id: 'worker 2' + }, { + id: 'worker 3' + } + ] + } + ] + } + + mock.onGet('/process/repos/').reply(200, sample) + + 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('deleteRepository', () => { + it('deletes a repo', async () => { + mock.onDelete('/process/repos/repoid/').reply(204) + + store.repositories = { repoid: {}, secondId: {} } + await store.deleteRepository('repoid') + + assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [ + { + 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 + }) + }) + }) + }) +}) -- GitLab