From 460df2fed7752dc4d8b444d46ceb4972cda6d0b5 Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Thu, 21 Nov 2024 12:59:44 +0100 Subject: [PATCH] fix(api-button): check instance --- flake.lock | 12 +- source/components/form/api-button.mjs | 833 +++++++++++++------------- 2 files changed, 425 insertions(+), 420 deletions(-) diff --git a/flake.lock b/flake.lock index 1a499d0e7..c24bd0bff 100644 --- a/flake.lock +++ b/flake.lock @@ -89,11 +89,11 @@ "systems": "systems_3" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -168,11 +168,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1731239293, - "narHash": "sha256-q2yjIWFFcTzp5REWQUOU9L6kHdCDmFDpqeix86SOvDc=", + "lastModified": 1731797254, + "narHash": "sha256-df3dJApLPhd11AlueuoN0Q4fHo/hagP75LlM5K1sz9g=", "owner": "nixos", "repo": "nixpkgs", - "rev": "9256f7c71a195ebe7a218043d9f93390d49e6884", + "rev": "e8c38b73aeb218e27163376a2d617e61a2ad9b59", "type": "github" }, "original": { diff --git a/source/components/form/api-button.mjs b/source/components/form/api-button.mjs index 6eccdbbd0..5a4bea7ab 100644 --- a/source/components/form/api-button.mjs +++ b/source/components/form/api-button.mjs @@ -12,31 +12,31 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { instanceSymbol } from "../../constants.mjs"; -import { buildMap } from "../../data/buildmap.mjs"; -import { Pathfinder } from "../../data/pathfinder.mjs"; -import { addAttributeToken } from "../../dom/attributes.mjs"; +import {instanceSymbol} from "../../constants.mjs"; +import {buildMap} from "../../data/buildmap.mjs"; +import {Pathfinder} from "../../data/pathfinder.mjs"; +import {addAttributeToken} from "../../dom/attributes.mjs"; import { - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_ROLE, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; import { - assembleMethodSymbol, - registerCustomElement, + assembleMethodSymbol, + registerCustomElement, } from "../../dom/customelement.mjs"; import { - findTargetElementFromEvent, - fireCustomEvent, + findTargetElementFromEvent, + fireCustomEvent, } from "../../dom/events.mjs"; -import { isArray, isPrimitive, isIterable } from "../../types/is.mjs"; -import { validateString } from "../../types/validate.mjs"; -import { ActionButton } from "./action-button.mjs"; -import { ApiButtonStyleSheet } from "./stylesheet/api-button.mjs"; -import { isObject, isFunction } from "../../types/is.mjs"; -import { getGlobal } from "../../types/global.mjs"; -import { Formatter } from "../../text/formatter.mjs"; +import {isArray, isPrimitive, isIterable} from "../../types/is.mjs"; +import {validateString} from "../../types/validate.mjs"; +import {ActionButton} from "./action-button.mjs"; +import {ApiButtonStyleSheet} from "./stylesheet/api-button.mjs"; +import {isObject, isFunction} from "../../types/is.mjs"; +import {getGlobal} from "../../types/global.mjs"; +import {Formatter} from "../../text/formatter.mjs"; -export { ApiButton }; +export {ApiButton}; /** * @private @@ -67,220 +67,220 @@ const containerElementSymbol = Symbol("containerElement"); * @fires monster-api-button-failed */ class ApiButton extends ActionButton { - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/form/api-button@@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} mapping - The mapping object. - * @property {string} mapping.selector - The selector to find the buttons in the response. - * @property {string} mapping.labelSelector - The selector to find the label for the button. - * @property {string} mapping.labelTemplate - The template to create the label for the button. - * @property {string} mapping.apiTemplate - The template to create the api for the button. - * @property {string} mapping.urlTemplate - The template to create the url for the button. - * @property {function} mapping.filter - The filter function to filter the buttons. - * @property {string} url - The url to fetch the data. - * @property {object} api - The api options. - * @property {object} api.fetch - The fetch options. - * @property {string} api.body - The body template. - * @property {object} callbacks - The callbacks object. - * @property {function} callbacks.beforeApi - The beforeApi callback. - * @property {object} fetch - The fetch options. - * @property {string} fetch.redirect - The redirect option. - * @property {string} fetch.method - The method option. - * @property {string} fetch.mode - The mode option. - * @property {string} fetch.credentials - The credentials option. - * @property {object} fetch.headers - The headers option. - * @property {string} fetch.headers.accept - The acceptance option. - * @extends {ActionButton.defaults} - */ - get defaults() { - const opts = Object.assign({}, super.defaults, { - mapping: { - selector: "*", - labelSelector: "", - labelTemplate: "", - apiTemplate: "", - urlTemplate: "", - filter: "", - }, - api: { - fetch: { - method: "POST", - redirect: "error", - mode: "same-origin", - credentials: "same-origin", - headers: { - accept: "application/json", - }, - }, - body: {}, - }, - url: "", - callbacks: { - beforeApi: null, - }, - fetch: { - redirect: "error", - method: "GET", - mode: "same-origin", - credentials: "same-origin", - headers: { - accept: "application/json", - }, - }, - }); - - opts["actions"]["execute"] = executeAPIButton.bind(self); - - return opts; - } - - /** - * - * @return {Promise} - */ - fetch(url) { - if (url instanceof URL) { - url = url.toString(); - } - - if (url !== undefined) { - url = validateString(url); - } - - return fetchData.call(this, url).then((map) => { - if ( - isObject(map) || - isArray(map) | (map instanceof Set) || - map instanceof Map - ) { - this.importButtons(map); - } - }); - } - - /** - * Import buttons from a map. - * - * @param {array|object|Map|Set} data - * @return {ApiButton} - * @throws {Error} map is not iterable - * @throws {Error} missing label configuration - */ - importButtons(data) { - const mappingOptions = this.getOption("mapping", {}); - const selector = mappingOptions?.["selector"]; - const labelSelector = mappingOptions?.["labelSelector"]; - const labelTemplate = mappingOptions?.["labelTemplate"]; - const apiTemplate = mappingOptions?.["apiTemplate"]; - let urlTemplate = mappingOptions?.["urlTemplate"]; - const filter = mappingOptions?.["filter"]; - - let flag = false; - let apiEqualUrl = false; - if (labelTemplate === "") { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty label template"); - flag = true; - } - - if (apiTemplate === "") { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty api template"); - flag = true; - } - - if (urlTemplate === "") { - urlTemplate = apiTemplate; - apiEqualUrl = true; - } - - if (flag === true) { - throw new Error("missing label or api configuration"); - } - - if (isPrimitive(labelSelector) && labelSelector !== "") { - const finder = new Pathfinder(data); - const label = finder.getVia(labelSelector); - this.setOption("labels.button", label); - this.value = label; - } - - let labelMap; - const urlMap = buildMap(data, selector, urlTemplate, apiTemplate, filter); - if (apiEqualUrl === true) { - labelMap = urlMap; - } else { - labelMap = buildMap(data, selector, labelTemplate, apiTemplate, filter); - } - - const buttons = []; - if (!isIterable(urlMap)) { - throw new Error("map is not iterable"); - } - - for (const [iterKey] of urlMap) { - const vmUrl = urlMap.get(iterKey); - const vmLabel = labelMap.get(iterKey); - buttons.push({ - label: vmLabel, - class: "monster-button-outline-primary monster-border-0", - action: this.getOption("actions.execute"), - url: vmUrl, - cmd: iterKey, - }); - } - - try { - this.updateI18n(); - } catch (e) { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - } - - this.setOption("buttons", buttons); - - fireCustomEvent(this, "monster-button-set", { - buttons: buttons, - }); - - return this; - } - - /** - * - * @return {ApiButton} - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - initControlReferences.call(this); - initEventHandler.call(this); - - return this; - } - - /** - * @return {string} - */ - static getTag() { - return "monster-api-button"; - } - - /** - * @return {Array<CSSStyleSheet>} - */ - static getCSSStyleSheet() { - const styles = super.getCSSStyleSheet(); - styles.push(ApiButtonStyleSheet); - return styles; - } + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/form/api-button@@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} mapping - The mapping object. + * @property {string} mapping.selector - The selector to find the buttons in the response. + * @property {string} mapping.labelSelector - The selector to find the label for the button. + * @property {string} mapping.labelTemplate - The template to create the label for the button. + * @property {string} mapping.apiTemplate - The template to create the api for the button. + * @property {string} mapping.urlTemplate - The template to create the url for the button. + * @property {function} mapping.filter - The filter function to filter the buttons. + * @property {string} url - The url to fetch the data. + * @property {object} api - The api options. + * @property {object} api.fetch - The fetch options. + * @property {string} api.body - The body template. + * @property {object} callbacks - The callbacks object. + * @property {function} callbacks.beforeApi - The beforeApi callback. + * @property {object} fetch - The fetch options. + * @property {string} fetch.redirect - The redirect option. + * @property {string} fetch.method - The method option. + * @property {string} fetch.mode - The mode option. + * @property {string} fetch.credentials - The credentials option. + * @property {object} fetch.headers - The headers option. + * @property {string} fetch.headers.accept - The acceptance option. + * @extends {ActionButton.defaults} + */ + get defaults() { + const opts = Object.assign({}, super.defaults, { + mapping: { + selector: "*", + labelSelector: "", + labelTemplate: "", + apiTemplate: "", + urlTemplate: "", + filter: "", + }, + api: { + fetch: { + method: "POST", + redirect: "error", + mode: "same-origin", + credentials: "same-origin", + headers: { + accept: "application/json", + }, + }, + body: {}, + }, + url: "", + callbacks: { + beforeApi: null, + }, + fetch: { + redirect: "error", + method: "GET", + mode: "same-origin", + credentials: "same-origin", + headers: { + accept: "application/json", + }, + }, + }); + + opts["actions"]["execute"] = executeAPIButton.bind(self); + + return opts; + } + + /** + * + * @return {Promise} + */ + fetch(url) { + if (url instanceof URL) { + url = url.toString(); + } + + if (url !== undefined) { + url = validateString(url); + } + + return fetchData.call(this, url).then((map) => { + if ( + isObject(map) || + isArray(map) | (map instanceof Set) || + map instanceof Map + ) { + this.importButtons(map); + } + }); + } + + /** + * Import buttons from a map. + * + * @param {array|object|Map|Set} data + * @return {ApiButton} + * @throws {Error} map is not iterable + * @throws {Error} missing label configuration + */ + importButtons(data) { + const mappingOptions = this.getOption("mapping", {}); + const selector = mappingOptions?.["selector"]; + const labelSelector = mappingOptions?.["labelSelector"]; + const labelTemplate = mappingOptions?.["labelTemplate"]; + const apiTemplate = mappingOptions?.["apiTemplate"]; + let urlTemplate = mappingOptions?.["urlTemplate"]; + const filter = mappingOptions?.["filter"]; + + let flag = false; + let apiEqualUrl = false; + if (labelTemplate === "") { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty label template"); + flag = true; + } + + if (apiTemplate === "") { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty api template"); + flag = true; + } + + if (urlTemplate === "") { + urlTemplate = apiTemplate; + apiEqualUrl = true; + } + + if (flag === true) { + throw new Error("missing label or api configuration"); + } + + if (isPrimitive(labelSelector) && labelSelector !== "") { + const finder = new Pathfinder(data); + const label = finder.getVia(labelSelector); + this.setOption("labels.button", label); + this.value = label; + } + + let labelMap; + const urlMap = buildMap(data, selector, urlTemplate, apiTemplate, filter); + if (apiEqualUrl === true) { + labelMap = urlMap; + } else { + labelMap = buildMap(data, selector, labelTemplate, apiTemplate, filter); + } + + const buttons = []; + if (!isIterable(urlMap)) { + throw new Error("map is not iterable"); + } + + for (const [iterKey] of urlMap) { + const vmUrl = urlMap.get(iterKey); + const vmLabel = labelMap.get(iterKey); + buttons.push({ + label: vmLabel, + class: "monster-button-outline-primary monster-border-0", + action: this.getOption("actions.execute"), + url: vmUrl, + cmd: iterKey, + }); + } + + try { + this.updateI18n(); + } catch (e) { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + } + + this.setOption("buttons", buttons); + + fireCustomEvent(this, "monster-button-set", { + buttons: buttons, + }); + + return this; + } + + /** + * + * @return {ApiButton} + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + initControlReferences.call(this); + initEventHandler.call(this); + + return this; + } + + /** + * @return {string} + */ + static getTag() { + return "monster-api-button"; + } + + /** + * @return {Array<CSSStyleSheet>} + */ + static getCSSStyleSheet() { + const styles = super.getCSSStyleSheet(); + styles.push(ApiButtonStyleSheet); + return styles; + } } /** @@ -290,145 +290,145 @@ class ApiButton extends ActionButton { * @param {Monster.Components.Form.ApiButton} element */ function executeAPIButton(event, button, element) { - const self = element; - - const fetchOptions = self.getOption("api.fetch", {}); - - const callback = self.getOption("callbacks.beforeApi"); - if (isFunction(callback)) { - callback.call(self, fetchOptions); - } - - let url = undefined; - let label = undefined; - let key = undefined; - - const attr = button.getAttribute("data-monster-insert-reference"); - if (attr) { - const index = attr.split("-")[1]; - const b = self.getOption("buttons." + index); - - url = b?.["url"]; - label = b?.["label"]; - key = b?.["cmd"]; - } - - const body = self.getOption("api.body"); - if (isObject(body)) { - const bodyString = JSON.stringify(body); - - const obj = { - url: url, - label: label, - value: self.getOption("value"), - key: key, - id: self.getOption("id"), - }; - - fetchOptions.body = new Formatter(obj, {}).format(bodyString); - } - - if (button instanceof HTMLElement) { - button.setState("activity"); - } - - fireCustomEvent(self, "monster-api-button-click", { - button, - }); - - const global = getGlobal(); - global - .fetch(url, fetchOptions) - .then((response) => { - if (!response.ok) { - if (button instanceof HTMLElement) { - button.setState("successful", 4000); - } - return Promise.reject(response); - } - - const contentType = response?.headers?.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - return response - .text() - .then((text) => { - try { - const data = JSON.parse(text); // Try to parse the response as JSON - - if (button instanceof HTMLElement) { - button.setState("successful", 4000); - } - - fireCustomEvent(self, "monster-api-button-successful", { - button, - data, - response, - contentType: response.headers.get("Content-Type"), - }); - } catch (error) { - if (button instanceof HTMLElement) { - button.setState("failed", 4000); - button.setMessage(error.message).showMessage(2000); - } - - fireCustomEvent(self, "monster-api-button-failed", { - button, - error, - response, - contentType: response.headers.get("Content-Type"), - }); - } - }) - .catch((error) => { - if (button instanceof HTMLElement) { - button.setState("failed", 4000); - button.setMessage("request failed").showMessage(2000); - } - - fireCustomEvent(self, "monster-api-button-failed", { - button, - error, - response, - contentType: response.headers.get("Content-Type"), - }); - }); - } else { - return response - .blob() - .then((data) => { - fireCustomEvent(self, "monster-api-button-successful", { - button, - data, - response, - contentType: response.headers.get("Content-Type"), - }); - }) - .catch((error) => { - if (button instanceof HTMLElement) { - button.setState("failed", 4000); - button.setMessage("request failed").showMessage(2000); - } - - fireCustomEvent(self, "monster-api-button-failed", { - button, - error, - response, - contentType: response.headers.get("Content-Type"), - }); - }); - } - }) - .catch((error) => { - if (button instanceof HTMLElement) { - button.setState("failed", 4000); - button.setMessage(error.message).showMessage(2000); - } - - fireCustomEvent(self, "monster-api-button-failed", { - button, - error, - }); - }); + const self = element; + + const fetchOptions = self.getOption("api.fetch", {}); + + const callback = self.getOption("callbacks.beforeApi"); + if (isFunction(callback)) { + callback.call(self, fetchOptions); + } + + let url = undefined; + let label = undefined; + let key = undefined; + + const attr = button.getAttribute("data-monster-insert-reference"); + if (attr) { + const index = attr.split("-")[1]; + const b = self.getOption("buttons." + index); + + url = b?.["url"]; + label = b?.["label"]; + key = b?.["cmd"]; + } + + const body = self.getOption("api.body"); + if (isObject(body)) { + const bodyString = JSON.stringify(body); + + const obj = { + url: url, + label: label, + value: self.getOption("value"), + key: key, + id: self.getOption("id"), + }; + + fetchOptions.body = new Formatter(obj, {}).format(bodyString); + } + + if (button instanceof HTMLElement) { + button.setState("activity"); + } + + fireCustomEvent(self, "monster-api-button-click", { + button, + }); + + const global = getGlobal(); + global + .fetch(url, fetchOptions) + .then((response) => { + if (!response.ok) { + if (button instanceof HTMLElement) { + button.setState("successful", 4000); + } + return Promise.reject(response); + } + + const contentType = response?.headers?.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return response + .text() + .then((text) => { + try { + const data = JSON.parse(text); // Try to parse the response as JSON + + if (button instanceof HTMLElement) { + button.setState("successful", 4000); + } + + fireCustomEvent(self, "monster-api-button-successful", { + button, + data, + response, + contentType: response.headers.get("Content-Type"), + }); + } catch (error) { + if (button instanceof HTMLElement) { + button.setState("failed", 4000); + button.setMessage(error.message).showMessage(2000); + } + + fireCustomEvent(self, "monster-api-button-failed", { + button, + error, + response, + contentType: response.headers.get("Content-Type"), + }); + } + }) + .catch((error) => { + if (button instanceof HTMLElement) { + button.setState("failed", 4000); + button.setMessage("request failed").showMessage(2000); + } + + fireCustomEvent(self, "monster-api-button-failed", { + button, + error, + response, + contentType: response.headers.get("Content-Type"), + }); + }); + } else { + return response + .blob() + .then((data) => { + fireCustomEvent(self, "monster-api-button-successful", { + button, + data, + response, + contentType: response.headers.get("Content-Type"), + }); + }) + .catch((error) => { + if (button instanceof HTMLElement) { + button.setState("failed", 4000); + button.setMessage("request failed").showMessage(2000); + } + + fireCustomEvent(self, "monster-api-button-failed", { + button, + error, + response, + contentType: response.headers.get("Content-Type"), + }); + }); + } + }) + .catch((error) => { + if (button instanceof HTMLElement) { + button.setState("failed", 4000); + button.setMessage(error.message).showMessage(2000); + } + + fireCustomEvent(self, "monster-api-button-failed", { + button, + error, + }); + }); } /** @@ -439,29 +439,29 @@ function executeAPIButton(event, button, element) { * @throws {TypeError} unsupported response */ function fetchData(url) { - if (!url) url = this.getOption("url"); - if (!url) return Promise.resolve(); - - const fetchOptions = this.getOption("fetch", {}); - - const global = getGlobal(); - return global - .fetch(url, fetchOptions) - .then((response) => { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - return response.text(); - } - - throw new TypeError(`unsupported response ${contentType}`); - }) - .then((text) => { - try { - return Promise.resolve(JSON.parse(text)); - } catch (e) { - throw new TypeError("the result cannot be parsed"); - } - }); + if (!url) url = this.getOption("url"); + if (!url) return Promise.resolve(); + + const fetchOptions = this.getOption("fetch", {}); + + const global = getGlobal(); + return global + .fetch(url, fetchOptions) + .then((response) => { + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return response.text(); + } + + throw new TypeError(`unsupported response ${contentType}`); + }) + .then((text) => { + try { + return Promise.resolve(JSON.parse(text)); + } catch (e) { + throw new TypeError("the result cannot be parsed"); + } + }); } /** @@ -469,22 +469,27 @@ function fetchData(url) { * @return {ApiButton} */ function initEventHandler() { - this[containerElementSymbol].addEventListener("click", (event) => { - const element = findTargetElementFromEvent( - event, - "data-monster-insert-reference", - ); - const attr = element.getAttribute("data-monster-insert-reference"); - if (attr) { - const index = attr.split("-")[1]; - const b = this.getOption("buttons." + index); - if (isObject(b) && isFunction(b?.action)) { - b.action(event, element, this); - } - } - }); - - return this; + this[containerElementSymbol].addEventListener("click", (event) => { + const element = findTargetElementFromEvent( + event, + "data-monster-insert-reference", + ); + + if (!(element instanceof HTMLElement)) { + return; + } + + const attr = element.getAttribute("data-monster-insert-reference"); + if (attr) { + const index = attr.split("-")[1]; + const b = this.getOption("buttons." + index); + if (isObject(b) && isFunction(b?.action)) { + b.action(event, element, this); + } + } + }); + + return this; } /** @@ -492,10 +497,10 @@ function initEventHandler() { * @return {ApiButton} */ function initControlReferences() { - this[containerElementSymbol] = this.shadowRoot.querySelector( - `[${ATTRIBUTE_ROLE}=container]`, - ); - return this; + this[containerElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}=container]`, + ); + return this; } registerCustomElement(ApiButton); -- GitLab