diff --git a/src/components/Dataset/RemoveModal.vue b/src/components/Dataset/RemoveModal.vue index 8e7c50b00b6a540e5606b97dc207bd06d45eb8d7..1ba157d8661525e148a20386c5e6e330d8559433 100644 --- a/src/components/Dataset/RemoveModal.vue +++ b/src/components/Dataset/RemoveModal.vue @@ -33,7 +33,7 @@ import Modal from '@/components/Modal.vue' import { mapActions, mapState } from 'pinia' import { useDatasetStore, useNotificationStore } from '@/stores' import { Dataset } from '@/types/dataset' -import { ElementList } from '@/types' +import { ElementList, Element } from '@/types' export default defineComponent({ components: { @@ -49,7 +49,7 @@ export default defineComponent({ props: { // The element to delete. element: { - type: Object as PropType<ElementList>, + type: Object as PropType<ElementList | Element>, required: true }, dataset: { @@ -66,7 +66,6 @@ export default defineComponent({ } }, data: () => ({ - opened: false, loading: false }), computed: { @@ -88,15 +87,12 @@ export default defineComponent({ ...mapActions(useDatasetStore, ['removeDatasetElement']), ...mapActions(useNotificationStore, ['notify']), updateModelValue (value: boolean) { this.$emit('update:modelValue', value) }, - open () { - this.opened = this.canRemove - }, async performRemove () { if (!this.canRemove) return this.loading = true try { await this.removeDatasetElement(this.dataset.id, this.element.id, this.set) - this.opened = false + this.updateModelValue(false) } finally { this.loading = false } diff --git a/src/components/Element/Datasets/ElementDatasets.vue b/src/components/Element/Datasets/ElementDatasets.vue new file mode 100644 index 0000000000000000000000000000000000000000..5dbdac806fdd5b6a3e253a75ed899e0e0aa92cb1 --- /dev/null +++ b/src/components/Element/Datasets/ElementDatasets.vue @@ -0,0 +1,54 @@ +<template> + <div class="loader m-auto is-size-3" v-if="datasets === null"></div> + <div class="message-body has-text-grey" v-else-if="datasets.length === 0"> + No datasets + </div> + <table v-else class="table is-fullwidth is-hoverable"> + <thead> + <th>Name</th> + <th>Set</th> + <th></th> + </thead> + <tbody> + <tr v-for="({ dataset, set }) in datasets" :key="dataset.id + set"> + <Row + :corpus-id="corpusId" + :dataset="dataset" + :set="set" + :element="element" + /> + </tr> + </tbody> + </table> +</template> + +<script lang="ts"> +import { defineComponent, PropType } from 'vue' +import { ElementDataset } from '@/types/dataset' +import { mapState } from 'pinia' +import { useElementsStore } from '@/stores' +import { Element } from '@/types' +import Row from './Row.vue' + +export default defineComponent({ + components: { + Row + }, + props: { + corpusId: { + type: String, + required: true + }, + element: { + type: Object as PropType<Element>, + required: true + } + }, + computed: { + ...mapState(useElementsStore, ['elementDatasets']), + datasets (): ElementDataset[] | null { + return this.elementDatasets?.[this.element.id] ?? null + } + } +}) +</script> diff --git a/src/components/Element/Datasets/Row.vue b/src/components/Element/Datasets/Row.vue new file mode 100644 index 0000000000000000000000000000000000000000..5391a3956f9f49229c8119adb786c670f4539a20 --- /dev/null +++ b/src/components/Element/Datasets/Row.vue @@ -0,0 +1,84 @@ +<template> + <td> + <router-link :to="{ name: 'dataset-details', params: { datasetId: dataset.id } }" title="Dataset details"> + {{ dataset.name }} + </router-link> + </td> + <td>{{ set }}</td> + <td> + <a + :title="title" + class="icon is-pulled-right" + v-on:click="removeElement" + > + <i class="icon-remove-square" :class="canRemove ? 'has-text-danger' : 'has-text-grey'" type="button"></i> + </a> + </td> + <RemoveModal + v-if="dataset && set" + v-model="removeElementModal" + :dataset="dataset" + :set="set" + :element="element" + /> +</template> + +<script lang="ts"> +import { corporaMixin } from '@/mixins' +import { defineComponent, PropType } from 'vue' +import { Dataset, ElementDataset } from '@/types/dataset' +import { mapState } from 'pinia' +import { useElementsStore } from '@/stores' +import RemoveModal from '@/components/Dataset/RemoveModal.vue' +import { Element } from '@/types' + +export default defineComponent({ + mixins: [ + corporaMixin + ], + components: { + RemoveModal + }, + props: { + corpusId: { + type: String, + required: true + }, + element: { + type: Object as PropType<Element>, + required: true + }, + dataset: { + type: Object as PropType<Dataset>, + required: true + }, + set: { + type: String, + required: true + } + }, + data: () => ({ + removeElementModal: false + }), + computed: { + ...mapState(useElementsStore, ['elementDatasets']), + datasets (): ElementDataset[] | null { + return this.elementDatasets?.[this.element.id] ?? null + }, + canRemove () { + return (this.dataset && this.dataset.state === 'open' && this.set && this.canWrite(this.corpus)) + }, + title () { + if (!this.canWrite(this.corpus)) return 'You do not have the required rights to edit this dataset' + else if (this.dataset.state !== 'open') return 'Only open datasets can be edited' + else return 'Remove element from this dataset' + } + }, + methods: { + removeElement () { + if (!this.canRemove) return + this.removeElementModal = true + } + } +}) +</script> diff --git a/src/components/Element/DeleteModal.vue b/src/components/Element/DeleteModal.vue index c3e34213bfe3763b10acaa85088beea761c5189f..c724126ad13ebd4fb293d2fd0bfd16e484fe2a17 100644 --- a/src/components/Element/DeleteModal.vue +++ b/src/components/Element/DeleteModal.vue @@ -9,7 +9,7 @@ <slot :open="open" :can-delete="canDelete"> <i class="icon-trash" - :class="canDelete ? 'has-text-danger' : 'has-text-gray'" + :class="canDelete ? 'has-text-danger' : 'has-text-grey'" v-on:click.prevent="open" ></i> </slot> diff --git a/src/components/Element/DetailsPanel.vue b/src/components/Element/DetailsPanel.vue index 56fc5542be37ef72352085d06f754f4691079e72..5da09f6974e79dfaaa34aae6f6edc47d1fe62dbe 100644 --- a/src/components/Element/DetailsPanel.vue +++ b/src/components/Element/DetailsPanel.vue @@ -64,30 +64,7 @@ <hr /> <DropdownContent id="datasets" title="Datasets"> - <div class="loader m-auto is-size-3" v-if="datasets === null"> - </div> - <div class="message-body has-text-grey" v-else-if="datasets.length === 0"> - No datasets - </div> - <table v-else class="table is-fullwidth is-hoverable"> - <thead> - <th>Name</th> - <th>Set</th> - </thead> - <tbody> - <tr v-for="({ dataset, set }) in datasets" :key="dataset.id + set"> - <td> - <router-link - :to="{ name: 'dataset-details', params: { datasetId: dataset.id } }" - title="Dataset details" - > - {{ dataset.name }} - </router-link> - </td> - <td>{{ set }}</td> - </tr> - </tbody> - </table> + <ElementDatasets :corpus-id="corpusId" :element="element" /> </DropdownContent> <EntityLinks @@ -121,6 +98,7 @@ import OrientationPanel from '@/components/Element/OrientationPanel.vue' import Transcriptions from '@/components/Element/Transcription' import TranscriptionsModal from '@/components/Element/Transcription/Modal.vue' import TranscriptionCreationForm from '@/components/Element/Transcription/CreationForm.vue' +import ElementDatasets from '@/components/Element/Datasets/ElementDatasets.vue' export default defineComponent({ mixins: [ @@ -135,7 +113,8 @@ export default defineComponent({ OrientationPanel, TranscriptionCreationForm, Transcriptions, - TranscriptionsModal + TranscriptionsModal, + ElementDatasets }, props: { /** diff --git a/src/stores/dataset.ts b/src/stores/dataset.ts index 9bf37836fdd2365327a774087fa37fb362e77a46..d9f3956380abc6820697a9d98a404f63af165cea 100644 --- a/src/stores/dataset.ts +++ b/src/stores/dataset.ts @@ -18,6 +18,7 @@ import { updateDataset } from '@/api' import { errorParser } from '@/helpers' +import { useElementsStore } from './elements' interface State { /** @@ -155,9 +156,17 @@ export const useDatasetStore = defineStore('dataset', { async removeDatasetElement (datasetId: UUID, elementId: UUID, set: string) { try { await deleteDatasetElement(datasetId, elementId, set) - const index = this.datasetElements[datasetId].findIndex(datasetElement => datasetElement.set === set && datasetElement.element.id === elementId) - if (index < 0) throw new Error(`Element ${elementId} not found in set ${set} of dataset ${datasetId}`) - this.datasetElements[datasetId].splice(index, 1) + if (this.datasetElements[datasetId]) { + const index = this.datasetElements[datasetId].findIndex(datasetElement => datasetElement.set === set && datasetElement.element.id === elementId) + if (index < 0) throw new Error(`Element ${elementId} not found in set ${set} of dataset ${datasetId}`) + this.datasetElements[datasetId].splice(index, 1) + } + const elementDatasets = useElementsStore().elementDatasets[elementId] + if (elementDatasets) { + const index = elementDatasets.findIndex(dataset => dataset.dataset.id === datasetId && dataset.set === set) + if (index < 0) throw new Error(`Dataset ${elementId} not found in set ${set} of dataset ${datasetId}`) + elementDatasets.splice(index, 1) + } } catch (err) { useNotificationStore().notify({ type: 'error', text: errorParser(err) }) throw err diff --git a/tests/unit/stores/datasets.spec.js b/tests/unit/stores/datasets.spec.js index f13163bfb16422b9e0f8a805bc8a730e90545e0c..ba705f99d99e0be9207b986c5673e68725d2c5b7 100644 --- a/tests/unit/stores/datasets.spec.js +++ b/tests/unit/stores/datasets.spec.js @@ -1,17 +1,18 @@ import { assert } from 'chai' import axios from 'axios' import { FakeAxios, assertRejects, setUpTestPinia, actionsCompletedPlugin } from '../testhelpers' -import { useDatasetStore, useNotificationStore } from '@/stores' +import { useDatasetStore, useElementsStore, useNotificationStore } from '@/stores' describe('datasets', () => { describe('actions', () => { - let mock, store, notificationStore + let mock, store, notificationStore, elementsStore before('Setting up mocks', () => { mock = new FakeAxios(axios) setUpTestPinia([actionsCompletedPlugin]) store = useDatasetStore() notificationStore = useNotificationStore() + elementsStore = useElementsStore() }) afterEach(() => { @@ -19,6 +20,7 @@ describe('datasets', () => { mock.reset() store.$reset() notificationStore.$reset() + elementsStore.$reset() }) after('Removing Axios mock', () => { @@ -690,6 +692,20 @@ describe('datasets', () => { store.datasetElements = { 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa': elements } + elementsStore.elementDatasets = { + 2: [ + { + set: 'unit-01', + dataset: { + id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + state: 'open', + name: 'Shin Seiki Evangelion', + description: 'Become legend, young boy!', + sets: ['unit-00', 'unit-01', 'unit-02'] + } + } + ] + } mock.onDelete('/datasets/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/elements/2/', 'unit-01').reply(204) @@ -738,6 +754,9 @@ describe('datasets', () => { } ] }) + assert.deepStrictEqual(elementsStore.elementDatasets, { + 2: [] + }) }) it('handles errors', async () => {