From 4835218d1a5b300990542a187ade18dd3afe142c Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Mon, 22 Jan 2024 15:10:56 +0100 Subject: [PATCH] fix: double fetch #134 --- playground/datatable/index.html | 5 +- playground/vite.config.js | 106 +-- .../components/datatable/datasource/rest.mjs | 672 ++++++++++-------- source/components/datatable/filter.mjs | 10 +- source/data/datasource/server/restapi.mjs | 1 + source/dom/customelement.mjs | 2 +- 6 files changed, 402 insertions(+), 394 deletions(-) diff --git a/playground/datatable/index.html b/playground/datatable/index.html index 6aaefbf65..e0d092010 100644 --- a/playground/datatable/index.html +++ b/playground/datatable/index.html @@ -22,11 +22,10 @@ <monster-config-manager></monster-config-manager> <h1>Datatable with Pagination </h1> - <!-- "url": "https://localhost.schukai.net:8443/api/commerce/orders/search?q=${path:query | default:order.customerUID>0}&page=${page}&orderby=${path:order | default:oid}", --> <script id="id-for-this-config" type="application/json"> { "read": { - "url": "./data.json", + "url": "http://localhost:8070/api/commerce/orders/search?q=${path:query | default:order.customerUID>0}&page=${page}&orderby=${path:order | default:oid}", "init": { "method": "GET", "headers": { @@ -181,4 +180,4 @@ </main> </body> -</html> \ No newline at end of file +</html> diff --git a/playground/vite.config.js b/playground/vite.config.js index 9d04bb106..12537104a 100644 --- a/playground/vite.config.js +++ b/playground/vite.config.js @@ -1,5 +1,5 @@ // vite.config.js -import {basename, resolve,join} from 'path' +import {basename, resolve, join} from 'path' import {defineConfig} from 'vite' import {existsSync} from 'fs' import banner from 'vite-plugin-banner' @@ -29,9 +29,9 @@ import {ViteMinifyPlugin} from 'vite-plugin-minify' // const dist = resolve(__dirname, '') -function getAppRootDir () { +function getAppRootDir() { let currentDir = __dirname - while(!existsSync(join(currentDir, 'package.json'))) { + while (!existsSync(join(currentDir, 'package.json'))) { currentDir = join(currentDir, '..') } return currentDir @@ -40,49 +40,6 @@ function getAppRootDir () { const rootDir = getAppRootDir() const playgroundDir = join(rootDir, 'playground') -// -// globSync(source + '/**/*.html', { -// ignore: [ -// '**/resource/**' -// ] -// }).forEach((file) => { -// -// let lang = file.split('/').slice(-2)[0] -// let key = lang + "/" + basename(file).replace('.html', '') -// -// files[key] = file; -// -// -// }) -// -// console.log(files) - - -// const rootDirectory = resolve(__dirname, '/'); - - - -// function createStyleSheetPlugin() { -// return { -// name: 'buildStart', -// buildStart() { -// // console.log('buildStart') -// // -// // exec('pnpm run -C ' + scriptDir + " build-style", (err, stdout, stderr) => { -// // if (err) { -// // //some err occurred -// // console.error(err) -// // } -// // }); -// -// }, -// async transform(raw, id, options) { -// //console.log(id, options,raw) -// } -// -// -// } -// } console.log(rootDir) export default defineConfig({ @@ -92,23 +49,6 @@ export default defineConfig({ root: playgroundDir, mode: 'development', logLevel: 'info', - // base: "/", - // build: { - // outDir: dist, - // emptyOutDir: true, - // assetsDir: "asset", - // rollupOptions: { - // input: files, - // - // output: { - // globals: { - // }, - // }, - // - // }, - // comments: false, - // minify: 'esbuild', - // }, esbuild: { minifyIdentifiers: true, minifySyntax: true, @@ -116,23 +56,16 @@ export default defineConfig({ target: 'es2015', legalComments: 'none' }, - server: { - port: 8070, - host: "localhost", - https: false, - debug: true - }, plugins: [ banner( `/**\n * name: ${pkg.name}\n * version: v${pkg.version}\n * description: ${pkg.description}\n * author: ${pkg.author}\n * homepage: ${pkg.homepage}\n */` ), - // mkcert({}), + ViteMinifyPlugin(), directoryPlugin({ baseDir: __dirname }), - // createStyleSheetPlugin() - + ], css: { postcss: { @@ -153,7 +86,34 @@ export default defineConfig({ postcssResponsiveType ] } - } + }, + + server: { + + port: 8070, + host: "localhost", + https: false, + debug: true, + ssl: { + key: resolve(__dirname, 'ssl/localhost.key'), + cert: resolve(__dirname, 'ssl/localhost.crt'), + }, + + proxy: { + '^/api/commerce/orders/search': { + target: 'http://localhost:8090', + changeOrigin: false, + + configure: (proxy, options) => { + + proxy.secure = false + proxy.agent = null + + }, + }, + + }, + }, }) diff --git a/source/components/datatable/datasource/rest.mjs b/source/components/datatable/datasource/rest.mjs index 9a5b8b40c..a0470a2e9 100644 --- a/source/components/datatable/datasource/rest.mjs +++ b/source/components/datatable/datasource/rest.mjs @@ -3,25 +3,25 @@ * 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 {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, + 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"; +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 }; +export {Rest}; /** * @private @@ -29,12 +29,21 @@ export { Rest }; */ 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", + "intersectionObserverObserver", ); /** @@ -63,23 +72,23 @@ const filterObserverSymbol = Symbol("filterObserver"); * @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"); - } - - /** + /** + * 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} * @@ -98,7 +107,8 @@ class Rest extends Datasource { * @property {Object} datatable Datatable definitions * @property {string} datatable.id The id of the datatable control * @property {Object} response Response definitions - * @property {string} response.errorMessagePath The path to the error message in the response + * @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 @@ -109,312 +119,346 @@ class Rest extends Datasource { * @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: true, - filter: false, - }, - - autoInit: { - intersectionObserver: false, - oneTime: true, - }, - - filter: { - id: undefined, - }, - - datatable: { - id: undefined, - }, - - response: { - errorMessagePath: "sys.message", - }, - }); - } - - /** - * - * @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); - } + 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); + } } /** * @private */ function removeFilter() { - const filterID = this.getOption("filter.id", undefined); - if (!filterID) return; + const filterID = this.getOption("filter.id", undefined); + if (!filterID) return; - const filterControl = findElementWithIdUpwards(this, filterID); + const filterControl = findElementWithIdUpwards(this, filterID); - if (filterControl && this[filterObserverSymbol]) { - filterControl?.detachObserver(this[filterObserverSymbol]); - } + 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(); - } - - response - .json() - .then((json) => { - const path = new Pathfinder(json); - const error = path.getVia( - this.getOption("response.errorMessagePath"), - ); - if (error) { - filterControl?.showFailureMessage(error); - return; - } - - filterControl?.showFailureMessage(e.message); - }) - .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]); + 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); + const autoInit = this.getOption("features.autoInit"); + validateBoolean(autoInit); - if (autoInit !== true) return; + if (autoInit !== true) return; - if (this.getOption("autoInit.intersectionObserver") === true) { - initIntersectionObserver.call(this); - return; - } + if (this.getOption("autoInit.intersectionObserver") === true) { + initIntersectionObserver.call(this); + return; + } - setTimeout(() => { - this.fetch().catch(() => {}); - }, 0); + 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); - } - } - }); - }; + 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); + 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); } /** @@ -422,8 +466,8 @@ function initIntersectionObserver() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <slot></slot>`; } diff --git a/source/components/datatable/filter.mjs b/source/components/datatable/filter.mjs index 60400c11a..81d45b026 100644 --- a/source/components/datatable/filter.mjs +++ b/source/components/datatable/filter.mjs @@ -254,7 +254,7 @@ class Filter extends CustomElement { }, }, - query: "", + query: undefined, defaultQuery: "", }); } @@ -441,7 +441,6 @@ function initFilter() { */ function escapeAttributeValue(input) { if (input === undefined || input === null) { - debugger; return input; } @@ -686,11 +685,16 @@ function initEventHandler() { } function initTabEvents() { + this[filterTabElementSymbol].addEventListener( "monster-tab-changed", (event) => { + const query = event?.detail?.data?.["data-monster-query"]; - this.setOption("query", query); + const q = this.getOption("query"); + if (query !== q) { + this.setOption("query", query); + } }, ); diff --git a/source/data/datasource/server/restapi.mjs b/source/data/datasource/server/restapi.mjs index bb80fbc88..dbce0b746 100644 --- a/source/data/datasource/server/restapi.mjs +++ b/source/data/datasource/server/restapi.mjs @@ -123,6 +123,7 @@ class RestAPI extends Server { * @throws {Error} the data cannot be read */ read() { + let init = this.getOption("read.init"); if (!isObject(init)) init = {}; if (!init["method"]) init["method"] = "GET"; diff --git a/source/dom/customelement.mjs b/source/dom/customelement.mjs index 634eb6c49..6470a42d5 100644 --- a/source/dom/customelement.mjs +++ b/source/dom/customelement.mjs @@ -465,7 +465,7 @@ class CustomElement extends HTMLElement { * @return {*} * @since 1.10.0 */ - getOption(path, defaultValue) { + getOption(path, defaultValue=undefined) { let value; try { -- GitLab