From 71a878d7c5bfb91a705ae25c1429cbac65f893ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olov=20Ylinenp=C3=A4=C3=A4?= Date: Wed, 14 Aug 2024 15:43:05 +0200 Subject: [PATCH 1/5] Remove obsolete item-sibling component It has not been used in a long time. Works are now handled like any other node. --- vue-client/src/App.vue | 2 +- .../src/components/inspector/entity-adder.vue | 23 - vue-client/src/components/inspector/field.vue | 23 - .../src/components/inspector/item-sibling.vue | 694 ------------------ 4 files changed, 1 insertion(+), 741 deletions(-) delete mode 100644 vue-client/src/components/inspector/item-sibling.vue diff --git a/vue-client/src/App.vue b/vue-client/src/App.vue index 68b579bbd..e6604729d 100644 --- a/vue-client/src/App.vue +++ b/vue-client/src/App.vue @@ -847,7 +847,7 @@ body { } } } - input, textarea, img, select, .ItemEntity, .ItemLocal, .EntityAction, .ItemSibling, .icon, .icon i, li, i.fa, div { + input, textarea, img, select, .ItemEntity, .ItemLocal, .EntityAction, .icon, .icon i, li, i.fa, div { &:focus { .focus-mixin-border(); } diff --git a/vue-client/src/components/inspector/entity-adder.vue b/vue-client/src/components/inspector/entity-adder.vue index 42097892c..68b3ecf6d 100644 --- a/vue-client/src/components/inspector/entity-adder.vue +++ b/vue-client/src/components/inspector/entity-adder.vue @@ -383,29 +383,6 @@ export default { addToHistory: true, }); }, - addSibling(obj) { - const linkObj = { '@id': `${this.inspector.data.record['@id']}#work` }; - const workObj = obj; - workObj['@id'] = linkObj['@id']; - - this.$store.dispatch('setInspectorStatusValue', { - property: 'lastAdded', - value: 'work', - }); - this.$store.dispatch('updateInspectorData', { - changeList: [ - { - path: `${this.path}`, - value: linkObj, - }, - { - path: 'work', - value: workObj, - }, - ], - addToHistory: true, - }); - }, addEmpty(typeId) { this.hide(); const shortenedType = StringUtil.getCompactUri(typeId, this.resources.context); diff --git a/vue-client/src/components/inspector/field.vue b/vue-client/src/components/inspector/field.vue index a84b28e0a..9c8fe2653 100644 --- a/vue-client/src/components/inspector/field.vue +++ b/vue-client/src/components/inspector/field.vue @@ -19,7 +19,6 @@ import ItemLocal from './item-local.vue'; import ItemError from './item-error.vue'; import ItemVocab from './item-vocab.vue'; import ItemType from './item-type.vue'; -import ItemSibling from './item-sibling.vue'; import ItemBoolean from './item-boolean.vue'; import ItemNumeric from './item-numeric.vue'; import ItemGrouped from './item-grouped.vue'; @@ -141,7 +140,6 @@ export default { 'item-entity': ItemEntity, 'item-value': ItemValue, 'item-local': ItemLocal, - 'item-sibling': ItemSibling, 'item-error': ItemError, 'item-vocab': ItemVocab, 'item-boolean': ItemBoolean, @@ -563,9 +561,6 @@ export default { if (this.isPlainObject(o) && this.isLinked(o)) { return 'entity'; } - if (this.isPlainObject(o) && o.hasOwnProperty('@id') && this.isInGraph(o)) { - return 'sibling'; - } if (this.isPlainObject(o) && !this.isLinked(o)) { return 'local'; } @@ -967,24 +962,6 @@ export default { :in-array="valueIsArray" :diff="diff" :should-expand="expandChildren || embellished" /> - - diff --git a/vue-client/src/components/inspector/item-sibling.vue b/vue-client/src/components/inspector/item-sibling.vue deleted file mode 100644 index 4e0a7fd7e..000000000 --- a/vue-client/src/components/inspector/item-sibling.vue +++ /dev/null @@ -1,694 +0,0 @@ - - - - - From 88196879ca84049977feb19ee20717b649c625c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olov=20Ylinenp=C3=A4=C3=A4?= Date: Fri, 16 Aug 2024 09:33:06 +0200 Subject: [PATCH 2/5] Remove dead code --- vue-client/src/components/inspector/field.vue | 28 ------------------- .../src/components/inspector/item-local.vue | 10 ------- 2 files changed, 38 deletions(-) diff --git a/vue-client/src/components/inspector/field.vue b/vue-client/src/components/inspector/field.vue index 9c8fe2653..16062b93f 100644 --- a/vue-client/src/components/inspector/field.vue +++ b/vue-client/src/components/inspector/field.vue @@ -130,7 +130,6 @@ export default { activeModal: false, removeHover: false, pasteHover: false, - foundChip: false, removed: false, uniqueIds: [], }; @@ -374,15 +373,6 @@ export default { } return `${this.parentPath}.${this.fieldKey}`; }, - isMarc() { - return this.fieldKey.startsWith('marc:'); - }, - isChild() { - if (this.parentPath !== 'mainEntity') { - return true; - } - return false; - }, isLangMapWithPartner() { return this.isLangMap && this.hasProp; }, @@ -584,17 +574,6 @@ export default { } return false; }, - isInGraph(o) { - const data = this.inspector.data; - for (const point in data) { - if (data[point] !== null) { - if (data[point]['@id'] === o['@id']) { - return true; - } - } - } - return false; - }, isEmbedded(o) { const type = o['@type']; if (!type || typeof type === 'undefined') { @@ -602,13 +581,6 @@ export default { } return VocabUtil.isEmbedded(type, this.resources.vocab, this.settings); }, - isChip(item) { - if (this.getDatatype(item) === 'entity') { - this.foundChip = true; - return true; - } - return false; - }, highLightLastAdded() { if (this.isLastAdded === true) { if (this.fieldValue === null || (isArray(this.fieldValue) && this.fieldValue.length === 0)) { diff --git a/vue-client/src/components/inspector/item-local.vue b/vue-client/src/components/inspector/item-local.vue index 687af16be..e0968cbdc 100644 --- a/vue-client/src/components/inspector/item-local.vue +++ b/vue-client/src/components/inspector/item-local.vue @@ -69,7 +69,6 @@ export default { }, data() { return { - inEdit: false, showCardInfo: false, extractDialogActive: false, propertyAdderOpened: false, @@ -225,9 +224,6 @@ export default { this.expand(); } }, - isHolding() { - return this.inspector.data.mainEntity['@type'] === 'Item'; - }, isHistoryView() { return this.diff !== null; }, @@ -249,12 +245,6 @@ export default { closeExtractDialog() { this.extractDialogActive = false; }, - openForm() { - this.inEdit = true; - }, - closeForm() { - this.inEdit = false; - }, addFocus() { this.focused = true; }, From 81208b0c7b5d2f575b0b6bd95f8d8a8d1eed25cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olov=20Ylinenp=C3=A4=C3=A4?= Date: Mon, 19 Aug 2024 17:04:12 +0200 Subject: [PATCH 3/5] Add id-pill component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the id-fnurgel pill seen in the top right corner of cards to its own component. Use it in item-local as well. Co-authored-by: Lars Rosenström --- .../src/components/inspector/item-local.vue | 10 ++ .../src/components/shared/entity-summary.vue | 82 ++----------- vue-client/src/components/shared/id-pill.vue | 110 ++++++++++++++++++ 3 files changed, 128 insertions(+), 74 deletions(-) create mode 100644 vue-client/src/components/shared/id-pill.vue diff --git a/vue-client/src/components/inspector/item-local.vue b/vue-client/src/components/inspector/item-local.vue index e0968cbdc..029480a11 100644 --- a/vue-client/src/components/inspector/item-local.vue +++ b/vue-client/src/components/inspector/item-local.vue @@ -9,6 +9,7 @@ import * as LayoutUtil from '@/utils/layout'; import { translatePhrase, labelByLang, capitalize } from '@/utils/filters'; import PropertyAdder from '@/components/inspector/property-adder.vue'; import EntityAction from '@/components/inspector/entity-action.vue'; +import IdPill from '@/components/shared/id-pill.vue'; import SearchWindow from './search-window.vue'; import ItemMixin from '../mixins/item-mixin.vue'; import LensMixin from '../mixins/lens-mixin.vue'; @@ -149,6 +150,9 @@ export default { } return false; }, + hasId() { + return this.item.hasOwnProperty('@id'); + }, getPath() { if (this.inArray) { return `${this.parentPath}[${this.index}]`; @@ -445,6 +449,7 @@ export default { 'property-adder': PropertyAdder, 'search-window': SearchWindow, 'entity-action': EntityAction, + 'id-pill': IdPill, }, }; @@ -510,6 +515,11 @@ export default { +
{ - self.recentlyCopiedId = true; - setTimeout(() => { - self.recentlyCopiedId = false; - }, 1000); - }, (e) => { - self.failedCopyId = true; - console.warn(e); - }); - }, importThis() { this.$emit('import-this'); }, @@ -332,18 +304,11 @@ export default { {{ topBarInformation }} {{ isLocal ? '{lokal entitet}' : '' }} {{ database }}
-
- {{ idAsFnurgel }} -
+
@@ -442,7 +407,7 @@ export default { display: flex; } - &-type, &-id { + &-type { display: block; overflow: hidden; text-overflow: ellipsis; @@ -461,37 +426,6 @@ export default { flex-grow: 2; flex-basis: 50%; } - &-id { - flex-grow: 0; - text-align: right; - text-transform: none; - color: @grey-very-dark-transparent; - background-color: @badge-color-transparent; - transition: background-color 0.5s ease; - letter-spacing: 0.5px; - font-size: 1.2rem; - font-weight: 400; - padding: 0 0.75em; - border-radius: 1em; - - &.recently-copied { - background-color: @brand-success; - color: @white; - } - } - &-idCopyIcon { - transition: all 0.25s ease; - margin: 0 0.25em 0 -0.25em; - overflow: hidden; - width: 1.2em; - opacity: 1; - cursor: pointer; - &.collapsedIcon { - margin: 0 0 0 0; - width: 0; - opacity: 0; - } - } &-sourceLabel { border: 1px solid; diff --git a/vue-client/src/components/shared/id-pill.vue b/vue-client/src/components/shared/id-pill.vue new file mode 100644 index 000000000..0c5f4c138 --- /dev/null +++ b/vue-client/src/components/shared/id-pill.vue @@ -0,0 +1,110 @@ + + + + + From bcf023b19f09e5f9205de6d74c34adc6bb714b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olov=20Ylinenp=C3=A4=C3=A4?= Date: Mon, 19 Aug 2024 17:06:49 +0200 Subject: [PATCH 4/5] Add support for linked SingleItem in holdings --- lxljs/vocab.js | 3 + vue-client/src/components/inspector/field.vue | 30 +++---- .../src/components/inspector/item-entity.vue | 10 +-- .../src/components/inspector/item-local.vue | 90 +++++++++++++++++-- .../inspector/reverse-relations.vue | 3 +- .../src/components/inspector/toolbar.vue | 16 ++-- .../src/components/mixins/item-mixin.vue | 26 +++--- vue-client/src/resources/json/i18n.json | 5 +- vue-client/src/settings.js | 3 + vue-client/src/store.js | 17 +++- vue-client/src/utils/data.js | 4 + vue-client/src/utils/http.js | 9 ++ vue-client/src/utils/record.js | 58 +++++++++++- vue-client/src/utils/shelfmark.js | 8 +- vue-client/src/views/Inspector.vue | 87 ++++++++++++++++-- 15 files changed, 309 insertions(+), 60 deletions(-) diff --git a/lxljs/vocab.js b/lxljs/vocab.js index b0fe7b396..e951140b2 100644 --- a/lxljs/vocab.js +++ b/lxljs/vocab.js @@ -172,6 +172,9 @@ export function getRecordType(mainEntityType, vocab, context) { // Mostly added as to not throw an error while the data is being picked up. return null; } + if (isSubClassOf(mainEntityType, 'SingleItem', vocab, context)) { + return 'SingleItem'; + } if (isSubClassOf(mainEntityType, 'Item', vocab, context)) { return 'Item'; } diff --git a/vue-client/src/components/inspector/field.vue b/vue-client/src/components/inspector/field.vue index 16062b93f..b99183970 100644 --- a/vue-client/src/components/inspector/field.vue +++ b/vue-client/src/components/inspector/field.vue @@ -266,7 +266,8 @@ export default { this.resources.context, this.resources.vocabClasses, ).map((item) => StringUtil.getCompactUri(item, this.resources.context)); - return fetchedRange; + const unselectableTypes = Object.values(this.settings.extractableMappedTypes); + return fetchedRange.filter((t) => !unselectableTypes.includes(t)); }, allSearchTypes() { if (this.allValuesFrom.length > 0) { @@ -387,12 +388,10 @@ export default { ); }, isCompositional() { - if (this.keyAsVocabProperty && this.keyAsVocabProperty.hasOwnProperty('category')) { - if (this.keyAsVocabProperty.category['@id'] === 'https://id.kb.se/vocab/compositional') { - return true; - } - } - return false; + return VocabUtil.hasCategory(this.keyAsVocabProperty, 'compositional', this.resources); + }, + isHistoryView() { + return this.diff !== null; }, hasSingleValue() { if (!isArray(this.fieldValue) || this.fieldValue.length === 1) { @@ -548,12 +547,12 @@ export default { if (this.fieldKey === '@type' || VocabUtil.getContextValue(this.fieldKey, '@type', this.resources.context) === '@vocab') { return 'vocab'; } + if (this.isPlainObject(o) && (!this.isLinked(o) || this.isInlined(o))) { + return 'local'; + } if (this.isPlainObject(o) && this.isLinked(o)) { return 'entity'; } - if (this.isPlainObject(o) && !this.isLinked(o)) { - return 'local'; - } if (this.range && this.range.length > 0 && this.range.every((r) => Object.keys(VocabUtil.XSD_NUMERIC_TYPES).includes(r))) { return 'numeric'; } @@ -569,10 +568,11 @@ export default { if (typeof o === 'undefined') { throw new Error('Cannot check link status of undefined object.'); } - if (o.hasOwnProperty('@id') && !o.hasOwnProperty('@type')) { - return true; - } - return false; + return o.hasOwnProperty('@id'); + }, + isInlined(o) { + // FIXME + return this.isLinked(o) && Object.keys(o).length > 1 && this.isCompositional && !this.isHistoryView; }, isEmbedded(o) { const type = o['@type']; @@ -915,7 +915,7 @@ export default { :diff="diff" :parent-path="path" /> - + 1; }, + isHistoryView() { + return this.diff != null; + }, }, watch: { 'inspector.event'(val) { @@ -128,9 +131,6 @@ export default { removeFocus() { this.focused = false; }, - isHistoryView() { - return this.diff != null; - }, }, components: { Menu, @@ -138,7 +138,7 @@ export default { ReverseRelations, }, created() { - if (this.$store.state.settings.defaultExpandedProperties.includes(this.fieldKey)) { + if (this.$store.state.settings.defaultExpandedProperties.includes(this.fieldKey) && !this.isHistoryView) { this.expand(); } }, @@ -151,7 +151,7 @@ export default { } }).catch((error) => console.error(error)); } - if (this.isExpanded && !this.isHistoryView()) { + if (this.isExpanded && !this.isHistoryView) { this.expand(); } if (this.isNewlyAdded) { diff --git a/vue-client/src/components/inspector/item-local.vue b/vue-client/src/components/inspector/item-local.vue index 029480a11..5335dcb27 100644 --- a/vue-client/src/components/inspector/item-local.vue +++ b/vue-client/src/components/inspector/item-local.vue @@ -6,6 +6,8 @@ import * as VocabUtil from 'lxljs/vocab'; import * as DisplayUtil from 'lxljs/display'; import * as StringUtil from 'lxljs/string'; import * as LayoutUtil from '@/utils/layout'; +import * as HttpUtil from "@/utils/http"; +import { DELETE_ON_SAVE } from '@/store'; import { translatePhrase, labelByLang, capitalize } from '@/utils/filters'; import PropertyAdder from '@/components/inspector/property-adder.vue'; import EntityAction from '@/components/inspector/entity-action.vue'; @@ -150,7 +152,18 @@ export default { } return false; }, - hasId() { + canExtractMappedType() { + // TODO open up for all users at some point + if (this.user.getActiveLibraryUri() !== 'https://libris.kb.se/library/S') { + return false; + } + + return this.extractableMappedType && !this.isInlinedRecord; + }, + extractableMappedType() { + return this.settings.extractableMappedTypes[this.item['@type']]; + }, + isInlinedRecord() { return this.item.hasOwnProperty('@id'); }, getPath() { @@ -290,7 +303,13 @@ export default { }, cloneThis() { const parentData = cloneDeep(get(this.inspector.data, this.parentPath)); - parentData.push(this.item); + if (this.isInlinedRecord) { + const o = cloneDeep(this.item); + delete o['@id']; + parentData.push(o); + } else { + parentData.push(this.item); + } this.$store.dispatch('setInspectorStatusValue', { property: 'lastAdded', @@ -322,6 +341,13 @@ export default { copyThis() { const userStorage = cloneDeep(this.userStorage); userStorage.copyClipboard = this.item; + if (this.isInlinedRecord) { + const o = cloneDeep(this.item); + delete o['@id']; + userStorage.copyClipboard = o; + } else { + userStorage.copyClipboard = this.item; + } this.$store.dispatch('setUserStorage', userStorage); this.$store.dispatch( 'pushNotification', @@ -346,6 +372,24 @@ export default { } }); }, + removeThisAndDeleteLinked() { + HttpUtil.getRelatedRecords({ o: this.item['@id'], _limit: 0 }, this.settings.apiPath) + .then((response) => { + // Before queuing the deletion of the linked record, check if there are any other + // records linking to it. If there are other links we won't be able to delete it. + // We assume that if there is exactly one link it is our own link. + if (response.totalItems === 1) { + const extraChangeList = [{ path: DELETE_ON_SAVE, id: this.item['@id'], type: this.item['@type'] }]; + this.removeThis(true, extraChangeList); + } else { + this.$store.dispatch('pushNotification', { type: 'danger', message: `${translatePhrase('Forbidden')} - ${translatePhrase('This entity may have active links')}` }); + } + }, (error) => { + const msg = 'Error checking for relations'; + this.$store.dispatch('pushNotification', { type: 'danger', message:`${translatePhrase(msg)} - ${error?.statusText || ''}` }); + console.log(msg, error); + }); + }, }, watch: { 'inspector.status.editing'(val) { @@ -499,7 +543,7 @@ export default { :title="item['@type']"> - {{ translatePhrase("The work is extracted once the instance is saved") }} + {{ translatePhrase("Extracted when the record is saved") }} {{ capitalize(labelByLang(item['@type'])) }}: @@ -516,7 +560,7 @@ export default {
@@ -559,7 +603,7 @@ export default { /> + + + + +
  • + +