From 0dd92d73e9c84b9f11a4a823b2001f3dfcbb194f Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Sun, 22 Feb 2026 20:19:33 +0000 Subject: [PATCH] Bug 39224: Vue framework component enhancements Enhances shared Vue components to support the identity providers admin interface and improve reusability: - AccordionWrapper: Adds dynamic slot support - FormElement: Adds textarea and select type support - FormSection: New component for grouping form fields - KohaTable: Adds column visibility and action customisation - RelationshipWidget: Adds slot for custom content - ResourceFormSave: Adds support for nested resources and custom save handlers - ResourceShow: Adds support for related resource display - ShowElement: Adds boolean and list display support - base-resource composable: Adds relationship management and improved error handling Test plan: 1. Verify existing Vue SPAs (e.g. ERM) still function correctly 2. Components should be backwards compatible Sponsored-by: ByWater Solutions --- .../js/vue/components/AccordionWrapper.vue | 20 ++- .../prog/js/vue/components/FormElement.vue | 11 +- .../prog/js/vue/components/FormSection.vue | 147 ++++++++++++++++++ .../prog/js/vue/components/KohaTable.vue | 9 +- .../js/vue/components/RelationshipWidget.vue | 2 + .../js/vue/components/ResourceFormSave.vue | 51 +++++- .../prog/js/vue/components/ResourceShow.vue | 90 +++++++++-- .../prog/js/vue/components/ShowElement.vue | 10 ++ .../prog/js/vue/composables/base-resource.js | 35 ++++- 9 files changed, 341 insertions(+), 34 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/FormSection.vue diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/AccordionWrapper.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/AccordionWrapper.vue index 64c35f77cb5..ddee8df9449 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/AccordionWrapper.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/AccordionWrapper.vue @@ -3,6 +3,7 @@ v-for="(group, counter) in accordionList" v-bind:key="counter" class="accordion" + :class="{ 'group-placeholder-accordion': group.placeholder }" >
-
+
+

+ {{ group.placeholder.description }} +

+
+
@@ -41,4 +47,14 @@ export default { }; - + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/FormElement.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/FormElement.vue index 8379c7eea33..a89c4d3553b 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/FormElement.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/FormElement.vue @@ -81,8 +81,8 @@ >{{ $__("Yes") }}: {{ $__("No") }}: -
+
+
{ + const saveAndNavigate = async ($event, partialResource = null) => { const { components, navigationOnFormSave, @@ -177,6 +197,12 @@ export default { router, idAttr, } = props.instancedResource; + + // Merge partialResource if provided (for section saves) + if (partialResource) { + Object.assign(resourceToSave.value, partialResource); + } + // Default to show const navigationAction = saveOptionSelected.value || @@ -185,18 +211,29 @@ export default { const idParamRequired = navigationAction === components.show || navigationAction === components.edit; - onFormSave($event, resourceToSave).then(resource => { - if (resource) { + + try { + const savedResource = await onFormSave( + $event, + resourceToSave.value + ); + if (savedResource) { router.push({ name: navigationAction, ...(idParamRequired && { params: { - [idAttr]: resource[idAttr], + [idAttr]: savedResource[idAttr], }, }), }); } - }); + } catch (error) { + // onFormSave or router push might throw, handled by instancedResource usually + } + }; + + const handleSectionSave = (groupName, updatedResource) => { + Object.assign(resourceToSave.value, updatedResource); }; const saveOptionSelected = ref( @@ -270,6 +307,7 @@ export default { resourceToSave, resourceForm, saveAndNavigate, + handleSectionSave, editMode, saveDropdownButtonActions, }; @@ -282,6 +320,7 @@ export default { FormElement, TabsWrapper, AccordionWrapper, + FormSection, // New Component DropdownButtons, }, name: "ResourceFormSave", diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ResourceShow.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ResourceShow.vue index 9d3dbb654fb..94a7168deaf 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ResourceShow.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ResourceShow.vue @@ -6,7 +6,8 @@ :resource="resource" :componentPropData="{ ...$props, ...$data }" /> -

+

{{ instancedResource.i18n.newLabel }}

+

{{ instancedResource.i18n.displayName + " #" + @@ -78,6 +79,25 @@

+
+ +
import Toolbar from "./Toolbar.vue"; import ShowElement from "./ShowElement.vue"; +import FormSection from "./FormSection.vue"; import { computed, onBeforeMount, ref } from "vue"; import TabsWrapper from "./TabsWrapper.vue"; import AccordionWrapper from "./AccordionWrapper.vue"; @@ -125,25 +146,63 @@ export default { const resource = ref(null); const additionalProps = ref({}); - onBeforeMount(() => { - props.instancedResource.getResource( - props.instancedResource.route.params[ + const isNewResource = computed( + () => + !props.instancedResource.route.params[ props.instancedResource.idAttr - ], - { - resource, - initialized, - instancedResource: props.instancedResource, - additionalProps, - }, - "show" - ); + ] + ); + + onBeforeMount(() => { + if (isNewResource.value) { + resource.value = {}; + initialized.value = true; + } else { + props.instancedResource.getResource( + props.instancedResource.route.params[ + props.instancedResource.idAttr + ], + { + resource, + initialized, + instancedResource: props.instancedResource, + additionalProps, + }, + "show" + ); + } }); + const handleSectionSave = async (groupName, updatedResource) => { + try { + const result = await props.instancedResource.onShowSectionSave( + groupName, + updatedResource, + resource.value, + isNewResource.value + ); + if (result) { + Object.assign(resource.value, result); + const newId = result[props.instancedResource.idAttr]; + if (isNewResource.value && newId) { + props.instancedResource.router.push({ + name: props.instancedResource.components.show, + params: { + [props.instancedResource.idAttr]: newId, + }, + }); + } + } + } catch (error) { + // errors are surfaced by the httpClient / setError + } + }; + const fieldList = computed(() => { const fieldGroupings = props.instancedResource.getFieldGroupings( "Show", - resource.value + // Pass null for new resources so the hasDataToDisplay filter is skipped + isNewResource.value ? null : resource.value ); const fieldsToAppend = props.instancedResource .appendToShow({ @@ -185,6 +244,8 @@ export default { additionalProps, fieldList, displayMode, + isNewResource, + handleSectionSave, }; }, props: { @@ -193,6 +254,7 @@ export default { components: { Toolbar, ShowElement, + FormSection, TabsWrapper, AccordionWrapper, SplitScreenWrapper, diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ShowElement.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ShowElement.vue index 34e50d36bf9..e3128d6f1f3 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ShowElement.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ShowElement.vue @@ -134,6 +134,16 @@ > +