From 1cf56b72426591fa1cfdc90dac92d4fb54e8611d Mon Sep 17 00:00:00 2001 From: ml bonhomme <bonhomme@teklia.com> Date: Thu, 11 Apr 2024 10:09:18 +0000 Subject: [PATCH] Use the new dataset set endpoints in the frontend --- src/api/dataset.ts | 26 +- src/api/process.js | 8 +- src/api/selection.ts | 16 +- .../Datasets/DatasetSets/CreateForm.vue | 104 ++++ .../Corpus/Datasets/DatasetSets/Row.vue | 144 +++++ src/components/Corpus/Datasets/EditModal.vue | 191 +++---- .../Element/Datasets/ElementDatasets.vue | 16 +- src/components/Element/Datasets/Row.vue | 42 +- src/components/Element/DetailsPanel.vue | 12 +- .../Navigation/DatasetFromSelectionModal.vue | 22 +- src/components/Navigation/ElementList.vue | 4 +- .../Navigation/ElementThumbnail.vue | 8 +- src/components/Process/Datasets/AddForm.vue | 102 ++-- src/components/Process/Datasets/Row.vue | 74 +-- src/store/process.js | 80 ++- src/stores/dataset.ts | 85 ++- src/stores/elements.ts | 20 +- src/types/dataset.ts | 11 +- src/views/Dataset/Details.vue | 49 +- src/views/Process/Configure.vue | 4 +- src/views/Process/Datasets.vue | 46 +- tests/unit/store/process.spec.js | 494 +++++++++--------- tests/unit/stores/datasets.spec.js | 480 +++++++++++++++-- tests/unit/stores/elements.spec.js | 20 +- 24 files changed, 1336 insertions(+), 722 deletions(-) create mode 100644 src/components/Corpus/Datasets/DatasetSets/CreateForm.vue create mode 100644 src/components/Corpus/Datasets/DatasetSets/Row.vue diff --git a/src/api/dataset.ts b/src/api/dataset.ts index 89cb9bf4d..9db7186d4 100644 --- a/src/api/dataset.ts +++ b/src/api/dataset.ts @@ -1,13 +1,15 @@ import axios from 'axios' import { CursorPaginationParameters, PageNumberPaginationParameters, unique } from '.' import { CursorPagination, PageNumberPagination, UUID } from '@/types' -import { Dataset, DatasetElementList, ElementDataset } from '@/types/dataset' +import { Dataset, DatasetElementList, ElementDatasetSet } from '@/types/dataset' export interface DatasetListParameters extends CursorPaginationParameters { set?: string | null } -export type DatasetEdit = Pick<Dataset, 'id' | 'name' | 'description' | 'sets'> +export type DatasetEdit = Pick<Dataset, 'id' | 'name' | 'description'> -export type DatasetCreate = Omit<DatasetEdit, 'id'> +export interface DatasetCreate extends Pick<Dataset, 'name' | 'description'> { + set_names: string[] +} export const listCorpusDataset = unique( async (corpusId: UUID, params: PageNumberPaginationParameters = {}): Promise<PageNumberPagination<Dataset>> => @@ -34,9 +36,9 @@ export const deleteDataset = unique( ) // List datasets containing a specific element -export const listElementDatasets = unique( - async (elementId: UUID, params: PageNumberPaginationParameters = {}): Promise<PageNumberPagination<ElementDataset>> => - (await axios.get(`/element/${elementId}/datasets/`, { params })).data +export const listElementDatasetSets = unique( + async (elementId: UUID, params: PageNumberPaginationParameters = {}): Promise<PageNumberPagination<ElementDatasetSet>> => + (await axios.get(`/element/${elementId}/sets/`, { params })).data ) export const listDatasetElements = unique( @@ -51,3 +53,15 @@ export const deleteDatasetElement = unique( export const cloneDataset = unique( async (datasetId: UUID): Promise<Dataset> => (await axios.post(`datasets/${datasetId}/clone/`)).data ) + +export const createDatasetSet = unique( + async (datasetId: UUID, name: string) => (await axios.post(`datasets/${datasetId}/sets/`, { name: name })).data +) + +export const updateDatasetSet = unique( + async (datasetId: UUID, setId: UUID, name: string) => (await axios.patch(`datasets/${datasetId}/sets/${setId}/`, { name: name })).data +) + +export const deleteDatasetSet = unique( + async (datasetId: UUID, setId: UUID) => await axios.delete(`datasets/${datasetId}/sets/${setId}/`) +) diff --git a/src/api/process.js b/src/api/process.js index dfd5aca0e..e8a873a68 100644 --- a/src/api/process.js +++ b/src/api/process.js @@ -40,10 +40,8 @@ export const clearProcess = unique(async id => await axios.delete(`/process/${id // Select elements with a worker activity failure on the process export const selectProcessFailures = unique(async id => await axios.post(`/process/${id}/select-failures/`)) -export const listProcessDatasets = unique(async ({ processId, ...params }) => (await axios.get(`/process/${processId}/datasets/`, { params })).data) +export const listProcessSets = unique(async ({ processId, ...params }) => (await axios.get(`/process/${processId}/sets/`, { params })).data) -export const createProcessDataset = unique((processId, datasetId, payload) => axios.post(`/process/${processId}/dataset/${datasetId}/`, payload)) +export const createProcessSet = unique((processId, setId, payload) => axios.post(`/process/${processId}/set/${setId}/`, payload)) -export const updateProcessDataset = unique((processId, datasetId, payload) => axios.patch(`/process/${processId}/dataset/${datasetId}/`, payload)) - -export const deleteProcessDataset = unique((processId, datasetId) => axios.delete(`/process/${processId}/dataset/${datasetId}/`)) +export const deleteProcessSet = unique((processId, setId) => axios.delete(`/process/${processId}/set/${setId}/`)) diff --git a/src/api/selection.ts b/src/api/selection.ts index 097b49489..f0623d603 100644 --- a/src/api/selection.ts +++ b/src/api/selection.ts @@ -2,19 +2,7 @@ import axios from 'axios' import { unique } from '.' import { UUID } from '@/types' -export 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 + * Add selected elements to a corpus' dataset set */ -export const createDatasetElementsSelection = unique(async (corpusId: UUID, params: CreateDatasetElementsSelectionParameters) => (await axios.post(`/corpus/${corpusId}/datasets/selection/`, params))) +export const createDatasetElementsSelection = unique(async (corpusId: UUID, setId: UUID) => (await axios.post(`/corpus/${corpusId}/datasets/selection/`, { set_id: setId }))) diff --git a/src/components/Corpus/Datasets/DatasetSets/CreateForm.vue b/src/components/Corpus/Datasets/DatasetSets/CreateForm.vue new file mode 100644 index 000000000..217ea1149 --- /dev/null +++ b/src/components/Corpus/Datasets/DatasetSets/CreateForm.vue @@ -0,0 +1,104 @@ +<template> + <div class="field is-grouped mt-1 mb-0"> + <div class="control mr-1"> + <input + class="input" + type="text" + v-model="setName" + :class="{ 'is-danger': createError.name }" + :disabled="!hasContribPrivilege || dataset.state !== 'open'" + /> + </div> + <div class="control"> + <button + class="button is-primary" + :class="{ 'is-loading': loading }" + :disabled="loading || !canCreate || undefined" + v-on:click="setCreate" + :title="createTitle" + > + <i class="icon-plus"></i> + </button> + </div> + </div> + <template v-if="createError.name"> + <p v-for="(err, i) in createError.name" :key="i" class="help has-text-danger">{{ err }}</p> + </template> +</template> + +<script lang="ts"> +import { mapGetters as mapVuexGetters } from 'vuex' +import { corporaMixin, truncateMixin } from '@/mixins' +import { PropType, defineComponent } from 'vue' +import { Dataset } from '@/types/dataset' +import { mapActions } from 'pinia' +import { useDatasetStore, useNotificationStore } from '@/stores' +import { isAxiosError } from 'axios' + +export default defineComponent({ + mixins: [corporaMixin, truncateMixin], + props: { + dataset: { + type: Object as PropType<Dataset>, + required: true + }, + // Used to reset the set name field when the modal is closed + modalOpen: { + type: Boolean, + required: true + } + }, + data: () => ({ + loading: false, + setName: '', + createError: { name: '' } + }), + computed: { + ...mapVuexGetters('auth', ['isVerified']), + hasContribPrivilege () { + return this.isVerified && this.corpus && this.canWrite(this.corpus) + }, + canCreate () { + return this.setName.trim() && this.hasContribPrivilege && this.dataset.state === 'open' + }, + corpusId () { + return this.dataset.corpus_id + }, + createTitle () { + if (this.dataset.state !== 'open') return 'Sets can only be created in open datasets' + else if (!this.hasContribPrivilege) return 'You must be a contributor on the project to create a set' + else if (this.canCreate) return 'Create new set' + return '' + } + }, + methods: { + ...mapActions(useDatasetStore, ['createDatasetSet']), + ...mapActions(useNotificationStore, ['notify']), + async setCreate () { + this.loading = true + if (!this.canCreate) return + try { + await this.createDatasetSet(this.dataset.id, this.setName) + this.notify({ type: 'success', text: `Dataset set ${this.truncateShort(this.setName)} created` }) + this.createError.name = '' + this.setName = '' + } catch (err) { + if (isAxiosError(err) && err.response?.status === 400 && err.response.data && 'name' in err.response.data) { + this.createError.name = err.response.data.name + } + } finally { + this.loading = false + } + } + }, + watch: { + modalOpen (newValue) { + // Reset set name field and errors when the modal is closed + if (newValue === false) { + this.setName = '' + this.createError.name = '' + } + } + } +}) +</script> diff --git a/src/components/Corpus/Datasets/DatasetSets/Row.vue b/src/components/Corpus/Datasets/DatasetSets/Row.vue new file mode 100644 index 000000000..4abf78b80 --- /dev/null +++ b/src/components/Corpus/Datasets/DatasetSets/Row.vue @@ -0,0 +1,144 @@ +<template> + <div class="field is-grouped mt-1 mb-0"> + <div class="control mr-1"> + <input + class="input" + type="text" + v-model="setName" + :class="{ 'is-danger': updateError.name }" + :disabled="!hasContribPrivilege || dataset.state !== 'open'" + /> + </div> + <div class="control mr-1"> + <button + class="button has-text-primary" + :class="{ 'is-loading': loading }" + :disabled="loading || !canUpdate || undefined" + v-on:click="setUpdate" + :title="updateTitle" + > + <i class="icon-edit"></i> + </button> + </div> + <div class="control"> + <button + class="button has-text-danger" + :class="{ 'is-loading': loading }" + :disabled="loading || !canDelete || undefined" + v-on:click="setDelete" + :title="deleteTitle" + > + <i class="icon-trash"></i> + </button> + </div> + </div> + <template v-if="updateError.name"> + <p v-for="(err, i) in updateError.name" :key="i" class="help has-text-danger">{{ err }}</p> + </template> +</template> + +<script lang="ts"> +import { mapGetters as mapVuexGetters } from 'vuex' +import { corporaMixin } from '@/mixins' +import { PropType, defineComponent } from 'vue' +import { DatasetSet, Dataset } from '@/types/dataset' +import { mapActions } from 'pinia' +import { useDatasetStore, useNotificationStore } from '@/stores' +import { isAxiosError } from 'axios' + +export default defineComponent({ + mixins: [corporaMixin], + props: { + datasetSet: { + type: Object as PropType<DatasetSet>, + required: true + }, + dataset: { + type: Object as PropType<Dataset>, + required: true + }, + // Used to reset the set name field when the modal is closed + modalOpen: { + type: Boolean, + required: true + } + }, + data: () => ({ + loading: false, + setName: '', + updateError: { name: '' } + }), + mounted () { + this.setName = this.datasetSet.name + }, + computed: { + ...mapVuexGetters('auth', ['isVerified']), + hasContribPrivilege () { + return this.isVerified && this.corpus && this.canWrite(this.corpus) + }, + hasAdminPrivilege () { + return this.isVerified && this.corpus && this.canAdmin(this.corpus) + }, + canUpdate () { + return this.datasetSet.name !== this.setName && this.hasContribPrivilege && this.dataset.state === 'open' + }, + canDelete () { + return this.hasAdminPrivilege && this.dataset.state === 'open' && this.dataset.sets.length > 1 + }, + corpusId () { + return this.dataset.corpus_id + }, + updateTitle () { + if (this.dataset.state !== 'open') return 'Sets can only be edited on open datasets' + else if (!this.hasContribPrivilege) return "You must be a contributor on the project to edit a dataset's sets" + else if (this.canUpdate) return 'Update set name' + return undefined + }, + deleteTitle () { + if (this.dataset.state !== 'open') return 'Sets can only be deleted on open datasets' + else if (!this.hasAdminPrivilege) return 'You must be an administrator on the project to delete dataset sets' + else if (this.dataset.sets.length <= 1) return 'This is the only set in the dataset and cannot be deleted' + return 'Delete set' + } + }, + methods: { + ...mapActions(useDatasetStore, ['updateDatasetSet', 'deleteDatasetSet']), + ...mapActions(useNotificationStore, ['notify']), + async setUpdate () { + if (!this.canUpdate) return + this.loading = true + try { + await this.updateDatasetSet(this.dataset.id, this.datasetSet.id, this.setName) + this.notify({ type: 'success', text: 'Dataset set updated' }) + this.updateError.name = '' + } catch (err) { + if (isAxiosError(err) && err.response?.status === 400 && err.response.data && 'name' in err.response.data) { + this.updateError.name = err.response.data.name + } + } finally { + this.loading = false + } + }, + async setDelete () { + this.loading = true + if (!this.canDelete) return + // Errors are handled (notification) in the store + try { + await this.deleteDatasetSet(this.dataset.id, this.datasetSet.id) + this.notify({ type: 'success', text: 'Dataset set deleted' }) + } finally { + this.loading = false + } + } + }, + watch: { + modalOpen (newValue) { + // Reset set name field and errors when the modal is closed + if (newValue === false) { + this.setName = this.datasetSet.name + this.updateError.name = '' + } + } + } +}) +</script> diff --git a/src/components/Corpus/Datasets/EditModal.vue b/src/components/Corpus/Datasets/EditModal.vue index 702309493..4a2decb68 100644 --- a/src/components/Corpus/Datasets/EditModal.vue +++ b/src/components/Corpus/Datasets/EditModal.vue @@ -3,83 +3,80 @@ :model-value="modelValue" v-on:update:model-value="value => $emit('update:modelValue', value)" :title="modalTitle" + is-large > <form v-on:submit.prevent="performCreate"> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Name</label> - </div> - <div class="field-body"> - <div class="field"> - <div class="control"> - <input - class="input" - v-model="newDataset.name" - type="text" - placeholder="Dataset name" - /> - </div> - <template v-if="fieldErrors.name"> - <p class="help is-danger">{{ fieldErrors.name }}</p> - </template> - </div> + <div class="field"> + <label class="label">Name</label> + <div class="control is-expanded"> + <input + class="input" + :class="{ 'is-danger': fieldErrors.name }" + v-model="newDataset.name" + type="text" + placeholder="Dataset name" + /> </div> + <template v-if="fieldErrors.name"> + <p class="help is-danger">{{ fieldErrors.name }}</p> + </template> </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Description</label> - </div> - <div class="field-body"> - <div class="field"> - <div class="control"> - <textarea - class="textarea" - v-model="newDataset.description" - type="text" - placeholder="Dataset description" - ></textarea> - </div> - <template v-if="fieldErrors.description"> - <p class="help is-danger">{{ fieldErrors.description }}</p> - </template> - </div> + <div class="field"> + <label class="label">Description</label> + <div class="control is-expanded"> + <textarea + class="textarea" + :class="{ 'is-danger': fieldErrors.description }" + v-model="newDataset.description" + placeholder="Dataset description" + ></textarea> </div> + <template v-if="fieldErrors.description"> + <p class="help is-danger">{{ fieldErrors.description }}</p> + </template> </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Sets</label> - </div> + <div class="field"> + <label class="label">Sets</label> <div class="field-body"> - <div class="field"> - <div class="control"> - <input - class="input" - v-model="newDataset.sets" - type="text" - placeholder="Dataset sets" - /> - </div> - <template v-if="fieldErrors.sets"> - <p v-if="!(typeof fieldErrors.sets === 'string')"> - <span - class="help is-danger" - v-for="(v, k) in fieldErrors.sets" - :key="k" - > - {{ k }}: {{ v }} - </span> - </p> - <p v-else> - <span class="help is-danger">{{ fieldErrors.sets }}</span> - </p> + <div> + <!-- Sets field for dataset edition --> + <template v-if="datasetInstance"> + <div v-for="dss in datasetInstance.sets" :key="dss.id"> + <DatasetSet :dataset-set="dss" :dataset="datasetInstance" :modal-open="modelValue" /> + </div> + <AddSetForm :dataset="datasetInstance" :modal-open="modelValue" /> </template> - <p class="help"> - Enter the set names, separated by commas. - <!-- This default value only applies when creating, not updating --> - <template v-if="!datasetInstance"> - <br />If this field is left empty, the created sets will be training, test and validation. + <!-- Sets field for dataset creation --> + <template v-else> + <div + class="control" + v-for="(item, i) in newDataset.sets" + :key="i" + > + <div class="field mt-1 mb-0"> + <input + class="input" + :class="{ 'is-danger': fieldErrors.set_names }" + type="text" + :value="item" + v-on:change="updateSetNames(i, $event.target.value)" + /> + </div> + </div> + <div> + <button + class="button is-primary mt-1" + :class="{ 'is-loading': loading }" + :disabled="loading || !hasContribPrivilege || undefined" + v-on:click="addSetField" + > + <i class="icon-plus"></i> + </button> + </div> + <template v-if="fieldErrors.set_names"> + <p class="help is-danger">{{ fieldErrors.set_names }}</p> </template> - </p> + </template> </div> </div> </div> @@ -89,7 +86,7 @@ <button type="submit" class="button is-primary" - :class="{ 'is-loading': createLoading }" + :class="{ 'is-loading': loading }" v-on:click="save" :disabled="!canSave" :title="saveButtonTitle" @@ -109,11 +106,15 @@ import { isEmpty } from 'lodash' import Modal from '@/components/Modal.vue' import { errorParser } from '@/helpers' import { useDatasetStore, useNotificationStore } from '@/stores' +import DatasetSet from '@/components/Corpus/Datasets/DatasetSets/Row.vue' +import AddSetForm from '@/components/Corpus/Datasets/DatasetSets/CreateForm.vue' export default { mixins: [corporaMixin, truncateMixin], components: { - Modal + Modal, + DatasetSet, + AddSetForm }, emits: ['dataset-action', 'update:modelValue'], props: { @@ -134,9 +135,9 @@ export default { newDataset: { name: '', description: '', - sets: '' + sets: [] }, - createLoading: false, + loading: false, fieldErrors: {} }), mounted () { @@ -144,7 +145,7 @@ export default { this.newDataset = { name: this.datasetInstance.name, description: this.datasetInstance.description, - sets: this.datasetInstance.sets.join(', ') + sets: this.datasetInstance.sets } } }, @@ -155,14 +156,7 @@ export default { }, modalTitle () { if (!this.datasetInstance) return 'Create a new dataset' - else return `Edit dataset ${this.truncateShort(this.datasetInstance.name)}` - }, - setList () { - // split the input of the sets field on ',' to make a list - let setList = this.newDataset.sets.split(',') - // trim list items and ignore empty items - setList = setList.map(item => item.trim()).filter(item => item.length > 0) - return setList + else return `Edit dataset ${this.truncateLong(this.datasetInstance.name)}` }, canSave () { return this.newDataset.name.trim() && this.newDataset.description.trim() @@ -177,13 +171,17 @@ export default { ...mapActions(useDatasetStore, ['createCorpusDataset', 'updateCorpusDataset']), ...mapActions(useNotificationStore, ['notify']), async save () { - if (!this.hasContribPrivilege || this.createLoading || this.invalidForm) { return } - this.createLoading = true + if (!this.hasContribPrivilege || this.loading || this.invalidForm) return + this.loading = true const data = { name: this.newDataset.name.trim(), description: this.newDataset.description.trim() } - if (!isEmpty(this.setList)) data.sets = this.setList + // Add list of set names to the payload when creating a new dataset + if (!this.datasetInstance) { + const setList = this.newDataset.sets.map(item => item.trim()).filter(item => item.length > 0) + if (!isEmpty(setList)) data.set_names = setList + } try { if (this.datasetInstance) { await this.updateCorpusDataset( @@ -195,7 +193,7 @@ export default { } else { await this.createCorpusDataset(this.corpusId, data) } - this.createLoading = false + this.loading = false // Sending a custom event to let the parent know that it must reload the list of datasets this.$emit('dataset-action') // Close the modal @@ -203,9 +201,9 @@ export default { } catch (e) { if (e.response?.status === 400 && e.response.data) { this.fieldErrors = this.parseFieldErrors(e.response.data) - } + } else this.notify({ type: 'error', text: errorParser(err) }) } finally { - this.createLoading = false + this.loading = false } }, parseFieldErrors (errors) { @@ -215,6 +213,17 @@ export default { .entries(errors) .map(([key, value]) => [key, this.parseFieldErrors(value)]) ) + }, + addSetField () { + this.newDataset.sets.push('') + }, + updateSetNames (i, newValue) { + if (this.newDataset.sets[i] === newValue) return + // Do not keep the last valid value in the list if the field is emptied + if (!String(newValue).length) { + (this.newDataset.sets.splice(i, 1, newValue)) + } + this.newDataset.sets.splice(i, 1, newValue) } }, watch: { @@ -225,13 +234,13 @@ export default { this.newDataset = { name: this.datasetInstance.name, description: this.datasetInstance.description, - sets: this.datasetInstance.sets.join(', ') + sets: this.datasetInstance.sets } } else { this.newDataset = { name: '', description: '', - sets: '' + sets: ['train', 'validation', 'test'] } } this.fieldErrors = {} @@ -240,7 +249,3 @@ export default { } } </script> - -<style lang="scss" scoped> -.field-label { min-width: 11ch } -</style> diff --git a/src/components/Element/Datasets/ElementDatasets.vue b/src/components/Element/Datasets/ElementDatasets.vue index 195843b28..ea7ff5b29 100644 --- a/src/components/Element/Datasets/ElementDatasets.vue +++ b/src/components/Element/Datasets/ElementDatasets.vue @@ -1,6 +1,6 @@ <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"> + <div class="loader m-auto is-size-3" v-if="datasetSets === null"></div> + <div class="message-body has-text-grey" v-else-if="datasetSets.length === 0"> No datasets </div> <table v-else class="table is-fullwidth is-hoverable"> @@ -11,10 +11,10 @@ <th></th> </thead> <tbody> - <tr v-for="elementdataset in datasets" :key="elementdataset.dataset.id + elementdataset.set"> + <tr v-for="elementset in datasetSets" :key="elementset.dataset.id + elementset.set"> <Row :corpus-id="corpusId" - :element-dataset="elementdataset" + :element-set="elementset" :element="element" /> </tr> @@ -24,7 +24,7 @@ <script lang="ts"> import { defineComponent, PropType } from 'vue' -import { ElementDataset } from '@/types/dataset' +import { ElementDatasetSet } from '@/types/dataset' import { mapState } from 'pinia' import { useElementsStore } from '@/stores' import { Element } from '@/types' @@ -45,9 +45,9 @@ export default defineComponent({ } }, computed: { - ...mapState(useElementsStore, ['elementDatasets']), - datasets (): ElementDataset[] | null { - return this.elementDatasets?.[this.element.id] ?? null + ...mapState(useElementsStore, ['elementDatasetSets']), + datasetSets (): ElementDatasetSet[] | null { + return this.elementDatasetSets?.[this.element.id] ?? null } } }) diff --git a/src/components/Element/Datasets/Row.vue b/src/components/Element/Datasets/Row.vue index 0285d344b..174c479a7 100644 --- a/src/components/Element/Datasets/Row.vue +++ b/src/components/Element/Datasets/Row.vue @@ -1,23 +1,23 @@ <template> <td> - <router-link :to="{ name: 'dataset-details', params: { datasetId: elementDataset.dataset.id } }" title="Dataset details"> - {{ elementDataset.dataset.name }} + <router-link :to="{ name: 'dataset-details', params: { datasetId: elementSet.dataset.id } }" title="Dataset details"> + {{ elementSet.dataset.name }} </router-link> </td> - <td>{{ elementDataset.set }}</td> + <td>{{ elementSet.set }}</td> <td class="shrink has-text-right"> <component - :is="elementDataset.previous ? 'router-link' : 'span'" - :class="!elementDataset.previous ? 'has-text-grey' : ''" - :to="elementDataset.previous ? { name: 'element-details', params: { id: elementDataset.previous } } : ''" + :is="elementSet.previous ? 'router-link' : 'span'" + :class="!elementSet.previous ? 'has-text-grey' : ''" + :to="elementSet.previous ? { name: 'element-details', params: { id: elementSet.previous } } : ''" title="Go to the previous element in this dataset and set" > <i class="icon-arrow-left"></i> </component> <component - :is="elementDataset.next ? 'router-link' : 'span'" - :class="!elementDataset.next ? 'has-text-grey' : ''" - :to="elementDataset.next ? { name: 'element-details', params: { id: elementDataset.next } } : ''" + :is="elementSet.next ? 'router-link' : 'span'" + :class="!elementSet.next ? 'has-text-grey' : ''" + :to="elementSet.next ? { name: 'element-details', params: { id: elementSet.next } } : ''" title="Go to the next element in this dataset and set" > <i class="icon-arrow-right"></i> @@ -33,10 +33,10 @@ </a> </td> <RemoveModal - v-if="elementDataset.dataset && elementDataset.set" + v-if="elementSet.dataset && elementSet.set" v-model="removeElementModal" - :dataset="elementDataset.dataset" - :set="elementDataset.set" + :dataset="elementSet.dataset" + :set="elementSet.set" :element="element" /> </template> @@ -44,7 +44,7 @@ <script lang="ts"> import { corporaMixin } from '@/mixins' import { defineComponent, PropType } from 'vue' -import { ElementDataset } from '@/types/dataset' +import { ElementDatasetSet } from '@/types/dataset' import { mapState } from 'pinia' import { useElementsStore } from '@/stores' import RemoveModal from '@/components/Dataset/RemoveModal.vue' @@ -66,8 +66,8 @@ export default defineComponent({ type: Object as PropType<Element>, required: true }, - elementDataset: { - type: Object as PropType<ElementDataset>, + elementSet: { + type: Object as PropType<ElementDatasetSet>, required: true } }, @@ -75,17 +75,17 @@ export default defineComponent({ removeElementModal: false }), computed: { - ...mapState(useElementsStore, ['elementDatasets']), - datasets (): ElementDataset[] | null { - return this.elementDatasets?.[this.element.id] ?? null + ...mapState(useElementsStore, ['elementDatasetSets']), + datasets (): ElementDatasetSet[] | null { + return this.elementDatasetSets?.[this.element.id] ?? null }, canRemove () { - return (this.elementDataset.dataset && this.elementDataset.dataset.state === 'open' && this.elementDataset.set && this.canWrite(this.corpus)) + return (this.elementSet.dataset && this.elementSet.dataset.state === 'open' && this.elementSet.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.elementDataset.dataset.state !== 'open') return 'Only open datasets can be edited' - else return 'Remove element from this dataset' + else if (this.elementSet.dataset.state !== 'open') return 'Only open datasets can be edited' + else return 'Remove element from this dataset set' } }, methods: { diff --git a/src/components/Element/DetailsPanel.vue b/src/components/Element/DetailsPanel.vue index 298ed3740..556f95366 100644 --- a/src/components/Element/DetailsPanel.vue +++ b/src/components/Element/DetailsPanel.vue @@ -80,7 +80,7 @@ import { corporaMixin } from '@/mixins' import { defineComponent, PropType } from 'vue' import { UUID_REGEX } from '@/config' import { UUID, Element } from '@/types' -import { ElementDataset } from '@/types/dataset' +import { ElementDatasetSet } from '@/types/dataset' import { mapState, mapActions } from 'pinia' import { useElementsStore } from '@/stores' @@ -128,7 +128,7 @@ export default defineComponent({ computed: { ...mapVuexState('elements', ['elements']), ...mapVuexState('classification', ['hasMLClasses']), - ...mapState(useElementsStore, ['elementDatasets']), + ...mapState(useElementsStore, ['elementDatasetSets']), ...mapVuexGetters('elements', { // canWrite and canAdmin are already defined in corporaMixin canWriteElement: 'canWrite', @@ -160,14 +160,14 @@ export default defineComponent({ // @ts-expect-error Some Element attributes like metadata are set on the fly on the former store return (this.element && this.element?.metadata) || [] }, - datasets (): ElementDataset[] | null { - return this.elementDatasets?.[this.elementId] ?? null + datasets (): ElementDatasetSet[] | null { + return this.elementDatasetSets?.[this.elementId] ?? null } }, methods: { ...mapVuexActions('classification', { classificationCreate: 'create' }), ...mapVuexActions('elements', { retrieveElement: 'get' }), - ...mapActions(useElementsStore, ['listElementDatasets']), + ...mapActions(useElementsStore, ['listElementDatasetSets']), async createClassification () { if (!this.canCreateClassification) return this.isSavingNewClassification = true @@ -195,7 +195,7 @@ export default defineComponent({ * or some element attributes are not displayed at all. */ if (!this.element || this.element.id !== id || !this.element.rights || !this.element.classifications) this.retrieveElement({ id }) - if (!Array.isArray(this.elementDatasets[id])) this.listElementDatasets(id, { with_neighbors: true }) + if (!Array.isArray(this.elementDatasetSets[id])) this.listElementDatasetSets(id, { with_neighbors: true }) } }, selectedNewClassification () { diff --git a/src/components/Navigation/DatasetFromSelectionModal.vue b/src/components/Navigation/DatasetFromSelectionModal.vue index d8502a21b..f009355fb 100644 --- a/src/components/Navigation/DatasetFromSelectionModal.vue +++ b/src/components/Navigation/DatasetFromSelectionModal.vue @@ -55,7 +55,7 @@ <div class="control"> <span class="select is-fullwidth"> <select - v-model="set" + v-model="selectedSet" :disabled="(!selectedDataset || loading) ?? null" :title="selectedDataset ? `Set elements will be added to on dataset ${selectedDataset.name}` : 'Please select a dataset first' " > @@ -64,9 +64,9 @@ <option v-for="value in selectedDataset.sets" :value="value" - :key="value" + :key="value.id" > - {{ value }} + {{ value.name }} </option> </template> </select> @@ -98,7 +98,7 @@ import { mapState, mapActions } from 'pinia' import { corporaMixin, truncateMixin } from '@/mixins' import { UUID_REGEX } from '@/config' import { UUID } from '@/types' -import { Dataset } from '@/types/dataset' +import { Dataset, DatasetSet } from '@/types/dataset' import Modal from '@/components/Modal.vue' import { defineComponent, PropType } from 'vue' import { useDatasetStore } from '@/stores' @@ -137,7 +137,7 @@ export default defineComponent({ /** * Name of the set elements will be added to */ - set: null as string | null + selectedSet: null as DatasetSet | null }), computed: { ...mapState(useDatasetStore, ['corpusDatasets', 'singleCorpusDatasets']), @@ -145,7 +145,7 @@ export default defineComponent({ return `Add ${this.corpus?.name} selection to a dataset` }, canAdd (): boolean { - return this.selectedDataset !== null && this.set !== null + return this.selectedDataset !== null && this.selectedSet !== null }, availableDatasets (): Dataset[] | null { if (this.corpusDatasets[this.corpusId] === undefined) return null @@ -157,19 +157,17 @@ export default defineComponent({ updateModelValue (value: boolean) { this.$emit('update:modelValue', value) }, async addToSelection () { if (!this.canAdd) return - if (!this.selectedDataset || !this.set) return + if (!this.selectedDataset || !this.selectedSet) return this.loading = true try { await this.addDatasetElementsSelection( this.corpusId, - { - dataset_id: this.selectedDataset.id, - set: this.set - } + this.selectedDataset.id, + this.selectedSet ) this.selectedDataset = null - this.set = null + this.selectedSet = null this.$emit('update:modelValue', false) } finally { this.loading = false diff --git a/src/components/Navigation/ElementList.vue b/src/components/Navigation/ElementList.vue index 618c80b61..cf8778824 100644 --- a/src/components/Navigation/ElementList.vue +++ b/src/components/Navigation/ElementList.vue @@ -52,7 +52,7 @@ import ElementThumbnail from './ElementThumbnail.vue' import ElementInLine from './ElementInLine.vue' import { ElementList } from '@/types' import { ProcessElementList } from '@/types/process' -import { Dataset } from '@/types/dataset' +import { Dataset, DatasetSet } from '@/types/dataset' function isElementList (element: ElementList | ProcessElementList): element is ElementList { return 'type' in element @@ -101,7 +101,7 @@ export default defineComponent({ default: null }, set: { - type: String, + type: Object as PropType<DatasetSet | null>, default: null } }, diff --git a/src/components/Navigation/ElementThumbnail.vue b/src/components/Navigation/ElementThumbnail.vue index ad4ed1c8b..24ce01f0e 100644 --- a/src/components/Navigation/ElementThumbnail.vue +++ b/src/components/Navigation/ElementThumbnail.vue @@ -69,10 +69,10 @@ <i class="icon-remove-square"></i> </button> <RemoveModal - v-if="dataset && dataset.state === 'open'" + v-if="dataset && set && dataset.state === 'open'" v-model="removeElementModal" :dataset="dataset" - :set="set" + :set="set.name" :element="element" /> <button @@ -104,7 +104,7 @@ import { useDisplayStore } from '@/stores' import DeleteModal from '@/components/Element/DeleteModal.vue' import PreviewDropdown from './PreviewDropdown.vue' -import { Dataset } from '@/types/dataset' +import { Dataset, DatasetSet } from '@/types/dataset' import { ElementList } from '@/types' import RemoveModal from '@/components/Dataset/RemoveModal.vue' @@ -132,7 +132,7 @@ export default defineComponent({ default: null }, set: { - type: String, + type: Object as PropType<DatasetSet | null>, default: null } }, diff --git a/src/components/Process/Datasets/AddForm.vue b/src/components/Process/Datasets/AddForm.vue index a9e8113f9..10fda7602 100644 --- a/src/components/Process/Datasets/AddForm.vue +++ b/src/components/Process/Datasets/AddForm.vue @@ -6,7 +6,7 @@ <td> <span class="select is-fullwidth"> <select - form="dataset-add" + form="set-add" v-model="corpusFilter" :disabled="loading || null" > @@ -19,17 +19,17 @@ <td> <span class="select is-fullwidth"> <select - form="dataset-add" + form="set-add" v-model="datasetId" :disabled="loading || !corpusFilter || !corpusDatasets?.length || null" > <option value="" disabled selected>—</option> - <!-- Display all datasets in the corpus, disabling the options for datasets that were already added to the process --> + <!-- Display all datasets in the corpus, disabling the option for datasets of which all sets are already added to the process --> <option - v-for="corpusDataset in singleCorpusDatasets(corpusFilter) ?? []" + v-for="corpusDataset in corpusDatasets ?? []" :key="corpusDataset.id" :value="corpusDataset.id" - :disabled="processDatasets[processId]?.some(({ dataset }) => dataset.id === corpusDataset.id) || null" + :disabled="allSetsSeletected(corpusDataset)" > {{ corpusDataset.name }} </option> @@ -41,18 +41,25 @@ </td> <template v-if="dataset"> <td> - <div v-for="set in dataset.sets" :key="set"> - <label class="checkbox"> - <input - type="checkbox" - v-model="datasetSets[set]" - /> - {{ truncateLong(set) }} - </label> - </div> - <template v-if="fieldErrors.sets"> - <p v-for="(error, i) in fieldErrors.sets" :key="i" class="help is-danger">{{ error }}</p> - </template> + <span class="select is-fullwidth"> + <select + form="set-add" + v-model="setIds" + :disabled="loading || !corpusFilter || !dataset?.sets.length || null" + > + <option :value="[]" disabled selected>—</option> + <option v-if="remainingSets(dataset).length > 1" :value="remainingSets(dataset)" :disabled="allSetsSeletected(dataset)">All sets</option> + <!-- Display all sets in the dataset, disabling the option for sets that are already added to the process --> + <option + v-for="datasetSet in dataset.sets ?? []" + :key="datasetSet.id" + :value="[datasetSet.id]" + :disabled="processSets[processId]?.some(( item ) => item.dataset.id === datasetId && item.set_name === datasetSet.name) || null" + > + {{ datasetSet.name }} + </option> + </select> + </span> </td> <td> <StateTag :state="dataset.state" /> @@ -61,7 +68,7 @@ <td :colspan="dataset ? 1 : 3" class="shrink has-text-right"> <button type="submit" - form="dataset-add" + form="set-add" class="button is-primary" :disabled="!canAdd || null" :title="addButtonTitle" @@ -73,7 +80,7 @@ The <select> and <button> are linked to this form by its ID, to allow pressing Enter anywhere to add the dataset. Putting a <form> inside of a <tr> or <td> is forbidden, so it is placed here and will be invisible. --> - <form id="dataset-add" v-on:submit.prevent="add"></form> + <form id="set-add" v-on:submit.prevent="add"></form> </tr> </template> @@ -106,12 +113,12 @@ export default { loading: false, corpusFilter: '', datasetId: '', - datasetSets: {}, + setIds: [], fieldErrors: {} }), computed: { ...mapState(useDatasetStore, ['singleCorpusDatasets']), - ...mapVuexState('process', ['processes', 'processDatasets']), + ...mapVuexState('process', ['processes', 'processSets']), process () { return this.processes[this.processId] }, @@ -126,49 +133,44 @@ export default { return this.corpusDatasets?.find(({ id }) => id === this.datasetId) }, canAdd () { - return !this.loading && !this.process.started && this.canAdmin(this.corpus) && this.dataset + return !this.loading && !this.process.started && this.canAdmin(this.corpus) && this.setIds.length }, addButtonTitle () { if (this.loading) return 'Loading…' if (this.process.started) return 'This process has already started and cannot be modified' if (!this.canAdmin(this.corpus)) return "You must have an admin access to the process' project to add a dataset" - if (!this.dataset) return 'You must select a dataset to add' - return 'Add a dataset to this process' + if (!this.setIds.length) return 'You must select a dataset set to add' + return 'Add a dataset set to this process' } }, methods: { ...mapActions(useDatasetStore, ['listCorpusDatasets']), - ...mapVuexActions('process', ['createProcessDataset']), + ...mapVuexActions('process', ['createProcessSet']), ...mapActions(useNotificationStore, ['notify']), + allSetsSeletected (corpusDataset) { + return this.processSets[this.processId]?.filter((item) => item.dataset.id === corpusDataset.id).length === corpusDataset.sets.length + }, + remainingSets (dataset) { + const selectedSets = this.processSets[this.processId]?.filter((item) => item.dataset.id === dataset.id).map((item) => item.set_name) ?? [] + return dataset.sets.filter((item) => !selectedSets.includes(item.name)).map((item) => item.id) + }, async add () { if (!this.canAdd) return this.loading = true - - const sets = [] - Object.keys(this.datasetSets).forEach((set) => { - if (this.datasetSets[set]) sets.push(set) - }) - // It is not possible to select no sets at all - if (!sets.length > 0) { - this.fieldErrors = { - ...this.fieldErrors, - sets: ['At least one set must be selected.'] - } - this.loading = false - return - } - - try { - await this.createProcessDataset({ processId: this.processId, dataset: this.dataset, sets }) - this.datasetId = '' - } catch (err) { - if (isAxiosError(err) && err.response?.status === 400 && err.response.data) { - this.fieldErrors = err.response.data + for (const setId of this.setIds) { + try { + await this.createProcessSet({ processId: this.processId, setId }) + } catch (err) { + if (isAxiosError(err) && err.response?.status === 400 && err.response.data) { + this.fieldErrors = err.response.data + } + this.notify({ type: 'error', text: errorParser(err) }) } - this.notify({ type: 'error', text: errorParser(err) }) - } finally { - this.loading = false } + // Reset the dataset filter if the 'all-sets' option has been used, as it is now disabled + if (this.setIds.length > 1) this.datasetId = '' + this.setIds = [] + this.loading = false } }, watch: { @@ -194,8 +196,8 @@ export default { datasetId (newValue, oldValue) { if (!this.dataset) return if (oldValue !== newValue) { - this.datasetSets = Object.fromEntries(this.dataset.sets.map(set => { return [[set], true] })) this.fieldErrors = {} + this.setIds = [] } } } diff --git a/src/components/Process/Datasets/Row.vue b/src/components/Process/Datasets/Row.vue index 7b55378e7..ea9df78dc 100644 --- a/src/components/Process/Datasets/Row.vue +++ b/src/components/Process/Datasets/Row.vue @@ -1,33 +1,19 @@ <template> <tr> <td class="shrink"> - <ItemId :item-id="processDataset.dataset.id" /> + <ItemId :item-id="processSet.id" /> </td> <td> {{ corpus.name }} </td> <td> - {{ processDataset.dataset.name }} + {{ processSet.dataset.name }} </td> <td> - <div v-for="set in processDataset.dataset.sets" :key="set"> - <label class="checkbox"> - <input - :disabled="loading || lastSet(set)" - type="checkbox" - :checked="processDataset.sets.includes(set)" - v-on:change="updateSets(set, $event.target.checked)" - :title="lastSet(set) ? 'At least one set must be selected' : ''" - /> - {{ truncateLong(set) }} - </label> - </div> - <template v-if="setErrors.length"> - <p v-for="(error, i) in setErrors" :key="i" class="help is-danger">{{ error }}</p> - </template> + {{ processSet.set_name }} </td> <td> - <StateTag :state="processDataset.dataset.state" /> + <StateTag :state="processSet.dataset.state" /> </td> <td v-if="!readOnly" class="shrink has-text-right"> <button @@ -49,7 +35,6 @@ import { mapActions } from 'pinia' import { corporaMixin, truncateMixin } from '@/mixins' import ItemId from '@/components/ItemId.vue' import StateTag from '@/components/Corpus/Datasets/StateTag.vue' -import { errorParser } from '@/helpers' import { useNotificationStore } from '@/stores' export default { @@ -62,7 +47,7 @@ export default { StateTag }, props: { - processDataset: { + processSet: { type: Object, required: true }, @@ -85,7 +70,7 @@ export default { return this.processes[this.processId] ?? {} }, corpusId () { - return this.processDataset.dataset.corpus_id + return this.processSet.dataset.corpus_id }, canRemove () { return !this.loading && this.process?.corpus && this.canAdmin(this.corpora[this.process.corpus]) && !this.process?.started @@ -98,61 +83,26 @@ export default { } }, methods: { - ...mapVuexActions('process', ['deleteProcessDataset', 'updateProcessDataset']), + ...mapVuexActions('process', ['deleteProcessSet']), ...mapActions(useNotificationStore, ['notify']), async remove () { if (!this.canRemove) return this.loading = true + const setId = this.processSet.dataset.sets.find((item) => item.name === this.processSet.set_name).id try { - await this.deleteProcessDataset({ + await this.deleteProcessSet({ processId: this.processId, - datasetId: this.processDataset.dataset.id, - processDatasetId: this.processDataset.id + setId, + processSetId: this.processSet.id }) } catch (err) { /* * Only remove the loading state when the deletion fails, not all the time. - * This gives time for the subsequent ListProcessDatasets to refresh the datasets and hide this deleted row + * This gives time for the subsequent listProcessSets to refresh the datasets and hide this deleted row * after the deletion is successful, without allowing the user to click the remove button again. */ this.loading = false } - }, - lastSet (set) { - // This set is the only one selected from the list; it cannot be unselected - return (this.processDataset.sets.length === 1 & this.processDataset.sets.includes(set)) - }, - async updateSets (set, checked) { - if (this.loading) return - this.loading = true - this.setErrors = [] - - // Copy the current process dataset sets - const sets = [...this.processDataset.sets] - // If the set is in the process dataset sets, and the checkbox is unchecked, remove it - const index = sets.indexOf(set) - if (index > -1 & !checked) sets.splice(index, 1) - // If the set is not in the process dataset sets, and the checkbox is checked, add it - else if (index === -1 & checked) sets.push(set) - - // It is not possible to select no sets at all - if (!sets.length > 0) { - this.setErrors.push('At least one set must be selected.') - this.loading = false - return - } - - try { - await this.updateProcessDataset({ - processId: this.processId, - datasetId: this.processDataset.dataset.id, - sets - }) - } catch (err) { - this.notify({ type: 'error', text: errorParser(err) }) - } finally { - this.loading = false - } } } } diff --git a/src/store/process.js b/src/store/process.js index 19c63839d..40d88093f 100644 --- a/src/store/process.js +++ b/src/store/process.js @@ -14,10 +14,10 @@ export const initialState = () => ({ // { [processId]: ElementsPaginatedResponse } processElementsPage: {}, /** - * ProcessDatasets by process ID. - * @type {{ [processId: string]: import('@/types').ProcessDataset[] }} + * ProcessSets by process ID. + * @type {{ [processId: string]: import('@/types').ProcessSet[] }} */ - processDatasets: {}, + processSets: {}, /** * Tasks for the current process, with an added `timeoutId` property for task polling */ @@ -132,38 +132,27 @@ export const mutations = { state.processElementsPage = { ...state.processElementsPage, [processId]: newResponse } }, - setProcessDatasets (state, { processId, results }) { - const processDatasetList = state.processDatasets[processId] || [] - results.forEach(newProcessDataset => { + setProcessSets (state, { processId, results }) { + const processSetList = state.processSets[processId] || [] + results.forEach(newProcessSet => { // Prevent duplicating datasets - if (!processDatasetList.some(processDataset => processDataset.id === newProcessDataset.id)) processDatasetList.push(newProcessDataset) + if (!processSetList.some(processSet => processSet.id === newProcessSet.id)) processSetList.push(newProcessSet) }) // Merge process datasets - state.processDatasets = { - ...state.processDatasets, - [processId]: processDatasetList + state.processSets = { + ...state.processSets, + [processId]: processSetList } }, - updateSingleProcessDataset (state, { processId, data }) { - const processDatasetList = state.processDatasets[processId] || [] - const index = processDatasetList.findIndex(({ id }) => id === data.id) + removeProcessSet (state, { processId, processSetId }) { + const processSetList = state.processSets[processId] || [] + const index = processSetList.findIndex(({ id }) => id === processSetId) if (index < 0) return - processDatasetList.splice(index, 1, data) - state.processDatasets = { - ...state.processDatasets, - [processId]: processDatasetList - } - }, - - removeProcessDataset (state, { processId, processDatasetId }) { - const processDatasetList = state.processDatasets[processId] || [] - const index = processDatasetList.findIndex(({ id }) => id === processDatasetId) - if (index < 0) return - processDatasetList.splice(index, 1) - state.processDatasets = { - ...state.processDatasets, - [processId]: processDatasetList + processSetList.splice(index, 1) + state.processSets = { + ...state.processSets, + [processId]: processSetList } }, @@ -360,38 +349,39 @@ export const actions = { commit('setProcessElementsPage', { processId, response }) }, - async listProcessDatasets ({ state, commit, dispatch }, { processId, page = 1 }) { + async listProcessSets ({ state, commit, dispatch }, { processId, page = 1 }) { // Do not start fetching process datasets if they have been retrieved already - if (page === 1 && state.processDatasets[processId]) return + if (page === 1 && state.processSets[processId]) return - const data = await api.listProcessDatasets({ processId, page }) + let data = null + try { + data = await api.listProcessSets({ processId, page }) + } catch (err) { + commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) + throw err + } if (!data || !data.number || page !== data.number) { // Avoid any loop - throw new Error(`Pagination failed while listing datasets for process "${processId}"`) + throw new Error(`Pagination failed while listing sets for process "${processId}"`) } - commit('setProcessDatasets', { processId, results: data.results }) + commit('setProcessSets', { processId, results: data.results }) // Load other pages - if (data.next) await dispatch('listProcessDatasets', { processId, page: page + 1 }) + if (data.next) await dispatch('listProcessSets', { processId, page: page + 1 }) }, - async createProcessDataset ({ commit }, { processId, dataset, sets }) { + async createProcessSet ({ commit }, { processId, setId }) { // Errors are handled in components/Process/Datasets/AddForm.vue - const resp = await api.createProcessDataset(processId, dataset.id, { sets }) - commit('setProcessDatasets', { processId, results: [resp.data] }) - }, - - async updateProcessDataset ({ commit }, { processId, datasetId, sets }) { - const resp = await api.updateProcessDataset(processId, datasetId, { sets }) - commit('updateSingleProcessDataset', { processId, data: resp.data }) + const resp = await api.createProcessSet(processId, setId) + commit('setProcessSets', { processId, results: [resp.data] }) }, - async deleteProcessDataset ({ commit }, { processId, datasetId, processDatasetId }) { + async deleteProcessSet ({ commit }, { processId, processSetId, setId }) { try { - await api.deleteProcessDataset(processId, datasetId) - commit('removeProcessDataset', { processId, processDatasetId }) + await api.deleteProcessSet(processId, setId) + commit('removeProcessSet', { processId, processSetId }) } catch (err) { commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) throw err diff --git a/src/stores/dataset.ts b/src/stores/dataset.ts index c66384894..81b769487 100644 --- a/src/stores/dataset.ts +++ b/src/stores/dataset.ts @@ -1,20 +1,22 @@ import { UUID } from '@/types' -import { Dataset, DatasetElementList } from '@/types/dataset' +import { Dataset, DatasetElementList, DatasetSet } from '@/types/dataset' import { defineStore } from 'pinia' import { useNotificationStore } from './notification' import { - CreateDatasetElementsSelectionParameters, DatasetCreate, DatasetEdit, cloneDataset, createDataset, createDatasetElementsSelection, + createDatasetSet, deleteDataset, deleteDatasetElement, + deleteDatasetSet, listCorpusDataset, listDatasetElements, retrieveDataset, - updateDataset + updateDataset, + updateDatasetSet } from '@/api' import { errorParser } from '@/helpers' import { useElementsStore } from './elements' @@ -33,7 +35,7 @@ interface State { */ datasetElementPagination: { [datasetId: UUID]: { - [set: string]: { datasetElements: DatasetElementList[], nextCursor: string | null } + [setName: string]: { datasetElements: DatasetElementList[], nextCursor: string | null } } } } @@ -127,12 +129,12 @@ export const useDatasetStore = defineStore('dataset', { }, // Add selected elements to a dataset of the same corpus - async addDatasetElementsSelection (corpusId: UUID, params: CreateDatasetElementsSelectionParameters) { + async addDatasetElementsSelection (corpusId: UUID, datasetId: UUID, datasetSet: DatasetSet) { try { - await createDatasetElementsSelection(corpusId, params) + await createDatasetElementsSelection(corpusId, datasetSet.id) // Reset an eventual cursor pagination listing elements of this set - delete this.datasetElementPagination[params.dataset_id]?.[params.set] - useNotificationStore().notify({ type: 'success', text: 'Elements have been added to the dataset.' }) + delete this.datasetElementPagination[datasetId]?.[datasetSet.name] + useNotificationStore().notify({ type: 'success', text: 'Elements have been added to the dataset set.' }) } catch (err) { useNotificationStore().notify({ type: 'error', text: errorParser(err) }) throw err @@ -145,34 +147,34 @@ export const useDatasetStore = defineStore('dataset', { * If no pagination was stored in the state, starts from the first page. * If a pagination existed and a next page is available, resumes paginating. */ - async nextDatasetElements (datasetId: UUID, set: string) { + async nextDatasetElements (datasetId: UUID, setName: string) { if (this.datasetElementPagination[datasetId] === undefined) this.datasetElementPagination[datasetId] = {} - const currentPage = this.datasetElementPagination[datasetId][set] + const currentPage = this.datasetElementPagination[datasetId][setName] if (currentPage && currentPage.nextCursor === null) { useNotificationStore().notify({ type: 'warning', text: 'All elements have been fetched already.' }) return } - // Return early when we know no element are part of this set - if (this.datasets[datasetId]?.set_elements?.[set] === 0) { - this.datasetElementPagination[datasetId][set] = { datasetElements: [], nextCursor: null } + // Return early when we know no element is part of this set + if (this.datasets[datasetId]?.set_elements?.[setName] === 0) { + this.datasetElementPagination[datasetId][setName] = { datasetElements: [], nextCursor: null } return } try { - const resp = await listDatasetElements(datasetId, { set, cursor: currentPage?.nextCursor ?? null }) + const resp = await listDatasetElements(datasetId, { set: setName, cursor: currentPage?.nextCursor ?? null }) const nextCursor = resp.next && new URL(resp.next).searchParams.get('cursor') - this.datasetElementPagination[datasetId][set] = { datasetElements: [...(currentPage?.datasetElements ?? []), ...resp.results], nextCursor } + this.datasetElementPagination[datasetId][setName] = { datasetElements: [...(currentPage?.datasetElements ?? []), ...resp.results], nextCursor } } catch (err) { useNotificationStore().notify({ type: 'error', text: errorParser(err) }) throw err } }, - async removeDatasetElement (datasetId: UUID, elementId: UUID, set: string) { + async removeDatasetElement (datasetId: UUID, elementId: UUID, setName: string) { try { - await deleteDatasetElement(datasetId, elementId, set) + await deleteDatasetElement(datasetId, elementId, setName) // When the action is called from the element details panel, this.datasetElementPagination[datasetId] can be undefined if (this.datasetElementPagination[datasetId]) { - const currentPage = this.datasetElementPagination[datasetId][set] + const currentPage = this.datasetElementPagination[datasetId][setName] if (currentPage) { const index = currentPage.datasetElements.findIndex(datasetElement => datasetElement.element.id === elementId) if (index >= 0) currentPage.datasetElements.splice(index, 1) @@ -180,17 +182,52 @@ export const useDatasetStore = defineStore('dataset', { } // Update dataset sets count const setElements = this.datasets[datasetId]?.set_elements - if (setElements && setElements[set] !== undefined) setElements[set] -= 1 + if (setElements && setElements[setName] !== undefined) setElements[setName] -= 1 // Remove element link to the dataset - const elementDatasets = useElementsStore().elementDatasets[elementId] - if (elementDatasets === undefined) return - 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) + const elementDatasetSets = useElementsStore().elementDatasetSets[elementId] + if (elementDatasetSets === undefined) return + const index = elementDatasetSets.findIndex(dss => dss.dataset.id === datasetId && dss.set === setName) + if (index < 0) throw new Error(`Element ${elementId} not found in set ${setName} of dataset ${datasetId}`) + elementDatasetSets.splice(index, 1) } catch (err) { useNotificationStore().notify({ type: 'error', text: errorParser(err) }) throw err } + }, + + async createDatasetSet (datasetId: UUID, name: string) { + try { + const data = await createDatasetSet(datasetId, name) + this.datasets[datasetId].sets.push(data) + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + throw err + } + }, + + async updateDatasetSet (datasetId: UUID, setId: UUID, name: string) { + try { + const data = await updateDatasetSet(datasetId, setId, name) + const datasetSets = this.datasets[datasetId].sets + const index = datasetSets.findIndex(dss => dss.id === setId) + if (index < 0) throw new Error(`Set ${setId} not found in dataset ${datasetId}`) + datasetSets[index] = data + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + throw err + } + }, + + async deleteDatasetSet (datasetId: UUID, setId: UUID) { + try { + await deleteDatasetSet(datasetId, setId) + const datasetSets = this.datasets[datasetId].sets + const index = datasetSets.findIndex(dss => dss.id === setId) + if (index < 0) throw new Error(`Set ${setId} not found in dataset ${datasetId}`) + datasetSets.splice(index, 1) + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + } } }, diff --git a/src/stores/elements.ts b/src/stores/elements.ts index 5e1cc4331..9701bdb93 100644 --- a/src/stores/elements.ts +++ b/src/stores/elements.ts @@ -2,7 +2,7 @@ import { defineStore } from 'pinia' import { PageNumberPaginationParameters } from '@/api' import { errorParser } from '@/helpers' import { UUID } from '@/types' -import { ElementDataset } from '@/types/dataset' +import { ElementDatasetSet } from '@/types/dataset' import { useNotificationStore } from '@/stores' import * as api from '@/api' @@ -10,31 +10,31 @@ interface State { /** * List of dataset that contains a specific element */ - elementDatasets: { [elementId: UUID]: ElementDataset[] } + elementDatasetSets: { [elementId: UUID]: ElementDatasetSet[] } } -interface ListElementDatasetsParameters extends PageNumberPaginationParameters { +interface ListElementSetsParameters extends PageNumberPaginationParameters { with_neighbors?: boolean } export const useElementsStore = defineStore('elements', { state: (): State => ({ - elementDatasets: {} + elementDatasetSets: {} }), actions: { - async listElementDatasets (elementId: UUID, params: ListElementDatasetsParameters = {}) { + async listElementDatasetSets (elementId: UUID, params: ListElementSetsParameters = {}) { // Avoid listing datasets twice for an element - if ((!params.page || params.page === 1) && this.elementDatasets?.[elementId]) return + if ((!params.page || params.page === 1) && this.elementDatasetSets?.[elementId]) return // List datasets containing a specific element through all pages try { - const response = await api.listElementDatasets(elementId, params) + const response = await api.listElementDatasetSets(elementId, params) // Progressively add results to the store - if (elementId in this.elementDatasets) this.elementDatasets[elementId].push(...response.results) - else this.elementDatasets[elementId] = response.results + if (elementId in this.elementDatasetSets) this.elementDatasetSets[elementId].push(...response.results) + else this.elementDatasetSets[elementId] = response.results // Follow pagination without awaiting, until we fetched all the data - if (response.next !== null) this.listElementDatasets(elementId, { ...params, page: response.number + 1 }) + if (response.next !== null) this.listElementDatasetSets(elementId, { ...params, page: response.number + 1 }) } catch (err) { useNotificationStore().notify({ type: 'error', text: errorParser(err) }) throw err diff --git a/src/types/dataset.ts b/src/types/dataset.ts index dbc43877c..166b51abe 100644 --- a/src/types/dataset.ts +++ b/src/types/dataset.ts @@ -3,12 +3,17 @@ import { UUID, ElementList } from '@/types' export type DatasetState = keyof typeof DATASET_STATES +export interface DatasetSet { + id: UUID + name: string +} + export interface Dataset { id: UUID state: DatasetState name: string description: string - sets: string[] + sets: DatasetSet[] set_elements: Record<string, number> | null corpus_id: UUID creator: string @@ -22,7 +27,7 @@ export interface DatasetElementList { element: ElementList } -export interface ElementDataset { +export interface ElementDatasetSet { dataset: Dataset set: string previous?: UUID | null @@ -32,5 +37,5 @@ export interface ElementDataset { export interface ProcessDataset { id: UUID, dataset: Dataset, - sets: string[] + set_name: string } diff --git a/src/views/Dataset/Details.vue b/src/views/Dataset/Details.vue index 0e3829443..88f9b13b9 100644 --- a/src/views/Dataset/Details.vue +++ b/src/views/Dataset/Details.vue @@ -35,13 +35,13 @@ <ul> <li v-for="set in dataset.sets" - :key="set" - :class="set === selectedSet ? 'is-active' : ''" + :key="set.id" + :class="set.id === selectedSet?.id ? 'is-active' : ''" > <a v-on:click="selectSet(set)"> - {{ set }} + {{ set.name }} <span class="tag is-rounded ml-1" :class="{ 'loader': dataset.set_elements === null }"> - {{ dataset.set_elements?.[set] ?? '—' }} + {{ dataset.set_elements?.[set.name] ?? '—' }} </span> </a> </li> @@ -50,18 +50,18 @@ <KeepAlive v-for="set in dataset.sets" - :key="set" + :key="set.id" > <ElementList - v-if="set === selectedSet" + v-if="set.id === selectedSet?.id" disabled max-size - :elements="datasetSetElements(set)" + :elements="datasetSetElements(set.name)" :dataset="dataset" :set="set" > <template v-slot:no-results> - <div v-if="!elementsLoading[set]" class="notification is-warning"> + <div v-if="!elementsLoading[set.name]" class="notification is-warning"> <span>No elements to display.</span> </div> <div v-else> @@ -73,11 +73,11 @@ </template> </ElementList> </KeepAlive> - <div v-if="elementsLoading[selectedSet]" class="loader"></div> + <div v-if="selectedSet && elementsLoading[selectedSet.name]" class="loader"></div> <button - v-else-if="(datasetStats[selectedSet] ?? Infinity) > datasetSetElements(selectedSet).length" + v-else-if=" selectedSet && (datasetStats[selectedSet.name] ?? Infinity) > datasetSetElements(selectedSet.name).length" class="button" - v-on:click="next(selectedSet)" + v-on:click="next(selectedSet.name)" > Load more… </button> @@ -120,6 +120,7 @@ import { useDatasetStore, useNotificationStore } from '@/stores' import ElementList from '@/components/Navigation/ElementList.vue' import StateTag from '@/components/Corpus/Datasets/StateTag.vue' import ItemId from '@/components/ItemId.vue' +import { DatasetSet } from '@/types/dataset' export default defineComponent({ mixins: [ @@ -140,7 +141,7 @@ export default defineComponent({ } }, data: () => ({ - selectedSet: '', + selectedSet: null as (DatasetSet | null), md: new MarkdownIt({ breaks: true }), confirmationClone: false, loading: false, @@ -149,7 +150,7 @@ export default defineComponent({ cloneLoading: false }), mounted () { - Mousetrap.bind('m', () => { this.next(this.selectedSet) }) + Mousetrap.bind('m', () => { if (this.selectedSet) this.next(this.selectedSet.name) }) }, beforeUnmount () { Mousetrap.unbind('m') @@ -188,19 +189,19 @@ export default defineComponent({ this.loading = false } }, - async next (set: string) { - if (this.elementsLoading[set]) return - this.elementsLoading[set] = true + async next (setName: string) { + if (this.elementsLoading[setName]) return + this.elementsLoading[setName] = true try { - await this.nextDatasetElements(this.datasetId, set) + await this.nextDatasetElements(this.datasetId, setName) } catch (err) { this.notify({ type: 'error', text: errorParser(err) }) } finally { - this.elementsLoading[set] = false + this.elementsLoading[setName] = false } }, - selectSet (set: string) { - if (this.selectedSet === set) return + selectSet (set: DatasetSet) { + if (this.selectedSet?.id === set.id) return this.selectedSet = set }, async clone () { @@ -215,8 +216,8 @@ export default defineComponent({ } }, // Elements from the current pagination. Defaults to an empty list - datasetSetElements (set: string) { - return (this.datasetElementPagination[this.datasetId]?.[set]?.datasetElements ?? []).map(({ element }) => element) + datasetSetElements (setName: string) { + return (this.datasetElementPagination[this.datasetId]?.[setName]?.datasetElements ?? []).map(({ element }) => element) } }, watch: { @@ -230,8 +231,8 @@ export default defineComponent({ selectedSet: { immediate: true, handler (newValue) { - if (!newValue || this.datasetElementPagination[this.datasetId]?.[newValue]) return - this.next(newValue) + if (!newValue || this.datasetElementPagination[this.datasetId]?.[newValue.name]) return + this.next(newValue.name) } } } diff --git a/src/views/Process/Configure.vue b/src/views/Process/Configure.vue index 6664410bc..8ec8aa89f 100644 --- a/src/views/Process/Configure.vue +++ b/src/views/Process/Configure.vue @@ -310,7 +310,7 @@ export default { ...mapVuexGetters('auth', ['hasFeature']), ...mapVuexState('process', [ 'processes', - 'processDatasets', + 'processSets', 'processWorkerRuns' ]), ...mapState(usePonosStore, ['farms']), @@ -344,7 +344,7 @@ export default { * @type {boolean} */ hasRequiredDatasets () { - return this.process?.mode !== 'dataset' || !Array.isArray(this.processDatasets[this.id]) || this.processDatasets[this.id].some(({ dataset }) => dataset.corpus_id === this.process.corpus) + return this.process?.mode !== 'dataset' || !Array.isArray(this.processSets[this.id]) || this.processSets[this.id].some(({ dataset }) => dataset.corpus_id === this.process.corpus) }, canRun () { return this.hasWorkerRuns && this.hasRequiredDatasets diff --git a/src/views/Process/Datasets.vue b/src/views/Process/Datasets.vue index eb6328fbc..80d3c3c83 100644 --- a/src/views/Process/Datasets.vue +++ b/src/views/Process/Datasets.vue @@ -2,8 +2,8 @@ <main class="container is-fluid"> <div v-if="processLoading" class="loading-content loader"></div> <template v-else-if="process.id"> - <h1 v-if="isConfigurable" class="title">Select process datasets</h1> - <h1 v-else-if="isReadOnly" class="title">Process datasets</h1> + <h1 v-if="isConfigurable" class="title">Select process dataset sets</h1> + <h1 v-else-if="isReadOnly" class="title">Process dataset sets</h1> <h2 v-if="isReadOnly" class="subtitle"> <div v-if="process.name"> Process {{ process.name }} @@ -21,16 +21,16 @@ <tr> <th class="shrink">ID</th> <th>Project</th> - <th>Name</th> - <th>Sets</th> - <th>State</th> + <th>Dataset</th> + <th>Set</th> + <th>Dataset state</th> <th v-if="!isReadOnly" class="shrink">Actions</th> </tr> - <DatasetRow - v-for="dataset in datasets" - :key="dataset.id" - :process-dataset="dataset" + <DatasetSetRow + v-for="datasetSet in datasetSets" + :key="datasetSet.id" + :process-set="datasetSet" :process-id="id" :read-only="isReadOnly" /> @@ -41,7 +41,7 @@ <div class="has-text-right"> <router-link :class="routerLinkClass" - :to="datasetsLoading ? '' : { name: 'process-configure', params: { id } }" + :to="setsLoading ? '' : { name: 'process-configure', params: { id } }" > {{ configureButtonText }} <i class="icon-arrow-right"></i> @@ -62,7 +62,7 @@ import { truncateMixin, corporaMixin } from '@/mixins' import { errorParser } from '@/helpers' import AddForm from '@/components/Process/Datasets/AddForm.vue' -import DatasetRow from '@/components/Process/Datasets/Row.vue' +import DatasetSetRow from '@/components/Process/Datasets/Row.vue' import ItemId from '@/components/ItemId.vue' export default { @@ -72,7 +72,7 @@ export default { ], components: { AddForm, - DatasetRow, + DatasetSetRow, ItemId }, props: { @@ -83,7 +83,7 @@ export default { }, data: () => ({ processLoading: false, - datasetsLoading: false, + setsLoading: false, nameFilter: '', typeFilter: '', classFilter: '' @@ -98,7 +98,7 @@ export default { /* * This process has started: replace this route with the process status page, * unless we were coming from the status page or one of the process configuration pages, or this is a dataset process, - * in which case we show the dataset selection page in read-only mode. + * in which case we show the dataset set selection page in read-only mode. */ if (this.process.mode === 'dataset') { this.list() @@ -113,12 +113,12 @@ export default { } }, computed: { - ...mapState('process', ['processes', 'processDatasets']), + ...mapState('process', ['processes', 'processSets']), process () { return this.processes[this.id] ?? {} }, - datasets () { - return this.processDatasets[this.id] + datasetSets () { + return this.processSets[this.id] }, isConfigurable () { return this.process?.mode === 'dataset' && !this.process?.started @@ -130,7 +130,7 @@ export default { return this.process.corpus }, routerLinkClass () { - if (this.datasetsLoading) return 'button is-primary configure is-loading' + if (this.setsLoading) return 'button is-primary configure is-loading' return 'button is-primary configure' }, configureButtonText () { @@ -139,7 +139,7 @@ export default { } }, methods: { - ...mapActions('process', ['retrieveProcess', 'listProcessDatasets']), + ...mapActions('process', ['retrieveProcess', 'listProcessSets']), ...mapMutations('notifications', ['notify']), async getProcess () { // Retrieve the process if it is not present nor complete in the store @@ -154,13 +154,13 @@ export default { } }, async list () { - this.datasetsLoading = true + this.setsLoading = true try { - this.listProcessDatasets({ processId: this.id }) + this.listProcessSets({ processId: this.id }) } catch (err) { - this.notify({ type: 'error', text: `An error occurred while listing process datasets: ${errorParser(err)}` }, { root: true }) + this.notify({ type: 'error', text: `An error occurred while listing process sets: ${errorParser(err)}` }, { root: true }) } finally { - this.datasetsLoading = false + this.setsLoading = false } } } diff --git a/tests/unit/store/process.spec.js b/tests/unit/store/process.spec.js index 69c309493..00bd42455 100644 --- a/tests/unit/store/process.spec.js +++ b/tests/unit/store/process.spec.js @@ -102,52 +102,84 @@ describe('process', () => { }) }) - describe('setProcessDatasets', () => { - it('sets datasets on a process', () => { + describe('setProcessSets', () => { + it('sets dataset sets on a process', () => { const state = { - processDatasets: { + processSets: { process2: [ - { id: 'datasetid' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + } ] } } - const datasets = [ - { id: 'dataset1' }, - { id: 'dataset2' } + const processSets = [ + { + id: 'processsetid2', + dataset: { id: 'datasetid' }, + set_name: 'validation' + }, + { + id: 'processsetid3', + dataset: { id: 'datasetid2' }, + set_name: 'test' + } ] - mutations.setProcessDatasets(state, { + mutations.setProcessSets(state, { processId: 'process1', - results: datasets + results: processSets }) assert.deepStrictEqual(state, { - processDatasets: { - process1: datasets, + processSets: { + process1: processSets, process2: [ - { id: 'datasetid' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + } ] } }) }) - it('adds datasets to an existing list of process datasets', () => { + it('adds sets to an existing list of process sets', () => { const state = { - processDatasets: { + processSets: { process1: [ - { id: 'dataset1' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + } ] } } - mutations.setProcessDatasets(state, { + mutations.setProcessSets(state, { processId: 'process1', results: [ - { id: 'dataset2' } + { + id: 'processsetid2', + dataset: { id: 'datasetid' }, + set_name: 'validation' + } ] }) assert.deepStrictEqual(state, { - processDatasets: { + processSets: { process1: [ - { id: 'dataset1' }, - { id: 'dataset2' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + }, + { + id: 'processsetid2', + dataset: { id: 'datasetid' }, + set_name: 'validation' + } ] } }) @@ -155,66 +187,53 @@ describe('process', () => { it('deduplicates', () => { const state = { - processDatasets: { + processSets: { process1: [ - { id: 'dataset1' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + } ] } } - mutations.setProcessDatasets(state, { + mutations.setProcessSets(state, { processId: 'process1', results: [ - { id: 'dataset1' }, - { id: 'dataset2' }, - { id: 'dataset1' }, - { id: 'dataset2' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + }, + { + id: 'processsetid2', + dataset: { id: 'datasetid' }, + set_name: 'validation' + }, + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + }, + { + id: 'processsetid2', + dataset: { id: 'datasetid' }, + set_name: 'validation' + } ] }) assert.deepStrictEqual(state, { - processDatasets: { + processSets: { process1: [ - { id: 'dataset1' }, - { id: 'dataset2' } - ] - } - }) - }) - }) - - describe('updateSingleProcessDataset', () => { - it('updates a single dataset on a process', () => { - const state = { - processDatasets: { - processid: [ { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test', 'validation', 'train'] - } - ] - } - } - mutations.updateSingleProcessDataset(state, { - processId: 'processid', - data: { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test'] - } - }) - assert.deepStrictEqual(state, { - processDatasets: { - processid: [ + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + }, { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test'] + id: 'processsetid2', + dataset: { id: 'datasetid' }, + set_name: 'validation' } ] } @@ -222,45 +241,65 @@ describe('process', () => { }) }) - describe('removeProcessDataset', () => { - it('removes a dataset from a process', () => { + describe('removeProcessSet', () => { + it('removes a set from a process', () => { const state = { - processDatasets: { + processSets: { process1: [ - { id: 'processdataset1' }, - { id: 'processdataset2' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + }, + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'validation' + } ] } } - mutations.removeProcessDataset(state, { + mutations.removeProcessSet(state, { processId: 'process1', - processDatasetId: 'processdataset1' + processSetId: 'processsetid' }) assert.deepStrictEqual(state, { - processDatasets: { + processSets: { process1: [ - { id: 'processdataset2' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'validation' + } ] } }) }) - it('ignores a dataset that is already removed', () => { + it('ignores a set that is already removed', () => { const state = { - processDatasets: { + processSets: { process1: [ - { id: 'processdataset2' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'validation' + } ] } } - mutations.removeProcessDataset(state, { + mutations.removeProcessSet(state, { processId: 'process1', - processDatasetId: 'processdataset1' + processSetId: 'processsetid2' }) assert.deepStrictEqual(state, { - processDatasets: { + processSets: { process1: [ - { id: 'processdataset2' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'validation' + } ] } }) @@ -648,9 +687,13 @@ describe('process', () => { processWorkerRuns: workerRunsSample, processes: processesSample, processElementsPage: processElementsSample, - processDatasets: { + processSets: { processid: [ - { id: 'datasetid' } + { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' + } ] }, tasks: { [taskSample.id]: taskSample }, @@ -660,7 +703,7 @@ describe('process', () => { mutations.reset(state) assert.deepStrictEqual(state, { processWorkerRuns: {}, - processDatasets: {}, + processSets: {}, processes: {}, processPage: {}, processElementsPage: {}, @@ -1162,111 +1205,114 @@ describe('process', () => { }) }) - describe('listProcessDatasets', () => { - it('lists datasets on a process', async () => { - const dataset1 = { - id: 'dataset1', - name: 'The Chosen One' + describe('listProcessSets', () => { + it('lists sets on a process', async () => { + const set1 = { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' } - const dataset2 = { - id: 'dataset2', - name: 'Plan B' + const set2 = { + id: 'processsetid2', + dataset: { id: 'datasetid' }, + set_name: 'validation' } - mock.onGet('/process/processid/datasets/', { params: { page: 1 } }).reply(200, { + mock.onGet('/process/processid/sets/', { params: { page: 1 } }).reply(200, { count: 2, previous: null, - next: '/process/processid/datasets/?page=2', + next: '/process/processid/sets/?page=2', number: 1, - results: [dataset1] + results: [set1] }) - mock.onGet('/process/processid/datasets/', { params: { page: 2 } }).reply(200, { + mock.onGet('/process/processid/sets/', { params: { page: 2 } }).reply(200, { count: 2, - previous: '/process/processid/datasets/?page=1', + previous: '/process/processid/sets/?page=1', next: null, number: 2, - results: [dataset2] + results: [set2] }) - await store.dispatch('process/listProcessDatasets', { processId: 'processid' }) + await store.dispatch('process/listProcessSets', { processId: 'processid' }) assert.deepStrictEqual(store.history, [ { - action: 'process/listProcessDatasets', + action: 'process/listProcessSets', payload: { processId: 'processid' } }, { - mutation: 'process/setProcessDatasets', + mutation: 'process/setProcessSets', payload: { processId: 'processid', results: [ - dataset1 + set1 ] } }, { - action: 'process/listProcessDatasets', + action: 'process/listProcessSets', payload: { processId: 'processid', page: 2 } }, { - mutation: 'process/setProcessDatasets', + mutation: 'process/setProcessSets', payload: { processId: 'processid', results: [ - dataset2 + set2 ] } } ]) - assert.deepStrictEqual(store.state.process.processDatasets, { + assert.deepStrictEqual(store.state.process.processSets, { processid: [ - dataset1, - dataset2 + set1, + set2 ] }) }) it('detects pagination errors', async () => { - const dataset1 = { - id: 'dataset1', - name: 'The Chosen One' + const set1 = { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' } // All requests on any page number will return page 1 - mock.onGet('/process/processid/datasets/').reply(200, { + mock.onGet('/process/processid/sets/').reply(200, { count: 2, previous: null, - next: '/process/processid/datasets/?page=2', + next: '/process/processid/sets/?page=2', number: 1, - results: [dataset1] + results: [set1] }) - const err = await assertRejects(async () => store.dispatch('process/listProcessDatasets', { processId: 'processid' })) + const err = await assertRejects(async () => store.dispatch('process/listProcessSets', { processId: 'processid' })) - assert.strictEqual(err.toString(), 'Error: Pagination failed while listing datasets for process "processid"') + assert.strictEqual(err.toString(), 'Error: Pagination failed while listing sets for process "processid"') assert.deepStrictEqual(store.history, [ { - action: 'process/listProcessDatasets', + action: 'process/listProcessSets', payload: { processId: 'processid' } }, { - mutation: 'process/setProcessDatasets', + mutation: 'process/setProcessSets', payload: { processId: 'processid', results: [ - dataset1 + set1 ] } }, { - action: 'process/listProcessDatasets', + action: 'process/listProcessSets', payload: { processId: 'processid', page: 2 @@ -1274,262 +1320,196 @@ describe('process', () => { } ]) - assert.deepStrictEqual(store.state.process.processDatasets, { + assert.deepStrictEqual(store.state.process.processSets, { processid: [ - dataset1 + set1 ] }) }) it('throws other errors', async () => { - const dataset1 = { - id: 'dataset1', - name: 'The Chosen One' + const set1 = { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' } - mock.onGet('/process/processid/datasets/', { params: { page: 1 } }).reply(200, { + mock.onGet('/process/processid/sets/', { params: { page: 1 } }).reply(200, { count: 2, previous: null, - next: '/process/processid/datasets/?page=2', + next: '/process/processid/sets/?page=2', number: 1, - results: [dataset1] + results: [set1] }) - mock.onGet('/process/processid/datasets/', { params: { page: 2 } }).reply(418) + mock.onGet('/process/processid/sets/', { params: { page: 2 } }).reply(418) - const err = await assertRejects(async () => store.dispatch('process/listProcessDatasets', { processId: 'processid' })) + const err = await assertRejects(async () => store.dispatch('process/listProcessSets', { processId: 'processid' })) assert.strictEqual(err.toString(), 'Error: Request failed with status code 418') assert.deepStrictEqual(store.history, [ { - action: 'process/listProcessDatasets', + action: 'process/listProcessSets', payload: { processId: 'processid' } }, { - mutation: 'process/setProcessDatasets', + mutation: 'process/setProcessSets', payload: { processId: 'processid', results: [ - dataset1 + set1 ] } }, { - action: 'process/listProcessDatasets', + action: 'process/listProcessSets', payload: { processId: 'processid', page: 2 } + }, + { + mutation: 'notifications/notify', + payload: { + text: 'Request failed with status code 418', + type: 'error' + } } ]) - assert.deepStrictEqual(store.state.process.processDatasets, { + assert.deepStrictEqual(store.state.process.processSets, { processid: [ - dataset1 + set1 ] }) }) - it('does not list datasets if they were already listed', async () => { + it('does not list sets if they were already listed', async () => { // The array exists for this process, meaning a list already ran - store.state.process.processDatasets.processid = [] + store.state.process.processSets.processid = [] - await store.dispatch('process/listProcessDatasets', { processId: 'processid' }) + await store.dispatch('process/listProcessSets', { processId: 'processid' }) assert.deepStrictEqual(store.history, [ { - action: 'process/listProcessDatasets', + action: 'process/listProcessSets', payload: { processId: 'processid' } } ]) - assert.deepStrictEqual(store.state.process.processDatasets, { + assert.deepStrictEqual(store.state.process.processSets, { processid: [] }) }) }) - describe('createProcessDataset', () => { - it('adds a dataset to a process', async () => { - mock.onPost('/process/processid/dataset/datasetid/').reply(201, { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test', 'validation'] + describe('createProcessSet', () => { + it('adds a dataset set to a process', async () => { + mock.onPost('/process/processid/set/setid/').reply(201, { + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' }) - await store.dispatch('process/createProcessDataset', { + await store.dispatch('process/createProcessSet', { processId: 'processid', - dataset: { id: 'datasetid' } + setId: 'setid' }) assert.deepStrictEqual(store.history, [ { - action: 'process/createProcessDataset', + action: 'process/createProcessSet', payload: { processId: 'processid', - dataset: { id: 'datasetid' } + setId: 'setid' } }, { - mutation: 'process/setProcessDatasets', + mutation: 'process/setProcessSets', payload: { processId: 'processid', results: [ { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test', 'validation'] + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' } ] } } ]) - assert.deepStrictEqual(store.state.process.processDatasets, { + assert.deepStrictEqual(store.state.process.processSets, { processid: [ { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test', 'validation'] + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' } ] }) }) }) - describe('updateProcessDataset', () => { - it('updates a process dataset', async () => { - store.state.process.processDatasets = { - processid: [ - { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['train', 'validation', 'test'] - } - ] - } - - mock.onPatch('/process/processid/dataset/datasetid/').reply(201, { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test'] - }) - - await store.dispatch('process/updateProcessDataset', { - processId: 'processid', - datasetId: 'datasetid', - sets: ['test'] - }) - - assert.deepStrictEqual(store.history, [ + describe('deleteProcessSet', () => { + it('removes a set from a process', async () => { + store.state.process.processSets.processid = [ { - action: 'process/updateProcessDataset', - payload: { - processId: 'processid', - datasetId: 'datasetid', - sets: ['test'] - } - }, - { - mutation: 'process/updateSingleProcessDataset', - payload: { - processId: 'processid', - data: { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test'] - } - } - } - ]) - - assert.deepStrictEqual(store.state.process.processDatasets, { - processid: [ - { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test'] - } - ] - }) - }) - }) - - describe('deleteProcessDataset', () => { - it('removes a dataset from a process', async () => { - store.state.process.processDatasets.processid = [ - { - id: 'processdatasetid', - dataset: { - id: 'datasetid' - }, - sets: ['test', 'validation'] + id: 'processsetid', + dataset: { id: 'datasetid' }, + set_name: 'test' } ] - mock.onDelete('/process/processid/dataset/datasetid/').reply(201) + mock.onDelete('/process/processid/set/setid/').reply(201) - await store.dispatch('process/deleteProcessDataset', { + await store.dispatch('process/deleteProcessSet', { processId: 'processid', - datasetId: 'datasetid', - processDatasetId: 'processdatasetid' + setId: 'setid', + processSetId: 'processsetid' }) assert.deepStrictEqual(store.history, [ { - action: 'process/deleteProcessDataset', + action: 'process/deleteProcessSet', payload: { processId: 'processid', - datasetId: 'datasetid', - processDatasetId: 'processdatasetid' + setId: 'setid', + processSetId: 'processsetid' } }, { - mutation: 'process/removeProcessDataset', + mutation: 'process/removeProcessSet', payload: { processId: 'processid', - processDatasetId: 'processdatasetid' + processSetId: 'processsetid' } } ]) - assert.deepStrictEqual(store.state.process.processDatasets, { + assert.deepStrictEqual(store.state.process.processSets, { processid: [] }) }) it('notifies of errors', async () => { - mock.onDelete('/process/processid/dataset/datasetid/').reply(418) + mock.onDelete('/process/processid/set/setid/').reply(418) - const err = await assertRejects(async () => store.dispatch('process/deleteProcessDataset', { + const err = await assertRejects(async () => store.dispatch('process/deleteProcessSet', { processId: 'processid', - datasetId: 'datasetid' + setId: 'setid' })) assert.strictEqual(err.toString(), 'Error: Request failed with status code 418') assert.deepStrictEqual(store.history, [ { - action: 'process/deleteProcessDataset', + action: 'process/deleteProcessSet', payload: { processId: 'processid', - datasetId: 'datasetid' + setId: 'setid' } }, { diff --git a/tests/unit/stores/datasets.spec.js b/tests/unit/stores/datasets.spec.js index b379601c7..974816302 100644 --- a/tests/unit/stores/datasets.spec.js +++ b/tests/unit/stores/datasets.spec.js @@ -41,14 +41,40 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] }, { id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', state: 'open', name: 'Mobile Suit Gundam Wing', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] } ] }, @@ -62,7 +88,24 @@ describe('datasets', () => { state: 'open', name: 'Sailor Moon', description: 'Moon prism power, make up!', - sets: ['neptune', 'uranus', 'pluto', 'saturn'] + sets: [ + { + name: 'neptune', + id: 'setid7' + }, + { + name: 'uranus', + id: 'setid8' + }, + { + name: 'pluto', + id: 'setid9' + }, + { + name: 'saturn', + id: 'setid10' + } + ] } ] } @@ -86,21 +129,64 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] }, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb': { id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', state: 'open', name: 'Mobile Suit Gundam Wing', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] }, 'cccccccc-cccc-cccc-cccc-cccccccccccc': { id: 'cccccccc-cccc-cccc-cccc-cccccccccccc', state: 'open', name: 'Sailor Moon', description: 'Moon prism power, make up!', - sets: ['neptune', 'uranus', 'pluto', 'saturn'] + sets: [ + { + name: 'neptune', + id: 'setid7' + }, + { + name: 'uranus', + id: 'setid8' + }, + { + name: 'pluto', + id: 'setid9' + }, + { + name: 'saturn', + id: 'setid10' + } + ] } }) }) @@ -120,20 +206,63 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] }, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb': { id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', state: 'open', name: 'Mobile Suit Gundam Wing', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] } } const requestData = { name: 'Sailor Moon', description: 'Moon prism power, make up!', - sets: ['neptune', 'uranus', 'pluto', 'saturn'] + sets: [ + { + name: 'neptune', + id: 'setid7' + }, + { + name: 'uranus', + id: 'setid8' + }, + { + name: 'pluto', + id: 'setid9' + }, + { + name: 'saturn', + id: 'setid10' + } + ] } const responseData = { ...requestData, @@ -150,21 +279,64 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] }, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb': { id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', state: 'open', name: 'Mobile Suit Gundam Wing', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] }, 'cccccccc-cccc-cccc-cccc-cccccccccccc': { id: 'cccccccc-cccc-cccc-cccc-cccccccccccc', state: 'open', name: 'Sailor Moon', description: 'Moon prism power, make up!', - sets: ['neptune', 'uranus', 'pluto', 'saturn'] + sets: [ + { + name: 'neptune', + id: 'setid7' + }, + { + name: 'uranus', + id: 'setid8' + }, + { + name: 'pluto', + id: 'setid9' + }, + { + name: 'saturn', + id: 'setid10' + } + ] } }) assert.deepStrictEqual(store.corpusDatasets, { @@ -202,14 +374,40 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] }, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb': { id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', state: 'open', name: 'Mobile Suit Gundam Wing', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] } } store.corpusDatasets = { @@ -223,7 +421,20 @@ describe('datasets', () => { state: 'open', name: 'Pretty Guardian Sailor Moon', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] } mock.onPatch('/datasets/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/', { name: 'Pretty Guardian Sailor Moon' }).reply(200, { ...responseData }) @@ -240,14 +451,40 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] }, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb': { id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', state: 'open', name: 'Pretty Guardian Sailor Moon', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] } }) assert.deepStrictEqual(store.corpusDatasets, { @@ -284,14 +521,40 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] }, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb': { id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', state: 'open', name: 'Mobile Suit Gundam Wing', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] } } store.corpusDatasets = { @@ -313,7 +576,20 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] } }) assert.deepStrictEqual(store.corpusDatasets, { @@ -341,16 +617,15 @@ describe('datasets', () => { 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) + mock.onPost('/corpus/corpusid/datasets/selection/', { set_id: 'setid' }).reply(204) - const params = { dataset_id: 'datasetid', set: 'test' } - await store.addDatasetElementsSelection('corpusid', params) + await store.addDatasetElementsSelection('corpusid', 'datasetid', { id: 'setid', name: 'setname' }) assert.deepStrictEqual(notificationStore.notifications, [ { id: 0, type: 'success', - text: 'Elements have been added to the dataset.' + text: 'Elements have been added to the dataset set.' } ]) }) @@ -369,9 +644,8 @@ describe('datasets', () => { } } - const params = { dataset_id: 'datasetid', set: 'test' } - mock.onPost('/corpus/corpusid/datasets/selection/', params).reply(204) - await store.addDatasetElementsSelection('corpusid', params) + mock.onPost('/corpus/corpusid/datasets/selection/', { set_id: 'setid' }).reply(204) + await store.addDatasetElementsSelection('corpusid', 'datasetid', { id: 'setid', name: 'test' }) assert.deepStrictEqual(store.datasetElementPagination, { datasetid: { @@ -384,11 +658,10 @@ describe('datasets', () => { }) it('throw errors', async () => { - mock.onPost('/corpus/corpusid/datasets/selection/', { dataset_id: 'datasetid', set: 'test' }).reply(400) + mock.onPost('/corpus/corpusid/datasets/selection/', { set_id: 'setid' }).reply(400) - const params = { dataset_id: 'datasetid', set: 'test' } await assertRejects( - async () => await store.addDatasetElementsSelection('corpusid', params) + async () => await store.addDatasetElementsSelection('corpusid', 'datasetid', { id: 'setid', name: 'setname' }) ) assert.deepStrictEqual(notificationStore.notifications, [ @@ -409,21 +682,64 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] }, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb': { id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', state: 'open', name: 'Mobile Suit Gundam Wing', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] }, 'cccccccc-cccc-cccc-cccc-cccccccccccc': { id: 'cccccccc-cccc-cccc-cccc-cccccccccccc', state: 'open', name: 'Sailor Moon', description: 'Moon prism power, make up!', - sets: ['neptune', 'uranus', 'pluto', 'saturn'] + sets: [ + { + name: 'neptune', + id: 'setid7' + }, + { + name: 'uranus', + id: 'setid8' + }, + { + name: 'pluto', + id: 'setid9' + }, + { + name: 'saturn', + id: 'setid10' + } + ] } } store.corpusDatasets = { @@ -440,14 +756,44 @@ describe('datasets', () => { state: 'open', name: 'Mobile Suit Gundam Wing', description: 'another dataset', - sets: ['training', 'test', 'validation'] + sets: [ + { + name: 'training', + id: 'setid4' + }, + { + name: 'test', + id: 'setid5' + }, + { + name: 'validation', + id: 'setid6' + } + ] }, { id: 'cccccccc-cccc-cccc-cccc-cccccccccccc', state: 'open', name: 'Sailor Moon', description: 'Moon prism power, make up!', - sets: ['neptune', 'uranus', 'pluto', 'saturn'] + sets: [ + { + name: 'neptune', + id: 'setid7' + }, + { + name: 'uranus', + id: 'setid8' + }, + { + name: 'pluto', + id: 'setid9' + }, + { + name: 'saturn', + id: 'setid10' + } + ] } ]) }) @@ -459,7 +805,20 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] } } store.corpusDatasets = { @@ -668,7 +1027,7 @@ describe('datasets', () => { } } } - elementsStore.elementDatasets = { + elementsStore.elementDatasetSets = { 2: [ { set: 'unit-00', @@ -677,7 +1036,20 @@ describe('datasets', () => { state: 'open', name: 'Shin Seiki Evangelion', description: 'Become legend, young boy!', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] } }, { @@ -687,7 +1059,20 @@ describe('datasets', () => { state: 'open', name: 'Set 01', description: 'A', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] } } ] @@ -706,7 +1091,7 @@ describe('datasets', () => { } } }) - assert.deepStrictEqual(elementsStore.elementDatasets, { + assert.deepStrictEqual(elementsStore.elementDatasetSets, { 2: [ { set: 'unit-01', @@ -715,7 +1100,20 @@ describe('datasets', () => { state: 'open', name: 'Set 01', description: 'A', - sets: ['unit-00', 'unit-01', 'unit-02'] + sets: [ + { + name: 'unit-00', + id: 'setid1' + }, + { + name: 'unit-01', + id: 'setid2' + }, + { + name: 'unit-02', + id: 'setid3' + } + ] } } ] diff --git a/tests/unit/stores/elements.spec.js b/tests/unit/stores/elements.spec.js index 4e9e8f349..0f40024d5 100644 --- a/tests/unit/stores/elements.spec.js +++ b/tests/unit/stores/elements.spec.js @@ -27,10 +27,10 @@ describe('elements.ts', () => { }) describe('actions', () => { - describe('listElementDatasets', () => { + describe('listElementDatasetSets', () => { it('returns early in case datasets have been fetched', async () => { - store.elementDatasets = { element_id: [] } - await store.listElementDatasets('element_id') + store.elementDatasetSets = { element_id: [] } + await store.listElementDatasetSets('element_id') await store.actionsCompleted() }) @@ -38,7 +38,7 @@ describe('elements.ts', () => { const pagination1 = { count: 2, previous: null, - next: '/element/element_id/datasets/?page=2', + next: '/element/element_id/sets/?page=2', number: 1, results: [elementDatasetsSample.results[0]] } @@ -49,13 +49,13 @@ describe('elements.ts', () => { number: 2, results: [elementDatasetsSample.results[1]] } - mock.onGet('/element/element_id/datasets/', { params: {} }).reply(200, pagination1) - mock.onGet('/element/element_id/datasets/', { params: { page: 2 } }).reply(200, pagination2) + mock.onGet('/element/element_id/sets/', { params: {} }).reply(200, pagination1) + mock.onGet('/element/element_id/sets/', { params: { page: 2 } }).reply(200, pagination2) - await store.listElementDatasets('element_id') + await store.listElementDatasetSets('element_id') await store.actionsCompleted() - assert.deepStrictEqual(store.elementDatasets, { + assert.deepStrictEqual(store.elementDatasetSets, { element_id: [ elementDatasetsSample.results[0], elementDatasetsSample.results[1] @@ -64,10 +64,10 @@ describe('elements.ts', () => { }) it('handles errors', async () => { - mock.onGet('/element/element_id/datasets/').reply(403, { detail: ['Forbidden'] }) + mock.onGet('/element/element_id/sets/').reply(403, { detail: ['Forbidden'] }) await assertRejects(async () => - await store.listElementDatasets('element_id') + await store.listElementDatasetSets('element_id') ) assert.deepStrictEqual(notificationStore.notifications, [ { id: 0, type: 'error', text: 'Forbidden' } -- GitLab