Bug 38201: Introduce RelationshipTableDisplay for nested Kohatables

Signed-off-by: Michaela Sieber <michaela.sieber@kit.edu>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Matt Blenkinsop 2025-01-31 12:55:59 +00:00 committed by Lucas Gass
parent f4c32d58aa
commit efe7a44da7
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
9 changed files with 265 additions and 442 deletions

View File

@ -44,6 +44,15 @@
name: proxy
required: false
type: string
- name: x-koha-embed
in: header
required: false
description: Embed list sent as a request header
type: array
items:
type: string
enum:
- package
- $ref: "../swagger.yaml#/parameters/match"
- $ref: "../swagger.yaml#/parameters/order_by"
- $ref: "../swagger.yaml#/parameters/page"
@ -208,4 +217,4 @@
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
erm: 1
erm: 1

View File

@ -192,6 +192,42 @@ export default {
});
return filterOptions;
},
getFieldGroupings() {
const groupings = this.resourceAttrs.reduce((acc, attr) => {
if (
attr.hasOwnProperty("group") &&
attr.group !== null &&
!acc.includes(attr.group)
) {
return [...acc, attr.group];
}
if (!attr.hasOwnProperty("group")) {
attr.group = "noGroupFound";
if (!acc.includes("noGroupFound")) {
return [...acc, "noGroupFound"];
}
}
return acc;
}, []);
if (groupings.length === 0) {
return [
{
name: null,
fields: this.resourceAttrs,
},
];
}
return groupings.reduce((acc, group) => {
const groupFields = this.resourceAttrs.filter(
ra => ra.group === group
);
const groupInfo = {
name: group === "noGroupFound" ? null : group,
fields: groupFields,
};
return [...acc, groupInfo];
}, []);
},
},
computed: {
newResource() {

View File

@ -32,6 +32,7 @@
goToResourceEdit,
doResourceDelete,
resourceName,
getFieldGroupings,
}"
/>
<ResourceFormAdd
@ -45,6 +46,7 @@
resource: newResource,
onSubmit,
resourceName,
getFieldGroupings,
}"
/>
</template>
@ -104,91 +106,76 @@ export default {
name: "publication_title",
required: true,
type: "text",
style: { width: "25rem" },
label: __("Publication title"),
},
{
name: "print_identifier",
type: "text",
style: { width: "25rem" },
label: __("Print-format identifier"),
},
{
name: "online_identifier",
type: "text",
style: { width: "25rem" },
label: __("Online-format identifier"),
},
{
name: "date_first_issue_online",
type: "text",
style: { width: "25rem" },
label: __("Date of first serial issue available online"),
},
{
name: "num_first_vol_online",
type: "text",
style: { width: "25rem" },
label: __("Number of first volume available online"),
},
{
name: "num_first_issue_online",
type: "text",
style: { width: "25rem" },
label: __("Number of first issue available online"),
},
{
name: "date_last_issue_online",
type: "text",
style: { width: "25rem" },
label: __("Date of last issue available online"),
},
{
name: "num_last_vol_online",
type: "text",
style: { width: "25rem" },
label: __("Number of last volume available online"),
},
{
name: "num_last_issue_online",
type: "text",
style: { width: "25rem" },
label: __("Number of last issue available online"),
},
{
name: "title_url",
type: "text",
style: { width: "25rem" },
label: __("Title-level URL"),
},
{
name: "first_author",
type: "text",
style: { width: "25rem" },
label: __("First author"),
},
{
name: "embargo_info",
type: "text",
style: { width: "25rem" },
label: __("Embargo information"),
},
{
name: "coverage_depth",
type: "text",
style: { width: "25rem" },
label: __("Coverage depth"),
},
{
name: "notes",
type: "text",
style: { width: "25rem" },
label: __("Notes"),
},
{
name: "publisher_name",
type: "text",
style: { width: "25rem" },
label: __("Publisher name"),
},
{
@ -196,48 +183,40 @@ export default {
type: "select",
label: __("Publication type"),
avCat: "av_title_publication_types",
style: { width: "25rem" },
},
{
name: "date_monograph_published_print",
type: "text",
style: { width: "25rem" },
label: __("Date the monograph is first published in print"),
},
{
name: "date_monograph_published_online",
type: "text",
style: { width: "25rem" },
label: __("Date the monograph is first published online"),
},
{
name: "monograph_volume",
type: "text",
style: { width: "25rem" },
label: __("Number of volume for monograph"),
},
{
name: "monograph_edition",
type: "text",
style: { width: "25rem" },
label: __("Edition of the monograph"),
},
{
name: "first_editor",
type: "text",
style: { width: "25rem" },
label: __("First editor"),
},
{
name: "parent_publication_title_id",
type: "text",
style: { width: "25rem" },
label: __("Title identifier of the parent publication"),
},
{
name: "preceding_publication_title_id",
type: "text",
style: { width: "25rem" },
label: __(
"Title identifier of any preceding publication title"
),
@ -245,7 +224,6 @@ export default {
{
name: "access_type",
type: "text",
style: { width: "25rem" },
label: __("Access type"),
},
{
@ -260,13 +238,69 @@ export default {
? __("Create bibliographic record")
: __("Update bibliographic record"),
value: false,
style: { width: "25rem" },
},
{
name: "resources",
type: "relationshipWidget",
group: __("Packages"),
apiClient: APIClient.erm.localPackages,
showElement: {
type: "component",
hidden: title => title.resources.length > 0,
componentPath: "./RelationshipTableDisplay.vue",
props: {
tableOptions: {
type: "object",
value: {
columns: [
{
title: __("Name"),
data: "package.name",
searchable: true,
orderable: true,
render: function (
data,
type,
row,
meta
) {
return (
'<a role="button" class="show">' +
escape_str(
`${row.package.name} (#${row.package.package_id})`
) +
"</a>"
);
},
},
],
options: {
embed: "package",
},
url:
APIClient.erm._baseURL +
"eholdings/local/resources",
},
},
apiClient: {
type: "object",
value: APIClient.erm.localPackages,
},
filters: {
type: "filter",
keys: {
title_id: { property: "title_id" },
},
},
resource: {
type: "resource",
},
resourceName: {
type: "string",
value: "package",
},
},
},
props: {
newRelationship: {
type: "object",
@ -338,10 +372,6 @@ export default {
value: "started_on_",
indexRequired: true,
},
required: {
type: "boolean",
value: true,
},
date_to: {
type: "string",
value: "ended_on_",
@ -594,3 +624,12 @@ export default {
name: "EHoldingsLocalTitlesResource",
};
</script>
<style scoped>
:deep(fieldset.rows ol table) {
display: table;
}
:deep(fieldset.rows label) {
width: 25rem;
}
</style>

View File

@ -1,345 +0,0 @@
<template>
<div v-if="!initialized">{{ $__("Loading") }}</div>
<div v-else-if="title" id="eholdings_title_show">
<Toolbar>
<ToolbarButton
action="edit"
@go-to-edit-resource="goToResourceEdit"
/>
<ToolbarButton
action="delete"
@delete-resource="doResourceDelete"
/>
</Toolbar>
<h2>
{{ $__("Title #%s").format(title.title_id) }}
</h2>
<div>
<fieldset class="rows">
<ol>
<li v-if="title.title_id">
<label>{{ $__("Title identifier") }}:</label>
<span>
{{ title.title_id }}
</span>
</li>
<li>
<label>{{ $__("Publication title") }}:</label>
<span>
{{ title.publication_title }}
<a
v-if="title.biblio_id"
:href="`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${title.biblio_id}`"
>
{{ $__("Local bibliographic record") }}
</a>
</span>
</li>
<li v-if="title.print_identifier">
<label>{{ $__("Print-format identifier") }}:</label>
<span>
{{ title.print_identifier }}
</span>
</li>
<li v-if="title.online_identifier">
<label>{{ $__("Online-format identifier") }}:</label>
<span>
{{ title.online_identifier }}
</span>
</li>
<li v-if="title.date_first_issue_online">
<label
>{{
$__(
"Date of first serial issue available online"
)
}}:</label
>
<span>
{{ title.date_first_issue_online }}
</span>
</li>
<li v-if="title.num_first_vol_online">
<label
>{{
$__("Number of first volume available online")
}}:</label
>
<span>
{{ title.num_first_vol_online }}
</span>
</li>
<li v-if="title.num_first_issue_online">
<label
>{{
$__("Number of first issue available online")
}}:</label
>
<span>
{{ title.num_first_issue_online }}
</span>
</li>
<li v-if="title.date_last_issue_online">
<label
>{{
$__("Date of last issue available online")
}}:</label
>
<span>
{{ title.date_last_issue_online }}
</span>
</li>
<li v-if="title.num_last_vol_online">
<label
>{{
$__("Number of last volume available online")
}}:</label
>
<span>
{{ title.num_last_vol_online }}
</span>
</li>
<li v-if="title.num_last_issue_online">
<label
>{{
$__("Number of last issue available online")
}}:</label
>
<span>
{{ title.num_last_issue_online }}
</span>
</li>
<li v-if="title.title_url">
<label>{{ $__("Title-level URL") }}:</label>
<span>
<a :href="title.title_url" target="_blank">{{
title.title_url
}}</a>
</span>
</li>
<li v-if="title.first_author">
<label>{{ $__("First author") }}:</label>
<span>
{{ title.first_author }}
</span>
</li>
<li v-if="title.embargo_info">
<label>{{ $__("Embargo information") }}:</label>
<span>
{{ title.embargo_info }}
</span>
</li>
<li v-if="title.coverage_depth">
<label>{{ $__("Coverage depth") }}:</label>
<span>
{{ title.coverage_depth }}
</span>
</li>
<li v-if="title.notes">
<label>{{ $__("Notes") }}:</label>
<span>
{{ title.notes }}
</span>
</li>
<li v-if="title.publisher_name">
<label>{{ $__("Publisher name") }}:</label>
<span>
{{ title.publisher_name }}
</span>
</li>
<li v-if="title.publication_type">
<label>{{ $__("Publication type") }}:</label>
<span
>{{
get_lib_from_av(
"av_title_publication_types",
title.publication_type
)
}}
</span>
</li>
<li v-if="title.date_monograph_published_print">
<label
>{{
$__(
"Date the monograph is first published in print"
)
}}:</label
>
<span>
{{ title.date_monograph_published_print }}
</span>
</li>
<li v-if="title.date_monograph_published_online">
<label
>{{
$__(
"Date the monograph is first published online"
)
}}:</label
>
<span>
{{ title.date_monograph_published_online }}
</span>
</li>
<li v-if="title.monograph_volume">
<label
>{{ $__("Number of volume for monograph") }}:</label
>
<span>
{{ title.monograph_volume }}
</span>
</li>
<li v-if="title.monograph_edition">
<label>{{ $__("Edition of the monograph") }}:</label>
<span>
{{ title.monograph_edition }}
</span>
</li>
<li v-if="title.first_editor">
<label>{{ $__("First editor") }}:</label>
<span>
{{ title.first_editor }}
</span>
</li>
<li v-if="title.parent_publication_title_id">
<label
>{{
$__(
"Title identifier of the parent publication"
)
}}:</label
>
<span>
{{ title.parent_publication_title_id }}
</span>
</li>
<li v-if="title.preceding_publication_title_id">
<label
>{{
$__(
"Title identifier of any preceding publication title"
)
}}:</label
>
<span>
{{ title.preceding_publication_title_id }}
</span>
</li>
<li v-if="title.access_type">
<label>{{ $__("Access type") }}:</label>
<span>
{{ title.access_type }}
</span>
</li>
<li>
<label>{{
$__("Packages (%s)").format(title.resources.length)
}}</label>
<div v-if="title.resources.length">
<EHoldingsTitlePackagesList
:resources="title.resources"
/>
</div>
</li>
</ol>
</fieldset>
<fieldset class="action">
<router-link
:to="{ name: 'EHoldingsLocalTitlesList' }"
role="button"
class="cancel"
>{{ $__("Close") }}</router-link
>
</fieldset>
</div>
</div>
</template>
<script>
import { inject } from "vue";
import EHoldingsTitlePackagesList from "./EHoldingsLocalTitlePackagesList.vue";
import { APIClient } from "../../fetch/api-client.js";
import Toolbar from "../Toolbar.vue";
import ToolbarButton from "../ToolbarButton.vue";
import EHoldingsLocalTitleResource from "./EHoldingsLocalTitleResource.vue";
export default {
extends: EHoldingsLocalTitleResource,
setup() {
const ERMStore = inject("ERMStore");
const { get_lib_from_av } = ERMStore;
return {
...EHoldingsLocalTitleResource.setup(),
get_lib_from_av,
};
},
data() {
return {
title: {
title_id: null,
publication_title: "",
external_id: "",
print_identifier: "",
online_identifier: "",
date_first_issue_online: "",
num_first_vol_online: "",
num_first_issue_online: "",
date_last_issue_online: "",
num_last_vol_online: "",
num_last_issue_online: "",
title_url: "",
first_author: "",
embargo_info: "",
coverage_depth: "",
notes: "",
publisher_name: "",
publication_type: "",
date_monograph_published_print: "",
date_monograph_published_online: "",
monograph_volume: "",
monograph_edition: "",
first_editor: "",
parent_publication_title_id: "",
preceding_publication_title_id: "",
access_type: "",
resources: [],
},
initialized: false,
};
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.getTitle(to.params.title_id);
});
},
beforeRouteUpdate(to, from) {
this.title = this.getTitle(to.params.title_id);
},
methods: {
getTitle(title_id) {
const client = APIClient.erm;
client.localTitles.get(title_id).then(
title => {
this.title = title;
this.initialized = true;
},
error => {}
);
},
},
components: {
EHoldingsTitlePackagesList,
Toolbar,
ToolbarButton,
},
name: "EHoldingsLocalTitlesShow",
};
</script>
<style scoped>
fieldset.rows label {
width: 25rem;
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<div v-if="!initialized">{{ $__("Loading") }}</div>
<div v-else :id="`${resourceName}_relationship_list`">
<div v-if="resourceCount > 0" class="page-section">
<KohaTable
ref="table"
v-bind="formattedTableOptions"
:searchable_additional_fields="searchable_additional_fields"
:searchable_av_options="searchable_av_options"
></KohaTable>
</div>
</div>
</template>
<script>
import Toolbar from "./Toolbar.vue";
import ToolbarButton from "./ToolbarButton.vue";
import { ref } from "vue";
import { APIClient } from "../fetch/api-client.js";
import KohaTable from "./KohaTable.vue";
import FormElement from "./FormElement.vue";
export default {
setup(props) {
const table = ref();
return {
table,
};
},
props: {
apiClient: Object,
i18n: Object,
tableOptions: Object,
filters: {
type: Object,
default: {},
},
resource: Object,
resourceName: String,
},
data() {
return {
resourceCount: 0,
initialized: false,
searchable_additional_fields: [],
searchable_av_options: [],
};
},
created() {
this.getResourceCount().then(() => {
if (this.hasAdditionalFields) {
this.getSearchableAdditionalFields().then(() =>
this.getSearchableAVOptions().then(
() => (this.initialized = true)
)
);
} else {
this.initialized = true;
}
});
},
methods: {
async getResourceCount() {
await this.apiClient.count().then(
count => {
this.resourceCount = count;
},
error => {}
);
},
async getSearchableAdditionalFields() {
const client = APIClient.additional_fields;
await client.additional_fields.getAll(this.resourceName).then(
searchable_additional_fields => {
this.searchable_additional_fields =
searchable_additional_fields.filter(
field => field.searchable
);
},
error => {}
);
},
async getSearchableAVOptions() {
const client_av = APIClient.authorised_values;
let av_cat_array = this.searchable_additional_fields
.filter(field => field.authorised_value_category_name)
.map(field => field.authorised_value_category_name);
await client_av.values
.getCategoriesWithValues([
...new Set(av_cat_array.map(av_cat => '"' + av_cat + '"')),
]) // unique
.then(av_categories => {
av_cat_array.forEach(av_cat => {
let av_match = av_categories.find(
element => element.category_name == av_cat
);
this.searchable_av_options[av_cat] =
av_match.authorised_values.map(av => ({
value: av.value,
label: av.description,
}));
});
});
},
},
computed: {
hasAdditionalFields() {
return this.resource.hasOwnProperty("extended_attributes");
},
formattedTableOptions() {
if (this.filters) {
this.tableOptions.url += "?q=" + JSON.stringify(this.filters);
}
return this.tableOptions;
},
},
components: { Toolbar, ToolbarButton, KohaTable, FormElement },
name: "RelationshipTableDisplay",
};
</script>
<style scoped>
.filters > input[type="checkbox"],
.filters > input[type="button"] {
margin-left: 1rem;
}
</style>

View File

@ -66,6 +66,7 @@ export default {
resource: Object,
onSubmit: Function,
resourceName: String,
getFieldGroupings: Function,
},
created() {
if (this.$route.params[this.idAttr]) {
@ -84,42 +85,6 @@ export default {
error => {}
);
},
getFieldGroupings() {
const groupings = this.resourceAttrs.reduce((acc, attr) => {
if (
attr.hasOwnProperty("group") &&
attr.group !== null &&
!acc.includes(attr.group)
) {
return [...acc, attr.group];
}
if (!attr.hasOwnProperty("group")) {
attr.group = "noGroupFound";
if (!acc.includes("noGroupFound")) {
return [...acc, "noGroupFound"];
}
}
return acc;
}, []);
if (groupings.length === 0) {
return [
{
name: null,
fields: this.resourceAttrs,
},
];
}
return groupings.reduce((acc, group) => {
const groupFields = this.resourceAttrs.filter(
ra => ra.group === group
);
const groupInfo = {
name: group === "noGroupFound" ? null : group,
fields: groupFields,
};
return [...acc, groupInfo];
}, []);
},
},
computed: {
resourceToAddOrEdit() {

View File

@ -16,30 +16,21 @@
{{ i18n.displayName + " #" + resource[idAttr] }}
</h2>
<div>
<fieldset class="rows">
<fieldset
class="rows"
v-for="(group, counter) in getFieldGroupings()"
v-bind:key="counter"
>
<legend v-if="group.name">{{ group.name }}</legend>
<ol>
<li
v-for="(attr, index) in resourceAttrs.filter(
attr => attr.showElement?.type !== 'relationship'
)"
v-for="(attr, index) in group.fields"
v-bind:key="index"
>
<ShowElement
:resource="resource"
:attr="attr"
:index="index"
/>
<ShowElement :resource="resource" :attr="attr" />
</li>
</ol>
</fieldset>
<template
v-for="(attr, index) in resourceAttrs.filter(
attr => attr.showElement?.type === 'relationship'
)"
v-bind:key="'rel-' + index"
>
<ShowElement :resource="resource" :attr="attr" />
</template>
<fieldset class="action">
<router-link
:to="{ name: listComponent }"
@ -72,6 +63,7 @@ export default {
goToResourceEdit: Function,
doResourceDelete: Function,
resourceName: String,
getFieldGroupings: Function,
},
created() {
this.getResource(this.$route.params[this.idAttr]);

View File

@ -1,5 +1,5 @@
<template>
<div
<template
v-if="
attribute.type == 'text' ||
attribute.type == 'textarea' ||
@ -18,8 +18,8 @@
}}
</span>
</LinkWrapper>
</div>
<div
</template>
<template
v-else-if="
attribute.type == 'av' ||
(attribute.type == 'select' && attribute.avCat)
@ -31,13 +31,13 @@
get_lib_from_av(attribute.avCat, resource[attribute.name])
}}</span>
</LinkWrapper>
</div>
<div v-else-if="attribute.type == 'boolean'">
</template>
<template v-else-if="attribute.type == 'boolean'">
<label>{{ attribute.label }}:</label>
<span v-if="resource[attribute.name]">{{ $__("Yes") }}</span>
<span v-else>{{ $__("No") }}</span>
</div>
<div v-else-if="attribute.type === 'table'">
</template>
<template v-else-if="attribute.type === 'table'">
<template v-if="attribute.hidden && attribute.hidden(resource)">
<label>{{ attribute.label }}</label>
<table>
@ -97,15 +97,16 @@
</tbody>
</table>
</template>
</div>
<div v-else-if="attribute?.type === 'component'">
</template>
<template v-else-if="attribute?.type === 'component'">
<template v-if="attribute.hidden && attribute.hidden(resource)">
<label v-if="attribute.label">{{ attribute.label }}:</label>
<component
:is="requiredComponent"
v-bind="requiredProps(true)"
></component>
</template>
</div>
</template>
<template v-else-if="attr.type == 'relationship' && attr.componentPath">
<template v-if="attribute.hidden && attribute.hidden(resource)">
<component

View File

@ -7,7 +7,6 @@ import EHoldingsLocalTitlesKBARTImport from "../components/ERM/EHoldingsLocalTit
import EHoldingsLocalPackagesList from "../components/ERM/EHoldingsLocalPackagesList.vue";
import EHoldingsLocalPackagesShow from "../components/ERM/EHoldingsLocalPackagesShow.vue";
import EHoldingsLocalResourcesShow from "../components/ERM/EHoldingsLocalResourcesShow.vue";
import EHoldingsLocalTitlesShow from "../components/ERM/EHoldingsLocalTitlesShow.vue";
import EHoldingsEBSCOPackagesList from "../components/ERM/EHoldingsEBSCOPackagesList.vue";
import EHoldingsEBSCOPackagesShow from "../components/ERM/EHoldingsEBSCOPackagesShow.vue";
import EHoldingsEBSCOResourcesShow from "../components/ERM/EHoldingsEBSCOResourcesShow.vue";
@ -167,9 +166,7 @@ export const routes = [
{
path: ":title_id",
name: "EHoldingsLocalTitlesShow",
component: markRaw(
EHoldingsLocalTitlesShow
),
component: markRaw(ResourceWrapper),
title: $__("Show title"),
},
{