View | Details | Raw Unified | Return to bug 39224
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/AccordionWrapper.vue (-2 / +18 lines)
Lines 3-8 Link Here
3
        v-for="(group, counter) in accordionList"
3
        v-for="(group, counter) in accordionList"
4
        v-bind:key="counter"
4
        v-bind:key="counter"
5
        class="accordion"
5
        class="accordion"
6
        :class="{ 'group-placeholder-accordion': group.placeholder }"
6
    >
7
    >
7
        <fieldset class="accordion-item">
8
        <fieldset class="accordion-item">
8
            <legend
9
            <legend
Lines 22-28 Link Here
22
                :aria-labelledby="`heading-${counter}`"
23
                :aria-labelledby="`heading-${counter}`"
23
                data-bs-parent="#formAccordion"
24
                data-bs-parent="#formAccordion"
24
            >
25
            >
25
                <fieldset class="accordion-body rows">
26
                <div v-if="group.placeholder" class="placeholder-content">
27
                    <p v-if="group.placeholder.description" class="mb-0">
28
                        {{ group.placeholder.description }}
29
                    </p>
30
                </div>
31
                <fieldset v-else class="accordion-body rows">
26
                    <slot name="accordionContent" :accordionGroup="group" />
32
                    <slot name="accordionContent" :accordionGroup="group" />
27
                </fieldset>
33
                </fieldset>
28
            </div>
34
            </div>
Lines 41-44 export default { Link Here
41
};
47
};
42
</script>
48
</script>
43
49
44
<style></style>
50
<style>
51
.group-placeholder-accordion > fieldset {
52
    background-color: var(--bs-info-bg-subtle);
53
    border-radius: 4px;
54
}
55
.group-placeholder-accordion .placeholder-content {
56
    color: #6c757d;
57
    font-style: italic;
58
    padding: 0.75rem 1rem;
59
}
60
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/FormElement.vue (-4 / +7 lines)
Lines 81-88 Link Here
81
            >{{ $__("Yes") }}:
81
            >{{ $__("Yes") }}:
82
            <InputRadio
82
            <InputRadio
83
                type="radio"
83
                type="radio"
84
                :name="attr.name"
84
                :name="getElementId"
85
                :id="attr.name + '_yes'"
85
                :id="getElementId + '_yes'"
86
                :value="true"
86
                :value="true"
87
                :checked="resource[attr.name] == true"
87
                :checked="resource[attr.name] == true"
88
                v-model="resource[attr.name]"
88
                v-model="resource[attr.name]"
Lines 92-99 Link Here
92
            >{{ $__("No") }}:
92
            >{{ $__("No") }}:
93
            <InputRadio
93
            <InputRadio
94
                type="radio"
94
                type="radio"
95
                :name="attr.name"
95
                :name="getElementId"
96
                :id="attr.name + '_no'"
96
                :id="getElementId + '_no'"
97
                :checked="resource[attr.name] == false"
97
                :checked="resource[attr.name] == false"
98
                :value="false"
98
                :value="false"
99
                v-model="resource[attr.name]"
99
                v-model="resource[attr.name]"
Lines 110-115 Link Here
110
            :required="!resource[attr.name] && attr.required"
110
            :required="!resource[attr.name] && attr.required"
111
            :disabled="disabled"
111
            :disabled="disabled"
112
            :multiple="attr.allowMultipleChoices"
112
            :multiple="attr.allowMultipleChoices"
113
            :taggable="attr.taggable || false"
114
            :create-option="attr.createOption"
115
            :placeholder="attr.placeholder"
113
            @option:selected="attr.onSelected && attr.onSelected(resource)"
116
            @option:selected="attr.onSelected && attr.onSelected(resource)"
114
        >
117
        >
115
            <template v-if="attr.required" #search="{ attributes, events }">
118
            <template v-if="attr.required" #search="{ attributes, events }">
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/FormSection.vue (+147 lines)
Line 0 Link Here
1
<template>
2
    <div class="page-section">
3
        <div class="d-flex justify-content-between align-items-center mb-2">
4
            <h2>
5
                <i v-if="group.icon" :class="group.icon"></i>
6
                {{ group.name }}
7
            </h2>
8
            <button
9
                v-if="!editing && !disabled"
10
                type="button"
11
                class="btn btn-default"
12
                @click="startEdit"
13
            >
14
                <i class="fa fa-pencil"></i>
15
                {{ $__("Edit") }}
16
            </button>
17
            <span v-if="disabled" class="text-muted">{{
18
                $__("Save basic settings first")
19
            }}</span>
20
        </div>
21
22
        <!-- VIEW MODE -->
23
        <template v-if="!editing || disabled">
24
            <fieldset class="rows">
25
                <ol>
26
                    <li v-for="attr in group.fields" :key="attr.name">
27
                        <ShowElement
28
                            :resource="resource"
29
                            :attr="attr"
30
                            :instancedResource="instancedResource"
31
                        />
32
                    </li>
33
                </ol>
34
            </fieldset>
35
        </template>
36
37
        <!-- EDIT MODE -->
38
        <template v-else>
39
            <fieldset class="rows">
40
                <ol>
41
                    <li v-for="(attr, index) in group.fields" :key="index">
42
                        <FormElement
43
                            :resource="localResource"
44
                            :attr="attr"
45
                            :index="index"
46
                        />
47
                    </li>
48
                </ol>
49
            </fieldset>
50
51
            <fieldset class="action">
52
                <button
53
                    type="button"
54
                    class="btn btn-primary"
55
                    @click="saveSection"
56
                >
57
                    <i class="fa fa-save"></i>
58
                    {{ $__("Save") }}
59
                </button>
60
                <button type="button" class="cancel" @click="cancelEdit">
61
                    {{ $__("Cancel") }}
62
                </button>
63
            </fieldset>
64
        </template>
65
    </div>
66
</template>
67
68
<script>
69
import { ref, watch, toRaw } from "vue";
70
import { $__ } from "@koha-vue/i18n";
71
import FormElement from "@koha-vue/components/FormElement.vue";
72
import ShowElement from "@koha-vue/components/ShowElement.vue"; // New Import
73
74
export default {
75
    name: "FormSection",
76
    components: {
77
        FormElement,
78
        ShowElement, // New Component
79
    },
80
    props: {
81
        group: {
82
            type: Object,
83
            required: true,
84
            // Example structure: { name: 'Section Title', icon: 'fa fa-info', fields: [...] }
85
        },
86
        resource: {
87
            type: Object,
88
            required: true,
89
        },
90
        instancedResource: {
91
            // New Prop
92
            type: Object,
93
            required: true,
94
        },
95
        startEditing: {
96
            type: Boolean,
97
            default: false,
98
        },
99
        disabled: {
100
            type: Boolean,
101
            default: false,
102
        },
103
    },
104
    emits: ["save", "cancel"],
105
    setup(props, { emit }) {
106
        const editing = ref(props.startEditing);
107
        const localResource = ref({});
108
109
        // Initialize localResource when the component is created or resource prop changes
110
        watch(
111
            () => props.resource,
112
            newResource => {
113
                localResource.value = { ...toRaw(newResource) };
114
            },
115
            { immediate: true }
116
        );
117
118
        const startEdit = () => {
119
            localResource.value = { ...toRaw(props.resource) }; // Copy original for cancellation
120
            editing.value = true;
121
        };
122
123
        const cancelEdit = () => {
124
            editing.value = false;
125
            // localResource is automatically reverted to original via watch effect on props.resource
126
        };
127
128
        const saveSection = () => {
129
            emit("save", props.group.name, localResource.value);
130
            editing.value = false;
131
        };
132
133
        return {
134
            editing,
135
            localResource,
136
            startEdit,
137
            cancelEdit,
138
            saveSection,
139
            $__,
140
        };
141
    },
142
};
143
</script>
144
145
<style scoped>
146
/* Add any scoped styles here if necessary, similar to ProviderWorkspace.vue */
147
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue (-2 / +7 lines)
Lines 247-253 export default { Link Here
247
            });
247
            });
248
248
249
            if (Object.keys(props.actions).length) {
249
            if (Object.keys(props.actions).length) {
250
                dt.on("draw", () => {
250
                const bindActionHandlers = () => {
251
                    const dataSet = dt.rows().data();
251
                    const dataSet = dt.rows().data();
252
                    Object.entries(props.actions).forEach(
252
                    Object.entries(props.actions).forEach(
253
                        ([col_id, actions]) => {
253
                        ([col_id, actions]) => {
Lines 271-277 export default { Link Here
271
                                });
271
                                });
272
                        }
272
                        }
273
                    );
273
                    );
274
                });
274
                };
275
                dt.on("draw", bindActionHandlers);
276
                // For tables using static `data` prop, the initial draw fires
277
                // during child mount (before this onMounted runs), so we must
278
                // bind handlers immediately for the rows already in the DOM.
279
                bindActionHandlers();
275
            }
280
            }
276
        });
281
        });
277
282
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/RelationshipWidget.vue (+2 lines)
Lines 56-66 export default { Link Here
56
        provide("resourceRelationships", props.resourceRelationships);
56
        provide("resourceRelationships", props.resourceRelationships);
57
57
58
        const addResourceRelationship = () => {
58
        const addResourceRelationship = () => {
59
            if (!props.resourceRelationships) return;
59
            props.resourceRelationships.push({
60
            props.resourceRelationships.push({
60
                ...props.newRelationshipDefaultAttrs,
61
                ...props.newRelationshipDefaultAttrs,
61
            });
62
            });
62
        };
63
        };
63
        const deleteResourceRelationship = counter => {
64
        const deleteResourceRelationship = counter => {
65
            if (!props.resourceRelationships) return;
64
            props.resourceRelationships.splice(counter, 1);
66
            props.resourceRelationships.splice(counter, 1);
65
        };
67
        };
66
        const getSelectOptions = filters => {
68
        const getSelectOptions = filters => {
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ResourceFormSave.vue (-6 / +45 lines)
Lines 67-73 Link Here
67
                    </ol>
67
                    </ol>
68
                </template>
68
                </template>
69
            </AccordionWrapper>
69
            </AccordionWrapper>
70
            <div v-else>
70
            <div
71
                v-else-if="
72
                    instancedResource.formGroupsDisplayMode !== 'sections'
73
                "
74
            >
71
                <fieldset
75
                <fieldset
72
                    v-for="(
76
                    v-for="(
73
                        group, counter
77
                        group, counter
Lines 90-95 Link Here
90
                    </ol>
94
                    </ol>
91
                </fieldset>
95
                </fieldset>
92
            </div>
96
            </div>
97
            <div v-else>
98
                <FormSection
99
                    v-for="(
100
                        group, counter
101
                    ) in instancedResource.getFieldGroupings('Form')"
102
                    v-bind:key="counter"
103
                    :group="group"
104
                    :resource="resourceToSave"
105
                    :instancedResource="instancedResource"
106
                    @save="
107
                        (groupName, updatedResource) =>
108
                            handleSectionSave(groupName, updatedResource)
109
                    "
110
                />
111
            </div>
93
            <fieldset
112
            <fieldset
94
                class="action"
113
                class="action"
95
                v-if="
114
                v-if="
Lines 122-127 import ButtonSubmit from "./ButtonSubmit.vue"; Link Here
122
import DropdownButtons from "./DropdownButtons.vue";
141
import DropdownButtons from "./DropdownButtons.vue";
123
import TabsWrapper from "./TabsWrapper.vue";
142
import TabsWrapper from "./TabsWrapper.vue";
124
import AccordionWrapper from "./AccordionWrapper.vue";
143
import AccordionWrapper from "./AccordionWrapper.vue";
144
import FormSection from "./FormSection.vue"; // New Import
125
import { $__ } from "@koha-vue/i18n";
145
import { $__ } from "@koha-vue/i18n";
126
146
127
export default {
147
export default {
Lines 169-175 export default { Link Here
169
            }
189
            }
170
        });
190
        });
171
191
172
        const saveAndNavigate = ($event, resourceToSave) => {
192
        const saveAndNavigate = async ($event, partialResource = null) => {
173
            const {
193
            const {
174
                components,
194
                components,
175
                navigationOnFormSave,
195
                navigationOnFormSave,
Lines 177-182 export default { Link Here
177
                router,
197
                router,
178
                idAttr,
198
                idAttr,
179
            } = props.instancedResource;
199
            } = props.instancedResource;
200
201
            // Merge partialResource if provided (for section saves)
202
            if (partialResource) {
203
                Object.assign(resourceToSave.value, partialResource);
204
            }
205
180
            // Default to show
206
            // Default to show
181
            const navigationAction =
207
            const navigationAction =
182
                saveOptionSelected.value ||
208
                saveOptionSelected.value ||
Lines 185-202 export default { Link Here
185
            const idParamRequired =
211
            const idParamRequired =
186
                navigationAction === components.show ||
212
                navigationAction === components.show ||
187
                navigationAction === components.edit;
213
                navigationAction === components.edit;
188
            onFormSave($event, resourceToSave).then(resource => {
214
189
                if (resource) {
215
            try {
216
                const savedResource = await onFormSave(
217
                    $event,
218
                    resourceToSave.value
219
                );
220
                if (savedResource) {
190
                    router.push({
221
                    router.push({
191
                        name: navigationAction,
222
                        name: navigationAction,
192
                        ...(idParamRequired && {
223
                        ...(idParamRequired && {
193
                            params: {
224
                            params: {
194
                                [idAttr]: resource[idAttr],
225
                                [idAttr]: savedResource[idAttr],
195
                            },
226
                            },
196
                        }),
227
                        }),
197
                    });
228
                    });
198
                }
229
                }
199
            });
230
            } catch (error) {
231
                // onFormSave or router push might throw, handled by instancedResource usually
232
            }
233
        };
234
235
        const handleSectionSave = (groupName, updatedResource) => {
236
            Object.assign(resourceToSave.value, updatedResource);
200
        };
237
        };
201
238
202
        const saveOptionSelected = ref(
239
        const saveOptionSelected = ref(
Lines 270-275 export default { Link Here
270
            resourceToSave,
307
            resourceToSave,
271
            resourceForm,
308
            resourceForm,
272
            saveAndNavigate,
309
            saveAndNavigate,
310
            handleSectionSave,
273
            editMode,
311
            editMode,
274
            saveDropdownButtonActions,
312
            saveDropdownButtonActions,
275
        };
313
        };
Lines 282-287 export default { Link Here
282
        FormElement,
320
        FormElement,
283
        TabsWrapper,
321
        TabsWrapper,
284
        AccordionWrapper,
322
        AccordionWrapper,
323
        FormSection, // New Component
285
        DropdownButtons,
324
        DropdownButtons,
286
    },
325
    },
287
    name: "ResourceFormSave",
326
    name: "ResourceFormSave",
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ResourceShow.vue (-14 / +76 lines)
Lines 6-12 Link Here
6
            :resource="resource"
6
            :resource="resource"
7
            :componentPropData="{ ...$props, ...$data }"
7
            :componentPropData="{ ...$props, ...$data }"
8
        />
8
        />
9
        <h2>
9
        <h2 v-if="isNewResource">{{ instancedResource.i18n.newLabel }}</h2>
10
        <h2 v-else>
10
            {{
11
            {{
11
                instancedResource.i18n.displayName +
12
                instancedResource.i18n.displayName +
12
                " #" +
13
                " #" +
Lines 78-83 Link Here
78
                </fieldset>
79
                </fieldset>
79
            </template>
80
            </template>
80
        </SplitScreenWrapper>
81
        </SplitScreenWrapper>
82
        <div v-else-if="displayMode == 'sections'">
83
            <FormSection
84
                v-for="(group, counter) in fieldList"
85
                v-bind:key="counter"
86
                :group="group"
87
                :resource="resource"
88
                :instancedResource="instancedResource"
89
                :startEditing="isNewResource"
90
                :disabled="
91
                    group.requiresId &&
92
                    isNewResource &&
93
                    !resource[instancedResource.idAttr]
94
                "
95
                @save="
96
                    (groupName, updatedResource) =>
97
                        handleSectionSave(groupName, updatedResource)
98
                "
99
            />
100
        </div>
81
        <div v-else>
101
        <div v-else>
82
            <fieldset
102
            <fieldset
83
                class="rows"
103
                class="rows"
Lines 113-118 Link Here
113
<script>
133
<script>
114
import Toolbar from "./Toolbar.vue";
134
import Toolbar from "./Toolbar.vue";
115
import ShowElement from "./ShowElement.vue";
135
import ShowElement from "./ShowElement.vue";
136
import FormSection from "./FormSection.vue";
116
import { computed, onBeforeMount, ref } from "vue";
137
import { computed, onBeforeMount, ref } from "vue";
117
import TabsWrapper from "./TabsWrapper.vue";
138
import TabsWrapper from "./TabsWrapper.vue";
118
import AccordionWrapper from "./AccordionWrapper.vue";
139
import AccordionWrapper from "./AccordionWrapper.vue";
Lines 125-149 export default { Link Here
125
        const resource = ref(null);
146
        const resource = ref(null);
126
        const additionalProps = ref({});
147
        const additionalProps = ref({});
127
148
128
        onBeforeMount(() => {
149
        const isNewResource = computed(
129
            props.instancedResource.getResource(
150
            () =>
130
                props.instancedResource.route.params[
151
                !props.instancedResource.route.params[
131
                    props.instancedResource.idAttr
152
                    props.instancedResource.idAttr
132
                ],
153
                ]
133
                {
154
        );
134
                    resource,
155
135
                    initialized,
156
        onBeforeMount(() => {
136
                    instancedResource: props.instancedResource,
157
            if (isNewResource.value) {
137
                    additionalProps,
158
                resource.value = {};
138
                },
159
                initialized.value = true;
139
                "show"
160
            } else {
140
            );
161
                props.instancedResource.getResource(
162
                    props.instancedResource.route.params[
163
                        props.instancedResource.idAttr
164
                    ],
165
                    {
166
                        resource,
167
                        initialized,
168
                        instancedResource: props.instancedResource,
169
                        additionalProps,
170
                    },
171
                    "show"
172
                );
173
            }
141
        });
174
        });
142
175
176
        const handleSectionSave = async (groupName, updatedResource) => {
177
            try {
178
                const result = await props.instancedResource.onShowSectionSave(
179
                    groupName,
180
                    updatedResource,
181
                    resource.value,
182
                    isNewResource.value
183
                );
184
                if (result) {
185
                    Object.assign(resource.value, result);
186
                    const newId = result[props.instancedResource.idAttr];
187
                    if (isNewResource.value && newId) {
188
                        props.instancedResource.router.push({
189
                            name: props.instancedResource.components.show,
190
                            params: {
191
                                [props.instancedResource.idAttr]: newId,
192
                            },
193
                        });
194
                    }
195
                }
196
            } catch (error) {
197
                // errors are surfaced by the httpClient / setError
198
            }
199
        };
200
143
        const fieldList = computed(() => {
201
        const fieldList = computed(() => {
144
            const fieldGroupings = props.instancedResource.getFieldGroupings(
202
            const fieldGroupings = props.instancedResource.getFieldGroupings(
145
                "Show",
203
                "Show",
146
                resource.value
204
                // Pass null for new resources so the hasDataToDisplay filter is skipped
205
                isNewResource.value ? null : resource.value
147
            );
206
            );
148
            const fieldsToAppend = props.instancedResource
207
            const fieldsToAppend = props.instancedResource
149
                .appendToShow({
208
                .appendToShow({
Lines 185-190 export default { Link Here
185
            additionalProps,
244
            additionalProps,
186
            fieldList,
245
            fieldList,
187
            displayMode,
246
            displayMode,
247
            isNewResource,
248
            handleSectionSave,
188
        };
249
        };
189
    },
250
    },
190
    props: {
251
    props: {
Lines 193-198 export default { Link Here
193
    components: {
254
    components: {
194
        Toolbar,
255
        Toolbar,
195
        ShowElement,
256
        ShowElement,
257
        FormSection,
196
        TabsWrapper,
258
        TabsWrapper,
197
        AccordionWrapper,
259
        AccordionWrapper,
198
        SplitScreenWrapper,
260
        SplitScreenWrapper,
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ShowElement.vue (+10 lines)
Lines 134-139 Link Here
134
            ></component>
134
            ></component>
135
        </template>
135
        </template>
136
    </template>
136
    </template>
137
    <template
138
        v-else-if="attr.type === 'relationshipWidget' && attr.componentProps"
139
    >
140
        <component
141
            :is="requiredComponent"
142
            :title="attr.group ? null : attr.label"
143
            :name="attr.name"
144
            v-bind="getComponentProps(true)"
145
        ></component>
146
    </template>
137
    <template
147
    <template
138
        v-else-if="
148
        v-else-if="
139
            attr.type == 'additional_fields' &&
149
            attr.type == 'additional_fields' &&
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/composables/base-resource.js (-7 / +29 lines)
Lines 415-421 export function useBaseResource(resourceConfig) { Link Here
415
        const attributesToConsider = resourceConfig.resourceAttrs.reduce(
415
        const attributesToConsider = resourceConfig.resourceAttrs.reduce(
416
            (acc, ra) => {
416
            (acc, ra) => {
417
                if (ra.type === "relationshipWidget") {
417
                if (ra.type === "relationshipWidget") {
418
                    ra.relationshipFields.forEach(relationshipField => {
418
                    ra.relationshipFields?.forEach(relationshipField => {
419
                        relationshipField.relationshipName = ra.name;
419
                        relationshipField.relationshipName = ra.name;
420
                    });
420
                    });
421
                }
421
                }
Lines 484-489 export function useBaseResource(resourceConfig) { Link Here
484
                fields: groupFields,
484
                fields: groupFields,
485
                hasDataToDisplay: false,
485
                hasDataToDisplay: false,
486
            };
486
            };
487
            if (
488
                groupFields.length > 0 &&
489
                groupFields.every(f => f.type === "group_placeholder")
490
            ) {
491
                groupInfo.placeholder = {
492
                    description: groupFields[0].description || null,
493
                };
494
                groupInfo.fields = [];
495
            }
496
            if (groupFields.some(f => f.groupMeta?.requiresId)) {
497
                groupInfo.requiresId = true;
498
            }
487
            if (
499
            if (
488
                component === "Show" &&
500
                component === "Show" &&
489
                resourceConfig.showGroupsDisplayMode === "splitScreen" &&
501
                resourceConfig.showGroupsDisplayMode === "splitScreen" &&
Lines 502-512 export function useBaseResource(resourceConfig) { Link Here
502
            }
514
            }
503
            if (component === "Show" && resource) {
515
            if (component === "Show" && resource) {
504
                groupFields.forEach(field => {
516
                groupFields.forEach(field => {
505
                    if (
517
                    if (resource[field.name] != null) {
506
                        resource[field.name] != null &&
507
                        (field.type !== "relationshipWidget" ||
508
                            resource[field.name].length > 0)
509
                    ) {
510
                        groupInfo.hasDataToDisplay = true;
518
                        groupInfo.hasDataToDisplay = true;
511
                    }
519
                    }
512
                });
520
                });
Lines 701-706 export function useBaseResource(resourceConfig) { Link Here
701
709
702
    created();
710
    created();
703
711
712
    const onShowSectionSave =
713
        resourceConfig.onShowSectionSave ||
714
        (async (sectionName, updatedResource, currentResource, isNew) => {
715
            const merged = { ...currentResource, ...updatedResource };
716
            if (isNew) {
717
                return await resourceConfig.apiClient.create(merged);
718
            } else {
719
                return await resourceConfig.apiClient.update(
720
                    merged,
721
                    merged[resourceConfig.idAttr]
722
                );
723
            }
724
        });
725
704
    return {
726
    return {
705
        ...resourceConfig,
727
        ...resourceConfig,
706
        ...moduleStoreUtils,
728
        ...moduleStoreUtils,
Lines 736-740 export function useBaseResource(resourceConfig) { Link Here
736
        route,
758
        route,
737
        router,
759
        router,
738
        i18n,
760
        i18n,
761
        onShowSectionSave,
739
    };
762
    };
740
}
763
}
741
- 

Return to bug 39224