Skip to content
Snippets Groups Projects
Commit 9ffeda3b authored by Bastien Abadie's avatar Bastien Abadie
Browse files

Merge branch 'nuke-all-entities' into 'master'

Remove EntityPanel

Closes #780

See merge request !1068
parents 3a1b6b6f ec44d7ef
No related branches found
No related tags found
1 merge request!1068Remove EntityPanel
......@@ -388,9 +388,6 @@ export const listElementLinks = unique(async id => (await axios.get(`/element/${
// List all elements linked to an entity
export const listEntityElements = unique(async ({ id, ...params }) => (await axios.get(`/entity/${id}/elements/`, { params })).data)
// List all entities linked to an element
export const listElementEntities = unique(async id => (await axios.get(`/element/${id}/entities/`)).data)
// List all entities linked to a transcription
export const listTranscriptionEntities = unique(async ({ id, ...params }) => (await axios.get(`/transcription/${id}/entities/`, { params })).data)
......
......@@ -11,8 +11,6 @@ export const initialState = () => ({
links: null,
// A page of elements related to an entity
elements: null,
// Entities in transcriptions and metadata per element
inElement: {},
/*
* Entities per transcription, with pagination status
* { [transcription ID]: { count, results, loaded } }
......@@ -33,19 +31,6 @@ export const mutations = {
setElements (state, elements) {
state.elements = clone(elements)
},
setInElement (state, { id, ...data }) {
// Empty object: remove instead of setting
if (!Object.keys(data).length) {
const newInElement = { ...state.inElement }
delete newInElement[id]
state.inElement = newInElement
return
}
state.inElement = {
...state.inElement,
[id]: data
}
},
setInTranscription (state, { id, ...page }) {
let payload = state.inTranscription[id]
if (!payload || payload.results === null) payload = paginatedPayload([], page.count)
......@@ -105,16 +90,6 @@ export const actions = {
}
},
async listInElement ({ commit }, { id }) {
try {
const data = await api.listElementEntities(id)
commit('setInElement', { id, ...data })
} catch (err) {
commit('setInElement', { id })
commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true })
}
},
async listInTranscription ({ commit, dispatch }, { transcriptionId, page = 1 }) {
// Init inTranscription on first response
if (page === 1) commit('resetInTranscription', transcriptionId)
......
import assert from 'assert'
import axios from 'axios'
import Vuex from 'vuex'
import AsyncComputed from 'vue-async-computed'
import { createLocalVue, shallowMount, RouterLinkStub } from '@vue/test-utils'
import store from '~/test/store'
import { FakeAxios } from '~/test/testhelpers'
import { workerVersionsSample } from '~/test/samples'
import EntityPanel from '~/vue/Entity/Panel'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(AsyncComputed)
describe('Entity', () => {
describe('Panel.vue', () => {
var mock
const entity1 = {
id: 'entity1',
name: 'Aron',
type: 'person',
worker_version_id: 'versionid'
}
const entity2 = {
id: 'entity2',
name: 'Tangela',
type: 'location',
worker_version_id: 'versionid'
}
const entity3 = {
id: 'entity3',
name: 'Hypno',
type: 'organization',
worker_version_id: null
}
const inElement = {
metadata: [
{ entity: entity1 },
{ entity: entity2 }
],
transcriptions: [
{ entity: entity2 },
{ entity: entity3 }
]
}
before(() => {
mock = new FakeAxios(axios)
store.state.process.workerVersions = {
versionid: workerVersionsSample.results[0]
}
/*
* A bug in the FakeStore prevents us from properly testing calls to `listInElement`,
* so we still set the state manually despite the Axios mock being in place
*/
store.state.entity.inElement = {
elementid: inElement
}
})
beforeEach(() => {
mock.onGet('/element/elementid/entities/').reply(200, inElement)
})
afterEach(() => {
mock.reset()
})
after(() => {
mock.restore()
store.reset()
})
it('displays unique entities from metadata and transcriptions', async () => {
const wrapper = shallowMount(EntityPanel, {
store,
localVue,
stubs: {
RouterLink: RouterLinkStub
},
propsData: {
elementId: 'elementid'
}
})
// Wait for the versionIds async computed to be ready
await new Promise(resolve => wrapper.vm.$watch('versionIds', resolve))
// Panel title
assert.ok(wrapper.get('.panel-heading').text().startsWith('3 entities\n'))
// Worker version filter
assert.deepStrictEqual(
wrapper.findAll('.panel-heading select option').wrappers.map(w => [w.attributes('value'), w.text()]),
[
['', 'Any worker version'],
['versionid', 'Worker 1 xxxxxxxx'],
['__manual__', 'Manual']
]
)
// Type filters
assert.deepStrictEqual(
wrapper.findAll('.panel-tabs a').wrappers.map(w => w.text()),
['all', '1 location', '1 organization', '1 person']
)
assert.strictEqual(wrapper.get('.panel-tabs a.is-active').text(), 'all')
// Entities
assert.deepStrictEqual(
wrapper.findAll('.panel-block').wrappers.map(w => [
w.get('.panel-icon i').attributes('class'),
w.get('.metadata-content').text()
]),
[
['icon-globe', 'Tangela x 2'],
['icon-organization', 'Hypno'],
['icon-user', 'Aron']
]
)
})
it('handles filtering by type', async () => {
const wrapper = shallowMount(EntityPanel, {
store,
localVue,
stubs: {
RouterLink: RouterLinkStub
},
propsData: {
elementId: 'elementid'
}
})
// Type filters
assert.deepStrictEqual(
wrapper.findAll('.panel-tabs a').wrappers.map(w => w.text()),
['all', '1 location', '1 organization', '1 person']
)
// Click on the location filter
await wrapper.findAll('.panel-tabs a').at(1).trigger('click')
assert.strictEqual(wrapper.get('.panel-tabs a.is-active').text(), '1 location')
// Panel title remains unchanged
assert.ok(wrapper.get('.panel-heading').text().startsWith('3 entities\n'))
// Entities
assert.strictEqual(wrapper.findAll('.panel-block').length, 1)
assert.strictEqual(wrapper.get('.panel-block .panel-icon i').attributes('class'), 'icon-globe')
assert.strictEqual(wrapper.get('.panel-block .metadata-content').text(), 'Tangela x 2')
})
it('handles filtering by worker version', async () => {
const wrapper = shallowMount(EntityPanel, {
store,
localVue,
stubs: {
RouterLink: RouterLinkStub
},
propsData: {
elementId: 'elementid'
}
})
// Wait for the versionIds async computed to be ready
await new Promise(resolve => wrapper.vm.$watch('versionIds', resolve))
// Worker version filter
assert.deepStrictEqual(
wrapper.findAll('.panel-heading select option').wrappers.map(w => [w.attributes('value'), w.text()]),
[
['', 'Any worker version'],
['versionid', 'Worker 1 xxxxxxxx'],
['__manual__', 'Manual']
]
)
await wrapper.findAll('.panel-heading select option').at(1).setSelected()
// Panel title remains unchanged
assert.ok(wrapper.get('.panel-heading').text().startsWith('3 entities\n'))
// Type filters
assert.deepStrictEqual(
wrapper.findAll('.panel-tabs a').wrappers.map(w => w.text()),
['all', '1 location', '1 person']
)
assert.strictEqual(wrapper.get('.panel-tabs a.is-active').text(), 'all')
// Entities
assert.deepStrictEqual(
wrapper.findAll('.panel-block').wrappers.map(w => [
w.get('.panel-icon i').attributes('class'),
w.get('.metadata-content').text()
]),
[
['icon-globe', 'Tangela x 2'],
['icon-user', 'Aron']
]
)
})
it('handles filtering by worker version and type', async () => {
const wrapper = shallowMount(EntityPanel, {
store,
localVue,
stubs: {
RouterLink: RouterLinkStub
},
propsData: {
elementId: 'elementid'
}
})
// Wait for the versionIds async computed to be ready
await new Promise(resolve => wrapper.vm.$watch('versionIds', resolve))
// Worker version filter
assert.deepStrictEqual(
wrapper.findAll('.panel-heading select option').wrappers.map(w => [w.attributes('value'), w.text()]),
[
['', 'Any worker version'],
['versionid', 'Worker 1 xxxxxxxx'],
['__manual__', 'Manual']
]
)
await wrapper.findAll('.panel-heading select option').at(1).setSelected()
await wrapper.findAll('.panel-tabs a').at(1).trigger('click')
// Panel title remains unchanged
assert.ok(wrapper.get('.panel-heading').text().startsWith('3 entities\n'))
// Type filters
assert.deepStrictEqual(
wrapper.findAll('.panel-tabs a').wrappers.map(w => w.text()),
['all', '1 location', '1 person']
)
assert.strictEqual(wrapper.get('.panel-tabs a.is-active').text(), '1 location')
// Entities
assert.strictEqual(wrapper.findAll('.panel-block').length, 1)
assert.strictEqual(wrapper.get('.panel-block .panel-icon i').attributes('class'), 'icon-globe')
assert.strictEqual(wrapper.get('.panel-block .metadata-content').text(), 'Tangela x 2')
})
it('resets the type filter when filtering on a worker version without entities of the selected type', async () => {
const wrapper = shallowMount(EntityPanel, {
store,
localVue,
stubs: {
RouterLink: RouterLinkStub
},
propsData: {
elementId: 'elementid'
}
})
// Wait for the versionIds async computed to be ready
await new Promise(resolve => wrapper.vm.$watch('versionIds', resolve))
// Use the organization filter
assert.strictEqual(wrapper.findAll('.panel-tabs a').at(2).text(), '1 organization')
await wrapper.findAll('.panel-tabs a').at(2).trigger('click')
// Use the worker filter; the only organization is not from this worker
await wrapper.findAll('.panel-heading select option').at(1).setSelected()
// Panel title remains unchanged
assert.ok(wrapper.get('.panel-heading').text().startsWith('3 entities\n'))
// Type filters
assert.deepStrictEqual(
wrapper.findAll('.panel-tabs a').wrappers.map(w => w.text()),
['all', '1 location', '1 person']
)
// 'all' filter is selected
assert.strictEqual(wrapper.get('.panel-tabs a.is-active').text(), 'all')
// Entities
assert.deepStrictEqual(
wrapper.findAll('.panel-block').wrappers.map(w => [
w.get('.panel-icon i').attributes('class'),
w.get('.metadata-content').text()
]),
[
['icon-globe', 'Tangela x 2'],
['icon-user', 'Aron']
]
)
})
})
})
......@@ -286,13 +286,6 @@ export const elementNeighborsSample = makeSampleResults([
}
])
export const entitiesInElementSample = {
transcriptions: [
entitiesInTranscriptionSample
],
metadata: []
}
export const linksSample = makeSampleResults(
[{
id: 'linkid',
......
......@@ -7,7 +7,6 @@ import {
entitiesSample,
linksSample,
elementSample,
entitiesInElementSample,
entitiesInTranscriptionSample
} from '~/test/samples'
import { FakeAxios, assertRejects } from '~/test/testhelpers'
......@@ -38,26 +37,6 @@ describe('entity', () => {
assert.deepStrictEqual(state.elements, elementSample)
})
describe('setInElement', () => {
it('sets the elements of an entity', () => {
const state = initialState()
mutations.setInElement(state, {
id: 'elementid',
...entitiesInElementSample
})
assert.deepStrictEqual(state.inElement, {
elementid: entitiesInElementSample
})
})
it('deletes the elements of an entity', () => {
const state = initialState()
state.inElement.elementid = entitiesInElementSample
mutations.setInElement(state, { id: 'elementid' })
assert.deepStrictEqual(state.inElement, {})
})
})
it('setInTranscription', () => {
const state = initialState()
mutations.setInTranscription(state, {
......@@ -97,7 +76,6 @@ describe('entity', () => {
entities: entitiesSample,
links: linksSample,
elements: elementSample,
inElement: entitiesInElementSample,
inTranscription: {
transcriptionid: {
results: entitiesInTranscriptionSample,
......@@ -266,26 +244,6 @@ describe('entity', () => {
})
})
it('listInElement', async () => {
mock.onGet('/element/elementid/entities/').reply(200, entitiesInElementSample)
await store.dispatch('entity/listInElement', { id: 'elementid' })
assert.deepStrictEqual(store.history, [
{
action: 'entity/listInElement',
payload: { id: 'elementid' }
},
{
mutation: 'entity/setInElement',
payload: {
id: 'elementid',
...entitiesInElementSample
}
}
])
})
describe('listInTranscription', () => {
it('lists entities in a transcription', async () => {
mock.onGet('/transcription/transcriptionid/entities/').reply(200, entitiesInTranscriptionSample)
......
......@@ -113,12 +113,10 @@
</DropdownContent>
<hr />
<template v-if="elementType.folder === false">
<EntityPanel :element-id="element.id" />
<hr />
<EntityLinks :element-id="element.id" />
</template>
<EntityLinks
:element-id="element.id"
v-if="elementType.folder === false"
/>
</template>
</div>
</template>
......@@ -133,7 +131,6 @@ import { groupBy, orderBy } from 'lodash'
import Modal from '~/vue/Modal'
import GroupedTranscriptions from './Transcription'
import HelpModal from '~/vue/HelpModal'
import EntityPanel from '~/vue/Entity/Panel'
import EntityLinks from '~/vue/Entity/Links'
import Classifications from './Classifications'
import MLClassSelect from '~/vue/MLClassSelect'
......@@ -150,7 +147,6 @@ export default {
components: {
Modal,
GroupedTranscriptions,
EntityPanel,
EntityLinks,
Classifications,
MLClassSelect,
......
<template>
<DropdownContent v-if="inElement[elementId]" title="All entities" :disabled="!entities.length">
<nav class="panel">
<div class="panel-heading">
{{ entities.length }} entit{{ entities.length > 1 ? 'ies' : 'y' }}
<span class="select is-small is-pulled-right">
<select v-model="workerVersionFilter">
<option value="">Any worker version</option>
<option v-for="[id, name] in versionIds" :key="id" :value="id">
{{ name }}
</option>
</select>
</span>
</div>
<p class="panel-tabs">
<a
:class="{ 'is-active': !typeFilter }"
v-on:click="filterPanel('')"
>all</a>
<a
v-for="(e, name) in entityTypes"
:key="name"
:class="{ 'is-active': typeFilter === name }"
v-on:click="filterPanel(name)"
>{{ e.length }} {{ name }}{{ e.length > 1 ? 's' : '' }}</a>
</p>
<a v-for="entity in filteredEntities" :key="entity.id" class="panel-block">
<span class="panel-icon">
<i :class="ENTITY_TYPES[entity.type].icon"></i>
</span>
<span class="metadata-content">
<router-link :to="{ name: 'entity-details', params: { id: entity.id } }">
<InterpretedDate
v-if="containsDates(entity)"
:dates="entity.dates"
:raw-date="entity.name"
/>
<span v-else>{{ entity.name }}</span>
</router-link>
<span v-if="entity.occurrence > 1" class="has-text-grey-light">x {{ entity.occurrence }}</span>
</span>
</a>
</nav>
</DropdownContent>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { sortBy, groupBy, isEmpty } from 'lodash'
import { ENTITY_TYPES, MANUAL_WORKER_VERSION } from '~/js/config'
import DropdownContent from '~/vue/DropdownContent'
import InterpretedDate from '~/vue/InterpretedDate'
export default {
props: {
elementId: {
type: String,
required: true
}
},
components: {
DropdownContent,
InterpretedDate
},
mounted () {
this.$store.dispatch('entity/listInElement', { id: this.elementId })
},
data: () => ({
ENTITY_TYPES,
typeFilter: '',
workerVersionFilter: ''
}),
asyncComputed: {
versionIds: {
get () {
const ids = new Set(this.entities.map(entity => entity.worker_version_id))
return Promise.all([...ids].map(async id => {
if (!id) return [MANUAL_WORKER_VERSION, 'Manual']
if (!this.workerVersions[id]) await this.getWorkerVersion(id)
const version = this.workerVersions[id]
return [id, version.worker.name + ' ' + version.revision.hash.substring(0, 8)]
}))
},
default: []
}
},
computed: {
...mapState('entity', ['inElement']),
...mapState('process', ['workerVersions']),
/**
* From an element's metadata and transcriptions, returns sorted unique entities,
* with an `occurrence` attribute indicating their total amount.
* Entities are sorted by descending occurrence, then type and name.
*/
entities () {
if (
!this.inElement ||
!this.inElement[this.elementId] ||
!this.inElement[this.elementId].transcriptions ||
!this.inElement[this.elementId].metadata
) {
return []
}
const items = this.inElement[this.elementId].transcriptions.concat(this.inElement[this.elementId].metadata)
const uniqueEntities = Object.values(groupBy(
// Ignore items without entities
items.filter(item => item.entity).map(item => item.entity),
entity => entity.id
)).map(group => {
// Get the first entity of each group and add its count
group[0].occurrence = group.length
return group[0]
})
return sortBy(uniqueEntities, entity => -entity.occurrence, entity => entity.type, entity => entity.name)
},
/**
* Entities filtered only by worker version.
*/
versionFilteredEntities () {
if (!this.workerVersionFilter) return this.entities
return this.entities.filter(entity => (entity.worker_version_id || MANUAL_WORKER_VERSION) === this.workerVersionFilter)
},
/**
* Entities filtered by both worker version and type.
*/
filteredEntities () {
if (!this.typeFilter) return this.versionFilteredEntities
return this.versionFilteredEntities.filter(entity => entity.type === this.typeFilter)
},
/**
* Entity types, filtered by worker version.
* Entity lists are included to display counts by type.
*/
entityTypes () {
return groupBy(this.versionFilteredEntities, e => e.type)
}
},
methods: {
...mapActions('process', ['getWorkerVersion']),
containsDates (meta) {
return !isEmpty(meta.dates)
},
filterPanel (type) {
this.typeFilter = type
}
},
watch: {
/**
* When the worker version filter changes, the type filter might point to a type that has no displayed entities.
* This resets the type filter to "all types" when there are no entities to show for this type.
*/
entityTypes (newValue) {
if (!this.typeFilter) return
if (!this.entityTypes[this.typeFilter] || !this.entityTypes[this.typeFilter].length) this.typeFilter = ''
}
}
}
</script>
<style scoped>
.panel-block {
justify-content: space-between;
cursor: default;
}
.metadata-content {
flex: 1;
}
</style>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment