diff --git a/src/api/index.ts b/src/api/index.ts index 80464f1b86cc04529c05ce63cbb16c8046c1d362..50332ccf57db71b961f0e66751faee189da05a43 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -24,7 +24,8 @@ export * from './process' export * from './repository' export * from './rights' export * from './search' -export * from './selection' +export * from './selection.js' +export * from './selection.ts' export * from './transcription' export * from './transkribus' export * from './user' diff --git a/src/api/selection.ts b/src/api/selection.ts new file mode 100644 index 0000000000000000000000000000000000000000..551e8830ad84e793391e1c9b545746967b5ade2c --- /dev/null +++ b/src/api/selection.ts @@ -0,0 +1,20 @@ +import axios from 'axios' +import { unique } from '.' +import { UUID } from '@/types' + +interface CreateDatasetElementsSelectionParameters { + /** + * ID of the dataset that will receive selected elements + */ + dataset_id: UUID + + /** + * Name of the set elements will be added to + */ + set: string +} + +/** + * Add selected elements to a corpus' dataset + */ +export const createDatasetElementsSelection = unique(async (corpusId: UUID, params: CreateDatasetElementsSelectionParameters) => (await axios.post(`/corpus/${corpusId}/datasets/selection/`, params))) diff --git a/src/components/Navigation/CorpusSelection.vue b/src/components/Navigation/CorpusSelection.vue index 8a5e9bc23c628cf8a3a507c4153dc1beca620454..dd36d15b0b786dfccf8b35e2e302c19667c935c2 100644 --- a/src/components/Navigation/CorpusSelection.vue +++ b/src/components/Navigation/CorpusSelection.vue @@ -39,11 +39,20 @@ :disabled="!canCreate || null" class="dropdown-item" v-on:click="createParentModal = canCreate" - :title="canCreate ? 'Link selected elements to another parent folder.' : executeDisabledTitle" + :title="canCreate ? 'Link selected elements to another parent folder.' : createDisabledTitle" > <i class="icon-direction"></i> Link to another parent </a> + <a + :disabled="!canCreate || null" + class="dropdown-item" + v-on:click="datasetSelectionModal = canCreate" + :title="canCreate ? 'Add those elements to a dataset.' : createDisabledTitle" + > + <i class="icon-bookmark"></i> + Add to a dataset + </a> <a :disabled="!canCreate || null" class="dropdown-item" @@ -176,6 +185,8 @@ <DeleteResultsModal v-model="deleteResultsModal" :corpus-id="corpusId" selection /> + <DatasetFromSelectionModal v-model="datasetSelectionModal" :corpus-id="corpusId" /> + <ElementList :elements="elements" max-size @@ -192,6 +203,7 @@ import { createProcessRedirect } from '@/helpers' import MLClassSelect from '@/components/MLClassSelect.vue' import Modal from '@/components/Modal.vue' import DeleteResultsModal from '@/components/Process/Workers/DeleteResultsModal.vue' +import DatasetFromSelectionModal from '@/components/Navigation/DatasetFromSelectionModal.vue' import ElementList from './ElementList' import FolderPicker from '@/components/Navigation/FolderPicker' @@ -204,7 +216,8 @@ export default { DeleteResultsModal, MLClassSelect, Modal, - FolderPicker + FolderPicker, + DatasetFromSelectionModal }, props: { corpusId: { @@ -225,7 +238,8 @@ export default { moveLoading: false, createParentModal: false, createParentLoading: false, - pickedFolder: null + pickedFolder: null, + datasetSelectionModal: false }), mounted () { if (this.hasMlClasses[this.corpusId] === undefined) { diff --git a/src/components/Navigation/DatasetFromSelectionModal.vue b/src/components/Navigation/DatasetFromSelectionModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..fd08ed755e3bb4953c6542eb6eafa89c5250dd0d --- /dev/null +++ b/src/components/Navigation/DatasetFromSelectionModal.vue @@ -0,0 +1,186 @@ +<template> + <Modal + :model-value="modelValue" + v-on:update:model-value="updateModelValue" + :title="truncateLong(title)" + > + <div v-if="availableDatasets === null" class="loader mx-auto is-size-1"></div> + <div v-else-if="availableDatasets?.length === 0" class="notification is-warning"> + <p>No dataset available.</p> + <p> + You can create one from the + <router-link :to="{ name: 'corpus-update', params: { corpusId } }" class="has-text-weight-semibold"> + corpus details page + </router-link> + . + </p> + </div> + <form v-else v-on:submit.prevent="addToSelection"> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Dataset</label> + </div> + <div class="field-body"> + <div class="field"> + <div class="control"> + <span class="select is-fullwidth"> + <select + v-model="selectedDataset" + :disabled="loading ?? null" + > + <option :value="null" selected disabled>—</option> + <template v-if="availableDatasets"> + <option + v-for="dataset in availableDatasets" + :value="dataset" + :key="dataset.id" + :title="dataset.description" + > + {{ truncateLong(dataset.name) }} + </option> + </template> + </select> + </span> + </div> + </div> + </div> + </div> + + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Set</label> + </div> + <div class="field-body"> + <div class="field"> + <div class="control"> + <span class="select is-fullwidth"> + <select + v-model="set" + :disabled="(!selectedDataset || loading) ?? null" + :title="selectedDataset ? `Set elements will be added to on dataset ${selectedDataset.name}` : 'Please select a dataset first' " + > + <option :value="null" selected disabled>—</option> + <template v-if="selectedDataset?.sets"> + <option + v-for="value in selectedDataset.sets" + :value="value" + :key="value" + > + {{ value }} + </option> + </template> + </select> + </span> + </div> + </div> + </div> + </div> + </form> + + <template v-slot:footer="{ close }"> + <button + class="button is-info" + :class="{ 'is-loading': loading }" + type="submit" + :disabled="!canAdd" + :title="canAdd ? 'Add selected elements to the dataset' : 'Please select a dataset and related set'" + v-on:click="addToSelection" + > + Add elements + </button> + <button class="button ml-auto" v-on:click="close">Cancel</button> + </template> + </Modal> +</template> + +<script lang="ts"> +import { + mapState as mapVuexState, + mapActions as mapVuexActions +} from 'vuex' +import { corporaMixin, truncateMixin } from '@/mixins' +import { UUID_REGEX } from '@/config' +import { Dataset, UUID } from '@/types' +import Modal from '@/components/Modal.vue' +import { defineComponent, PropType } from 'vue' + +export default defineComponent({ + mixins: [ + corporaMixin, + truncateMixin + ], + components: { + Modal + }, + emits: { + 'update:modelValue': (value: boolean) => typeof value === 'boolean' + }, + props: { + modelValue: { + type: Boolean, + default: false + }, + /** + * Id of the corpus to delete results on + */ + corpusId: { + type: String as PropType<UUID>, + validator: value => typeof value === 'string' && UUID_REGEX.test(value), + required: true + } + }, + data: () => ({ + loading: false, + /** + * ID of the dataset that will receive selected elements + */ + selectedDataset: null as Dataset | null, + /** + * Name of the set elements will be added to + */ + set: null as string | null + }), + computed: { + ...mapVuexState('corpora', ['corpusDatasets']), + title (): string { + return `Add ${this.corpus?.name} selection to a dataset` + }, + canAdd (): boolean { + return this.selectedDataset !== null && this.set !== null + }, + availableDatasets (): Dataset[] | null { + return this.corpusDatasets?.[this.corpusId] ?? null + } + }, + methods: { + ...mapVuexActions('corpora', ['listCorpusDatasets', 'addDatasetElementsSelection']), + updateModelValue (value: boolean) { this.$emit('update:modelValue', value) }, + async addToSelection () { + if (!this.canAdd) return + + this.loading = true + try { + await this.addDatasetElementsSelection({ + corpusId: this.corpusId, + params: { + dataset_id: this.selectedDataset?.id, + set: this.set + } + }) + this.selectedDataset = null + this.set = null + this.$emit('update:modelValue', false) + } finally { + this.loading = false + } + } + }, + watch: { + modelValue (newValue: boolean) { + if (newValue && this.availableDatasets === null) { + this.listCorpusDatasets({ corpusId: this.corpusId }) + } + } + } +}) +</script> diff --git a/src/store/corpora.js b/src/store/corpora.js index f00143acca9d1a1efdaa10921fe5f0031db1a394..5f6aa688137af7bf0d91b4f077988bcbb6576192 100644 --- a/src/store/corpora.js +++ b/src/store/corpora.js @@ -419,6 +419,17 @@ export const actions = { } }, + // Add selected elements to a dataset of the same corpus + async addDatasetElementsSelection ({ commit }, { corpusId, params }) { + try { + await api.createDatasetElementsSelection(corpusId, params) + commit('notifications/notify', { type: 'success', text: 'Elements have been added to the dataset.' }, { root: true }) + } catch (err) { + commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) + throw err + } + }, + async listExports ({ commit }, { corpusId, ...payload }) { try { commit('setExports', await api.listExports(corpusId, removeEmptyStrings(payload))) diff --git a/tests/unit/store/corpora.spec.js b/tests/unit/store/corpora.spec.js index 40465fa10a8f6e2e10fd021be1e750bc54b1df4e..e647c104c432a3da72a4558632543dd7934afe2c 100644 --- a/tests/unit/store/corpora.spec.js +++ b/tests/unit/store/corpora.spec.js @@ -2229,6 +2229,34 @@ describe('corpora', () => { }) }) + describe('addDatasetElementsSelection', () => { + it('adds elements from a selection to a dataset', async () => { + mock.onPost('/corpus/corpusid/datasets/selection/', { dataset_id: 'datasetid', set: 'test' }).reply(204) + + const params = { dataset_id: 'datasetid', set: 'test' } + await store.dispatch('corpora/addDatasetElementsSelection', { corpusId: 'corpusid', params }) + + assert.deepStrictEqual(store.history, [ + { action: 'corpora/addDatasetElementsSelection', payload: { corpusId: 'corpusid', params } }, + { mutation: 'notifications/notify', payload: { type: 'success', text: 'Elements have been added to the dataset.' } } + ]) + }) + + it('throw errors', async () => { + mock.onPost('/corpus/corpusid/datasets/selection/', { dataset_id: 'datasetid', set: 'test' }).reply(400) + + const params = { dataset_id: 'datasetid', set: 'test' } + await assertRejects( + async () => await store.dispatch('corpora/addDatasetElementsSelection', { corpusId: 'corpusid', params }) + ) + + assert.deepStrictEqual(store.history, [ + { action: 'corpora/addDatasetElementsSelection', payload: { corpusId: 'corpusid', params } }, + { mutation: 'notifications/notify', payload: { type: 'error', text: 'Request failed with status code 400' } } + ]) + }) + }) + describe('listExports', () => { it('lists exports in a corpus', async () => { mock.onGet('/corpus/corpusid/export/').reply(200, { count: 1, results: [exportSample] }) diff --git a/tsconfig.json b/tsconfig.json index 3c4e82d9cc875746b66a4875cfdff25cffe20101..10c3abfe11f0e36c0b06cca5151da6662031ed56 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,8 @@ "strict": true, "jsx": "preserve", "allowJs": true, + "allowImportingTsExtensions": true, + "noEmit": true, "importHelpers": true, "moduleResolution": "node", "skipLibCheck": true,