Bug 38201: Add a ResourceList component

This patch adds a ResourceList component to handle the abstraction of the current List components

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-12-02 17:08:37 +00:00 committed by Lucas Gass
parent 2dfa887f24
commit c787e8821b
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
6 changed files with 461 additions and 12 deletions

View File

@ -57,6 +57,11 @@ export default {
if (prop.type === "boolean") {
acc[key] = prop.value;
}
if (key === "disabled") {
const currentValue = acc[key];
acc[key] = !!currentValue;
}
return acc;
}, {});
const attr = show ? this.attr.showElement : this.attr;

View File

@ -1,5 +1,6 @@
<script>
import { inject } from "vue";
import { build_url } from "../composables/datatables";
export default {
setup(props) {
@ -7,6 +8,9 @@ export default {
const { setConfirmationDialog, setMessage, setError, setWarning } =
inject("mainStore");
const AVStore = inject("AVStore");
const { get_lib_from_av, map_av_dt_filter } = AVStore;
const format_date = $date;
const patron_to_html = $patron_to_html;
@ -18,6 +22,11 @@ export default {
setWarning,
format_date,
patron_to_html,
logged_in_user,
escape_str,
get_lib_from_av,
map_av_dt_filter,
build_url,
};
},
methods: {
@ -166,6 +175,26 @@ export default {
additionalFieldsChanged(additionalFieldValues, resource) {
resource.extended_attributes = additionalFieldValues;
},
getFilters(query, filterData) {
const filters = filterData ? filterData : this.tableFilters;
const filterOptions = filters.reduce((acc, filter) => {
acc[filter.name] = filter.value;
return acc;
}, {});
Object.keys(query).forEach(key => {
if (
filterOptions.hasOwnProperty(key) &&
query[key] !== filterOptions[key]
) {
filterOptions[key] = query[key];
}
if (!filterOptions.hasOwnProperty(key)) {
filterOptions[key] = query[key];
}
});
return filterOptions;
},
},
name: "BaseResource",
props: {

View File

@ -1,4 +1,21 @@
<template>
<ResourceList
v-if="action === 'list'"
v-bind="{
api_client,
i18n,
tableOptions,
resource_name,
goToResourceShow,
goToResourceEdit,
doResourceDelete,
goToResourceAdd,
tableFilters,
getFilters,
filterTable,
tableUrl,
}"
/>
<ResourceShow
v-if="action === 'show'"
v-bind="{
@ -32,9 +49,10 @@ import { storeToRefs } from "pinia";
import { APIClient } from "../../fetch/api-client.js";
import ResourceShow from "../ResourceShow.vue";
import ResourceFormAdd from "../ResourceFormAdd.vue";
import ResourceList from "../ResourceList.vue";
export default {
components: { ResourceShow, ResourceFormAdd },
components: { ResourceShow, ResourceFormAdd, ResourceList },
extends: BaseResource,
props: {
action: String,
@ -51,6 +69,9 @@ export default {
av_agreement_relationships,
} = storeToRefs(AVStore);
const vendorStore = inject("vendorStore");
const { vendors } = storeToRefs(vendorStore);
return {
...BaseResource.setup({
resource_name: "agreement",
@ -64,6 +85,8 @@ export default {
resource_table_url: APIClient.erm._baseURL + "agreements",
i18n: {
display_name: __("Agreement"),
display_name_lc: __("agreement"),
display_name_plural: __("agreements"),
},
av_agreement_statuses,
av_agreement_closure_reasons,
@ -72,10 +95,15 @@ export default {
av_agreement_license_statuses,
av_agreement_license_location,
av_agreement_relationships,
agreement_table_settings,
vendors,
}),
};
},
data() {
const tableFilters = this.getTableFilters();
const defaults = this.getFilters(this.$route.query, tableFilters);
return {
resource_attrs: [
{
@ -631,6 +659,55 @@ export default {
documents: [],
extended_attributes: [],
},
tableOptions: {
columns: this.getTableColumns(),
options: {
embed: "user_roles,vendor,extended_attributes,+strings",
},
url: () => this.tableUrl(defaults),
table_settings: this.agreement_table_settings,
add_filters: true,
filters_options: {
1: () =>
this.vendors.map(e => {
e["_id"] = e["id"];
e["_str"] = e["name"];
return e;
}),
3: () => this.map_av_dt_filter("av_agreement_statuses"),
4: () =>
this.map_av_dt_filter("av_agreement_closure_reasons"),
5: [
{ _id: 0, _str: this.$__("No") },
{ _id: 1, _str: this.$__("Yes") },
],
6: () =>
this.map_av_dt_filter(
"av_agreement_renewal_priorities"
),
},
actions: {
0: ["show"],
"-1": this.embedded
? [
{
select: {
text: this.$__("Select"),
icon: "fa fa-check",
},
},
]
: ["edit", "delete"],
},
default_filters: {
"user_roles.user_id": function () {
return defaults.by_mine
? logged_in_user.borrowernumber
: "";
},
},
},
tableFilters,
};
},
methods: {
@ -776,6 +853,162 @@ export default {
);
}
},
getTableColumns() {
let get_lib_from_av = this.get_lib_from_av;
let escape_str = this.escape_str;
return [
{
title: __("Name"),
data: "me.name:me.agreement_id",
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return (
'<a role="button" class="show">' +
escape_str(`${row.name} (#${row.agreement_id})`) +
"</a>"
);
},
},
{
title: __("Vendor"),
data: "vendor_id",
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return row.vendor_id != undefined
? '<a href="/cgi-bin/koha/acqui/supplier.pl?booksellerid=' +
row.vendor_id +
'">' +
escape_str(row.vendor.name) +
"</a>"
: "";
},
},
{
title: __("Description"),
data: "description",
searchable: true,
orderable: true,
},
{
title: __("Status"),
data: "status",
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return escape_str(
get_lib_from_av("av_agreement_statuses", row.status)
);
},
},
{
title: __("Closure reason"),
data: "closure_reason",
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return escape_str(
get_lib_from_av(
"av_agreement_closure_reasons",
row.closure_reason
)
);
},
},
{
title: __("Is perpetual"),
data: "is_perpetual",
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return escape_str(
row.is_perpetual ? __("Yes") : __("No")
);
},
},
{
title: __("Renewal priority"),
data: "renewal_priority",
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return escape_str(
get_lib_from_av(
"av_agreement_renewal_priorities",
row.renewal_priority
)
);
},
},
];
},
getTableFilters() {
return [
{
name: "by_expired",
type: "checkbox",
label: __("Filter by expired"),
value: false,
onChange: function (filters) {
if (filters.by_expired) {
filters.max_expiration_date = new Date()
.toISOString()
.substring(0, 10);
} else {
filters.max_expiration_date = "";
}
return filters;
},
},
{
name: "max_expiration_date",
type: "component",
label: __("on"),
componentPath: "./FlatPickrWrapper.vue",
props: {
id: {
type: "string",
value: "max_expiration_date_filter",
},
disabled: {
type: "resourceProperty",
resourceProperty: "by_expired",
},
},
value: "",
},
{
name: "by_mine",
type: "checkbox",
label: __("Show mine only"),
value: false,
},
];
},
tableUrl(filters) {
let url = this.getResourceTableUrl();
if (filters?.by_expired)
url += "?max_expiration_date=" + filters.max_expiration_date;
return url;
},
async filterTable(filters, table, embedded = false) {
if (!embedded) {
if (filters.by_expired && !filters.max_expiration_date) {
filters.max_expiration_date = new Date()
.toISOString()
.substring(0, 10);
}
if (!filters.by_expired) {
filters.max_expiration_date = "";
}
let { href } = this.$router.resolve({ name: "AgreementsList" });
let new_route = this.build_url(href, filters);
this.$router.push(new_route);
}
table.redraw(this.tableUrl(filters));
},
},
computed: {
newResource() {

View File

@ -1,5 +1,5 @@
<template>
<div v-if="attr.type == 'text'">
<template v-if="attr.type == 'text'">
<label
:for="`${attr.name}${index}`"
:class="{ required: attr.required }"
@ -12,8 +12,8 @@
:required="attr.required ? true : false"
/>
<span v-if="attr.required" class="required">{{ $__("Required") }}</span>
</div>
<div v-else-if="attr.type == 'textarea'">
</template>
<template v-else-if="attr.type == 'textarea'">
<label
:for="`${attr.name}${index}`"
:class="{ required: attr.required }"
@ -28,8 +28,22 @@
:required="attr.required ? true : false"
/>
<span v-if="attr.required" class="required">{{ $__("Required") }}</span>
</div>
<div v-else-if="attr.type == 'boolean'">
</template>
<template v-else-if="attr.type == 'checkbox'">
<label
:for="`${attr.name}${index}`"
:class="{ required: attr.required }"
>{{ attr.label }}:</label
>
<input
type="checkbox"
:id="`${attr.name}${index}`"
v-model="resource[attr.name]"
@change="attr.onChange && attr.onChange(resource)"
/>
<span v-if="attr.required" class="required">{{ $__("Required") }}</span>
</template>
<template v-else-if="attr.type == 'boolean'">
<label :for="`${attr.name}${index}`">{{ attr.label }}:</label>
<label class="radio" :for="`${attr.name}${index}` + '_yes'"
>{{ $__("Yes") }}:
@ -51,8 +65,8 @@
v-model="resource[attr.name]"
/>
</label>
</div>
<div v-else-if="attr.type == 'select'">
</template>
<template v-else-if="attr.type == 'select'">
<label
:for="`${attr.name}${index}`"
:class="{ required: attr.required }"
@ -78,8 +92,8 @@
</template>
</v-select>
<span v-if="attr.required" class="required">{{ $__("Required") }}</span>
</div>
<div v-else-if="attr.type == 'component'">
</template>
<template v-else-if="attr.type == 'component'">
<label
v-if="attr.label"
:for="attr.name"
@ -100,7 +114,7 @@
v-on="getEventHandlers()"
></component>
<span v-if="attr.required" class="required">{{ $__("Required") }}</span>
</div>
</template>
<template v-else-if="attr.type == 'relationship' && attr.componentPath">
<component
:is="requiredComponent"

View File

@ -0,0 +1,168 @@
<template>
<div v-if="!initialized">{{ $__("Loading") }}</div>
<div v-else id="resource_list">
<Toolbar v-if="!embedded">
<ToolbarButton
action="add"
@go-to-add-resource="goToResourceAdd"
:title="$__('New %s').format(i18n.display_name_lc)"
/>
</Toolbar>
<fieldset
v-if="tableFilters?.length > 0 && resource_count > 0"
class="filters"
>
<template
v-for="(filter, index) in tableFilters"
v-bind:key="index"
>
<FormElement
:resource="filters"
:attr="filter"
:index="index"
/>
</template>
<input
@click="filterTable(filters, table, embedded)"
id="filterTable"
type="button"
:value="$__('Filter')"
/>
</fieldset>
<div v-if="resource_count > 0" class="page-section">
<KohaTable
ref="table"
v-bind="tableOptions"
:searchable_additional_fields="searchable_additional_fields"
:searchable_av_options="searchable_av_options"
@show="goToResourceShow"
@edit="goToResourceEdit"
@delete="doResourceDelete"
></KohaTable>
</div>
<div v-else class="alert alert-info">
{{
$__("There are no %s defined").format(i18n.display_name_plural)
}}
</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() {
const table = ref();
return {
table,
license_table_settings,
};
},
props: {
embedded: { type: Boolean, default: false },
api_client: Object,
i18n: Object,
tableOptions: Object,
resource_name: String,
goToResourceShow: Function,
goToResourceEdit: Function,
doResourceDelete: Function,
goToResourceAdd: Function,
tableFilters: Array,
getFilters: Function,
filterTable: Function,
tableUrl: Function,
},
data: function () {
return {
resource_count: 0,
initialized: false,
searchable_additional_fields: [],
searchable_av_options: [],
filters: this.getFilters(this.$route.query),
router: this.$router,
};
},
created() {
if (this.embedded) {
this.getResourceCount().then(() => (this.initialized = true));
} else {
this.getResourceCount().then(() =>
this.getSearchableAdditionalFields().then(() =>
this.getSearchableAVOptions().then(
() => (this.initialized = true)
)
)
);
}
},
methods: {
async getResourceCount() {
await this.api_client.count().then(
count => {
this.resource_count = count;
},
error => {}
);
},
async getSearchableAdditionalFields() {
const client = APIClient.additional_fields;
await client.additional_fields.getAll(this.resource_name).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,
}));
});
});
},
redrawTable() {
if (this.filterTable) {
this.filterTable();
} else {
this.table.redraw(this.tableUrl(this.filters));
}
},
},
components: { Toolbar, ToolbarButton, KohaTable, FormElement },
name: "ResourcesList",
};
</script>
<style scoped>
.filters > input[type="checkbox"],
.filters > input[type="button"] {
margin-left: 1rem;
}
</style>

View File

@ -56,7 +56,7 @@ export const routes = [
{
path: "",
name: "AgreementsList",
component: markRaw(AgreementsList),
component: markRaw(ResourceWrapper),
},
{
path: ":agreement_id",