diff --git a/src/api/metadata.ts b/src/api/metadata.ts index bc3b26848c8a77974870750e37f326d261aa662d..b96e7041e1f8bcb9067897a866da3605337fe9b0 100644 --- a/src/api/metadata.ts +++ b/src/api/metadata.ts @@ -42,7 +42,7 @@ export const updateAllowedMetadata = unique( // Create a metadata. export const createMetadata = unique( - async ({ elementId, ...data }: MetaDataCreate): Promise<MetaData> => + async (elementId: UUID, data: MetaDataCreate): Promise<MetaData> => (await axios.post(`/element/${elementId}/metadata/`, data)).data ) @@ -57,5 +57,5 @@ export const deleteMetadata = unique(async (id: UUID) => await axios.delete(`/me // List element metadata. export const listMetadata = unique( - async (id: UUID): Promise<PageNumberPagination<MetaData>> => (await axios.get(`/element/${id}/metadata/`)).data + async (id: UUID): Promise<MetaData[]> => (await axios.get(`/element/${id}/metadata/`)).data ) diff --git a/src/components/Element/DetailsPanel.vue b/src/components/Element/DetailsPanel.vue index 556f95366098094b2063230db18bf1336fff4f89..c6e156a422a1ecca7691f4889412f944365c4f2d 100644 --- a/src/components/Element/DetailsPanel.vue +++ b/src/components/Element/DetailsPanel.vue @@ -156,10 +156,6 @@ export default defineComponent({ classifications () { return (this.element && this.element.classifications) || [] }, - metadata () { - // @ts-expect-error Some Element attributes like metadata are set on the fly on the former store - return (this.element && this.element?.metadata) || [] - }, datasets (): ElementDatasetSet[] | null { return this.elementDatasetSets?.[this.elementId] ?? null } diff --git a/src/components/Element/Metadata/Metadata.vue b/src/components/Element/Metadata/Metadata.vue index c3772f7fb1c18450f8db1d6c21e9c9e1c9e322ed..36c2d6afe0ae829887fdc92eefec8f714eb6399d 100644 --- a/src/components/Element/Metadata/Metadata.vue +++ b/src/components/Element/Metadata/Metadata.vue @@ -31,13 +31,13 @@ <button class="button is-danger" :class="{ 'is-loading': isLoading }" - v-on:click="deleteMetadata" + v-on:click="metadataDelete" > Delete </button> </template> </Modal> - <form v-on:submit.prevent="updateMetadata"> + <form v-on:submit.prevent="metadataUpdate"> <Modal v-if="selectedMetadata" v-model="editModal" @@ -194,7 +194,7 @@ import { import { METADATA_TYPES } from '@/config' import { corporaMixin } from '@/mixins' -import { useDisplayStore } from '@/stores' +import { useDisplayStore, useMetaDataStore } from '@/stores' import DateInput from '@/components/DateInput' import Modal from '@/components/Modal' @@ -238,17 +238,15 @@ export default { ...mapVuexState('corpora', ['corpusAllowedMetadata']), ...mapVuexState('elements', ['elements']), ...mapState(useDisplayStore, ['lastMetadataName', 'lastMetadataType', 'dropdowns']), + ...mapState(useMetaDataStore, ['metadata']), allowedMetadata () { return this.corpusAllowedMetadata[this.corpusId] || [] }, element () { return this.elements[this.elementId] }, - metadata () { - return (this.element && this.element.metadata) || [] - }, count () { - return (this.metadata && this.metadata.length) + return this.elementMetadata?.length }, markdownMetadata () { return this.editableMetadata.filter(m => m.type === 'markdown') @@ -256,10 +254,13 @@ export default { standardMetadata () { return this.editableMetadata.filter(m => m.type !== 'markdown') }, + elementMetadata () { + return this.metadata[this.elementId] + }, editableMetadata () { // Annotate each metadata with an editable property if (!this.count) return - return this.metadata.map(md => ({ + return this.elementMetadata.map(md => ({ ...md, editable: this.isEditable(md) })) @@ -271,6 +272,7 @@ export default { methods: { ...mapVuexMutations('notifications', ['notify']), ...mapActions(useDisplayStore, ['setLastMetadata']), + ...mapActions(useMetaDataStore, ['updateMetadata', 'createMetadata', 'deleteMetadata', 'listMetadata']), isEditable (md) { return this.isAdmin || (this.canWrite(this.corpus) && this.isAllowed(md)) }, @@ -325,17 +327,14 @@ export default { } return isEmpty(this.formErrors) }, - async updateMetadata () { + async metadataUpdate () { // Assert fields are valid if (!this.validateForm()) return - let endpoint = 'elements/updateMetadata' - if (this.createModalForm) endpoint = 'elements/createMetadata' + let method = this.updateMetadata + if (this.createModalForm) method = this.createMetadata this.isLoading = true try { - await this.$store.dispatch(endpoint, { - elementId: this.elementId, - metadata: this.selectedMetadata - }) + await method(this.elementId, this.selectedMetadata) this.editModal = false this.setLastMetadata(this.selectedMetadata.name, this.selectedMetadata.type) } catch (err) { @@ -344,13 +343,10 @@ export default { this.isLoading = false } }, - async deleteMetadata () { + async metadataDelete () { this.isLoading = true try { - await this.$store.dispatch('elements/deleteMetadata', { - elementId: this.elementId, - metadata: this.selectedMetadata - }) + await this.deleteMetadata(this.elementId, this.selectedMetadata) this.deleteModal = false } finally { this.isLoading = false @@ -414,8 +410,8 @@ export default { * This check does not use the computed properties because this might be called with a newValue from a watcher, * in which case it is not guaranteed that the computed properties are up to date. */ - if (!elementId || this.elements[elementId]?.metadata) return - this.$store.dispatch('elements/listMetadata', { elementId }) + if (!elementId || this.metadata[elementId]) return + this.listMetadata(elementId) } }, watch: { diff --git a/src/store/elements.js b/src/store/elements.js index 18335060e881f8f54310a6b271e391a2125cbf20..2698c2dd4983e8f45e3661748f2a6f456e035d00 100644 --- a/src/store/elements.js +++ b/src/store/elements.js @@ -40,16 +40,6 @@ const mergeElement = (state, element) => { } } -/** - * Methods used to edit an element attribute in a generic way. - * Attribute has to be an Array (e.g. A metadata or a classification). - */ -const setInArray = (attribute, state, { elementId, value }) => { - const element = state.elements[elementId] - if (!element) return - mergeElement(state, { ...element, [attribute]: value }) -} - const createInArray = (attribute, state, { elementId, value }) => { const element = state.elements[elementId] if (!element) return @@ -256,12 +246,6 @@ export const mutations = { } }, - // Element metadata mutations - setMetadata (...args) { setInArray('metadata', ...args) }, - addMetadata (...args) { createInArray('metadata', ...args) }, - updateMetadata (...args) { updateInArray('metadata', ...args) }, - removeMetadata (...args) { removeFromArray('metadata', ...args) }, - // Element classifications mutations addClassification (...args) { createInArray('classifications', ...args) }, updateClassification (...args) { updateInArray('classifications', ...args) }, @@ -457,59 +441,6 @@ export const actions = { commit('addNeighbors', { element: payload.id, neighbors: null }) commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) } - }, - - /** - * Perform a request to create a new metadata. - */ - async createMetadata ({ commit }, { metadata, elementId }) { - try { - const resp = await api.createMetadata({ elementId, ...metadata }) - commit('addMetadata', { elementId, value: resp }) - return resp - } catch (err) { - commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) - throw err - } - }, - - /** - * Perform a request to update a metadata. - */ - async updateMetadata ({ commit }, { metadata, elementId }) { - try { - const resp = await api.updateMetadata(metadata) - commit('updateMetadata', { elementId, value: resp }) - return resp - } catch (err) { - commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) - throw err - } - }, - - /** - * Perform a request to delete a metadata. - */ - async deleteMetadata ({ commit }, { metadata, elementId }) { - try { - await api.deleteMetadata(metadata.id) - commit('removeMetadata', { elementId, value: metadata }) - } catch (err) { - commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) - throw err - } - }, - - /** - * Perform a request to list metadata on an element. - */ - async listMetadata ({ commit }, { elementId }) { - try { - const resp = await api.listMetadata(elementId) - commit('setMetadata', { elementId, value: resp }) - } catch (err) { - commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) - } } } diff --git a/src/store/index.js b/src/store/index.js index 41c78a304462651d76900609767cc526d3641cab..54b01d3d41bdf5795f9293e1e0504da3c585adb0 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -14,7 +14,8 @@ import { useRightsStore, useSearchStore, useWorkerStore, - useRepositoryStore + useRepositoryStore, + useMetaDataStore } from '@/stores' /** @@ -53,7 +54,8 @@ export const piniaStores = [ useRightsStore, useSearchStore, useWorkerStore, - useRepositoryStore + useRepositoryStore, + useMetaDataStore ] export const actions = { diff --git a/src/stores/index.ts b/src/stores/index.ts index c55f4860517330108f8f44f6f6ae55319b1acd97..f13a26bfbc76eccc8416e3719ea54d2bceee2b0e 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -16,3 +16,4 @@ export { useExportStore } from './exports' export { useEntityStore } from './entity' export { useTranscriptionStore } from './transcription' export { useRepositoryStore } from './repos' +export { useMetaDataStore } from './metadata' diff --git a/src/stores/metadata.ts b/src/stores/metadata.ts new file mode 100644 index 0000000000000000000000000000000000000000..a50f20c8ec5426420fe5a746a335bb383ef6d4eb --- /dev/null +++ b/src/stores/metadata.ts @@ -0,0 +1,70 @@ +import { defineStore } from 'pinia' +import { + TypedMetaData, + MetaDataCreate, + MetaData, + UUID +} from '@/types' +import * as api from '@/api' +import { useNotificationStore } from '.' +import { errorParser } from '@/helpers' + +interface State { + metadata: { + [elementId: UUID]: MetaData[] + } +} + +export const useMetaDataStore = defineStore('metadata', { + state: (): State => ({ + metadata: {} + }), + actions: { + async createMetadata (elementId: UUID, data: MetaDataCreate) { + try { + const resp = await api.createMetadata(elementId, data) + if (this.metadata[elementId]) { + this.metadata[elementId].push(resp) + } else { + this.metadata[elementId] = [resp] + } + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + throw err + } + }, + async updateMetadata (elementId: UUID, metadata: TypedMetaData) { + try { + if (!this.metadata[elementId]) return + const resp = await api.updateMetadata(metadata) + const elementMetadata = this.metadata[elementId] + const index = elementMetadata.findIndex(md => md.id === resp.id) + if (index < 0) return + elementMetadata.splice(index, 1, resp) + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + throw new Error(`Metadata ${metadata.id} not found on element ${elementId}`) + } + }, + async deleteMetadata (elementId: UUID, metadata: TypedMetaData) { + try { + await api.deleteMetadata(metadata.id) + const elementMetadata = this.metadata[elementId] + const index = elementMetadata.findIndex(md => md.id === metadata.id) + if (index < 0) throw new Error(`Metadata ${metadata.id} not found on element ${elementId}`) + elementMetadata.splice(index, 1) + this.metadata[elementId] = elementMetadata + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + } + }, + async listMetadata (elementId: UUID) { + try { + this.metadata[elementId] = await api.listMetadata(elementId) + } catch (err) { + useNotificationStore().notify({ type: 'error', text: errorParser(err) }) + throw err + } + } + } +}) diff --git a/src/types/index.ts b/src/types/index.ts index a744af9c3d156ec9462266a5213fd090b6525152..7e8983845a1ac60c3709605df32b957295899917 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -227,7 +227,6 @@ export interface StringMetaData extends BaseMetaData { export type TypedMetaData = StringMetaData | NumericMetaData export interface MetaDataCreate extends Omit<TypedMetaData, 'id'> { - elementId: UUID worker_version?: UUID worker_run_id?: UUID } diff --git a/tests/unit/store/elements.spec.js b/tests/unit/store/elements.spec.js index 97971cc78a331a96c28f7261e81cd11f97e92db1..d094abb21dd52df2eb7f544df959319f158829b8 100644 --- a/tests/unit/store/elements.spec.js +++ b/tests/unit/store/elements.spec.js @@ -410,91 +410,6 @@ describe('elements', () => { }) }) }) - - describe('setMetadata', () => { - it('sets metadata on an element', () => { - const state = { - elements: { - element1: { - id: 'element1', - metadata: [{ id: 'meta1', name: 'AAA' }] - } - } - } - mutations.setMetadata(state, { elementId: 'element1', value: [{ id: 'meta2', name: 'BBB' }] }) - assert.deepStrictEqual(state, { - elements: { - element1: { - id: 'element1', - metadata: [{ id: 'meta2', name: 'BBB' }] - } - } - }) - }) - }) - - describe('addMetadata', () => { - it('adds a new metadata on an element', () => { - const state = { - elements: { - element1: { - id: 'element1', - metadata: [{ id: 'meta1', name: 'AAA' }] - } - } - } - mutations.addMetadata(state, { elementId: 'element1', value: { id: 'meta2', name: 'BBB' } }) - assert.deepStrictEqual(state, { - elements: { - element1: { - id: 'element1', - metadata: [{ id: 'meta1', name: 'AAA' }, { id: 'meta2', name: 'BBB' }] - } - } - }) - }) - }) - - describe('removeMetdata', () => { - it('removes a metadata', () => { - const state = { - elements: { - element1: { id: 'element1', metadata: [{ id: 'meta1', name: 'AAA' }] } - } - } - mutations.removeMetadata(state, { elementId: 'element1', value: { id: 'meta1' } }) - assert.deepStrictEqual(state, { - elements: { element1: { id: 'element1', metadata: [] } } - }) - }) - }) - - describe('updateMetdata', () => { - it('updates the metadata of an element', () => { - const state = { - elements: { - element1: { - id: 'element1', - classifications: [{ id: 'classif1', state: 'pending' }], - metadata: [{ id: 'meta1', name: 'AAA' }] - } - } - } - mutations.updateMetadata(state, { - elementId: 'element1', - value: { id: 'meta1', name: 'BBB' } - }) - assert.deepStrictEqual(state, { - elements: { - element1: { - id: 'element1', - classifications: [{ id: 'classif1', state: 'pending' }], - metadata: [{ id: 'meta1', name: 'BBB' }] - } - } - }) - }) - }) }) describe('actions', () => { @@ -1150,300 +1065,6 @@ describe('elements', () => { assert.deepStrictEqual(store.state.elements.neighbors, { elementid: null }) }) }) - - describe('listMetadata', () => { - it('lists metadata on an element', async () => { - const metadata = { - id: 'metadataid', - name: 'blah', - type: 'text', - value: 'lol' - } - mock.onGet('/element/elementid/metadata/').reply(200, [metadata]) - store.state.elements.elements.elementid = { - id: 'elementid' - } - - await store.dispatch('elements/listMetadata', { elementId: 'elementid' }) - - assert.deepStrictEqual(store.history, [ - { - action: 'elements/listMetadata', - payload: { - elementId: 'elementid' - } - }, - { - mutation: 'elements/setMetadata', - payload: { - elementId: 'elementid', - value: [metadata] - } - } - ]) - assert.deepStrictEqual(store.state.elements.elements.elementid, { - id: 'elementid', - metadata: [metadata] - }) - }) - - it('handles errors', async () => { - mock.onGet('/element/elementid/metadata/').reply(500) - - await store.dispatch('elements/listMetadata', { elementId: 'elementid' }) - - assert.deepStrictEqual(store.history, [ - { - action: 'elements/listMetadata', - payload: { - elementId: 'elementid' - } - }, - { - mutation: 'notifications/notify', - payload: { - type: 'error', - text: 'Request failed with status code 500' - } - } - ]) - }) - }) - - describe('createMetadata', () => { - it('creates a metadata on an element', async () => { - const payload = { - name: 'blah', - type: 'text', - value: 'lol' - } - const metadata = { - id: 'metadataid', - ...payload - } - mock.onPost('/element/elementid/metadata/').reply(201, metadata) - store.state.elements.elements.elementid = { - id: 'elementid' - } - - await store.dispatch('elements/createMetadata', { elementId: 'elementid', metadata: payload }) - - assert.deepStrictEqual(store.history, [ - { - action: 'elements/createMetadata', - payload: { - elementId: 'elementid', - metadata: payload - } - }, - { - mutation: 'elements/addMetadata', - payload: { - elementId: 'elementid', - value: metadata - } - } - ]) - assert.deepStrictEqual(store.state.elements.elements.elementid, { - id: 'elementid', - metadata: [metadata] - }) - }) - - it('handles errors', async () => { - const payload = { - name: 'blah', - type: 'text', - value: 'lol' - } - mock.onPost('/element/elementid/metadata/').reply(500) - - await assertRejects(async () => store.dispatch('elements/createMetadata', { - elementId: 'elementid', - metadata: payload - })) - - assert.deepStrictEqual(store.history, [ - { - action: 'elements/createMetadata', - payload: { - elementId: 'elementid', - metadata: payload - } - }, - { - mutation: 'notifications/notify', - payload: { - type: 'error', - text: 'Request failed with status code 500' - } - } - ]) - }) - }) - - describe('updateMetadata', () => { - it('updates a metadata', async () => { - const metadata = { - id: 'metadataid', - name: 'blah', - type: 'text', - value: 'lol' - } - mock.onPatch('/metadata/metadataid/').reply(200, { - ...metadata, - value: 'new value' - }) - store.state.elements.elements.elementid = { - id: 'elementid', - metadata: [metadata] - } - - await store.dispatch('elements/updateMetadata', { - elementId: 'elementid', - metadata: { - ...metadata, - value: 'new value' - } - }) - - assert.deepStrictEqual(store.history, [ - { - action: 'elements/updateMetadata', - payload: { - elementId: 'elementid', - metadata: { - ...metadata, - value: 'new value' - } - } - }, - { - mutation: 'elements/updateMetadata', - payload: { - elementId: 'elementid', - value: { - ...metadata, - value: 'new value' - } - } - } - ]) - assert.deepStrictEqual(store.state.elements.elements.elementid, { - id: 'elementid', - metadata: [ - { - ...metadata, - value: 'new value' - } - ] - }) - }) - - it('handles errors', async () => { - mock.onPatch('/metadata/metadataid/').reply(500) - - await assertRejects(async () => store.dispatch('elements/updateMetadata', { - elementId: 'elementid', - metadata: { - id: 'metadataid', - value: 'new value' - } - })) - - assert.deepStrictEqual(store.history, [ - { - action: 'elements/updateMetadata', - payload: { - elementId: 'elementid', - metadata: { - id: 'metadataid', - value: 'new value' - } - } - }, - { - mutation: 'notifications/notify', - payload: { - type: 'error', - text: 'Request failed with status code 500' - } - } - ]) - }) - }) - - describe('deleteMetadata', () => { - it('deletes a metadata', async () => { - const metadata = { - id: 'metadataid', - name: 'blah', - type: 'text', - value: 'lol' - } - mock.onDelete('/metadata/metadataid/').reply(204) - store.state.elements.elements.elementid = { - id: 'elementid', - metadata: [metadata] - } - - await store.dispatch('elements/deleteMetadata', { - elementId: 'elementid', - metadata - }) - - assert.deepStrictEqual(store.history, [ - { - action: 'elements/deleteMetadata', - payload: { - elementId: 'elementid', - metadata - } - }, - { - mutation: 'elements/removeMetadata', - payload: { - elementId: 'elementid', - value: metadata - } - } - ]) - assert.deepStrictEqual(store.state.elements.elements.elementid, { - id: 'elementid', - metadata: [] - }) - }) - - it('handles errors', async () => { - mock.onDelete('/metadata/metadataid/').reply(500) - - await assertRejects(async () => store.dispatch('elements/deleteMetadata', { - elementId: 'elementid', - metadata: { - id: 'metadataid' - } - })) - - assert.deepStrictEqual(store.history, [ - { - action: 'elements/deleteMetadata', - payload: { - elementId: 'elementid', - metadata: { - id: 'metadataid' - } - } - }, - { - mutation: 'notifications/notify', - payload: { - type: 'error', - text: 'Request failed with status code 500' - } - } - ]) - }) - }) }) describe('getters', () => { diff --git a/tests/unit/stores/metadata.spec.js b/tests/unit/stores/metadata.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..fc7001458122f6907b86bb2de291657f2cbb71cd --- /dev/null +++ b/tests/unit/stores/metadata.spec.js @@ -0,0 +1,167 @@ +import axios from 'axios' +import { assert } from 'chai' +import { createPinia, setActivePinia } from 'pinia' +import { useMetaDataStore } from '@/stores' +import { FakeAxios } from '../testhelpers' + +describe('metadata', () => { + describe('actions', () => { + let mock, store + + before('Setting up mocks', () => { + mock = new FakeAxios(axios) + setActivePinia(createPinia()) + store = useMetaDataStore() + }) + + afterEach(() => { + // Remove any handlers, but leave mocking in place + mock.reset() + store.$reset() + }) + + after('Removing Axios mock', () => { + mock.restore() + }) + + describe('createMetadata', () => { + it('create a metadata for an element', async () => { + const payload = { + type: 'text', + name: 'Metadata', + value: 'metadata value', + entity_id: 'entity_1', + worker_run_id: 'worker_run_1' + } + mock.onPost('/element/element_1/metadata/').reply(201, { + id: 'metadata_1', + type: 'text', + name: 'Metadata', + value: 'metadata value', + entity_id: 'entity_1', + worker_run_id: 'worker_run_1' + }) + + await store.createMetadata('element_1', payload) + + assert.deepStrictEqual(store.metadata, { + element_1: [ + { + id: 'metadata_1', + type: 'text', + name: 'Metadata', + value: 'metadata value', + entity_id: 'entity_1', + worker_run_id: 'worker_run_1' + } + ] + }) + }) + }) + + describe('updateMetadata', () => { + it('Update metadata of an element', async () => { + const payload = { + id: 'metadata_1', + type: 'numeric', + name: 'Metadata', + value: 12, + entity_id: 'entity_2' + } + + store.metadata = { + element_1: [ + { + id: 'metadata_1', + type: 'text', + name: 'Metadata name', + value: 'Metadata value', + entity_id: 'entity_1' + } + ] + } + mock.onPatch('/metadata/metadata_1/').reply(200, payload) + + await store.updateMetadata('element_1', payload) + + assert.deepStrictEqual(store.metadata, { + element_1: [ + { + id: 'metadata_1', + type: 'numeric', + name: 'Metadata', + value: 12, + entity_id: 'entity_2' + } + ] + }) + }) + }) + + describe('deleteMetadata', () => { + it('Delete a metadata', async () => { + store.metadata = { + element_1: [ + { + id: 'metadata_1', + type: 'text', + name: 'Metadata', + value: 'metadata value', + entity_id: 'entity_1' + } + ] + } + + mock.onDelete('/metadata/metadata_1/').reply(204) + + await store.deleteMetadata('element_1', { + id: 'metadata_1' + }) + + assert.deepStrictEqual(store.metadata, { element_1: [] }) + }) + }) + + describe('listMetadata', () => { + it('List MetaData of an element', async () => { + mock.onGet('/element/element_1/metadata/').reply(200, [ + { + id: 'metadata_1', + name: 'Metadata', + type: 'text', + value: 'metadata value', + entity_id: 'entity_1' + }, + { + id: 'metadata_2', + name: 'Metadata', + type: 'text', + value: 'metadata value', + entity_id: 'entity_2' + } + ]) + + await store.listMetadata('element_1') + + assert.deepStrictEqual(store.metadata, { + element_1: [ + { + id: 'metadata_1', + name: 'Metadata', + type: 'text', + value: 'metadata value', + entity_id: 'entity_1' + }, + { + id: 'metadata_2', + name: 'Metadata', + type: 'text', + value: 'metadata value', + entity_id: 'entity_2' + } + ] + }) + }) + }) + }) +})