Koha-clms/koha-tmpl/intranet-tmpl/prog/js/vue/composables/base-resource.js

760 lines
29 KiB
JavaScript

import { computed, inject, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { build_url } from "../composables/datatables";
import { storeToRefs } from "pinia";
import {
$__,
$__x,
$__n,
$__nx,
$__p,
$__px,
$__np,
$__npx,
} from "@koha-vue/i18n";
/**
* A composable that provides utilities for the resource component.
*
* The utilities provided by this composable are documented individually below.
*
* @param {Object} resourceConfig - The resource component instance.
* This can take in the following parameters
* REQUIRED:
* @param {String} resourceConfig.resourceName - The name of the resource.
* @param {String} resourceConfig.idAttr - The name of the id attribute of the resource.
* @param {String} resourceConfig.nameAttr - The name attribute of the resource.
* @param {String} resourceConfig.components - An object containing the names of the components ("show", "add", "list", "edit")
* @param {Object} resourceConfig.i18n - The i18n object.
* @param {Object} resourceConfig.apiClient - The API client for the resource.
* @param {Object} resourceConfig.table - Contains the table configuration.
* @param {String} resourceConfig.table.resourceTableUrl - The URL to the resource table.
* @param {Array} resourceConfig.resourceAttrs - An array of attributes that relate to the resource's properties and how they should appear in the form/show/list components.
* @param {Object} resourceConfig.props - The props passed to the resource component.
* OPTIONAL:
* @param {String} resourceConfig.moduleStore - The name of the module store that holds resource related data (authorised values, permissions etc)
* @param {Boolean} resourceConfig.table.addAdditionalFilters - A flag to indicate whether to add filters to the list component for the datatable.
* @param {Object} resourceConfig.table.filters - The table filters for the resource. Follows the format for the resourceAttrs parameter.
* @param {Function} resourceConfig.afterResourceFetch - A function to call after the resource is fetched. This can be used to edit resource data or fetch additional data
* @param {Boolean} resourceConfig.embedded - A flag to indicate whether the resource is actually being used as a child component of another resource e.g. embedding a list of agreements into EBSCO package agreements
* @param {String} resourceConfig.extendedAttributesResourceType - The resource type for extended attributes, if applicable.
* @param {String} resourceConfig.extendedAttributesFieldGroup - The field group that you would like the additional fields to be displayed in.
* @param {Function} resourceConfig.defaultToolbarButtons - A function to amend default buttons in the toolbar.
* @param {Function} resourceConfig.additionalToolbarButtons - A function to add additional buttons to the toolbar.
* @param {String} resourceConfig.formGroupsDisplayMode - The display mode for the form groups if not the default. Can be one of the following: "accordion", "tabs".
* @param {Array} resourceConfig.stickyToolbar - The names of the components with a toolbar that should be sticky.
* @param {Array} resourceConfig.navigationOnFormSave - The name of the component that should be navigated to when saving the resource creation/edit form. Defaults to the show component
*
* @return {Object} An object containing the utilities provided by this composable.
*/
export function useBaseResource(resourceConfig) {
const router = useRouter();
const route = useRoute();
const {
setConfirmationDialog,
setMessage,
setError,
setWarning,
updateConfirmationDialogInputs,
} = inject("mainStore");
const navigationStore = inject("navigationStore");
const { breadcrumbMetadata } = storeToRefs(navigationStore);
const moduleStoreUtils = {};
if (resourceConfig.moduleStore) {
const moduleStore = inject(resourceConfig.moduleStore);
const { get_lib_from_av, map_av_dt_filter, isUserPermitted } =
moduleStore;
const { authorisedValues, userPermissions } = storeToRefs(moduleStore);
if (authorisedValues) {
moduleStoreUtils.get_lib_from_av = get_lib_from_av;
moduleStoreUtils.map_av_dt_filter = map_av_dt_filter;
moduleStoreUtils.authorisedValues = authorisedValues.value;
}
if (userPermissions) {
moduleStoreUtils.userPermissions = userPermissions.value;
moduleStoreUtils.isUserPermitted = isUserPermitted;
}
}
let i18n = resourceConfig.i18n;
/**
**********************************************************************
* TOOLBAR RELATED METHODS
**********************************************************************
*/
/**
* A function that returns a set of buttons to display in the toolbar, based on the current resource and component.
* The function takes two arguments: the resource and the component.
* 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
* @return {Array<Object>} An array of buttons
*/
const toolbarButtons = computed(() => {
return (resource, component, componentData) => {
let defaultButtons = defaultToolbarButtons(resource);
defaultButtons = resourceConfig.hasOwnProperty(
"defaultToolbarButtons"
)
? resourceConfig.defaultToolbarButtons(
defaultButtons,
resource || {}
)
: defaultButtons;
const additionalButtons = resourceConfig.hasOwnProperty(
"additionalToolbarButtons"
)
? resourceConfig.additionalToolbarButtons(
resource || {},
componentData
)
: 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);
};
});
/**
* 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 => {
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: [],
};
};
/**
* Navigates to the creation page of the given resource.
*
* @return {void}
*/
const goToResourceAdd = () => {
router.push({
name: resourceConfig.components.add,
});
};
/**
* 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: resourceConfig.components.edit,
params: {
[resourceConfig.idAttr]: resource
? resource[resourceConfig.idAttr]
: resourceConfig.newResource[resourceConfig.idAttr],
},
});
};
/**
* 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: resourceConfig.components.show,
params: {
[resourceConfig.idAttr]: resource[resourceConfig.idAttr],
},
});
};
/**
* Navigates to the list page of the given resource.
*
* @return {void}
*/
const goToResourceList = () => {
router.push({
name: resourceConfig.components.list,
});
};
/**
* 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[resourceConfig.idAttr]
: resourceConfig.newResource[resourceConfig.idAttr];
let resourceName = resource
? resource[resourceConfig.nameAttr]
: resourceConfig.newResource[resourceConfig.nameAttr];
setConfirmationDialog(
{
title: i18n.deleteConfirmationMessage,
message: resourceName,
accept_label: $__("Yes, delete"),
cancel_label: $__("No, do not delete"),
},
() => {
resourceConfig.apiClient.delete(resourceId).then(
success => {
setMessage(
i18n.deleteSuccessMessage.format(resourceName),
true
);
if (typeof callback === "function") {
callback();
} else {
if (resourceConfig.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) => {
resourceConfig.$emit(
"select-resource",
resource[resourceConfig.idAttr]
);
};
/*
**********************************************************************
* DATATABLES RELATED METHODS
**********************************************************************
*/
/**
* Return the URL for the resource table.
*
* @return {string}
*/
const getResourceTableUrl = () => {
return resourceConfig.table.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: resourceConfig.components.show,
params: { [resourceConfig.idAttr]: id },
}).href;
};
/**
* Builds an object of filter name-value pairs based on the provided
* query object and filterData ('filters' 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.length
? filterData
: resourceConfig.table.additionalFilters
? resourceConfig.table.additionalFilters
: [];
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 in the List component
* This is a default method that returns an empty string.
* It can be overridden at the resource level if filters are required
*
* @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 [];
};
/*
**********************************************************************
* RESOURCE DISPLAY RELATED METHODS
**********************************************************************
*/
/**
* A function that can be used to add additional content to the show view
* of a resource. The function should return an array of attributes such as those in 'resourceAttrs' in the module specific resources
*
* @returns {Array}
*/
const appendToShow = () => {
return [];
};
/**
* 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 attributesToConsider = resourceConfig.resourceAttrs.reduce(
(acc, ra) => {
if (ra.type === "relationshipWidget") {
ra.relationshipFields.forEach(relationshipField => {
relationshipField.relationshipName = ra.name;
});
}
const shouldFieldBeHidden =
ra.hideIn && typeof ra.hideIn === "function"
? ra.hideIn()
: ra.hideIn;
if (
shouldFieldBeHidden &&
!shouldFieldBeHidden.includes(component)
) {
return [...acc, ra];
}
if (
shouldFieldBeHidden &&
shouldFieldBeHidden.includes(component)
) {
return acc;
}
return [...acc, ra];
},
[]
);
if (resourceConfig.extendedAttributesResourceType) {
attributesToConsider.push({
name: "additional_fields",
type: "additional_fields",
extended_attributes_resource_type:
resourceConfig.extendedAttributesResourceType,
...(resourceConfig.extendedAttributesFieldGroup && {
group: resourceConfig.extendedAttributesFieldGroup,
}),
});
}
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" &&
resourceConfig.showGroupsDisplayMode === "splitScreen" &&
resourceConfig.splitScreenGroupings.length > 0
) {
const splitScreenGrouping =
resourceConfig.splitScreenGroupings.find(
g => g.name === group
);
if (splitScreenGrouping) {
groupInfo.splitPane = splitScreenGrouping.pane;
} else {
// Default to first pane
groupInfo.splitPane = 1;
}
}
if (component === "Show" && resource) {
groupFields.forEach(field => {
const hasStandardData =
resource[field.name] != null &&
(field.type !== "relationshipWidget" ||
resource[field.name].length > 0);
let hasFormattedData = false;
if (field.format && typeof field.format === "function") {
const formatted = field.format(
resource[field.name],
resource
);
hasFormattedData =
formatted !== null &&
formatted !== undefined &&
formatted !== "";
}
if (hasStandardData || hasFormattedData) {
groupInfo.hasDataToDisplay = true;
}
});
if (!groupInfo.hasDataToDisplay) {
return [...acc];
}
}
return [...acc, groupInfo];
}, []);
};
/**
* Retrieves a resource by id and assigns it to the component data.
* If the resource component has an 'afterResourceFetch' method, it is called
* with the component data, the fetched resource and the caller as arguments.
* When the resource is retrieved, the 'initialized' flag is set to true.
* @param {String} resourceId - The id of the resource to retrieve.
* @param {Object} componentData - The component data to assign the resource to.
* @param {String} caller - The name of the caller that requested the resource
* (e.g. 'form', 'list', 'show').
*/
const getResource = async (resourceId, componentData, caller) => {
resourceConfig.apiClient.get(resourceId).then(
resource => {
//TODO: Rename this 'resource' to 'fetchedResource'. Needs to also be renamed in ResourceFormSave and ResourceShow
// This is to make it clear that this is the fetchedResource (data), not the resource component class
componentData.resource.value = resource;
breadcrumbMetadata.value = resource;
if (componentData.instancedResource.afterResourceFetch) {
componentData.instancedResource.afterResourceFetch(
componentData,
resource,
caller
);
}
componentData.initialized.value = true;
},
error => {}
);
};
/**
* 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 (!resourceConfig.resourceName.endsWith("s"))
return resourceConfig.resourceName + "s";
return resourceConfig.resourceName;
});
/*
**********************************************************************
* RESOURCE CREATION RELATED METHODS
**********************************************************************
*/
/**
* 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;
};
/**
* 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(() => {
resourceConfig.resourceToBeGenerated =
resourceConfig.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 (resourceConfig.extendedAttributesResourceType) {
resourceConfig.resourceToBeGenerated.extended_attributes = [];
}
resourceConfig.resourceToBeGenerated[resourceConfig.idAttr] = null;
return resourceConfig.resourceToBeGenerated;
});
/*
**********************************************************************
* RESOURCE UTILITY METHODS
**********************************************************************
*/
/**
* Determines if the resource has additional fields.
*
* @return {Boolean} true if the resource has additional fields, false otherwise.
*/
const hasAdditionalFields = computed(() => {
return !!resourceConfig.extendedAttributesResourceType;
});
const refreshTemplate = ref(0);
/**
* Toggle the refreshTemplate flag of the resource.
* This flag is used to force a reload of the template by Vue.
* It is typically used when the data of the resource has been updated.
*
* @return {void}
*/
const refreshTemplateState = () => {
refreshTemplate.value++;
};
/**
* 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 => {
const { authorisedValues } = moduleStoreUtils;
if (!attrs) return;
attrs.forEach(attr => {
if (attr.type === "select" && typeof attr.avCat === "string") {
const avKey = attr.avCat;
const avArray = authorisedValues[avKey];
if ((!avArray || !avArray.length) && attr.fallbackType) {
attr.type = attr.fallbackType;
} else {
attr.options = avArray;
attr.requiredKey = "value";
attr.selectLabel = "description";
}
}
if (attr.relationshipFields?.length) {
populateAttributesWithAuthorisedValues(attr.relationshipFields);
}
});
};
/**
* Initializes the component by populating resource attributes
* with their respective authorised values. This ensures that
* any select or relationship attributes have the correct options
* or authorised values assigned before the component is used.
*/
const created = () => {
if (resourceConfig.resourceAttrs) {
populateAttributesWithAuthorisedValues(
resourceConfig.resourceAttrs
);
}
};
created();
return {
...resourceConfig,
...moduleStoreUtils,
getFilterValues,
toolbarButtons,
defaultToolbarButtons,
additionalToolbarButtons,
goToResourceAdd,
getFieldGroupings,
created,
getResourceTableUrl,
getResourceShowURL,
goToResourceEdit,
getResource,
goToResourceShow,
goToResourceList,
appendToShow,
doResourceSelect,
doResourceDelete,
additionalFieldsChanged,
getTableFilterFormElementsLabel,
getTableFilterFormElements,
refreshTemplateState,
refreshTemplate,
resourceNamePlural,
hasAdditionalFields,
newResource,
setConfirmationDialog,
setMessage,
setError,
setWarning,
updateConfirmationDialogInputs,
build_url,
route,
router,
i18n,
};
}