Bug 32030: Improve form UI

* Align inputs, textareas and selects
* Use vue-select for dropdown lists

Note that the only way I found to make the select required is to follow
what is on their doc https://vue-select.org/guide/validation.html#required

However we need our own vue-select-required component to avoid the
repetition (the #search slot). I've tried (but failed) on
https://gitlab.com/joubu/Koha/-/commits/erm-v-select-required

Signed-off-by: Jonathan Field <jonathan.field@ptfs-europe.com>

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
Jonathan Druart 2022-07-28 17:26:46 +02:00 committed by Tomas Cohen Arazi
parent fccf6acc04
commit cd2884d907
No known key found for this signature in database
GPG Key ID: 0A272EA1B2F3C15F
15 changed files with 240 additions and 348 deletions

View File

@ -102,7 +102,7 @@ describe("Agreement CRUD operations", () => {
"have.length",
1
); // name, description, status
cy.get("#agreement_status").select(agreement.status);
cy.get("#agreement_status .vs__search").type(agreement.status + '{enter}',{force:true});
cy.contains("Add new period").click();
cy.get("#agreements_add").contains("Submit").click();
@ -215,7 +215,7 @@ describe("Agreement CRUD operations", () => {
"have.value",
agreements[0].description
);
cy.get("#agreement_status").should("have.value", agreement.status);
cy.get("#agreement_status .vs__selected").contains("Active");
cy.get("#agreement_is_perpetual_no").should("be.checked");
cy.get("#started_on_0").invoke("val").should("eq", dates["today_us"]);
cy.get("#ended_on_0").invoke("val").should("eq", dates["tomorrow_us"]);

View File

@ -77,8 +77,8 @@ describe("License CRUD operations", () => {
cy.get("#license_name").type(license.name);
cy.get("#license_description").type(license.description);
cy.get("#licenses_add").contains("Submit").click();
cy.get("#license_type").select(license.type);
cy.get("#license_status").select(license.status);
cy.get("#license_type .vs__search").type(license.type + '{enter}',{force:true});
cy.get("#license_status .vs__search").type(license.status + '{enter}',{force:true});
cy.get("#started_on").click();
cy.get(".flatpickr-calendar")
@ -143,8 +143,8 @@ describe("License CRUD operations", () => {
"have.value",
license.description
);
cy.get("#license_type").should("have.value", license.type);
cy.get("#license_status").should("have.value", license.status);
cy.get("#license_type .vs__selected").contains("Local");
cy.get("#license_status .vs__selected").contains("Active");
cy.get("#started_on").invoke("val").should("eq", dates["today_us"]);
cy.get("#ended_on").invoke("val").should("eq", dates["tomorrow_us"]);

View File

@ -67,8 +67,8 @@ describe("Package CRUD operations", () => {
1
);
cy.get("#package_name").type(erm_package.name);
cy.get("#package_type").select(erm_package.package_type);
cy.get("#package_content_type").select(erm_package.content_type);
cy.get("#package_type .vs__search").type(erm_package.package_type + '{enter}',{force:true});
cy.get("#package_content_type .vs__search").type(erm_package.content_type + '{enter}',{force:true});
// Submit the form, get 500
cy.intercept("POST", "/api/v1/erm/eholdings/local/packages", {
@ -116,8 +116,8 @@ describe("Package CRUD operations", () => {
// Form has been correctly filled in
cy.get("#package_name").should("have.value", erm_package.name);
cy.get("#package_type").should("have.value", erm_package.package_type);
cy.get("#package_content_type").should("have.value", erm_package.content_type);
cy.get("#package_type .vs__selected").contains("Complete");
cy.get("#package_content_type .vs__selected").contains("Print");
// Submit the form, get 500
cy.intercept("PUT", "/api/v1/erm/eholdings/local/packages/*", {

View File

@ -19,69 +19,57 @@
<label :for="`license_id_${counter}`"
>{{ $t("License") }}:</label
>
<select
<v-select
:id="`license_id_${counter}`"
v-model="agreement_license.license_id"
required
label="name"
:reduce="(l) => l.license_id"
:options="licenses"
>
<option value=""></option>
<option
v-for="license in licenses"
:key="license.license_id"
:value="license.license_id"
:selected="
license.license_id ==
agreement_license.license_id
? true
: false
"
>
{{ license.name }}
</option>
</select>
<template #search="{ attributes, events }">
<input
:required="!agreement_license.license_id"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
<li>
<label :for="`license_status_${counter}`"
>{{ $t("Status") }}:</label
>
<select v-model="agreement_license.status" required>
<option value=""></option>
<option
v-for="r in av_agreement_license_statuses"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
r.authorised_value == agreement_license.status
? true
: false
"
>
{{ r.lib }}
</option>
</select>
<v-select
:id="`license_status_${counter}`"
v-model="agreement_license.status"
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_agreement_license_statuses"
>
<template #search="{ attributes, events }">
<input
:required="!agreement_license.status"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
<li>
<label :for="`license_location_${counter}`"
>{{ $t("Physical location") }}:
</label>
<select v-model="agreement_license.physical_location">
<option value=""></option>
<option
v-for="r in av_agreement_license_location"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
r.authorised_value ==
agreement_license.physical_location
? true
: false
"
>
{{ r.lib }}
</option>
</select>
<v-select
:id="`license_location_${counter}`"
v-model="agreement_license.physical_location"
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_agreement_license_location"
/>
</li>
<li>
<label :for="`license_notes_${counter}`"

View File

@ -18,49 +18,44 @@
<label :for="`related_agreement_id_${counter}`"
>{{ $t("Related agreement") }}:
</label>
<select
v-model="relationship.related_agreement_id"
<v-select
:id="`related_agreement_id_${counter}`"
v-model="relationship.related_agreement_id"
label="name"
:reduce="(a) => a.agreement_id"
:options="agreements"
>
<option value=""></option>
<option
v-for="agreement in agreements"
:key="agreement.agreement_id"
:value="agreement.agreement_id"
:selected="
agreement.agreement_id ==
relationship.related_agreement_id
? true
: false
"
>
{{ agreement.name }}
</option>
</select>
<template #search="{ attributes, events }">
<input
:required="!relationship.related_agreement_id"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
<li>
<label :for="`related_agreement_relationship_${counter}`"
>{{ $t("Relationship") }}:
</label>
<select
v-model="relationship.relationship"
<v-select
:id="`related_agreement_relationship_${counter}`"
required
v-model="relationship.relationship"
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_agreement_relationships"
>
<option value=""></option>
<option
v-for="r in av_agreement_relationships"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
r.authorised_value == relationship.relationship
? true
: false
"
>
{{ r.lib }}
</option>
</select>
<template #search="{ attributes, events }">
<input
:required="!relationship.relationship"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
<li>

View File

@ -29,21 +29,22 @@
<label :for="`user_role_${counter}`"
>{{ $t("Role") }}:</label
>
<select v-model="user_role.role" required>
<option value=""></option>
<option
v-for="r in av_agreement_user_roles"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
r.authorised_value == user_role.role
? true
: false
"
>
{{ r.lib }}
</option>
</select>
<v-select
:id="`user_role_${counter}`"
v-model="user_role.role"
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_agreement_user_roles"
>
<template #search="{ attributes, events }">
<input
:required="!user_role.role"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
</ol>

View File

@ -25,24 +25,13 @@
<label for="agreement_vendor_id"
>{{ $t("Vendor") }}:</label
>
<select
<v-select
id="agreement_vendor_id"
v-model="agreement.vendor_id"
>
<option value=""></option>
<option
v-for="vendor in vendors"
:key="vendor.vendor_id"
:value="vendor.id"
:selected="
vendor.id == agreement.vendor_id
? true
: false
"
>
{{ vendor.name }}
</option>
</select>
label="name"
:reduce="(vendor) => vendor.id"
:options="vendors"
/>
</li>
<li>
<label for="agreement_description"
@ -62,55 +51,40 @@
<label for="agreement_status"
>{{ $t("Status") }}:</label
>
<select
<v-select
id="agreement_status"
v-model="agreement.status"
@change="onStatusChange($event)"
required
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_agreement_statuses"
@option:selected="onStatusChanged"
:required="!agreement.status"
>
<option value=""></option>
<option
v-for="status in av_agreement_statuses"
:key="status.authorised_values"
:value="status.authorised_value"
:selected="
status.authorised_value ==
agreement.status
? true
: false
"
>
{{ status.lib }}
</option>
</select>
<template #search="{ attributes, events }">
<input
:required="!agreement.status"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
<li>
<label for="agreement_closure_reason"
>{{ $t("Closure reason") }}:</label
>
<select
<v-select
id="agreement_closure_reason"
v-model="agreement.closure_reason"
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_agreement_closure_reasons"
:disabled="
agreement.status == 'closed' ? false : true
"
>
<option value=""></option>
<option
v-for="r in av_agreement_closure_reasons"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
r.authorised_value ==
agreement.closure_reason
? true
: false
"
>
{{ r.lib }}
</option>
</select>
/>
</li>
<li>
<label for="agreement_is_perpetual" class="radio"
@ -141,22 +115,13 @@
<label for="agreement_renewal_priority"
>{{ $t("Renewal priority") }}:</label
>
<select v-model="agreement.renewal_priority">
<option value=""></option>
<option
v-for="p in av_agreement_renewal_priorities"
:key="p.authorised_values"
:value="p.authorised_value"
:selected="
p.authorised_value ==
agreement.renewal_priority
? true
: false
"
>
{{ p.lib }}
</option>
</select>
<v-select
id="agreement_renewal_priority"
v-model="agreement.renewal_priority"
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_agreement_renewal_priorities"
/>
</li>
<li>
<label for="agreement_license_info"
@ -382,8 +347,8 @@ export default {
}
)
},
onStatusChange(event) {
if (event.target.value != 'closed') {
onStatusChanged(e) {
if (e.authorised_value != 'closed') {
this.agreement.closure_reason = ''
}
}

View File

@ -15,29 +15,25 @@
</legend>
<ol>
<li>
<label :for="`agreement_id_${counter}`"
<label :for="`agreement_id_${counter}`" class="required"
>{{ $t("Agreement") }}:
</label>
<select
v-model="package_agreement.agreement_id"
<v-select
:id="`agreement_id_${counter}`"
required
v-model="package_agreement.agreement_id"
label="name"
:reduce="(a) => a.agreement_id"
:options="agreements"
>
<option value=""></option>
<option
v-for="agreement in agreements"
:key="agreement.agreement_id"
:value="agreement.agreement_id"
:selected="
agreement.agreement_id ==
package_agreement.agreement_id
? true
: false
"
>
{{ agreement.name }}
</option>
</select>
<template #search="{ attributes, events }">
<input
:required="!package_agreement.agreement_id"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
</ol>

View File

@ -25,70 +25,35 @@
<label for="package_vendor_id"
>{{ $t("Vendor") }}:</label
>
<select
<v-select
id="package_vendor_id"
v-model="erm_package.vendor_id"
>
<option value=""></option>
<option
v-for="vendor in vendors"
:key="vendor.vendor_id"
:value="vendor.id"
:selected="
vendor.id == erm_package.vendor_id
? true
: false
"
>
{{ vendor.name }}
</option>
</select>
label="name"
:reduce="(vendor) => vendor.id"
:options="vendors"
/>
</li>
<li>
<label for="package_type">{{ $t("Type") }}:</label>
<select
<v-select
id="package_type"
v-model="erm_package.package_type"
>
<option value=""></option>
<option
v-for="type in av_package_types"
:key="type.authorised_values"
:value="type.authorised_value"
:selected="
type.authorised_value ==
erm_package.package_type
? true
: false
"
>
{{ type.lib }}
</option>
</select>
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_package_types"
/>
</li>
<li>
<label for="package_content_type">{{
$t("Content type: ")
}}</label>
<select
<v-select
id="package_content_type"
v-model="erm_package.content_type"
>
<option value=""></option>
<option
v-for="type in av_package_content_types"
:key="type.authorised_values"
:value="type.authorised_value"
:selected="
type.authorised_value ==
erm_package.content_type
? true
: false
"
>
{{ type.lib }}
</option>
</select>
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_package_content_types"
/>
</li>
<EHoldingsPackageAgreements

View File

@ -223,25 +223,13 @@
<label for="title_publication_type"
>{{ $t("Publication type") }}:</label
>
<select
<v-select
id="title_publication_type"
v-model="title.publication_type"
>
<option value=""></option>
<option
v-for="type in av_title_publication_types"
:key="type.authorised_values"
:value="type.authorised_value"
:selected="
type.authorised_value ==
title.publication_type
? true
: false
"
>
{{ type.lib }}
</option>
</select>
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_title_publication_types"
/>
</li>
<li>

View File

@ -15,45 +15,41 @@
</legend>
<ol>
<li>
<label :for="`resource_id_${counter}`" class="required"
<label
:for="`resource_package_id_${counter}`"
class="required"
>{{ $t("Package") }}:
</label>
<select
<!-- Parse to integer, resource.package_id is an integer, but GET /packages return package_id as string -->
<v-select
:id="`resource_package_id_${counter}`"
v-model="resource.package_id"
:id="`resource_id_${counter}`"
required
label="name"
:reduce="(p) => parseInt(p.package_id)"
:options="packages"
>
<option value=""></option>
<option
v-for="p in packages"
:key="p.package_id"
:value="p.package_id"
:selected="
p.package_id == resource.package_id
? true
: false
"
>
{{ p.name }}
</option>
</select>
<template #search="{ attributes, events }">
<input
:required="!resource.package_id"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
<li>
<label for="title_vendor_id">{{ $t("Vendor") }}:</label>
<select id="title_vendor_id" v-model="resource.vendor_id">
<option value=""></option>
<option
v-for="vendor in vendors"
:key="vendor.vendor_id"
:value="vendor.id"
:selected="
vendor.id == resource.vendor_id ? true : false
"
>
{{ vendor.name }}
</option>
</select>
<label :for="`resource_vendor_id_${counter}`"
>{{ $t("Vendor") }}:</label
>
<v-select
:id="`resource_vendor_id_${counter}`"
v-model="resource.vendor_id"
label="name"
:reduce="(vendor) => vendor.id"
:options="vendors"
/>
</li>
<li>

View File

@ -161,8 +161,22 @@ export default {
};
</script>
<style scoped>
<style src="vue-select/dist/vue-select.css"></style>
<style>
a.router-link-active {
font-weight: 700;
}
</style>
form .v-select {
display: inline-block;
background-color: white;
width: 30%;
}
.v-select, input:not([type=submit]):not([type=search]):not([type=button]), textarea {
border-color: rgba(60,60,60,0.26);
border-width: 1px;
border-radius: 4px;
min-width: 30%;
}
</style>

View File

@ -25,26 +25,14 @@
<label for="license_vendor_id"
>{{ $t("Vendor") }}:</label
>
<select
<v-select
id="license_vendor_id"
v-model="license.vendor_id"
>
<option value=""></option>
<option
v-for="vendor in vendors"
:key="vendor.vendor_id"
:value="vendor.id"
:selected="
vendor.id == license.vendor_id
? true
: false
"
>
{{ vendor.name }}
</option>
</select>
label="name"
:reduce="(vendor) => vendor.id"
:options="vendors"
/>
</li>
<li>
<label for="license_description"
>{{ $t("Description") }}:
@ -61,51 +49,44 @@
</li>
<li>
<label for="license_type">{{ $t("Type") }}:</label>
<select
<v-select
id="license_type"
v-model="license.type"
required
label="lib"
:reduce="(av) => av.authorised_value"
:options="av_license_types"
>
<option value=""></option>
<option
v-for="type in av_license_types"
:key="type.authorised_values"
:value="type.authorised_value"
:selected="
type.authorised_value == license.type
? true
: false
"
>
{{ type.lib }}
</option>
</select>
<template #search="{ attributes, events }">
<input
:required="!license.type"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
<li>
<label for="license_status"
>{{ $t("Status") }}:</label
>
<select
<v-select
id="license_status"
v-model="license.status"
required
:reduce="(av) => av.authorised_value"
:options="av_license_statuses"
label="lib"
>
<option value=""></option>
<option
v-for="status in av_license_statuses"
:key="status.authorised_values"
:value="status.authorised_value"
:selected="
status.authorised_value ==
license.status
? true
: false
"
>
{{ status.lib }}
</option>
</select>
<template #search="{ attributes, events }">
<input
:required="!license.status"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $t("Required") }}</span>
</li>
<li>

View File

@ -5,6 +5,7 @@ import { createPinia } from "pinia";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faPlus, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import vSelect from "vue-select";
library.add(faPlus, faPencil, faTrash);
@ -31,7 +32,8 @@ const app = createApp(App)
.use(createPinia())
.use(router)
.use(i18n)
.component("font-awesome-icon", FontAwesomeIcon);
.component("font-awesome-icon", FontAwesomeIcon)
.component("v-select", vSelect);
app.config.unwrapInjectedRef = true;
app.mount("#erm");
const mainStore = useMainStore();

View File

@ -38,7 +38,8 @@
"vue": "^3.2.31",
"vue-flatpickr-component": "^9",
"vue-i18n": "^9.1.10",
"vue-router": "^4.0.14"
"vue-router": "^4.0.14",
"vue-select": "^4.0.0-beta.5"
},
"scripts": {
"build": "node_modules/.bin/gulp build",