From b2aa2313b2b67dfb02a262aa895755a3563b7b4f Mon Sep 17 00:00:00 2001 From: Theo Lesage <tlesage@teklia.com> Date: Wed, 29 May 2024 07:50:40 +0000 Subject: [PATCH] Create entity type store --- src/api/entity.ts | 17 +- src/api/entityType.ts | 19 + src/api/index.ts | 1 + .../Corpus/EntityType/CreateForm.vue | 43 ++- src/components/Corpus/EntityType/List.vue | 25 +- src/components/Corpus/EntityType/Row.vue | 82 +++-- src/store/corpora.js | 80 +--- src/stores/entityTypes.ts | 67 ++++ src/stores/index.ts | 1 + tests/unit/store/corpora.spec.js | 347 ------------------ tests/unit/stores/entitytypes.spec.js | 260 +++++++++++++ 11 files changed, 449 insertions(+), 493 deletions(-) create mode 100644 src/api/entityType.ts create mode 100644 src/stores/entityTypes.ts create mode 100644 tests/unit/stores/entitytypes.spec.js diff --git a/src/api/entity.ts b/src/api/entity.ts index 8eaebd209..a13d9d4fc 100644 --- a/src/api/entity.ts +++ b/src/api/entity.ts @@ -1,7 +1,7 @@ import axios from 'axios' import { PageNumberPaginationParameters, unique } from '.' import { ElementTiny, PageNumberPagination, UUID } from '@/types' -import { Entity, EntityLight, EntityType, TranscriptionEntity } from '@/types/entity' +import { Entity, EntityLight, TranscriptionEntity } from '@/types/entity' // Retrieve an entity export const retrieveEntity = unique(async (id: UUID): Promise<Entity> => (await axios.get(`/entity/${id}/`)).data) @@ -26,18 +26,3 @@ interface TranscriptionEntityListParameters extends PageNumberPaginationParamete // List all entities linked to a transcription export const listTranscriptionEntities = unique(async (id: UUID, params: TranscriptionEntityListParameters = {}): Promise<PageNumberPagination<TranscriptionEntity>> => (await axios.get(`/transcription/${id}/entities/`, { params })).data) - -// List a corpus's entity types -export const listCorpusEntityTypes = unique(async (id: UUID, params: PageNumberPaginationParameters = {}): Promise<PageNumberPagination<EntityType>> => (await axios.get(`/corpus/${id}/entity-types/`, { params })).data) - -type EntityTypeCreatePayload = Omit<EntityType, 'id'> -type EntityTypeUpdatePayload = Partial<EntityTypeCreatePayload> - -// Create a new corpus entity type -export const createCorpusEntityType = async (type: EntityTypeCreatePayload): Promise<EntityType> => (await axios.post('/entity/types/', type)).data - -// Edit a corpus entity type -export const updateCorpusEntityType = async (id: UUID, data: EntityTypeUpdatePayload): Promise<EntityType> => (await axios.patch(`/entity/types/${id}/`, data)).data - -// Delete a corpus entity type -export const deleteCorpusEntityType = unique(async (id: UUID) => (await axios.delete(`/entity/types/${id}/`)).data) diff --git a/src/api/entityType.ts b/src/api/entityType.ts new file mode 100644 index 000000000..546653167 --- /dev/null +++ b/src/api/entityType.ts @@ -0,0 +1,19 @@ +import axios from 'axios' +import { PageNumberPaginationParameters, unique } from '.' +import { PageNumberPagination, UUID } from '@/types' +import { EntityType } from '@/types/entity' + +// List a corpus's entity types +export const listCorpusEntityTypes = unique(async (id: UUID, params: PageNumberPaginationParameters = {}): Promise<PageNumberPagination<EntityType>> => (await axios.get(`/corpus/${id}/entity-types/`, { params })).data) + +type EntityTypeCreatePayload = Omit<EntityType, 'id'> +type EntityTypeUpdatePayload = Partial<EntityTypeCreatePayload> + +// Create a new corpus entity type +export const createCorpusEntityType = async (corpusId: UUID, type: EntityTypeCreatePayload): Promise<EntityType> => (await axios.post('/entity/types/', { corpus: corpusId, ...type })).data + +// Edit a corpus entity type +export const updateCorpusEntityType = async (id: UUID, data: EntityTypeUpdatePayload): Promise<EntityType> => (await axios.patch(`/entity/types/${id}/`, data)).data + +// Delete a corpus entity type +export const deleteCorpusEntityType = unique(async (id: UUID) => (await axios.delete(`/entity/types/${id}/`)).data) diff --git a/src/api/index.ts b/src/api/index.ts index f766eea1b..f0dc1548f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -9,6 +9,7 @@ export * from './dataset' export * from './element' export * from './elementType' export * from './entity' +export * from './entityType' export * from './export' export * from './files' export * from './image' diff --git a/src/components/Corpus/EntityType/CreateForm.vue b/src/components/Corpus/EntityType/CreateForm.vue index cef6809da..eb0c9cfda 100644 --- a/src/components/Corpus/EntityType/CreateForm.vue +++ b/src/components/Corpus/EntityType/CreateForm.vue @@ -4,6 +4,7 @@ <td> <input class="input is-fullwidth" + :class="{ 'is-danger': errors.name?.length > 0 }" type="text" :disabled="!canEdit || null" v-model="fields.name" @@ -15,6 +16,7 @@ <td class="shrink"> <input class="input color-picker" + :class="{ 'is-danger': errors.color?.length > 0 }" type="color" pattern="[0-9a-fA-F]{6}" :disabled="!canEdit || null" @@ -32,16 +34,23 @@ </tr> </template> -<script> +<script lang='ts'> import { corporaMixin } from '@/mixins.js' +import { mapActions } from 'pinia' +import { useEntityTypesStore, useNotificationStore } from '@/stores' +import { isAxiosError } from 'axios' +import { defineComponent, PropType } from 'vue' +import { UUID } from '@/types' +import { UUID_REGEX } from '@/config' -export default { +export default defineComponent({ mixins: [ corporaMixin ], props: { corpusId: { - type: String, + type: String as PropType<UUID>, + validator: value => typeof value === 'string' && UUID_REGEX.test(value), required: true } }, @@ -51,7 +60,7 @@ export default { name: '', color: '#ff0000' }, - errors: {} + errors: {} as { [key: string]: string[] } }), computed: { canEdit () { @@ -64,28 +73,36 @@ export default { } }, methods: { + ...mapActions(useEntityTypesStore, { createEntityType: 'create' }), + ...mapActions(useNotificationStore, ['notify']), async create () { - if (!this.allowCreate) return + if (!this.allowCreate || this.loading) return this.loading = true - this.errors = {} + this.errors = { + name: [], + color: [] + } try { - await this.$store.dispatch('corpora/createCorpusEntityType', { - corpus: this.corpus.id, - name: this.fields.name, - color: this.fields.color.replace('#', '') - }) + await this.createEntityType( + this.corpus.id, + { + name: this.fields.name, + color: this.fields.color.replace('#', '') + } + ) + this.notify({ type: 'success', text: `Entity type ${this.fields.name} successfully created.` }) this.fields = { name: '', color: '#ff0000' } } catch (err) { - this.errors = err.response.data + if (isAxiosError(err) && err.response) this.errors = err.response.data } finally { this.loading = false } } } -} +}) </script> <style scoped> diff --git a/src/components/Corpus/EntityType/List.vue b/src/components/Corpus/EntityType/List.vue index 7b5c9f3e0..946debb5b 100644 --- a/src/components/Corpus/EntityType/List.vue +++ b/src/components/Corpus/EntityType/List.vue @@ -10,7 +10,7 @@ </thead> <tbody> <Row - v-for="type in corpusEntityTypes[corpusId]" + v-for="type in entityTypes[corpusId]" :key="type.name" :type="type" :corpus-id="corpusId" @@ -23,7 +23,12 @@ </template> <script> -import { mapState, mapActions, mapMutations, mapGetters } from 'vuex' +import { + mapGetters as mapVuexGetters +} from 'vuex' +import { mapActions, mapState } from 'pinia' +import { useEntityTypesStore, useNotificationStore } from '@/stores' + import { errorParser } from '@/helpers' import { corporaMixin } from '@/mixins.js' import Row from './Row' @@ -47,20 +52,20 @@ export default { loading: false }), computed: { - ...mapState('corpora', ['corpusEntityTypes']), - ...mapGetters('auth', ['isVerified']), + ...mapState(useEntityTypesStore, ['entityTypes']), + ...mapVuexGetters('auth', ['isVerified']), hasAdminPrivilege () { return this.isVerified && this.corpus && this.canAdmin(this.corpus) } }, methods: { - ...mapActions('corpora', ['listCorpusEntityTypes']), - ...mapMutations('notifications', ['notify']), - async listEntityTypes () { - if (this.corpusEntityTypes[this.corpusId]) return + ...mapActions(useEntityTypesStore, { listEntityTypes: 'list' }), + ...mapActions(useNotificationStore, ['notify']), + async list () { + if (this.loading || this.entityTypes[this.corpusId]) return this.loading = true try { - await this.listCorpusEntityTypes({ corpusId: this.corpusId }) + await this.listEntityTypes(this.corpusId) } catch (err) { this.notify({ type: 'error', text: `An error occurred listing entity types: ${errorParser(err)}` }) } finally { @@ -70,7 +75,7 @@ export default { }, watch: { corpusId: { - handler () { this.listEntityTypes() }, + handler: 'list', immediate: true } } diff --git a/src/components/Corpus/EntityType/Row.vue b/src/components/Corpus/EntityType/Row.vue index 11dc8c85c..08e418f39 100644 --- a/src/components/Corpus/EntityType/Row.vue +++ b/src/components/Corpus/EntityType/Row.vue @@ -8,8 +8,9 @@ <td> <input class="input is-fullwidth" + :class="{ 'is-danger': errors.name?.length > 0 }" type="text" - :disabled="loading || null" + :disabled="loading || undefined" required v-model="fields.name" /> @@ -20,8 +21,9 @@ <td class="shrink"> <input class="input color-picker" + :class="{ 'is-danger': errors.color?.length > 0 }" type="color" - :disabled="loading || null" + :disabled="loading || undefined" required pattern="[0-9a-fA-F]{6}" v-model="fields.color" @@ -33,7 +35,7 @@ <td> <button class="button is-success" - :disabled="!allowUpdate || null" + :disabled="!allowUpdate || undefined" v-on:click="save" > <i class="icon-check"></i> @@ -50,7 +52,7 @@ <p class="control"> <button class="button" - :disabled="!canEdit || null" + :disabled="!canEdit || undefined" v-on:click="edit" > <i class="icon-edit has-text-primary"></i> @@ -59,7 +61,7 @@ <p class="control"> <button class="button has-text-danger" - :disabled="!canEdit || null" + :disabled="!canEdit || undefined" v-on:click="destroyModal = canEdit" > <i class="icon-trash"></i> @@ -86,12 +88,19 @@ </tr> </template> -<script> +<script lang='ts'> import { corporaMixin } from '@/mixins.js' import Modal from '@/components/Modal.vue' -import ItemId from '@/components/ItemId' +import ItemId from '@/components/ItemId.vue' +import { mapActions } from 'pinia' +import { useEntityTypesStore, useNotificationStore } from '@/stores' +import { isAxiosError } from 'axios' +import { defineComponent, PropType } from 'vue' +import { UUID } from '@/types' +import { EntityType } from '@/types/entity' +import { UUID_REGEX } from '@/config' -export default { +export default defineComponent({ mixins: [ corporaMixin ], @@ -101,20 +110,23 @@ export default { }, props: { corpusId: { - type: String, + type: String as PropType<UUID>, + validator: value => typeof value === 'string' && UUID_REGEX.test(value), required: true }, type: { - type: Object, + type: Object as PropType<EntityType>, required: true } }, data: () => ({ loading: false, fields: { - id: null + id: '', + name: '', + color: '' }, - errors: {}, + errors: {} as { [key: string]: string[] }, destroyModal: false }), computed: { @@ -129,25 +141,38 @@ export default { } }, methods: { + ...mapActions(useEntityTypesStore, ['update', 'delete']), + ...mapActions(useNotificationStore, ['notify']), edit () { - this.fields = { id: this.type.id, name: this.type.name, color: `#${this.type.color}` } + this.fields = { + id: this.type.id, + name: this.type.name, + color: `#${this.type.color}` + } }, async save () { if (!this.allowUpdate) return this.loading = true - this.errors = {} + this.errors = { + name: [], + color: [] + } try { - await this.$store.dispatch('corpora/updateCorpusEntityType', { - corpusId: this.corpusId, - id: this.fields.id, - name: this.fields.name, - color: this.fields.color.replace('#', '') - }) + await this.update( + this.corpusId, + this.type.id, + { + name: this.fields.name, + color: this.fields.color.replace('#', '') + }) + this.notify({ type: 'success', text: 'Entity type successfully updated.' }) this.fields = { - id: null + id: '', + name: '', + color: '' } } catch (err) { - this.errors = err.response.data + if (isAxiosError(err) && err.response) this.errors = err.response.data } finally { this.loading = false } @@ -156,19 +181,20 @@ export default { if (!this.canEdit || this.loading) return this.loading = true try { - await this.$store.dispatch('corpora/deleteCorpusEntityType', { - corpusId: this.corpus.id, - typeId: this.type.id - }) + await this.delete( + this.corpus.id, + this.type.id + ) + this.notify({ type: 'success', text: `Entity type ${this.type.name} successfully deleted.` }) } finally { this.loading = false } }, - squareStyle (color) { + squareStyle (color: string) { return { 'background-color': `#${color}` } } } -} +}) </script> <style scoped> diff --git a/src/store/corpora.js b/src/store/corpora.js index 0088674aa..7005000cb 100644 --- a/src/store/corpora.js +++ b/src/store/corpora.js @@ -10,9 +10,7 @@ export const initialState = () => ({ * Set to true as soon as the corpora list is loaded. * Prevents trying to reload the corpora when there are none available. */ - corporaLoaded: false, - // { [corpusId]: EntityTypes[] } - corpusEntityTypes: {} + corporaLoaded: false }) const parseTypes = typeList => typeList.reduce((types, type) => { @@ -76,37 +74,6 @@ export const mutations = { assign(state, initialState()) }, - setCorpusEntityTypes (state, { corpusId, results }) { - const updatedList = state.corpusEntityTypes[corpusId] || [] - results.forEach(newType => { - // Prevent duplicating entity types - if (!updatedList.some(type => type.name === newType.name)) updatedList.push(newType) - }) - // Merge corpus entity types - state.corpusEntityTypes = { - ...state.corpusEntityTypes, - [corpusId]: updatedList - } - }, - - updateCorpusEntityType (state, { corpusId, data }) { - if (!state.corpusEntityTypes[corpusId]) throw new Error(`Entity Types for corpus ${corpusId} not found`) - const typesList = [...state.corpusEntityTypes[corpusId]] - const index = typesList.findIndex(item => item.id === data.id) - if (index < 0) throw new Error(`Entity Type ${data.id} not found in corpus ${corpusId}`) - typesList.splice(index, 1, data) - state.corpusEntityTypes[corpusId] = typesList - }, - - removeCorpusEntityType (state, { corpusId, typeId }) { - if (!state.corpusEntityTypes[corpusId]) throw new Error(`Entity Types for corpus ${corpusId} not found`) - const typesList = [...state.corpusEntityTypes[corpusId]] - const index = typesList.findIndex(item => item.id === typeId) - if (index < 0) throw new Error(`Entity Type ${typeId} not found in corpus ${corpusId}`) - typesList.splice(index, 1) - state.corpusEntityTypes[corpusId] = typesList - }, - addDefaultCorpus (state, { id, name }) { state.corpora = { ...state.corpora, @@ -185,51 +152,6 @@ export const actions = { return corpus.types[slug] }, - async listCorpusEntityTypes ({ state, commit, dispatch }, { corpusId, page = 1 }) { - // Do not start fetching corpus entity types if they have been retrieved already - if (page === 1 && state.corpusEntityTypes[corpusId]) return - const data = await api.listCorpusEntityTypes(corpusId, { page }) - commit('setCorpusEntityTypes', { corpusId, results: data.results }) - if (!data || !data.number || page !== data.number) { - // Avoid any loop - throw new Error(`Pagination failed listing entity types for corpus "${corpusId}"`) - } - // Load other pages - if (data.next) await dispatch('listCorpusEntityTypes', { corpusId, page: page + 1 }) - }, - - async createCorpusEntityType ({ commit }, { ...type }) { - try { - const data = await api.createCorpusEntityType(type) - commit('setCorpusEntityTypes', { corpusId: type.corpus, results: [data] }) - } catch (err) { - commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) - throw err - } - }, - - async updateCorpusEntityType ({ commit }, { corpusId, ...type }) { - try { - const id = type.id - delete type.id - const data = await api.updateCorpusEntityType(id, type) - commit('updateCorpusEntityType', { corpusId, data }) - } catch (err) { - commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) - throw err - } - }, - - async deleteCorpusEntityType ({ commit }, { corpusId, typeId }) { - try { - await api.deleteCorpusEntityType(typeId) - commit('removeCorpusEntityType', { corpusId, typeId }) - } catch (err) { - commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) - throw err - } - }, - async createType ({ commit }, { corpus, ...type }) { try { const data = await api.createType({ corpus, ...type }) diff --git a/src/stores/entityTypes.ts b/src/stores/entityTypes.ts new file mode 100644 index 000000000..319e06f4f --- /dev/null +++ b/src/stores/entityTypes.ts @@ -0,0 +1,67 @@ +import { errorParser } from '@/helpers' +import * as api from '@/api' +import { defineStore } from 'pinia' +import { UUID } from '@/types' +import { EntityType } from '@/types/entity' +import { useNotificationStore } from '.' + +interface State { + entityTypes: { + [corpusId: UUID]: { + [typeId: UUID]: EntityType + } + } +} + +export const useEntityTypesStore = defineStore('entityTypes', { + state: (): State => ({ + entityTypes: {} + }), + actions: { + async list (corpusId: UUID, page = 1) { + // Do not start fetching corpus entity types if they have been retrieved already + if (page === 1 && this.entityTypes[corpusId]) return + const data = await api.listCorpusEntityTypes(corpusId, { page }) + if (!this.entityTypes[corpusId]) this.entityTypes[corpusId] = {} + this.entityTypes[corpusId] = { + ...this.entityTypes[corpusId], + ...Object.fromEntries(data.results.map(type => [type.id, type])) + } + if (!data || !data.number || page !== data.number) { + // Avoid any loop + throw new Error(`Pagination failed listing entity types for project "${corpusId}"`) + } + // Continue loading pages + if (data.next) await this.list(corpusId, page + 1) + }, + async create (corpusId: UUID, type: Omit<EntityType, 'id'>) { + try { + const data = await api.createCorpusEntityType(corpusId, type) + this.entityTypes[corpusId][data.id] = data + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + throw err + } + }, + async update (corpusId: UUID, typeId: UUID, type: Omit<EntityType, 'id'>) { + try { + if (!this.entityTypes[corpusId][typeId]) throw new Error(`Entity type ${typeId} not found in project ${corpusId}.`) + const data = await api.updateCorpusEntityType(typeId, type) + this.entityTypes[corpusId][typeId] = data + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + throw err + } + }, + async delete (corpusId: UUID, typeId: UUID) { + try { + if (!this.entityTypes[corpusId][typeId]) throw new Error(`Entity type ${typeId} not found in project ${corpusId}.`) + await api.deleteCorpusEntityType(typeId) + delete this.entityTypes[corpusId][typeId] + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + throw err + } + } + } +}) diff --git a/src/stores/index.ts b/src/stores/index.ts index 75cb08816..1c32c5a04 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -19,3 +19,4 @@ export { useTranscriptionStore } from './transcription' export { useMetaDataStore } from './metadata' export { useAllowedMetaDataStore } from './allowedMetadata' export { useClassificationStore } from './classification' +export { useEntityTypesStore } from './entityTypes' diff --git a/tests/unit/store/corpora.spec.js b/tests/unit/store/corpora.spec.js index 03c14e639..93429ccfd 100644 --- a/tests/unit/store/corpora.spec.js +++ b/tests/unit/store/corpora.spec.js @@ -310,163 +310,6 @@ describe('corpora', () => { }) }) - describe('setCorpusEntityTypes', () => { - it('updates entity types on a corpus', () => { - const state = { - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' } - ] - } - } - const newEntityTypes = [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id3', name: 'type number three', color: '180cf7' }, - { id: 'id4', name: '4th type', color: 'f70cbd' } - ] - - mutations.setCorpusEntityTypes(state, { corpusId: 'corpus1', results: newEntityTypes }) - - assert.deepStrictEqual(state, { - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' }, - { id: 'id4', name: '4th type', color: 'f70cbd' } - ] - } - }) - }) - }) - - describe('updateCorpusEntityType', () => { - it('updates an Entity Type in state.store.corpusEntityTypes[corpusId]', async () => { - const state = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' } - ] - } - } - const expected = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'first type', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' } - ] - } - } - mutations.updateCorpusEntityType(state, { corpusId: 'corpus1', data: { id: 'id1', name: 'first type', color: 'ff0000' } }) - assert.deepStrictEqual(state, expected) - }) - it('throws an error if the type id is not found in the corpus Entity Types', async () => { - const state = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' } - ] - } - } - assert.throws( - () => mutations.updateCorpusEntityType(state, { corpusId: 'corpus1', data: { id: 'id3', name: 'a name', color: 'ffffff' } }), - 'Entity Type id3 not found in corpus corpus1' - ) - }) - it('throws an error if there is no state.store.corpusEntityTypes[corpusId]', async () => { - const state = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: {} - } - assert.throws( - () => mutations.updateCorpusEntityType(state, { corpusId: 'corpus1', data: { id: 'id1', name: 'first type', color: 'ff0000' } }), - 'Entity Types for corpus corpus1 not found' - ) - }) - }) - - describe('removeCorpusEntityType', () => { - it('removes an Entity Type from state.store.corpusEntityTypes[corpusId]', async () => { - const state = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' }, - { id: 'id4', name: '4th type', color: 'f70cbd' } - ] - } - } - const expected = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' }, - { id: 'id4', name: '4th type', color: 'f70cbd' } - ] - } - } - mutations.removeCorpusEntityType(state, { corpusId: 'corpus1', typeId: 'id3' }) - assert.deepStrictEqual(state, expected) - }) - it('throws an error if the type id is not found in the corpus Entity Types', async () => { - const state = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' }, - { id: 'id4', name: '4th type', color: 'f70cbd' } - ] - } - } - const expected = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' }, - { id: 'id4', name: '4th type', color: 'f70cbd' } - ] - } - } - assert.throws( - () => mutations.removeCorpusEntityType(state, { corpusId: 'corpus1', typeId: 'id5' }), - 'Entity Type id5 not found in corpus corpus1' - ) - assert.deepStrictEqual(state, expected) - }) - it('handles errors', async () => { - const state = { - corpora: {}, - corporaLoaded: false, - corpusEntityTypes: {} - } - assert.throws( - () => mutations.removeCorpusEntityType(state, { corpusId: 'corpus1', mdId: 'oneId' }), - 'Entity Types for corpus corpus1 not found' - ) - }) - }) - it('reset', () => { const state = { corpora: { @@ -1082,195 +925,5 @@ describe('corpora', () => { ]) }) }) - - describe('listCorpusEntityTypes', () => { - it('lists entity types in a corpus', async () => { - const pages = [{ - count: 4, - number: 1, - next: 'nextpage', - results: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' } - ] - }, { - count: 4, - number: 2, - next: null, - results: [{ id: 'id4', name: '4th type', color: 'f70cbd' }] - }] - mock.onGet('/corpus/corpusid/entity-types/', { params: { page: 1 } }).reply(200, pages[0]) - mock.onGet('/corpus/corpusid/entity-types/', { params: { page: 2 } }).reply(200, pages[1]) - - await store.dispatch('corpora/listCorpusEntityTypes', { corpusId: 'corpusid' }) - - assert.deepStrictEqual(store.history, [ - { action: 'corpora/listCorpusEntityTypes', payload: { corpusId: 'corpusid' } }, - { mutation: 'corpora/setCorpusEntityTypes', payload: { corpusId: 'corpusid', results: pages[0].results } }, - { action: 'corpora/listCorpusEntityTypes', payload: { corpusId: 'corpusid', page: 2 } }, - { mutation: 'corpora/setCorpusEntityTypes', payload: { corpusId: 'corpusid', results: pages[1].results } } - ]) - - assert.deepStrictEqual(store.state.corpora.corpusEntityTypes, { - corpusid: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' }, - { id: 'id4', name: '4th type', color: 'f70cbd' } - ] - }) - }) - }) - - describe('createCorpusEntityType', () => { - it('creates a new entity type in a corpus', async () => { - store.state.corpora.corpusEntityTypes = { - testCorpus: [ - { id: 'id1', name: 'type1', color: 'ff0000' } - ] - } - const response = { - id: 'id2', - name: 'other type', - color: 'f7240c' - } - mock.onPost('/entity/types/').reply(201, response) - - await store.dispatch('corpora/createCorpusEntityType', { corpus: 'testCorpus', name: 'other type', color: 'f7240c' }) - - assert.deepStrictEqual(store.state.corpora.corpusEntityTypes, { - testCorpus: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'other type', color: 'f7240c' } - ] - }) - assert.deepStrictEqual(store.history, [ - { - action: 'corpora/createCorpusEntityType', - payload: { corpus: 'testCorpus', name: 'other type', color: 'f7240c' } - }, - { - mutation: 'corpora/setCorpusEntityTypes', - payload: { corpusId: 'testCorpus', results: [{ id: 'id2', name: 'other type', color: 'f7240c' }] } - } - ]) - }) - - it('handles errors', async () => { - mock.onPost('/entity/types/').reply(400) - await assertRejects(async () => store.dispatch('corpora/createCorpusEntityType', { corpus: 'testCorpus', name: 'type1' })) - - assert.deepStrictEqual(store.history, [ - { - action: 'corpora/createCorpusEntityType', - payload: { corpus: 'testCorpus', name: 'type1' } - }, - { - mutation: 'notifications/notify', - payload: { - type: 'error', - text: 'Request failed with status code 400' - } - } - ]) - }) - }) - - describe('updateCorpusEntityType', () => { - it('updates an existing Entity Type in a corpus', async () => { - store.state.corpora.corpusEntityTypes = { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'second type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' } - ] - } - const response = { id: 'id2', name: 'another type', color: '170dd9' } - mock.onPatch('/entity/types/id2/').reply(200, response) - await store.dispatch('corpora/updateCorpusEntityType', { id: 'id2', name: 'another type', color: '170dd', corpusId: 'corpus1' }) - assert.deepStrictEqual(store.state.corpora.corpusEntityTypes, { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'another type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' } - ] - }) - assert.deepStrictEqual(store.history, [ - { - action: 'corpora/updateCorpusEntityType', - payload: { id: 'id2', name: 'another type', color: '170dd', corpusId: 'corpus1' } - }, - { - mutation: 'corpora/updateCorpusEntityType', - payload: { corpusId: 'corpus1', data: { id: 'id2', name: 'another type', color: '170dd9' } } - } - ]) - }) - it('handles errors', async () => { - mock.onPatch('/entity/types/id5/').reply(400) - await assertRejects(async () => await store.dispatch('corpora/updateCorpusEntityType', { id: 'id5', name: 'bonk', color: '170dd', corpusId: 'corpus1' })) - assert.deepStrictEqual(store.history, [ - { - action: 'corpora/updateCorpusEntityType', - payload: { id: 'id5', name: 'bonk', color: '170dd', corpusId: 'corpus1' } - }, - { - mutation: 'notifications/notify', - payload: { - type: 'error', - text: 'Request failed with status code 400' - } - } - ]) - }) - }) - - describe('deleteCorpusEntityType', () => { - it('deletes an Entity Type from a corpus', async () => { - store.state.corpora.corpusEntityTypes = { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id2', name: 'another type', color: '170dd9' }, - { id: 'id3', name: 'type number three', color: '180cf7' } - ] - } - mock.onDelete('/entity/types/id2/').reply(204) - await store.dispatch('corpora/deleteCorpusEntityType', { corpusId: 'corpus1', typeId: 'id2' }) - assert.deepStrictEqual(store.state.corpora.corpusEntityTypes, { - corpus1: [ - { id: 'id1', name: 'type1', color: 'ff0000' }, - { id: 'id3', name: 'type number three', color: '180cf7' } - ] - }) - assert.deepStrictEqual(store.history, [ - { - action: 'corpora/deleteCorpusEntityType', - payload: { corpusId: 'corpus1', typeId: 'id2' } - }, - { - mutation: 'corpora/removeCorpusEntityType', - payload: { corpusId: 'corpus1', typeId: 'id2' } - } - ]) - }) - it('handles errors', async () => { - mock.onDelete('/entity/types/id5/').reply(400) - await assertRejects(async () => await store.dispatch('corpora/deleteCorpusEntityType', { corpusId: 'corpus1', typeId: 'id5' })) - assert.deepStrictEqual(store.history, [ - { - action: 'corpora/deleteCorpusEntityType', - payload: { corpusId: 'corpus1', typeId: 'id5' } - }, - { - mutation: 'notifications/notify', - payload: { - type: 'error', - text: 'Request failed with status code 400' - } - } - ]) - }) - }) }) }) diff --git a/tests/unit/stores/entitytypes.spec.js b/tests/unit/stores/entitytypes.spec.js new file mode 100644 index 000000000..d8bc7e6dd --- /dev/null +++ b/tests/unit/stores/entitytypes.spec.js @@ -0,0 +1,260 @@ +import { assert } from 'chai' +import axios from 'axios' +import { assertRejects, FakeAxios } from '../testhelpers.js' +import { createPinia, setActivePinia } from 'pinia' +import { useEntityTypesStore, useNotificationStore } from '@/stores' + +describe('entityTypes', () => { + describe('actions', () => { + let mock, store, notificationStore + + before('Setting up mocks', () => { + mock = new FakeAxios(axios) + setActivePinia(createPinia()) + store = useEntityTypesStore() + notificationStore = useNotificationStore() + }) + + afterEach(() => { + // Remove any handlers, but leave mocking in place + mock.reset() + store.$reset() + notificationStore.$reset() + }) + + after('Removing Axios mock', () => { + // Remove mocking entirely + mock.restore() + }) + + describe('list', () => { + it('lists entity types in a corpus', async () => { + const pages = [{ + count: 4, + number: 1, + next: 'nextpage', + results: [ + { id: 'id1', name: 'type1', color: 'ff0000' }, + { id: 'id2', name: 'second type', color: '170dd9' }, + { id: 'id3', name: 'type number three', color: '180cf7' } + ] + }, { + count: 4, + number: 2, + next: null, + results: [{ id: 'id4', name: '4th type', color: 'f70cbd' }] + }] + mock.onGet('/corpus/corpusid/entity-types/', { params: { page: 1 } }).reply(200, pages[0]) + mock.onGet('/corpus/corpusid/entity-types/', { params: { page: 2 } }).reply(200, pages[1]) + + await store.list('corpusid') + + assert.deepStrictEqual(store.entityTypes, { + corpusid: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'second type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' }, + id4: { id: 'id4', name: '4th type', color: 'f70cbd' } + } + }) + }) + }) + + describe('create', () => { + it('creates a new entity type in a corpus', async () => { + store.entityTypes = { + testCorpus: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' } + } + } + const response = { + id: 'id2', + name: 'other type', + color: 'f7240c' + } + mock.onPost('/entity/types/').reply(201, response) + + await store.create('testCorpus', { name: 'other type', color: 'f7240c' }) + + assert.deepStrictEqual(store.entityTypes, { + testCorpus: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'other type', color: 'f7240c' } + } + }) + }) + + it('handles errors', async () => { + store.entityTypes = { + testCorpus: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' } + } + } + mock.onPost('/entity/types/').reply(400) + await assertRejects(async () => store.create('testCorpus', { name: 'type1' })) + + assert.deepStrictEqual(store.entityTypes, { + testCorpus: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' } + } + }) + + assert.deepStrictEqual(notificationStore.notifications, [ + { + id: 0, + type: 'error', + text: 'Request failed with status code 400' + } + ]) + }) + }) + + describe('update', () => { + it('updates an existing entity type in a corpus', async () => { + store.entityTypes = { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'second type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' } + } + } + const response = { id: 'id2', name: 'another type', color: '170dd9' } + mock.onPatch('/entity/types/id2/').reply(200, response) + await store.update('corpus1', 'id2', { name: 'another type', color: '170dd9' }) + assert.deepStrictEqual(store.entityTypes, { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'another type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' } + } + }) + }) + + it("can't update if the entity type doesn't exist", async () => { + store.entityTypes = { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'second type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' }, + id4: { id: 'id4', name: 'type 4', color: 'cccccc' } + } + } + await assertRejects(async () => await store.update('corpus1', 'id5', { name: 'bonk', color: '170dd9' })) + assert.deepStrictEqual(store.entityTypes, { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'second type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' }, + id4: { id: 'id4', name: 'type 4', color: 'cccccc' } + } + }) + assert.deepStrictEqual(notificationStore.notifications, [ + { + id: 0, + type: 'error', + text: 'Entity type id5 not found in project corpus1.' + } + ]) + }) + + it('handles errors', async () => { + store.entityTypes = { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'second type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' }, + id4: { id: 'id4', name: 'type 4', color: 'cccccc' } + } + } + mock.onPatch('/entity/types/id4/').reply(400) + await assertRejects(async () => await store.update('corpus1', 'id4', { name: 'bonk', color: '170dd9' })) + assert.deepStrictEqual(store.entityTypes, { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'second type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' }, + id4: { id: 'id4', name: 'type 4', color: 'cccccc' } + } + }) + assert.deepStrictEqual(notificationStore.notifications, [ + { + id: 0, + type: 'error', + text: 'Request failed with status code 400' + } + ]) + }) + }) + + describe('delete', () => { + it('deletes an entity type from a corpus', async () => { + store.entityTypes = { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'another type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' } + } + } + mock.onDelete('/entity/types/id2/').reply(204) + await store.delete('corpus1', 'id2') + assert.deepStrictEqual(store.entityTypes, { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' } + } + }) + }) + + it("can't delete an undefined entity type", async () => { + store.entityTypes = { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'another type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' } + } + } + await assertRejects(async () => await store.delete('corpus1', 'id4')) + assert.deepStrictEqual(store.entityTypes, { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'another type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' } + } + }) + assert.deepStrictEqual(notificationStore.notifications, [ + { + id: 0, + type: 'error', + text: 'Entity type id4 not found in project corpus1.' + } + ]) + }) + + it('handles errors', async () => { + store.entityTypes = { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'another type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' } + } + } + mock.onDelete('/entity/types/id3/').reply(400) + await assertRejects(async () => await store.delete('corpus1', 'id3')) + assert.deepStrictEqual(store.entityTypes, { + corpus1: { + id1: { id: 'id1', name: 'type1', color: 'ff0000' }, + id2: { id: 'id2', name: 'another type', color: '170dd9' }, + id3: { id: 'id3', name: 'type number three', color: '180cf7' } + } + }) + assert.deepStrictEqual(notificationStore.notifications, [ + { + id: 0, + type: 'error', + text: 'Request failed with status code 400' + } + ]) + }) + }) + }) +}) -- GitLab