Bug 38201: Drop extends in favour of the composition API
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:
parent
d1e264f703
commit
d101343f9d
|
|
@ -1,65 +1,49 @@
|
|||
<template>
|
||||
<ResourceList
|
||||
v-if="routeAction === 'list'"
|
||||
v-bind="{
|
||||
...passCommonProps(),
|
||||
...optionalResourceProps,
|
||||
}"
|
||||
:instancedResource="instancedResource"
|
||||
@select-resource="$emit('select-resource', $event)"
|
||||
>
|
||||
<template #toolbar="{ resource, componentPropData }">
|
||||
<Toolbar
|
||||
v-if="!optionalResourceProps.embedded"
|
||||
:toolbarButtons="toolbarButtons"
|
||||
:toolbarButtons="instancedResource.toolbarButtons"
|
||||
component="list"
|
||||
:resource="resource"
|
||||
:i18n="i18n"
|
||||
:i18n="instancedResource.i18n"
|
||||
:componentPropData="componentPropData"
|
||||
/>
|
||||
</template>
|
||||
<template #filters="{ table }">
|
||||
<ResourceListFilters
|
||||
v-if="optionalResourceProps.addFiltersToList"
|
||||
:tableFilters="tableFilters"
|
||||
:embedded="optionalResourceProps.embedded"
|
||||
:filterTable="filterTable"
|
||||
:getFilterValues="getFilterValues"
|
||||
:label="getTableFilterFormElementsLabel()"
|
||||
v-if="instancedResource.addFiltersToList"
|
||||
:instancedResource="instancedResource"
|
||||
:table="table"
|
||||
/>
|
||||
</template>
|
||||
</ResourceList>
|
||||
<ResourceShow
|
||||
v-if="routeAction === 'show'"
|
||||
v-bind="{
|
||||
...passCommonProps(),
|
||||
...optionalResourceProps,
|
||||
}"
|
||||
:instancedResource="instancedResource"
|
||||
>
|
||||
<template #toolbar="{ resource, componentPropData }">
|
||||
<Toolbar
|
||||
:toolbarButtons="toolbarButtons"
|
||||
:toolbarButtons="instancedResource.toolbarButtons"
|
||||
component="show"
|
||||
:resource="resource"
|
||||
:i18n="i18n"
|
||||
:i18n="instancedResource.i18n"
|
||||
:componentPropData="componentPropData"
|
||||
/>
|
||||
</template>
|
||||
</ResourceShow>
|
||||
<ResourceFormAdd
|
||||
v-if="['add', 'edit'].includes(routeAction)"
|
||||
v-bind="{
|
||||
...passCommonProps(),
|
||||
newResource,
|
||||
onSubmit,
|
||||
...optionalResourceProps,
|
||||
}"
|
||||
:instancedResource="instancedResource"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inject } from "vue";
|
||||
import { build_url } from "../composables/datatables";
|
||||
import ResourceListFilters from "./ResourceListFilters.vue";
|
||||
import ResourceShow from "./ResourceShow.vue";
|
||||
import ResourceFormAdd from "./ResourceFormAdd.vue";
|
||||
|
|
@ -67,6 +51,10 @@ import ResourceList from "./ResourceList.vue";
|
|||
import Toolbar from "./Toolbar.vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
routeAction: String,
|
||||
instancedResource: Object,
|
||||
},
|
||||
components: {
|
||||
ResourceListFilters,
|
||||
ResourceShow,
|
||||
|
|
@ -75,571 +63,28 @@ export default {
|
|||
Toolbar,
|
||||
},
|
||||
setup(props) {
|
||||
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;
|
||||
|
||||
return {
|
||||
...props,
|
||||
setConfirmationDialog,
|
||||
setMessage,
|
||||
setError,
|
||||
setWarning,
|
||||
format_date,
|
||||
patron_to_html,
|
||||
...(typeof logged_in_user !== "undefined" && { logged_in_user }),
|
||||
escape_str,
|
||||
get_lib_from_av,
|
||||
map_av_dt_filter,
|
||||
build_url,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
const optionalResourceProps = {
|
||||
embedded: this.embedded || false,
|
||||
embedded: this.instancedResource.embedded || false,
|
||||
extendedAttributesResourceType:
|
||||
this.extendedAttributesResourceType || null,
|
||||
addFiltersToList: this.addFiltersToList || null,
|
||||
formGroupsDisplayMode: this.formGroupsDisplayMode || null,
|
||||
appendToShow: this.appendToShow || (() => []),
|
||||
nameAttr: this.nameAttr || null,
|
||||
idAttr: this.idAttr || null,
|
||||
this.instancedResource.extendedAttributesResourceType || null,
|
||||
addFiltersToList: this.instancedResource.addFiltersToList || null,
|
||||
formGroupsDisplayMode:
|
||||
this.instancedResource.formGroupsDisplayMode || null,
|
||||
appendToShow: this.instancedResource.appendToShow || (() => []),
|
||||
nameAttr: this.instancedResource.nameAttr || null,
|
||||
idAttr: this.instancedResource.idAttr || null,
|
||||
};
|
||||
return {
|
||||
resourceToBeGenerated: {},
|
||||
optionalResourceProps,
|
||||
refreshTemplate: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Returns an object containing common props that are passed to all components
|
||||
* in this resource component hierarchy.
|
||||
*
|
||||
* @return {Object} The object containing the common props.
|
||||
*/
|
||||
passCommonProps() {
|
||||
const commonProps = {
|
||||
idAttr: this.idAttr,
|
||||
apiClient: this.apiClient,
|
||||
i18n: this.i18n,
|
||||
tableOptions: this.tableOptions,
|
||||
goToResourceShow: this.goToResourceShow,
|
||||
goToResourceEdit: this.goToResourceEdit,
|
||||
doResourceDelete: this.doResourceDelete,
|
||||
goToResourceAdd: this.goToResourceAdd,
|
||||
doResourceSelect: this.doResourceSelect,
|
||||
getResourceShowURL: this.getResourceShowURL,
|
||||
hasAdditionalFields: this.hasAdditionalFields,
|
||||
getFieldGroupings: this.getFieldGroupings,
|
||||
resourceAttrs: this.resourceAttrs,
|
||||
listComponent: this.listComponent,
|
||||
resourceNamePlural: this.resourceNamePlural,
|
||||
getResource: this.getResource,
|
||||
key: this.refreshTemplate,
|
||||
};
|
||||
|
||||
return commonProps;
|
||||
},
|
||||
/**
|
||||
* Navigates to the show page of the given resource.
|
||||
*
|
||||
* @param {Object} [resource] - The resource to navigate to
|
||||
* @param {DataTable} [dt] - The DataTable (optional)
|
||||
* @param {Event} [event] - The event to prevent default handling of (optional)
|
||||
* @return {void}
|
||||
*/
|
||||
goToResourceShow(resource, dt, event) {
|
||||
event?.preventDefault();
|
||||
this.$router.push({
|
||||
name: this.showComponent,
|
||||
params: { [this.idAttr]: resource[this.idAttr] },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates the URL for the show page of the given resource.
|
||||
*
|
||||
* @param {Object} resource - The resource to generate the URL for
|
||||
* @return {string} The URL for the show page of the given resource
|
||||
*/
|
||||
getResourceShowURL(id) {
|
||||
return this.$router.resolve({
|
||||
name: this.showComponent,
|
||||
params: { [this.idAttr]: id },
|
||||
}).href;
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigates to the edit page of the given resource.
|
||||
*
|
||||
* @param {Object} resource - The resource to navigate to (optional)
|
||||
* @return {void}
|
||||
*/
|
||||
goToResourceEdit(resource) {
|
||||
this.$router.push({
|
||||
name: this.editComponent,
|
||||
params: {
|
||||
[this.idAttr]: resource
|
||||
? resource[this.idAttr]
|
||||
: this.newResource[this.idAttr],
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Navigates to the creation page of the given resource.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
goToResourceAdd() {
|
||||
this.$router.push({
|
||||
name: this.addComponent,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Navigates to the list page of the given resource.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
goToResourceList() {
|
||||
this.$router.push({
|
||||
name: this.listComponent,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Resource deletion handler.
|
||||
* Accepts an optional callback function to run after deletion.
|
||||
* If no callback is provided, does the following:
|
||||
* - If deleting from show component, navigates to resource list component.
|
||||
* - If deleting from resource list component, redraws the table.
|
||||
*
|
||||
* @param {Object} resource - The resource to delete (optional)
|
||||
* @param {Object} callback - Callback to call after deletion (optional)
|
||||
* @return {void}
|
||||
*/
|
||||
doResourceDelete(resource, callback) {
|
||||
let resourceId = resource
|
||||
? resource[this.idAttr]
|
||||
: this.newResource[this.idAttr];
|
||||
let resourceName = resource
|
||||
? resource[this.nameAttr]
|
||||
: this.newResource[this.nameAttr];
|
||||
|
||||
this.setConfirmationDialog(
|
||||
{
|
||||
title: this.i18n.deleteConfirmationMessage,
|
||||
message: resourceName,
|
||||
accept_label: this.$__("Yes, delete"),
|
||||
cancel_label: this.$__("No, do not delete"),
|
||||
},
|
||||
() => {
|
||||
this.apiClient.delete(resourceId).then(
|
||||
success => {
|
||||
this.setMessage(
|
||||
this.i18n.deleteSuccessMessage.format(
|
||||
resourceName
|
||||
),
|
||||
true
|
||||
);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
} else {
|
||||
if (this.$props.routeAction === "list") {
|
||||
callback.ajax.reload();
|
||||
} else {
|
||||
this.goToResourceList();
|
||||
}
|
||||
}
|
||||
},
|
||||
error => {}
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Return the URL for the resource table.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
getResourceTableUrl() {
|
||||
return this.resourceTableUrl;
|
||||
},
|
||||
/**
|
||||
* Emits the 'select-resource' event with the id of the provided resource.
|
||||
*
|
||||
* @param {Object} resource - The resource object containing the id attribute.
|
||||
* @param {Object} dt - DataTables instance (not used in this function).
|
||||
* @param {Event} event - The event object (not used in this function).
|
||||
*/
|
||||
|
||||
doResourceSelect(resource, dt, event) {
|
||||
this.$emit("select-resource", resource[this.idAttr]);
|
||||
},
|
||||
/**
|
||||
* Populates the resource attributes with authorised values based on their types.
|
||||
*
|
||||
* For each attribute:
|
||||
* - If the attribute type is "select" and it has an authorised value category (avCat),
|
||||
* it sets the options from the component's corresponding property.
|
||||
* - If the attribute type is "relationship" and it has component properties,
|
||||
* it assigns authorised values to any marked as type 'av'.
|
||||
* - If the attribute has relationship fields, the function is recursively called
|
||||
* to populate them as well.
|
||||
*
|
||||
* @param {Array} attrs - The array of attributes to be populated with authorised values.
|
||||
*/
|
||||
populateAttributesWithAuthorisedValues(attrs) {
|
||||
if (!attrs) return;
|
||||
attrs.forEach(attr => {
|
||||
if (attr.type === "select" && typeof attr.avCat === "string") {
|
||||
const avKey = attr.avCat;
|
||||
attr.options = this[avKey];
|
||||
attr.requiredKey = "value";
|
||||
attr.selectLabel = "description";
|
||||
}
|
||||
if (attr.type == "relationship" && attr.componentProps) {
|
||||
Object.keys(attr.componentProps).forEach(key => {
|
||||
if (attr.componentProps[key].type == "av") {
|
||||
attr.componentProps[key].av = this[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
if (attr.relationshipFields?.length) {
|
||||
this.populateAttributesWithAuthorisedValues(
|
||||
attr.relationshipFields
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Updates the extended_attributes property of the provided resource
|
||||
*
|
||||
* @param {Array} additionalFieldValues - Array of objects with
|
||||
* name and value properties.
|
||||
* @param {Object} resource - The resource object whose
|
||||
* extended_attributes property is updated.
|
||||
*/
|
||||
additionalFieldsChanged(additionalFieldValues, resource) {
|
||||
resource.extended_attributes = additionalFieldValues;
|
||||
},
|
||||
/**
|
||||
* Builds an object of filter name-value pairs based on the provided
|
||||
* query object and filterData (tableFilters by default).
|
||||
*
|
||||
* Iterates over the query object keys and updates the filterOptions
|
||||
* object with the new values. If a filter name is not found in
|
||||
* filterOptions, it is added.
|
||||
*
|
||||
* @param {Object} query - The query object (taken from the URL params)containing the filter values.
|
||||
* @param {Array} filterData - The array of filter objects (optional).
|
||||
* @return {Object}
|
||||
*/
|
||||
getFilterValues(query, filterData) {
|
||||
const filters = filterData
|
||||
? filterData
|
||||
: this.tableFilters
|
||||
? 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;
|
||||
},
|
||||
/**
|
||||
* Gets the label to display before the filters
|
||||
*
|
||||
* @return {String} The label
|
||||
*/
|
||||
getTableFilterFormElementsLabel() {
|
||||
return "";
|
||||
},
|
||||
/**
|
||||
* Gets the array of filters for the table, if required.
|
||||
* This is a default method that returns an empty array.
|
||||
* It can be overridden at the resource level if filters are required
|
||||
*
|
||||
* @return {Array} The array of filters for the table.
|
||||
*/
|
||||
getTableFilterFormElements() {
|
||||
return [];
|
||||
},
|
||||
/**
|
||||
* Gets the list of default buttons to add to the toolbar, for each view: list, show, edit
|
||||
* It can be overridden at the resource level if the default buttons are not required
|
||||
*
|
||||
* @return {Object} keys must be "list", "show" or "edit", values are functions.
|
||||
*/
|
||||
defaultToolbarButtons(resource, i18n) {
|
||||
return {
|
||||
list: [
|
||||
{
|
||||
action: "add",
|
||||
onClick: () => this.goToResourceAdd(),
|
||||
title: i18n.newLabel,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
show: [
|
||||
{
|
||||
action: "edit",
|
||||
onClick: () => this.goToResourceEdit(resource),
|
||||
title: this.$__("Edit"),
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
action: "delete",
|
||||
onClick: () => this.doResourceDelete(resource),
|
||||
title: this.$__("Delete"),
|
||||
index: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns a default empty set of additional buttons
|
||||
* Additional buttons should be added in the resource specific component
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
additionalToolbarButtons() {
|
||||
return {
|
||||
list: [],
|
||||
show: [],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* This method takes a component name (e.g. 'Form' or 'Show') and an optional resource object
|
||||
* and returns an array of grouped objects that determine which fields should be grouped together
|
||||
* in the relevant component. Each group object contains the name of the group and an array of the
|
||||
* field objects that belong to that group.
|
||||
*
|
||||
* It first filters the resource attributes to only include those that are not hidden
|
||||
* in the given component. Then it groups the attributes by the group name
|
||||
* (or "noGroupFound" if there is no group name). If the component is 'Show', it also
|
||||
* checks if the resource object has data to display in each group. If not,
|
||||
* the group is not included.
|
||||
*
|
||||
* @param {String} component - The component name (e.g. 'Form' or 'Show').
|
||||
* @param {Object} resource - The resource object (optional).
|
||||
* @return {Array} The array of group objects.
|
||||
*/
|
||||
getFieldGroupings(component, resource) {
|
||||
const attributesToConsider = this.resourceAttrs.reduce(
|
||||
(acc, ra) => {
|
||||
if (ra.hideIn && !ra.hideIn.includes(component)) {
|
||||
return [...acc, ra];
|
||||
}
|
||||
if (ra.hideIn && ra.hideIn.includes(component)) {
|
||||
return acc;
|
||||
}
|
||||
return [...acc, ra];
|
||||
},
|
||||
[]
|
||||
);
|
||||
if (this.extendedAttributesResourceType) {
|
||||
attributesToConsider.push({
|
||||
name: "additional_fields",
|
||||
type: "additional_fields",
|
||||
extended_attributes_resource_type:
|
||||
this.extendedAttributesResourceType,
|
||||
});
|
||||
}
|
||||
const groupings = attributesToConsider.reduce((acc, attr) => {
|
||||
if (
|
||||
attr.hasOwnProperty("group") &&
|
||||
attr.group !== null &&
|
||||
!acc.includes(attr.group)
|
||||
) {
|
||||
return [...acc, attr.group];
|
||||
}
|
||||
if (!attr.hasOwnProperty("group")) {
|
||||
attr.group = "noGroupFound";
|
||||
if (!acc.includes("noGroupFound")) {
|
||||
return [...acc, "noGroupFound"];
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
if (groupings.length === 0) {
|
||||
return [
|
||||
{
|
||||
name: null,
|
||||
fields: attributesToConsider,
|
||||
},
|
||||
];
|
||||
}
|
||||
// FIXME - if no group is defined in accordion mode then the section doesn't have the dropdown applied
|
||||
return groupings.reduce((acc, group) => {
|
||||
const groupFields = attributesToConsider.filter(
|
||||
ra => ra.group === group
|
||||
);
|
||||
const groupInfo = {
|
||||
name: group === "noGroupFound" ? null : group,
|
||||
fields: groupFields,
|
||||
hasDataToDisplay: false,
|
||||
};
|
||||
if (component === "Show" && resource) {
|
||||
groupFields.forEach(field => {
|
||||
if (
|
||||
resource[field.name] != null &&
|
||||
(field.type !== "relationshipWidget" ||
|
||||
resource[field.name].length > 0)
|
||||
) {
|
||||
groupInfo.hasDataToDisplay = true;
|
||||
}
|
||||
});
|
||||
if (!groupInfo.hasDataToDisplay) {
|
||||
return [...acc];
|
||||
}
|
||||
}
|
||||
return [...acc, groupInfo];
|
||||
}, []);
|
||||
},
|
||||
async getResource(resourceId, componentData, caller) {
|
||||
this.apiClient.get(resourceId).then(
|
||||
resource => {
|
||||
componentData.resource = resource;
|
||||
if (this.afterResourceFetch) {
|
||||
this.afterResourceFetch(
|
||||
componentData,
|
||||
resource,
|
||||
caller
|
||||
);
|
||||
}
|
||||
componentData.initialized = true;
|
||||
},
|
||||
error => {}
|
||||
);
|
||||
},
|
||||
refreshTemplateState() {
|
||||
this.refreshTemplate = !this.refreshTemplate;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Generates a new resource object with default values.
|
||||
*
|
||||
* This method initializes the `resourceToBeGenerated` property by iterating
|
||||
* over `resourceAttrs`, assigning default values based on attribute types.
|
||||
* - If an attribute has a `defaultValue`, it is set accordingly.
|
||||
* - For text, textarea, and select types, an empty string is assigned.
|
||||
* - For boolean and checkbox types, a false value is assigned.
|
||||
* - For additional fields or relationship widgets, an empty array is assigned
|
||||
* to `extended_attributes` or the attribute name.
|
||||
* - If none of the above apply, a null value is assigned.
|
||||
* The `idAttr` of the resource is also initialized to null.
|
||||
*
|
||||
* @return {Object} The newly generated resource object with default values.
|
||||
*/
|
||||
newResource() {
|
||||
this.resourceToBeGenerated = this.resourceAttrs.reduce(
|
||||
(acc, attr) => {
|
||||
if (attr.hasOwnProperty("defaultValue")) {
|
||||
acc[attr.name] = attr.defaultValue;
|
||||
return acc;
|
||||
}
|
||||
if (["text", "textarea", "select"].includes(attr.type)) {
|
||||
acc[attr.name] = "";
|
||||
return acc;
|
||||
}
|
||||
if (["boolean", "checkbox"].includes(attr.type)) {
|
||||
acc[attr.name] = false;
|
||||
return acc;
|
||||
}
|
||||
if (attr.type === "relationshipWidget") {
|
||||
acc[attr.name] = [];
|
||||
return acc;
|
||||
}
|
||||
acc[attr.name] = null;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
if (this.extendedAttributesResourceType) {
|
||||
this.resourceToBeGenerated.extended_attributes = [];
|
||||
}
|
||||
this.resourceToBeGenerated[this.idAttr] = null;
|
||||
return this.resourceToBeGenerated;
|
||||
},
|
||||
/**
|
||||
* Determines if the resource has additional fields.
|
||||
*
|
||||
* @return {Boolean} true if the resource has additional fields, false otherwise.
|
||||
*/
|
||||
hasAdditionalFields() {
|
||||
return !!this.extendedAttributesResourceType;
|
||||
},
|
||||
/**
|
||||
* Returns the plural form of the resource name.
|
||||
*
|
||||
* This method checks if the `resourceName` ends with 's'. If it does not,
|
||||
* it appends 's' to create the plural form. Otherwise returns `resourceName`.
|
||||
*
|
||||
* @return {String} The plural form of the resource name.
|
||||
*/
|
||||
resourceNamePlural() {
|
||||
if (!this.resourceName.endsWith("s"))
|
||||
return this.resourceName + "s";
|
||||
return this.resourceName;
|
||||
},
|
||||
/**
|
||||
* A function that returns a set of buttons to display in the toolbar, based on the current resource and component.
|
||||
* The function takes three arguments: the resource, the component and the i18n object.
|
||||
* It returns an array of buttons. The buttons are a combination of the default buttons and the additional buttons.
|
||||
* The default buttons are defined in the defaultToolbarButtons function and the additional buttons are defined in the
|
||||
* additionalToolbarButtons function at the resource level.
|
||||
*
|
||||
* @param {Object} resource The current resource
|
||||
* @param {String} component The current component
|
||||
* @param {Object} i18n The i18n object
|
||||
* @return {Array<Object>} An array of buttons
|
||||
*/
|
||||
toolbarButtons() {
|
||||
const defaultToolbarButtons = this.defaultToolbarButtons;
|
||||
const additionalToolbarButtons = this.additionalToolbarButtons;
|
||||
|
||||
return (resource, component, i18n, componentData) => {
|
||||
const defaultButtons = defaultToolbarButtons(resource, i18n);
|
||||
const additionalButtons = additionalToolbarButtons(
|
||||
resource,
|
||||
componentData
|
||||
);
|
||||
|
||||
//FIXME: we need to check that no indexes match between the default buttons and additional buttons
|
||||
// If we add to the default buttons in future it could mess up indexing
|
||||
|
||||
return [
|
||||
...(defaultButtons[component] || []),
|
||||
...(additionalButtons[component] || []).filter(
|
||||
button => Object.keys(button).length > 0
|
||||
),
|
||||
].sort((a, b) => a.index - b.index);
|
||||
};
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.resourceAttrs) {
|
||||
this.populateAttributesWithAuthorisedValues(this.resourceAttrs);
|
||||
}
|
||||
},
|
||||
name: "BaseResource",
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
<template>
|
||||
<BaseResource
|
||||
:routeAction="routeAction"
|
||||
:instancedResource="this"
|
||||
></BaseResource>
|
||||
</template>
|
||||
<script>
|
||||
import { inject } from "vue";
|
||||
import BaseResource from "../BaseResource.vue";
|
||||
import { useBaseResource } from "../../composables/base-resource.js";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { APIClient } from "../../fetch/api-client.js";
|
||||
|
||||
export default {
|
||||
extends: BaseResource,
|
||||
// extends: BaseResource,
|
||||
props: {
|
||||
routeAction: String,
|
||||
embedded: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const format_date = $date;
|
||||
const patron_to_html = $patron_to_html;
|
||||
const AVStore = inject("AVStore");
|
||||
const {
|
||||
av_agreement_statuses,
|
||||
|
|
@ -25,52 +35,74 @@ export default {
|
|||
const vendorStore = inject("vendorStore");
|
||||
const { vendors } = storeToRefs(vendorStore);
|
||||
|
||||
return {
|
||||
...BaseResource.setup({
|
||||
resourceName: "agreement",
|
||||
nameAttr: "name",
|
||||
idAttr: "agreement_id",
|
||||
showComponent: "AgreementsShow",
|
||||
listComponent: "AgreementsList",
|
||||
addComponent: "AgreementsFormAdd",
|
||||
editComponent: "AgreementsFormAddEdit",
|
||||
apiClient: APIClient.erm.agreements,
|
||||
resourceTableUrl:
|
||||
APIClient.erm.httpClient._baseURL + "agreements",
|
||||
i18n: {
|
||||
deleteConfirmationMessage: __(
|
||||
"Are you sure you want to remove this agreement?"
|
||||
),
|
||||
deleteSuccessMessage: __("Agreement %s deleted"),
|
||||
displayName: __("Agreement"),
|
||||
editLabel: __("Edit agreement #%s"),
|
||||
emptyListMessage: __("There are no agreements defined"),
|
||||
newLabel: __("New agreement"),
|
||||
const getTableFilterFormElements = () => {
|
||||
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;
|
||||
},
|
||||
},
|
||||
embedded: props.embedded,
|
||||
extendedAttributesResourceType: "agreement",
|
||||
addFiltersToList: true,
|
||||
av_agreement_statuses,
|
||||
av_agreement_closure_reasons,
|
||||
av_agreement_renewal_priorities,
|
||||
av_user_roles,
|
||||
av_agreement_license_statuses,
|
||||
av_agreement_license_location,
|
||||
av_agreement_relationships,
|
||||
agreement_table_settings,
|
||||
vendors,
|
||||
}),
|
||||
{
|
||||
name: "max_expiration_date",
|
||||
type: "date",
|
||||
label: __("on"),
|
||||
componentProps: {
|
||||
disabled: {
|
||||
resourceProperty: "by_expired",
|
||||
qualifier: "!",
|
||||
},
|
||||
},
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "by_mine",
|
||||
type: "checkbox",
|
||||
label: __("Show mine only"),
|
||||
value: false,
|
||||
},
|
||||
];
|
||||
};
|
||||
},
|
||||
data() {
|
||||
const tableFilters = this.getTableFilterFormElements();
|
||||
const defaults = this.getFilterValues(this.$route.query, tableFilters);
|
||||
|
||||
return {
|
||||
const extendedAttributesResourceType = "agreement";
|
||||
|
||||
const baseResource = useBaseResource({
|
||||
resourceName: "agreement",
|
||||
nameAttr: "name",
|
||||
idAttr: "agreement_id",
|
||||
showComponent: "AgreementsShow",
|
||||
listComponent: "AgreementsList",
|
||||
addComponent: "AgreementsFormAdd",
|
||||
editComponent: "AgreementsFormAddEdit",
|
||||
apiClient: APIClient.erm.agreements,
|
||||
resourceTableUrl: APIClient.erm.httpClient._baseURL + "agreements",
|
||||
i18n: {
|
||||
deleteConfirmationMessage: __(
|
||||
"Are you sure you want to remove this agreement?"
|
||||
),
|
||||
deleteSuccessMessage: __("Agreement %s deleted"),
|
||||
displayName: __("Agreement"),
|
||||
editLabel: __("Edit agreement #%s"),
|
||||
emptyListMessage: __("There are no agreements defined"),
|
||||
newLabel: __("New agreement"),
|
||||
},
|
||||
embedded: props.embedded,
|
||||
addFiltersToList: true,
|
||||
extendedAttributesResourceType,
|
||||
resourceAttrs: [
|
||||
{
|
||||
name: this.idAttr,
|
||||
label: this.$__("ID"),
|
||||
name: "agreement_id",
|
||||
label: __("ID"),
|
||||
type: "text",
|
||||
hideIn: ["Form", "Show"],
|
||||
},
|
||||
|
|
@ -147,17 +179,17 @@ export default {
|
|||
{
|
||||
name: this.$__("Period start"),
|
||||
value: "started_on",
|
||||
format: this.format_date,
|
||||
format: format_date,
|
||||
},
|
||||
{
|
||||
name: this.$__("Period end"),
|
||||
value: "ended_on",
|
||||
format: this.format_date,
|
||||
format: format_date,
|
||||
},
|
||||
{
|
||||
name: this.$__("Cancellation deadline"),
|
||||
value: "cancellation_deadline",
|
||||
format: this.format_date,
|
||||
format: format_date,
|
||||
},
|
||||
{
|
||||
name: this.$__("Notes"),
|
||||
|
|
@ -240,7 +272,7 @@ export default {
|
|||
{
|
||||
name: this.$__("Name"),
|
||||
value: "patron",
|
||||
format: this.patron_to_html,
|
||||
format: patron_to_html,
|
||||
},
|
||||
{
|
||||
name: this.$__("Role"),
|
||||
|
|
@ -609,6 +641,30 @@ export default {
|
|||
hideIn: ["List"],
|
||||
},
|
||||
],
|
||||
av_agreement_statuses,
|
||||
av_agreement_closure_reasons,
|
||||
av_agreement_renewal_priorities,
|
||||
av_user_roles,
|
||||
av_agreement_license_statuses,
|
||||
av_agreement_license_location,
|
||||
av_agreement_relationships,
|
||||
agreement_table_settings,
|
||||
vendors,
|
||||
tableFilters: getTableFilterFormElements(),
|
||||
props: props,
|
||||
});
|
||||
|
||||
baseResource.created();
|
||||
return {
|
||||
...baseResource,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
const defaults = this.getFilterValues(
|
||||
this.$route.query,
|
||||
this.tableFilters
|
||||
);
|
||||
return {
|
||||
tableOptions: {
|
||||
options: {
|
||||
embed: "user_roles,vendor,extended_attributes,+strings",
|
||||
|
|
@ -657,7 +713,6 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
tableFilters,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -794,44 +849,6 @@ export default {
|
|||
);
|
||||
}
|
||||
},
|
||||
getTableFilterFormElements() {
|
||||
return [
|
||||
{
|
||||
name: "by_expired",
|
||||
type: "checkbox",
|
||||
label: this.$__("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: "date",
|
||||
label: this.$__("on"),
|
||||
componentProps: {
|
||||
disabled: {
|
||||
resourceProperty: "by_expired",
|
||||
qualifier: "!",
|
||||
},
|
||||
},
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "by_mine",
|
||||
type: "checkbox",
|
||||
label: this.$__("Show mine only"),
|
||||
value: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
tableUrl(filters) {
|
||||
let url = this.getResourceTableUrl();
|
||||
if (filters?.by_expired)
|
||||
|
|
@ -857,5 +874,8 @@ export default {
|
|||
},
|
||||
emits: ["select-resource"],
|
||||
name: "AgreementResource",
|
||||
components: {
|
||||
BaseResource,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
<template>
|
||||
<div v-if="!initialized">{{ $__("Loading") }}</div>
|
||||
<div v-else :id="`${resourceNamePlural}_add`">
|
||||
<h2 v-if="resourceToAddOrEdit[idAttr]">
|
||||
{{ i18n.editLabel.format(resourceToAddOrEdit[idAttr]) }}
|
||||
<div v-else :id="`${instancedResource.resourceNamePlural}_add`">
|
||||
<h2 v-if="resourceToAddOrEdit[instancedResource.idAttr]">
|
||||
{{
|
||||
instancedResource.i18n.editLabel.format(
|
||||
resourceToAddOrEdit[instancedResource.idAttr]
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<h2 v-else>{{ i18n.newLabel }}</h2>
|
||||
<h2 v-else>{{ instancedResource.i18n.newLabel }}</h2>
|
||||
<ul
|
||||
v-if="formGroupsDisplayMode == 'tabs'"
|
||||
v-if="instancedResource.formGroupsDisplayMode == 'tabs'"
|
||||
class="nav nav-tabs"
|
||||
role="tablist"
|
||||
>
|
||||
<li
|
||||
v-for="(tab, counter) in getFieldGroupings('Form')"
|
||||
v-for="(tab, counter) in instancedResource.getFieldGroupings(
|
||||
'Form'
|
||||
)"
|
||||
class="nav-item"
|
||||
:key="`tab${counter}`"
|
||||
>
|
||||
|
|
@ -27,10 +33,15 @@
|
|||
>
|
||||
</li>
|
||||
</ul>
|
||||
<form @submit="onSubmit($event, resourceToAddOrEdit)">
|
||||
<div v-if="formGroupsDisplayMode == 'tabs'" class="tab-content">
|
||||
<form @submit="instancedResource.onSubmit($event, resourceToAddOrEdit)">
|
||||
<div
|
||||
v-if="instancedResource.formGroupsDisplayMode == 'tabs'"
|
||||
class="tab-content"
|
||||
>
|
||||
<div
|
||||
v-for="(group, counter) in getFieldGroupings('Form')"
|
||||
v-for="(
|
||||
group, counter
|
||||
) in instancedResource.getFieldGroupings('Form')"
|
||||
v-bind:key="counter"
|
||||
:id="group.name?.replace(/\s/g, '_')"
|
||||
role="tabpanel"
|
||||
|
|
@ -59,9 +70,15 @@
|
|||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="formGroupsDisplayMode == 'accordion'">
|
||||
<div
|
||||
v-else-if="
|
||||
instancedResource.formGroupsDisplayMode == 'accordion'
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(group, counter) in getFieldGroupings('Form')"
|
||||
v-for="(
|
||||
group, counter
|
||||
) in instancedResource.getFieldGroupings('Form')"
|
||||
v-bind:key="counter"
|
||||
class="accordion"
|
||||
>
|
||||
|
|
@ -106,7 +123,9 @@
|
|||
</div>
|
||||
<div v-else>
|
||||
<fieldset
|
||||
v-for="(group, counter) in getFieldGroupings('Form')"
|
||||
v-for="(
|
||||
group, counter
|
||||
) in instancedResource.getFieldGroupings('Form')"
|
||||
v-bind:key="counter"
|
||||
class="rows"
|
||||
>
|
||||
|
|
@ -128,7 +147,7 @@
|
|||
<fieldset class="action">
|
||||
<ButtonSubmit />
|
||||
<router-link
|
||||
:to="{ name: listComponent }"
|
||||
:to="{ name: instancedResource.listComponent }"
|
||||
role="button"
|
||||
class="cancel"
|
||||
>{{ $__("Cancel") }}</router-link
|
||||
|
|
@ -139,6 +158,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive } from "vue";
|
||||
import FormElement from "./FormElement.vue";
|
||||
import ButtonSubmit from "./ButtonSubmit.vue";
|
||||
|
||||
|
|
@ -151,27 +171,24 @@ export default {
|
|||
};
|
||||
},
|
||||
props: {
|
||||
idAttr: String,
|
||||
apiClient: Object,
|
||||
i18n: Object,
|
||||
listComponent: String,
|
||||
newResource: Object,
|
||||
onSubmit: Function,
|
||||
resourceNamePlural: String,
|
||||
getFieldGroupings: Function,
|
||||
formGroupsDisplayMode: String,
|
||||
getResource: Function,
|
||||
instancedResource: Object,
|
||||
},
|
||||
created() {
|
||||
if (this.$route.params[this.idAttr]) {
|
||||
this.getResource(this.$route.params[this.idAttr], this, "form");
|
||||
if (this.$route.params[this.instancedResource.idAttr]) {
|
||||
this.instancedResource.getResource(
|
||||
this.$route.params[this.instancedResource.idAttr],
|
||||
this,
|
||||
"form"
|
||||
);
|
||||
} else {
|
||||
this.initialized = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resourceToAddOrEdit() {
|
||||
return this.resource || this.newResource;
|
||||
return (
|
||||
this.resource || reactive(this.instancedResource.newResource)
|
||||
);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div v-if="!initialized">{{ $__("Loading") }}</div>
|
||||
<div v-else :id="`${resourceNamePlural}_list`">
|
||||
<div v-else :id="`${instancedResource.resourceNamePlural}_list`">
|
||||
<slot name="toolbar" :componentPropData="{ ...$props, ...$data }" />
|
||||
<template v-if="resourceCount > 0">
|
||||
<slot name="filters" :table="table" />
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
></KohaTable>
|
||||
</div>
|
||||
<div v-else class="alert alert-info">
|
||||
{{ i18n.emptyListMessage }}
|
||||
{{ instancedResource.i18n.emptyListMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -40,22 +40,7 @@ export default {
|
|||
};
|
||||
},
|
||||
props: {
|
||||
embedded: Boolean,
|
||||
apiClient: Object,
|
||||
i18n: Object,
|
||||
tableOptions: Object,
|
||||
goToResourceShow: Function,
|
||||
goToResourceEdit: Function,
|
||||
doResourceDelete: Function,
|
||||
doResourceSelect: Function,
|
||||
getResourceShowURL: Function,
|
||||
resourceName: String,
|
||||
resourceNamePlural: String,
|
||||
hasAdditionalFields: { type: Boolean, default: false },
|
||||
extendedAttributesResourceType: String,
|
||||
resourceAttrs: Array,
|
||||
nameAttr: String,
|
||||
idAttr: String,
|
||||
instancedResource: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -64,19 +49,21 @@ export default {
|
|||
searchable_additional_fields: [],
|
||||
searchable_av_options: [],
|
||||
tableEvents: {
|
||||
show: this.goToResourceShow,
|
||||
edit: this.goToResourceEdit,
|
||||
delete: this.doResourceDelete,
|
||||
select: this.doResourceSelect,
|
||||
show: this.instancedResource.goToResourceShow,
|
||||
edit: this.instancedResource.goToResourceEdit,
|
||||
delete: this.instancedResource.doResourceDelete,
|
||||
select: this.instancedResource.doResourceSelect,
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.embedded) {
|
||||
this.getResourceCount().then(() => (this.initialized = true));
|
||||
if (this.instancedResource.embedded) {
|
||||
this.getResourceCount().then(
|
||||
() => (this.instancedResource.initialized = true)
|
||||
);
|
||||
} else {
|
||||
this.getResourceCount().then(() => {
|
||||
if (this.hasAdditionalFields) {
|
||||
if (this.instancedResource.hasAdditionalFields) {
|
||||
this.getSearchableAdditionalFields().then(() =>
|
||||
this.getSearchableAVOptions().then(
|
||||
() => (this.initialized = true)
|
||||
|
|
@ -90,7 +77,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
async getResourceCount() {
|
||||
await this.apiClient.count().then(
|
||||
await this.instancedResource.apiClient.count().then(
|
||||
count => {
|
||||
this.resourceCount = count;
|
||||
},
|
||||
|
|
@ -100,7 +87,7 @@ export default {
|
|||
async getSearchableAdditionalFields() {
|
||||
const client = APIClient.additional_fields;
|
||||
await client.additional_fields
|
||||
.getAll(this.extendedAttributesResourceType)
|
||||
.getAll(this.instancedResource.extendedAttributesResourceType)
|
||||
.then(
|
||||
searchable_additional_fields => {
|
||||
this.searchable_additional_fields =
|
||||
|
|
@ -135,8 +122,8 @@ export default {
|
|||
});
|
||||
},
|
||||
getTableColumns(resourceAttrs) {
|
||||
let get_lib_from_av = this.get_lib_from_av;
|
||||
let thisResource = this;
|
||||
let get_lib_from_av = this.instancedResource.get_lib_from_av;
|
||||
let thisResource = this.instancedResource;
|
||||
|
||||
const columns = resourceAttrs.reduce((acc, attr, i) => {
|
||||
if (
|
||||
|
|
@ -151,7 +138,7 @@ export default {
|
|||
acc.push(attr.tableColumnDefinition);
|
||||
return acc;
|
||||
}
|
||||
if (attr.name === this.idAttr) {
|
||||
if (attr.name === this.instancedResource.idAttr) {
|
||||
acc.push({
|
||||
title: attr.label,
|
||||
data: attr.name,
|
||||
|
|
@ -171,7 +158,7 @@ export default {
|
|||
});
|
||||
return acc;
|
||||
}
|
||||
if (attr.name === this.nameAttr) {
|
||||
if (attr.name === this.instancedResource.nameAttr) {
|
||||
acc.push({
|
||||
title: attr.label,
|
||||
data: attr.name,
|
||||
|
|
@ -264,22 +251,21 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
tableOptionsWithColumns() {
|
||||
this.tableOptions.columns = this.getTableColumns(
|
||||
this.resourceAttrs
|
||||
this.instancedResource.tableOptions.columns = this.getTableColumns(
|
||||
this.instancedResource.resourceAttrs
|
||||
);
|
||||
return this.tableOptions;
|
||||
return this.instancedResource.tableOptions;
|
||||
},
|
||||
tableEventList() {
|
||||
const actionButtons = this.tableOptions.actions["-1"].reduce(
|
||||
(acc, curr) => {
|
||||
if (typeof curr === "object") {
|
||||
const actionName = Object.keys(curr)[0];
|
||||
acc[actionName] = curr[actionName].callback;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const actionButtons = this.instancedResource.tableOptions.actions[
|
||||
"-1"
|
||||
].reduce((acc, curr) => {
|
||||
if (typeof curr === "object") {
|
||||
const actionName = Object.keys(curr)[0];
|
||||
acc[actionName] = curr[actionName].callback;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return { ...this.tableEvents, ...actionButtons };
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
<template>
|
||||
<fieldset v-if="tableFilters?.length > 0" class="filters">
|
||||
<template v-if="label">{{ label }}{{ " " }}</template>
|
||||
<template v-for="(filter, index) in tableFilters" v-bind:key="index">
|
||||
<fieldset v-if="instancedResource.tableFilters?.length > 0" class="filters">
|
||||
<template v-if="instancedResource.getTableFilterFormElementsLabel()"
|
||||
>{{ instancedResource.getTableFilterFormElementsLabel()
|
||||
}}{{ " " }}</template
|
||||
>
|
||||
<template
|
||||
v-for="(filter, index) in instancedResource.tableFilters"
|
||||
v-bind:key="index"
|
||||
>
|
||||
<FormElement :resource="filters" :attr="filter" :index="index" />
|
||||
</template>
|
||||
<input
|
||||
@click="filterTable(filters, table, embedded)"
|
||||
@click="
|
||||
instancedResource.filterTable(
|
||||
filters,
|
||||
table,
|
||||
instancedResource.embedded
|
||||
)
|
||||
"
|
||||
id="filterTable"
|
||||
type="button"
|
||||
:value="$__('Filter')"
|
||||
|
|
@ -18,17 +30,13 @@ import FormElement from "./FormElement.vue";
|
|||
export default {
|
||||
components: { FormElement },
|
||||
props: {
|
||||
embedded: Boolean,
|
||||
tableFilters: { type: Array, default: [] },
|
||||
getFilterValues: Function,
|
||||
filterTable: Function | null,
|
||||
label: String | "",
|
||||
instancedResource: Object,
|
||||
table: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filters: this.getFilterValues
|
||||
? this.getFilterValues(this.$route.query)
|
||||
filters: this.instancedResource.getFilterValues
|
||||
? this.instancedResource.getFilterValues(this.$route.query)
|
||||
: {},
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
<template>
|
||||
<div v-if="!initialized">{{ $__("Loading") }}</div>
|
||||
<div v-else :id="`${resourceNamePlural}_show`">
|
||||
<div v-else :id="`${instancedResource.resourceNamePlural}_show`">
|
||||
<slot
|
||||
name="toolbar"
|
||||
:resource="resource"
|
||||
:componentPropData="{ ...$props, ...$data }"
|
||||
/>
|
||||
<h2>
|
||||
{{ i18n.displayName + " #" + resource[idAttr] }}
|
||||
{{
|
||||
instancedResource.i18n.displayName +
|
||||
" #" +
|
||||
resource[instancedResource.idAttr]
|
||||
}}
|
||||
</h2>
|
||||
<div>
|
||||
<fieldset
|
||||
class="rows"
|
||||
v-for="(group, counter) in getFieldGroupings('Show', resource)"
|
||||
v-for="(group, counter) in instancedResource.getFieldGroupings(
|
||||
'Show',
|
||||
resource
|
||||
)"
|
||||
v-bind:key="counter"
|
||||
>
|
||||
<legend v-if="group.name">{{ group.name }}</legend>
|
||||
|
|
@ -27,7 +34,7 @@
|
|||
</fieldset>
|
||||
<fieldset
|
||||
class="rows"
|
||||
v-for="(item, counter) in appendToShow(this)"
|
||||
v-for="(item, counter) in instancedResource.appendToShow(this)"
|
||||
v-bind:key="counter"
|
||||
>
|
||||
<legend v-if="item.name">{{ item.name }}</legend>
|
||||
|
|
@ -35,7 +42,7 @@
|
|||
</fieldset>
|
||||
<fieldset class="action">
|
||||
<router-link
|
||||
:to="{ name: listComponent }"
|
||||
:to="{ name: instancedResource.listComponent }"
|
||||
role="button"
|
||||
class="cancel"
|
||||
>{{ $__("Close") }}</router-link
|
||||
|
|
@ -57,18 +64,14 @@ export default {
|
|||
};
|
||||
},
|
||||
props: {
|
||||
idAttr: String,
|
||||
apiClient: Object,
|
||||
i18n: Object,
|
||||
listComponent: String,
|
||||
resourceNamePlural: String,
|
||||
getFieldGroupings: Function,
|
||||
appendToShow: Function,
|
||||
afterResourceFetch: Function,
|
||||
getResource: Function,
|
||||
instancedResource: Object,
|
||||
},
|
||||
created() {
|
||||
this.getResource(this.$route.params[this.idAttr], this, "show");
|
||||
this.instancedResource.getResource(
|
||||
this.$route.params[this.instancedResource.idAttr],
|
||||
this,
|
||||
"show"
|
||||
);
|
||||
},
|
||||
components: { Toolbar, ShowElement },
|
||||
name: "ResourceShow",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,593 @@
|
|||
import { computed, inject } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { $__ } from "../i18n";
|
||||
import { build_url } from "../composables/datatables";
|
||||
|
||||
export function useBaseResource(context) {
|
||||
const router = useRouter();
|
||||
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;
|
||||
|
||||
const instancedResource = {
|
||||
...Object.keys(context).reduce((acc, key) => {
|
||||
acc[key] = context[key];
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the creation page of the given resource.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
const goToResourceAdd = () => {
|
||||
router.push({
|
||||
name: instancedResource.addComponent,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the edit page of the given resource.
|
||||
*
|
||||
* @param {Object} resource - The resource to navigate to (optional)
|
||||
* @return {void}
|
||||
*/
|
||||
const goToResourceEdit = resource => {
|
||||
router.push({
|
||||
name: instancedResource.editComponent,
|
||||
params: {
|
||||
[instancedResource.idAttr]: resource
|
||||
? resource[instancedResource.idAttr]
|
||||
: instancedResource.newResource[instancedResource.idAttr],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the URL for the resource table.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
const getResourceTableUrl = () => {
|
||||
return instancedResource.resourceTableUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the URL for the show page of the given resource.
|
||||
*
|
||||
* @param {Object} resource - The resource to generate the URL for
|
||||
* @return {string} The URL for the show page of the given resource
|
||||
*/
|
||||
const getResourceShowURL = id => {
|
||||
return router.resolve({
|
||||
name: instancedResource.showComponent,
|
||||
params: { [instancedResource.idAttr]: id },
|
||||
}).href;
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the show page of the given resource.
|
||||
*
|
||||
* @param {Object} [resource] - The resource to navigate to
|
||||
* @param {DataTable} [dt] - The DataTable (optional)
|
||||
* @param {Event} [event] - The event to prevent default handling of (optional)
|
||||
* @return {void}
|
||||
*/
|
||||
const goToResourceShow = (resource, dt, event) => {
|
||||
event?.preventDefault();
|
||||
router.push({
|
||||
name: instancedResource.showComponent,
|
||||
params: {
|
||||
[instancedResource.idAttr]: resource[instancedResource.idAttr],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds an object of filter name-value pairs based on the provided
|
||||
* query object and filterData (tableFilters by default).
|
||||
*
|
||||
* Iterates over the query object keys and updates the filterOptions
|
||||
* object with the new values. If a filter name is not found in
|
||||
* filterOptions, it is added.
|
||||
*
|
||||
* @param {Object} query - The query object (taken from the URL params)containing the filter values.
|
||||
* @param {Array} filterData - The array of filter objects (optional).
|
||||
* @return {Object}
|
||||
*/
|
||||
const getFilterValues = (query, filterData) => {
|
||||
const filters = filterData
|
||||
? filterData
|
||||
: instancedResource.tableFilters
|
||||
? instancedResource.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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds an object of filter name-value pairs based on the provided
|
||||
* query object and filterData (tableFilters by default).
|
||||
*
|
||||
* Iterates over the query object keys and updates the filterOptions
|
||||
* object with the new values. If a filter name is not found in
|
||||
* filterOptions, it is added.
|
||||
*
|
||||
* @param {Object} query - The query object (taken from the URL params)containing the filter values.
|
||||
* @param {Array} filterData - The array of filter objects (optional).
|
||||
* @return {Object}
|
||||
*/
|
||||
const getFilters = (query, filterData) => {
|
||||
const filters = filterData
|
||||
? filterData
|
||||
: instancedResource.tableFilters
|
||||
? instancedResource.tableFilters
|
||||
: [];
|
||||
const filterOptions = filters.reduce((acc, filter) => {
|
||||
acc[filter.name] = filter.value;
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list of default buttons to add to the toolbar, for each view: list, show, edit
|
||||
* It can be overridden at the resource level if the default buttons are not required
|
||||
*
|
||||
* @return {Object} keys must be "list", "show" or "edit", values are functions.
|
||||
*/
|
||||
const defaultToolbarButtons = (resource, i18n) => {
|
||||
return {
|
||||
list: [
|
||||
{
|
||||
action: "add",
|
||||
onClick: () => goToResourceAdd(),
|
||||
title: i18n.newLabel,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
show: [
|
||||
{
|
||||
action: "edit",
|
||||
onClick: () => goToResourceEdit(resource),
|
||||
title: __("Edit"),
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
action: "delete",
|
||||
onClick: () => doResourceDelete(resource),
|
||||
title: __("Delete"),
|
||||
index: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a default empty set of additional buttons
|
||||
* Additional buttons should be added in the resource specific component
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const additionalToolbarButtons = () => {
|
||||
return {
|
||||
list: [],
|
||||
show: [],
|
||||
};
|
||||
};
|
||||
|
||||
const appendToShow = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* This method takes a component name (e.g. 'Form' or 'Show') and an optional resource object
|
||||
* and returns an array of grouped objects that determine which fields should be grouped together
|
||||
* in the relevant component. Each group object contains the name of the group and an array of the
|
||||
* field objects that belong to that group.
|
||||
*
|
||||
* It first filters the resource attributes to only include those that are not hidden
|
||||
* in the given component. Then it groups the attributes by the group name
|
||||
* (or "noGroupFound" if there is no group name). If the component is 'Show', it also
|
||||
* checks if the resource object has data to display in each group. If not,
|
||||
* the group is not included.
|
||||
*
|
||||
* @param {String} component - The component name (e.g. 'Form' or 'Show').
|
||||
* @param {Object} resource - The resource object (optional).
|
||||
* @return {Array} The array of group objects.
|
||||
*/
|
||||
const getFieldGroupings = (component, resource) => {
|
||||
const displayProperty = `hideIn${component}`;
|
||||
const attributesToConsider = instancedResource.resourceAttrs.filter(
|
||||
ra => !ra.hasOwnProperty(displayProperty) || !ra[displayProperty]
|
||||
);
|
||||
const groupings = attributesToConsider.reduce((acc, attr) => {
|
||||
if (
|
||||
attr.hasOwnProperty("group") &&
|
||||
attr.group !== null &&
|
||||
!acc.includes(attr.group)
|
||||
) {
|
||||
return [...acc, attr.group];
|
||||
}
|
||||
if (!attr.hasOwnProperty("group")) {
|
||||
attr.group = "noGroupFound";
|
||||
if (!acc.includes("noGroupFound")) {
|
||||
return [...acc, "noGroupFound"];
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
if (groupings.length === 0) {
|
||||
return [
|
||||
{
|
||||
name: null,
|
||||
fields: attributesToConsider,
|
||||
},
|
||||
];
|
||||
}
|
||||
// FIXME - if no group is defined in accordion mode then the section doesn't have the dropdown applied
|
||||
return groupings.reduce((acc, group) => {
|
||||
const groupFields = attributesToConsider.filter(
|
||||
ra => ra.group === group
|
||||
);
|
||||
const groupInfo = {
|
||||
name: group === "noGroupFound" ? null : group,
|
||||
fields: groupFields,
|
||||
hasDataToDisplay: false,
|
||||
};
|
||||
if (component === "Show" && resource) {
|
||||
groupFields.forEach(field => {
|
||||
if (
|
||||
resource[field.name] != null &&
|
||||
(field.type !== "relationshipWidget" ||
|
||||
resource[field.name].length > 0)
|
||||
) {
|
||||
groupInfo.hasDataToDisplay = true;
|
||||
}
|
||||
});
|
||||
if (!groupInfo.hasDataToDisplay) {
|
||||
return [...acc];
|
||||
}
|
||||
}
|
||||
return [...acc, groupInfo];
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Populates the resource attributes with authorised values based on their types.
|
||||
*
|
||||
* For each attribute:
|
||||
* - If the attribute type is "select" and it has an authorised value category (avCat),
|
||||
* it sets the options from the component's corresponding property.
|
||||
* - If the attribute type is "relationship" and it has component properties,
|
||||
* it assigns authorised values to any marked as type 'av'.
|
||||
* - If the attribute has relationship fields, the function is recursively called
|
||||
* to populate them as well.
|
||||
*
|
||||
* @param {Array} attrs - The array of attributes to be populated with authorised values.
|
||||
*/
|
||||
const populateAttributesWithAuthorisedValues = attrs => {
|
||||
if (!attrs) return;
|
||||
attrs.forEach(attr => {
|
||||
if (attr.type === "select" && typeof attr.avCat === "string") {
|
||||
const avKey = attr.avCat;
|
||||
const avArray = instancedResource[avKey].value;
|
||||
attr.options = avArray;
|
||||
attr.requiredKey = "value";
|
||||
attr.selectLabel = "description";
|
||||
}
|
||||
if (attr.type == "relationship" && attr.componentProps) {
|
||||
Object.keys(attr.componentProps).forEach(key => {
|
||||
if (attr.componentProps[key].type == "av") {
|
||||
attr.componentProps[key].av = instancedResource[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
if (attr.relationshipFields?.length) {
|
||||
populateAttributesWithAuthorisedValues(attr.relationshipFields);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const created = () => {
|
||||
if (instancedResource.resourceAttrs) {
|
||||
populateAttributesWithAuthorisedValues(
|
||||
instancedResource.resourceAttrs
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getResource = async (resourceId, componentData, caller) => {
|
||||
instancedResource.apiClient.get(resourceId).then(
|
||||
resource => {
|
||||
//TODO: Rename this 'resource' to 'fetchedResource'. Needs to also be renamed in ResourceFormAdd and ResourceShow
|
||||
// This is to make it clear that this is the fetchedResource (data), not the resource component class
|
||||
componentData.resource = resource;
|
||||
if (instancedResource.afterResourceFetch) {
|
||||
instancedResource.afterResourceFetch(
|
||||
componentData,
|
||||
resource,
|
||||
caller
|
||||
);
|
||||
}
|
||||
componentData.initialized = true;
|
||||
},
|
||||
error => {}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the list page of the given resource.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
const goToResourceList = () => {
|
||||
router.push({
|
||||
name: instancedResource.listComponent,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Resource deletion handler.
|
||||
* Accepts an optional callback function to run after deletion.
|
||||
* If no callback is provided, does the following:
|
||||
* - If deleting from show component, navigates to resource list component.
|
||||
* - If deleting from resource list component, redraws the table.
|
||||
*
|
||||
* @param {Object} resource - The resource to delete (optional)
|
||||
* @param {Object} callback - Callback to call after deletion (optional)
|
||||
* @return {void}
|
||||
*/
|
||||
const doResourceDelete = (resource, callback) => {
|
||||
let resourceId = resource
|
||||
? resource[instancedResource.idAttr]
|
||||
: instancedResource.newResource[instancedResource.idAttr];
|
||||
let resourceName = resource
|
||||
? resource[instancedResource.nameAttr]
|
||||
: instancedResource.newResource[instancedResource.nameAttr];
|
||||
|
||||
setConfirmationDialog(
|
||||
{
|
||||
title: instancedResource.i18n.deleteConfirmationMessage,
|
||||
message: resourceName,
|
||||
accept_label: $__("Yes, delete"),
|
||||
cancel_label: $__("No, do not delete"),
|
||||
},
|
||||
() => {
|
||||
instancedResource.apiClient.delete(resourceId).then(
|
||||
success => {
|
||||
setMessage(
|
||||
instancedResource.i18n.deleteSuccessMessage.format(
|
||||
resourceName
|
||||
),
|
||||
true
|
||||
);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
} else {
|
||||
if (
|
||||
instancedResource.props.routeAction === "list"
|
||||
) {
|
||||
callback.ajax.reload();
|
||||
} else {
|
||||
goToResourceList();
|
||||
}
|
||||
}
|
||||
},
|
||||
error => {}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits the 'select-resource' event with the id of the provided resource.
|
||||
*
|
||||
* @param {Object} resource - The resource object containing the id attribute.
|
||||
* @param {Object} dt - DataTables instance (not used in this function).
|
||||
* @param {Event} event - The event object (not used in this function).
|
||||
*/
|
||||
|
||||
const doResourceSelect = (resource, dt, event) => {
|
||||
this.$emit("select-resource", resource[instancedResource.idAttr]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the extended_attributes property of the provided resource
|
||||
*
|
||||
* @param {Array} additionalFieldValues - Array of objects with
|
||||
* name and value properties.
|
||||
* @param {Object} resource - The resource object whose
|
||||
* extended_attributes property is updated.
|
||||
*/
|
||||
const additionalFieldsChanged = (additionalFieldValues, resource) => {
|
||||
resource.extended_attributes = additionalFieldValues;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the label to display before the filters
|
||||
*
|
||||
* @return {String} The label
|
||||
*/
|
||||
const getTableFilterFormElementsLabel = () => {
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the array of filters for the table, if required.
|
||||
* This is a default method that returns an empty array.
|
||||
* It can be overridden at the resource level if filters are required
|
||||
*
|
||||
* @return {Array} The array of filters for the table.
|
||||
*/
|
||||
const getTableFilterFormElements = () => {
|
||||
return [];
|
||||
};
|
||||
|
||||
const refreshTemplateState = () => {
|
||||
this.refreshTemplate = !this.refreshTemplate;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that returns a set of buttons to display in the toolbar, based on the current resource and component.
|
||||
* The function takes three arguments: the resource, the component and the i18n object.
|
||||
* It returns an array of buttons. The buttons are a combination of the default buttons and the additional buttons.
|
||||
* The default buttons are defined in the defaultToolbarButtons function and the additional buttons are defined in the
|
||||
* additionalToolbarButtons function at the resource level.
|
||||
*
|
||||
* @param {Object} resource The current resource
|
||||
* @param {String} component The current component
|
||||
* @param {Object} i18n The i18n object
|
||||
* @return {Array<Object>} An array of buttons
|
||||
*/
|
||||
const toolbarButtons = computed(() => {
|
||||
return (resource, component, i18n, componentData) => {
|
||||
const defaultButtons = defaultToolbarButtons(resource, i18n);
|
||||
const additionalButtons = additionalToolbarButtons(
|
||||
resource,
|
||||
componentData
|
||||
);
|
||||
|
||||
//FIXME: we need to check that no indexes match between the default buttons and additional buttons
|
||||
// If we add to the default buttons in future it could mess up indexing
|
||||
|
||||
return [
|
||||
...(defaultButtons[component] || []),
|
||||
...(additionalButtons[component] || []).filter(
|
||||
button => Object.keys(button).length > 0
|
||||
),
|
||||
].sort((a, b) => a.index - b.index);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Generates a new resource object with default values.
|
||||
*
|
||||
* This method initializes the `resourceToBeGenerated` property by iterating
|
||||
* over `resourceAttrs`, assigning default values based on attribute types.
|
||||
* - If an attribute has a `defaultValue`, it is set accordingly.
|
||||
* - For text, textarea, and select types, an empty string is assigned.
|
||||
* - For boolean and checkbox types, a false value is assigned.
|
||||
* - For additional fields or relationship widgets, an empty array is assigned
|
||||
* to `extended_attributes` or the attribute name.
|
||||
* - If none of the above apply, a null value is assigned.
|
||||
* The `idAttr` of the resource is also initialized to null.
|
||||
*
|
||||
* @return {Object} The newly generated resource object with default values.
|
||||
*/
|
||||
const newResource = computed(() => {
|
||||
instancedResource.resourceToBeGenerated =
|
||||
instancedResource.resourceAttrs.reduce((acc, attr) => {
|
||||
if (attr.hasOwnProperty("defaultValue")) {
|
||||
acc[attr.name] = attr.defaultValue;
|
||||
return acc;
|
||||
}
|
||||
if (["text", "textarea", "select"].includes(attr.type)) {
|
||||
acc[attr.name] = "";
|
||||
return acc;
|
||||
}
|
||||
if (["boolean", "checkbox"].includes(attr.type)) {
|
||||
acc[attr.name] = false;
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
attr.name === "additional_fields" ||
|
||||
attr.type === "relationshipWidget"
|
||||
) {
|
||||
acc[
|
||||
attr.name === "additional_fields"
|
||||
? "extended_attributes"
|
||||
: attr.name
|
||||
] = [];
|
||||
return acc;
|
||||
}
|
||||
acc[attr.name] = null;
|
||||
return acc;
|
||||
}, {});
|
||||
instancedResource.resourceToBeGenerated[instancedResource.idAttr] =
|
||||
null;
|
||||
return instancedResource.resourceToBeGenerated;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the plural form of the resource name.
|
||||
*
|
||||
* This method checks if the `resourceName` ends with 's'. If it does not,
|
||||
* it appends 's' to create the plural form. Otherwise returns `resourceName`.
|
||||
*
|
||||
* @return {String} The plural form of the resource name.
|
||||
*/
|
||||
const resourceNamePlural = computed(() => {
|
||||
if (!instancedResource.resourceName.endsWith("s"))
|
||||
return instancedResource.resourceName + "s";
|
||||
return instancedResource.resourceName;
|
||||
});
|
||||
|
||||
/**
|
||||
* Determines if the resource has additional fields.
|
||||
*
|
||||
* @return {Boolean} true if the resource has additional fields, false otherwise.
|
||||
*/
|
||||
const hasAdditionalFields = computed(() => {
|
||||
return instancedResource.resourceAttrs.some(
|
||||
attr => attr.name === "additional_fields"
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...instancedResource,
|
||||
getFilterValues,
|
||||
getFilters,
|
||||
toolbarButtons,
|
||||
defaultToolbarButtons,
|
||||
additionalToolbarButtons,
|
||||
goToResourceAdd,
|
||||
getFieldGroupings,
|
||||
created,
|
||||
getResourceTableUrl,
|
||||
getResourceShowURL,
|
||||
goToResourceEdit,
|
||||
getResource,
|
||||
goToResourceShow,
|
||||
goToResourceList,
|
||||
appendToShow,
|
||||
doResourceSelect,
|
||||
doResourceDelete,
|
||||
additionalFieldsChanged,
|
||||
getTableFilterFormElementsLabel,
|
||||
getTableFilterFormElements,
|
||||
refreshTemplateState,
|
||||
resourceNamePlural,
|
||||
hasAdditionalFields,
|
||||
newResource,
|
||||
setConfirmationDialog,
|
||||
setMessage,
|
||||
setError,
|
||||
setWarning,
|
||||
format_date,
|
||||
patron_to_html,
|
||||
map_av_dt_filter,
|
||||
get_lib_from_av,
|
||||
build_url,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue