Bug 38201: Add a ResourceFormAdd component

Dries out adding and editing resources

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 2024-11-29 18:00:29 +00:00 committed by Lucas Gass
parent e7a53871f5
commit 2dfa887f24
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
8 changed files with 313 additions and 37 deletions

View File

@ -1,9 +1,5 @@
<template>
<slot />
</template>
<script>
import { defineAsyncComponent, inject } from "vue";
import { inject } from "vue";
export default {
setup(props) {

View File

@ -11,6 +11,18 @@
doResourceDelete,
}"
/>
<ResourceFormAdd
v-if="['add', 'edit'].includes(action)"
v-bind="{
id_attr,
api_client,
i18n,
resource_attrs,
list_component,
resource: newResource,
onSubmit,
}"
/>
</template>
<script>
@ -19,9 +31,10 @@ import BaseResource from "../BaseResource.vue";
import { storeToRefs } from "pinia";
import { APIClient } from "../../fetch/api-client.js";
import ResourceShow from "../ResourceShow.vue";
import ResourceFormAdd from "../ResourceFormAdd.vue";
export default {
components: { ResourceShow },
components: { ResourceShow, ResourceFormAdd },
extends: BaseResource,
props: {
action: String,
@ -110,6 +123,12 @@ export default {
label: __("Status"),
av_cat: "av_agreement_statuses",
show_in_table: true,
onSelected: resource => {
if (resource.status !== "closed") {
resource.closure_reason = null;
}
return resource;
},
},
{
name: "closure_reason",
@ -117,6 +136,7 @@ export default {
label: __("Closure reason"),
av_cat: "av_agreement_closure_reasons",
show_in_table: true,
disabled: agreement => agreement.status !== "closed",
},
{
name: "is_perpetual",
@ -594,8 +614,174 @@ export default {
],
},
],
agreement: {
agreement_id: null,
name: "",
vendor_id: null,
description: "",
status: "",
closure_reason: "",
is_perpetual: false,
renewal_priority: "",
license_info: "",
periods: [],
user_roles: [],
agreement_licenses: [],
agreement_relationships: [],
documents: [],
extended_attributes: [],
},
};
},
methods: {
checkForm(agreement) {
let errors = [];
let agreement_licenses = agreement.agreement_licenses;
// Do not use al.license.name here! Its name is not the one linked with al.license_id
// At this point al.license is meaningless, form/template only modified al.license_id
const license_ids = agreement_licenses.map(al => al.license_id);
const duplicate_license_ids = license_ids.filter(
(id, i) => license_ids.indexOf(id) !== i
);
if (duplicate_license_ids.length) {
errors.push(this.$__("A license is used several times"));
}
const related_agreement_ids = agreement.agreement_relationships.map(
rs => rs.related_agreement_id
);
const duplicate_related_agreement_ids =
related_agreement_ids.filter(
(id, i) => related_agreement_ids.indexOf(id) !== i
);
if (duplicate_related_agreement_ids.length) {
errors.push(
this.$__(
"An agreement is used as relationship several times"
)
);
}
if (
agreement_licenses.filter(al => al.status == "controlling")
.length > 1
) {
errors.push(
this.$__("Only one controlling license is allowed")
);
}
if (
agreement_licenses.filter(al => al.status == "controlling")
.length > 1
) {
errors.push(
this.$__("Only one controlling license is allowed")
);
}
let documents_with_uploaded_files = agreement.documents.filter(
doc => typeof doc.file_content !== "undefined"
);
if (
documents_with_uploaded_files.filter(
doc => atob(doc.file_content).length >= max_allowed_packet
).length >= 1
) {
errors.push(
this.$__("File size exceeds maximum allowed: %s MB").format(
(max_allowed_packet / (1024 * 1024)).toFixed(2)
)
);
}
agreement.user_roles.forEach((user, i) => {
if (user.patron_str === "") {
errors.push(
this.$__("Agreement user %s is missing a user").format(
i + 1
)
);
}
});
this.setWarning(errors.join("<br>"));
return !errors.length;
},
onSubmit(e, agreementToSave) {
e.preventDefault();
//let agreement= Object.assign( {} ,this.agreement); // copy
let agreement = JSON.parse(JSON.stringify(agreementToSave)); // copy
let agreement_id = agreement.agreement_id;
if (!this.checkForm(agreement)) {
return false;
}
delete agreement.agreement_id;
delete agreement.vendor;
delete agreement._strings;
agreement.is_perpetual = agreement.is_perpetual ? true : false;
if (agreement.vendor_id == "") {
agreement.vendor_id = null;
}
agreement.periods = agreement.periods.map(
({ agreement_id, agreement_period_id, ...keepAttrs }) =>
keepAttrs
);
agreement.user_roles = agreement.user_roles.map(
({ patron, patron_str, ...keepAttrs }) => keepAttrs
);
agreement.agreement_licenses = agreement.agreement_licenses.map(
({
license,
agreement_id,
agreement_license_id,
...keepAttrs
}) => keepAttrs
);
agreement.agreement_relationships =
agreement.agreement_relationships.map(
({ related_agreement, ...keepAttrs }) => keepAttrs
);
agreement.documents = agreement.documents.map(
({ file_type, uploaded_on, ...keepAttrs }) => keepAttrs
);
delete agreement.agreement_packages;
if (agreement_id) {
this.api_client.update(agreement, agreement_id).then(
success => {
this.setMessage(this.$__("Agreement updated"));
this.$router.push({ name: "AgreementsList" });
},
error => {}
);
} else {
this.api_client.create(agreement).then(
success => {
this.setMessage(this.$__("Agreement created"));
this.$router.push({ name: "AgreementsList" });
},
error => {}
);
}
},
},
computed: {
newResource() {
return this[this.resource_name];
},
},
created() {
//IMPROVEME: We need this for now to assign the correct av array from setup to the attr options in data
this.assignAVs(this.resource_attrs);

View File

@ -91,20 +91,6 @@ export default {
}
});
},
watch: {
"agreement.status": function (newVal, oldVal) {
var index = this.resource_attrs.findIndex(function (attr) {
return attr.name === "closure_reason";
});
if (newVal != "closed") {
this.agreement.closure_reason = "";
this.resource_attrs[index].disabled = true;
} else {
this.resource_attrs[index].disabled = false;
}
},
},
methods: {
async getAgreement(agreement_id) {
const client = APIClient.erm;

View File

@ -65,7 +65,8 @@
:reduce="av => av[attr.requiredKey]"
:options="selectOptions"
:required="!resource[attr.name] && attr.required"
:disabled="attr.disabled"
:disabled="disabled"
@option:selected="attr.onSelected && attr.onSelected(resource)"
>
<template v-if="attr.required" #search="{ attributes, events }">
<input
@ -101,7 +102,11 @@
<span v-if="attr.required" class="required">{{ $__("Required") }}</span>
</div>
<template v-else-if="attr.type == 'relationship' && attr.componentPath">
<component :is="requiredComponent" v-bind="requiredProps()"></component>
<component
:is="requiredComponent"
v-bind="requiredProps()"
v-on="getEventHandlers()"
></component>
</template>
</template>
@ -134,6 +139,13 @@ export default {
}
return this.options;
},
disabled() {
if (typeof this.attr.disabled === "function") {
return this.attr.disabled(this.resource);
} else {
return this.attr.disabled || false;
}
},
},
methods: {
isVModelRequired(componentPath) {

View File

@ -0,0 +1,106 @@
<template>
<div v-if="!initialized">{{ $__("Loading") }}</div>
<div v-else id="resources_add">
<h2 v-if="resourceToAddOrEdit.resource_id">
{{
$__("Edit") +
" " +
i18n.display_name +
" #" +
resourceToAddOrEdit.resource_id
}}
</h2>
<h2 v-else>{{ $__("New") + " " + i18n.display_name }}</h2>
<div>
<form @submit="onSubmit($event, resourceToAddOrEdit)">
<fieldset class="rows">
<ol>
<li
v-for="(attr, index) in resource_attrs.filter(
attr => attr.type !== 'relationship'
)"
v-bind:key="index"
>
<FormElement
:resource="resourceToAddOrEdit"
:attr="attr"
:index="index"
/>
</li>
</ol>
</fieldset>
<template
v-for="(attr, index) in resource_attrs.filter(
attr => attr.type === 'relationship'
)"
v-bind:key="'rel-' + index"
>
<FormElement :resource="resourceToAddOrEdit" :attr="attr" />
</template>
<fieldset class="action">
<ButtonSubmit />
<router-link
:to="{ name: list_component }"
role="button"
class="cancel"
>{{ $__("Cancel") }}</router-link
>
</fieldset>
</form>
</div>
</div>
</template>
<script>
import FormElement from "./FormElement.vue";
import ButtonSubmit from "./ButtonSubmit.vue";
export default {
data() {
return {
initialized: false,
resourceToEdit: null,
};
},
props: {
id_attr: String,
api_client: Object,
i18n: Object,
resource_attrs: Array,
list_component: String,
resource: Object,
onSubmit: Function,
},
created() {
if (this.$route.params[this.id_attr]) {
this.getResource(this.$route.params[this.id_attr]);
} else {
this.initialized = true;
}
},
methods: {
async getResource(resource_id) {
this.api_client.get(resource_id).then(
resource => {
this.resourceToEdit = resource;
this.initialized = true;
},
error => {}
);
},
},
computed: {
resourceToAddOrEdit() {
if (this.resourceToEdit) {
return this.resourceToEdit;
}
return this.resource;
},
},
components: {
ButtonSubmit,
FormElement,
},
name: "ResourceFormAdd",
};
</script>

View File

@ -4,11 +4,11 @@
<Toolbar>
<ToolbarButton
action="edit"
@go-to-edit-resource="goToResourceEdit"
@go-to-edit-resource="goToResourceEdit(resource)"
/>
<ToolbarButton
action="delete"
@delete-resource="doResourceDelete"
@delete-resource="doResourceDelete(resource)"
/>
</Toolbar>

View File

@ -9,11 +9,6 @@ import ResourceShow from "./ResourceShow.vue";
export default {
components: { ResourceShow, BaseResource },
data() {
return {
component: null,
};
},
methods: {
getComponent() {
const routeResource = this.$route.meta.self.parent.resource;
@ -23,12 +18,7 @@ export default {
const importPath = `./${
resourceModule ? `${resourceModule}/` : ""
}${componentName}Resource.vue`;
const component = defineAsyncComponent(
() => import(`${importPath}`)
);
this.component = component;
return component;
return defineAsyncComponent(() => import(`${importPath}`));
},
},
computed: {

View File

@ -67,13 +67,13 @@ export const routes = [
{
path: "add",
name: "AgreementsFormAdd",
component: markRaw(AgreementsFormAdd),
component: markRaw(ResourceWrapper),
title: $__("Add agreement"),
},
{
path: "edit/:agreement_id",
name: "AgreementsFormAddEdit",
component: markRaw(AgreementsFormAdd),
component: markRaw(ResourceWrapper),
title: $__("Edit agreement"),
},
],