/** * Copyright 2023 schukai GmbH * SPDX-License-Identifier: AGPL-3.0 */ import {addAttributeToken} from "../../../dom/attributes.mjs"; import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs"; import {Datasource, dataSourceSymbol} from "../datasource.mjs"; import {DatasourceStyleSheet} from "../stylesheet/datasource.mjs"; import {instanceSymbol} from "../../../constants.mjs"; import { assembleMethodSymbol, registerCustomElement, } from "../../../dom/customelement.mjs"; import {RestAPI} from "../../../data/datasource/server/restapi.mjs"; import {Formatter} from "../../../text/formatter.mjs"; import {clone} from "../../../util/clone.mjs"; import {validateBoolean} from "../../../types/validate.mjs"; import {findElementWithIdUpwards} from "../../../dom/util.mjs"; import {Observer} from "../../../types/observer.mjs"; import {Pathfinder} from "../../../data/pathfinder.mjs"; import {fireCustomEvent} from "../../../dom/events.mjs"; export {Rest}; /** * @private * @type {symbol} */ const intersectionObserverHandlerSymbol = Symbol("intersectionObserverHandler"); /** * @private * Original at source/components/datatable/datasource/rest.mjs * @type {symbol} */ const rawDataSymbol = Symbol.for( "@schukai/monster/data/datasource/server/restapi/rawdata", ); /** * @private * @type {symbol} */ const intersectionObserverObserverSymbol = Symbol( "intersectionObserverObserver", ); /** * @private * @type {symbol} */ const filterObserverSymbol = Symbol("filterObserver"); /** * The Datasource component is a basic class for the datatable component. * * <img src="./images/rest.png"> * * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library * * @startuml rest.png * skinparam monochrome true * skinparam shadowing false * HTMLElement <|-- CustomElement * CustomElement <|-- Datasource * Datasource <|-- Rest * @enduml * * @copyright schukai GmbH * @memberOf Monster.Components.Datatable.Datasource * @summary A rest api datasource */ class Rest extends Datasource { /** * the constructor of the class */ constructor() { super(); this[dataSourceSymbol] = new RestAPI(); } /** * This method is called by the `instanceof` operator. * @returns {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/datasource/rest@@instance"); } /** * To set the options via the html tag the attribute `data-monster-options` must be used. * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} * * The individual configuration values can be found in the table. * * @property {Object} templates Template definitions * @property {string} templates.main Main template * @property {Object} features Feature definitions * @property {boolean} features.autoInit If true, the component is initialized automatically * @property {boolean} features.filter If true, the component is initialized automatically * @property {Object} autoInit Auto init definitions * @property {boolean} autoInit.intersectionObserver If true, the intersection observer is initialized automatically * @property {boolean} autoInit.oneTime If true, the intersection observer is initialized only once * @property {Object} filter Filter definitions * @property {string} filter.id The id of the filter control * @property {Object} datatable Datatable definitions * @property {string} datatable.id The id of the datatable control * @property {Object} response Response definitions * @property {Object} response.path Path definitions (changed in 3.56.0) * @property {string} response.path.message Path to the message (changed in 3.56.0) * @property {Object} read Read configuration * @property {string} read.url The url of the rest api * @property {string} read.method The method of the rest api * @property {Object} read.parameters The parameters of the rest api * @property {Object} read.parameters.filter The filter of the rest api * @property {Object} read.parameters.orderBy The order by of the rest api * @property {Object} read.parameters.page The page of the rest api * @property {Object} write Write configuration */ get defaults() { const restOptions = new RestAPI().defaults; restOptions.read.parameters = { filter: undefined, oderBy: undefined, page: "1", }; return Object.assign({}, super.defaults, restOptions, { templates: { main: getTemplate(), }, features: { autoInit: false, filter: false, }, autoInit: { intersectionObserver: false, oneTime: true, }, filter: { id: undefined, }, datatable: { id: undefined, }, response: { path: { message: "sys.message", code: "sys.code", } }, }); } /** * * @param {string} page * @param {string} query * @param {string} orderBy * @returns {Monster.Components.Datatable.Datasource.Rest} */ setParameters({page, query, orderBy}) { const parameters = this.getOption("read.parameters"); if (query !== undefined) { parameters.query = `${query}`; parameters.page = "1"; } // after a query the page is set to 1, so if the page is not set, it is set to 1 if (page !== undefined) parameters.page = `${page}`; if (orderBy !== undefined) parameters.order = `${orderBy}`; this.setOption("read.parameters", parameters); return this; } /** * * @return {Monster.Components.Form.Form} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initEventHandler.call(this); initAutoInit.call(this); } /** * @deprecated 2023-06-25 * @returns {Promise<never>|*} */ reload() { return this.fetch(); } /** * Fetches the data from the rest api * @returns {Promise<never>|*} */ fetch() { const opt = clone(this.getOption("read")); this[dataSourceSymbol].setOption("read", opt); let url = this.getOption("read.url"); const formatter = new Formatter(this.getOption("read.parameters")); if (!url) { return Promise.reject(new Error("No url defined")); } url = formatter.format(url); this[dataSourceSymbol].setOption("read.url", url); return new Promise((resolve, reject) => { fireCustomEvent(this, "monster-datasource-fetch", { datasource: this, }); setTimeout(() => { this[dataSourceSymbol] .read() .then((response) => { fireCustomEvent(this, "monster-datasource-fetched", { datasource: this, }); resolve(response); }) .catch((error) => { fireCustomEvent(this, "monster-datasource-error", { error: error, }); addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString()); reject(error); }); }, 0); }); } /** * * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [DatasourceStyleSheet]; } /** * @private * @return {string} */ static getTag() { return "monster-datasource-rest"; } /** * This method activates the intersection observer manually. * For this purpose, the option `autoInit.intersectionObserver` must be set to `false`. * * @returns {Monster.Components.Datatable.Datasource.Rest} */ initIntersectionObserver() { initIntersectionObserver.call(this); return this; } /** * @private */ connectedCallback() { super.connectedCallback(); setTimeout(() => { if (this.getOption("features.filter", false) === true) { initFilter.call(this); } }, 0); } /** * @private */ disconnectedCallback() { super.disconnectedCallback(); removeFilter.call(this); } read() { return this.fetch(); } /** * Fetches the data from the rest api * @returns {Promise<never>|*} */ write() { const opt = clone(this.getOption("write")); this[dataSourceSymbol].setOption("write", opt); let url = this.getOption("write.url"); const formatter = new Formatter(this.getOption("write.parameters")); if (!url) { return Promise.reject(new Error("No url defined")); } url = formatter.format(url); this[dataSourceSymbol].setOption("write.url", url); return new Promise((resolve, reject) => { fireCustomEvent(this, "monster-datasource-fetch", { datasource: this, }); setTimeout(() => { this[dataSourceSymbol] .write() .then((response) => { fireCustomEvent(this, "monster-datasource-fetched", { datasource: this, }); resolve(response); }) .catch((error) => { fireCustomEvent(this, "monster-datasource-error", { error: error, }); addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString()); reject(error); }); }, 0); }); } } /** * @private */ function removeFilter() { const filterID = this.getOption("filter.id", undefined); if (!filterID) return; const filterControl = findElementWithIdUpwards(this, filterID); if (filterControl && this[filterObserverSymbol]) { filterControl?.detachObserver(this[filterObserverSymbol]); } } /** * @private */ function initFilter() { const filterID = this.getOption("filter.id", undefined); if (!filterID) throw new Error("filter feature is enabled but no filter id is defined"); const filterControl = findElementWithIdUpwards(this, filterID); if (!filterControl) throw new Error( "filter feature is enabled but no filter control with id " + filterID + " is found", ); this[filterObserverSymbol] = new Observer(() => { const query = filterControl.getOption("query", undefined); this.setParameters({query: query}); this.fetch() .then((response) => { if (!(response instanceof Response)) { throw new Error("Response is not an instance of Response"); } if (response?.ok === true) { this.dispatchEvent(new CustomEvent("reload", {bubbles: true})); filterControl?.showSuccess(); } if (response.bodyUsed === true) { return handleIntersectionObserver.call(this, response[rawDataSymbol], response, filterControl); } response .text() .then((jsonAsText) => { let json; try { json = JSON.parse(jsonAsText); } catch (e) { let message = e instanceof Error ? e.message : `${e}`; filterControl?.showFailureMessage(message); return Promise.reject(e); } return handleIntersectionObserver.call(this, json, response, filterControl); }) .catch((e) => { filterControl?.showFailureMessage(e.message); }); }) .catch((e) => { this.dispatchEvent( new CustomEvent("error", {bubbles: true, detail: e}), ); if (!(e instanceof Error)) { e = new Error(e); } filterControl?.showFailureMessage(e.message); return Promise.reject(e); }); }); filterControl.attachObserver(this[filterObserverSymbol]); } function handleIntersectionObserver(json, response, filterControl) { const path = new Pathfinder(json); const codePath = this.getOption("response.path.code"); if (path.exists(codePath)) { const code = `${path.getVia(codePath)}`; if (code && code === "200") { filterControl?.showSuccess(); return Promise.resolve(response); } const messagePath = this.getOption("response.path.message"); if (path.exists(messagePath)) { const message = path.getVia(messagePath); filterControl?.showFailureMessage(message); return Promise.reject(new Error(message)); } return Promise.reject(new Error("Response code is not 200")); } } /** * @private */ function initAutoInit() { const autoInit = this.getOption("features.autoInit"); validateBoolean(autoInit); if (autoInit !== true) return; if (this.getOption("autoInit.intersectionObserver") === true) { initIntersectionObserver.call(this); return; } setTimeout(() => { this.fetch().catch(() => { }); }, 0); } function initEventHandler() { this[intersectionObserverHandlerSymbol] = (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { if (entry.intersectionRatio > 0) { this.fetch(); } // only load once if ( this.getOption("autoInit.oneTime") === true && this[intersectionObserverObserverSymbol] !== undefined ) { this[intersectionObserverObserverSymbol].unobserve(this); } } }); }; } function initIntersectionObserver() { this.classList.add("intersection-observer"); const options = { root: null, rootMargin: "0px", threshold: 0.1, }; this[intersectionObserverObserverSymbol] = new IntersectionObserver( this[intersectionObserverHandlerSymbol], options, ); this[intersectionObserverObserverSymbol].observe(this); } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <slot></slot>`; } registerCustomElement(Rest);