diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7133b2d4145f86547721996590a415900f3671..26e188f8ede95c5c709e3229b455645da840cc28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,5 @@ # Changelog - - ## [3.70.0] - 2024-06-25 ### Add Features @@ -9,15 +7,18 @@ - complete change of form control to a derivation of dataset [#216](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/216) - new dataset feature refreshOnMutation [#215](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/215) - new comprehensive options display [#213](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/213) + ### Bug Fixes - initialize of loaded html fields [#210](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/210) - values from the value attribute are now displayed correctly after loading the options. [#212](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/212) - If a value is specified in the select, it is now also displayed with a label from the options. [#212](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/212) + ### Changes - doc, little bugs and tidy - test adjustments and minor layout adjustments + ### Code Refactoring - adjustments to the form stylesheets [#214](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/214) diff --git a/development/issues/closed/210.mjs b/development/issues/closed/210.mjs index 42c71cbdb8aa3b635d5725c7f23361eda2d0c63a..66f0b33f6a6947901a587a80fd5730c52aa58d07 100644 --- a/development/issues/closed/210.mjs +++ b/development/issues/closed/210.mjs @@ -32,80 +32,10 @@ domReady.then(() => { return; } fieldSet.insertAdjacentHTML('beforeend', html); - - -// form.refresh(); + // form.refresh(); }); }); - - -/** - * Interne Daten löschen, nicht übertragen - * Änderungen werden global geändert - * diese Werte stehen nach dem speichern nicht mehr zur Verfügung - -for (const [key, value] of Object.entries(data.dataset)) { - for (const [k, v] of Object.entries(data.dataset[key])) { - if (k.indexOf(internalDataPrefix) !== -1) { - delete data.dataset[key][k]; - }; - } -} - -//nicht mit den Original daten arbeiten -//da sich so auch die Originaldaten ändern -let currentData = clone(data); - -let d = diff(currentData.dataset,lastData.dataset); - -/** - * Daten für den nächsten Vergleich speichern - -lastData = clone(currentData); - -let changedKeys = []; -for (const [key, value] of Object.entries(d)) { - let index = value.path[0] - if(changedKeys.includes(index)===false){ - changedKeys.push(index); - } -} - -/** - * Daten die sich nicht geändert haben löschen - -let updateData = []; -for (const [key, value] of Object.entries(currentData.dataset)) { - if(changedKeys.includes(key)===true){ - updateData.push(currentData.dataset[key]); - } -}; - - -/** - * Zeit umformatieren für die Daten die noch - * übertragen werden sollen - -for (const [key, value] of Object.entries(updateData)) { - let time = updateData[key]?.scheduler?.time; - if (time !== undefined) { - /** - * Locale Zeit '2024-03-22T11:22:33' - - let d = new Date(Date.parse(time)); - /** - * UTC Zeit '2024-03-22T10:22:33.000Z' - - d = d.toISOString(); - updateData[key].scheduler.time = d; - } - -} - -return updateData; - -*/ \ No newline at end of file diff --git a/development/issues/closed/217.html b/development/issues/closed/217.html new file mode 100644 index 0000000000000000000000000000000000000000..8b88b8bdb22e407aa8b4668533d1ccdac4f429ad --- /dev/null +++ b/development/issues/closed/217.html @@ -0,0 +1,71 @@ + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>only try to transfer the part that has been changed #217</title> + <script src="./217.mjs" type="module"></script> + </head> + <body> + <h1>only try to transfer the part that has been changed #217</h1> + <p></p> + <ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/217">Issue #217</a></li> + <li><a href="/">Back to overview</a></li> + </ul> + <main> + + <monster-datasource-rest id="ds210" + data-monster-option-read-url="/issue-217.json" + data-monster-option-write-url="/issue-217" + data-monster-option-write-acceptedstatus="400::200" + data-monster-option-features-autoinit="true"> + </monster-datasource-rest> + + <monster-dataset data-monster-option-index="0" + data-monster-option-datasource-selector="#ds210" + data-monster-option-mapping-data=""> + ID: + <div data-monster-replace="path:data.id"></div> + <div data-monster-replace="path:data.name"></div> + </monster-dataset> + + + <monster-form data-monster-option-datasource-selector="#ds210" + data-monster-option-mapping-data="" + data-monster-option-features-mutationobserver="true" + > + + <monster-field-set data-monster-option-labels-title="my title"> + + <label for="id">field id</label><input id="id" type="number" + data-monster-attributes="value path:data.id" + data-monster-bind="path:data.id"> + + <label for="field1">field id</label><input id="field1" + data-monster-attributes="value path:data.field1" + data-monster-bind="path:data.field1"> + + <label for="field2">field id</label><input id="field2" + data-monster-attributes="value path:data.field2" + data-monster-bind="path:data.field2"> + + <label for="field3">field id</label><input id="field3" + data-monster-attributes="value path:data.field3" + data-monster-bind="path:data.field3"> + + + </monster-field-set> + <monster-field-set data-monster-option-labels-title=""> + + <monster-datasource-save-button data-monster-option-datasource-selector="#ds210">ok + </monster-datasource-save-button> + + </monster-field-set> + + + </monster-form> + + </main> + </body> + </html> diff --git a/development/issues/closed/217.mjs b/development/issues/closed/217.mjs new file mode 100644 index 0000000000000000000000000000000000000000..c9737abad038f65c70bbe0accad1b6356f40cf12 --- /dev/null +++ b/development/issues/closed/217.mjs @@ -0,0 +1,28 @@ +/** + * @file development/issues/open/217.mjs + * @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/217 + * @description only try to transfer the part that has been changed + * @issue 217 + */ + +import "../../../source/components/style/property.pcss"; +import "../../../source/components/style/color.pcss"; +import "../../../source/components/style/normalize.pcss"; +import "../../../source/components/style/typography.pcss"; +import "../../../source/components/style/form.pcss"; + +import "../../../source/components/datatable/datasource/rest.mjs"; +import "../../../source/components/datatable/save-button.mjs"; +import "../../../source/components/form/form.mjs"; +import "../../../source/components/form/field-set.mjs"; +import "../../../source/components/form/select.mjs"; +import "../../../source/components/form/context-help.mjs"; +import "../../../source/components/form/context-error.mjs"; +import {domReady} from "../../../source/dom/ready.mjs"; + +domReady.then(() => { + + + +}); + diff --git a/development/mock/issue-217.js b/development/mock/issue-217.js new file mode 100644 index 0000000000000000000000000000000000000000..d3af360ed74fe4c3da01186f59eeb8ce37996272 --- /dev/null +++ b/development/mock/issue-217.js @@ -0,0 +1,76 @@ +const json = + `{ + "0": { + "id": 1000, + "field1": "dataset 1, value field 1", + "field2": "dataset 1, value field 2", + "field3": "dataset 1, value field 3" + }, + "1": { + "id": 1001, + "field1": "dataset 2, value field 1", + "field2": "dataset 2, value field 2", + "field3": "dataset 2, value field 3" + } + + }`; + + +// check if json is valid +JSON.parse(json) + +const json400Error=`[ + { + "sys": { + "type": "Error", + "message": "Invalid request" + } + } + ]` + +// check if json is valid +JSON.parse(json400Error) + + +const json200Error=`[ + { + "sys": { + "type": "Error", + "message": "Invalid request" + } + } + ]` + +// check if json is valid +JSON.parse(json200Error) + +export default [ + { + url: '/issue-217.json', + method: 'get', + rawResponse: async (req, res) => { + res.setHeader('Content-Type', 'application/json') + res.statusCode = 200 + + setTimeout(function() { + res.end(json) + }, 10); + }, + }, + + { + url: '/issue-217', + method: 'post', + rawResponse: async (req, res) => { + res.setHeader('Content-Type', 'application/json') + res.statusCode = 400 + + setTimeout(function() { + res.end(json400Error) + }, 10); + }, + } + + + +]; \ No newline at end of file diff --git a/source/components/datatable/datasource/rest.mjs b/source/components/datatable/datasource/rest.mjs index 7d8fad6bca58b37ca5a9c4c30df2aebd35256011..e24ee30c6e7492d6cf173474470b9aa7ec5f4312 100644 --- a/source/components/datatable/datasource/rest.mjs +++ b/source/components/datatable/datasource/rest.mjs @@ -12,25 +12,27 @@ * 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 {diff} from "../../../data/diff.mjs"; +import {addAttributeToken} from "../../../dom/attributes.mjs"; +import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs"; +import {isArray} from "../../../types/is.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 @@ -44,7 +46,7 @@ const intersectionObserverHandlerSymbol = Symbol("intersectionObserverHandler"); * @type {symbol} */ const rawDataSymbol = Symbol.for( - "@schukai/monster/data/datasource/server/restapi/rawdata", + "@schukai/monster/data/datasource/server/restapi/rawdata", ); /** @@ -52,7 +54,7 @@ const rawDataSymbol = Symbol.for( * @type {symbol} */ const intersectionObserverObserverSymbol = Symbol( - "intersectionObserverObserver", + "intersectionObserverObserver", ); /** @@ -81,23 +83,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} * @@ -128,405 +130,409 @@ 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: 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 {void} - */ - [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); - }); - } + 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 {void} + */ + [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); + } + + /** + * @returns {Promise<never>|*} + */ + 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 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"); - if ( query===undefined) { - return; - } - 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) { - const 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]); + 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"); + if (query === undefined) { + return; + } + 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) { + const 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")); - } + 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; + const autoInit = this.getOption("features.autoInit"); + validateBoolean(autoInit); + + 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); } /** @@ -534,8 +540,8 @@ function initIntersectionObserver() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <slot></slot>`; } diff --git a/source/components/datatable/save-button.mjs b/source/components/datatable/save-button.mjs index 9908bc2104412c7c082a61dad1b9c9d292ad1ee9..e5490d27848a91f6632f3397bf6404086138204b 100644 --- a/source/components/datatable/save-button.mjs +++ b/source/components/datatable/save-button.mjs @@ -77,9 +77,6 @@ class SaveButton extends CustomElement { * @property {Object} classes The classes * @property {string} classes.bar The bar class * @property {string} classes.badge The badge class - * @property {object} mapping The mapping - * @property {string} mapping.data The data - * @property {number} mapping.index The index * @property {Array} ignoreChanges The ignore changes (regex) * @property {Array} data The data * @return {Object} @@ -105,11 +102,6 @@ class SaveButton extends CustomElement { changes: "0", - mapping: { - data: "dataset", - index: 0, - }, - ignoreChanges: [], data: {}, diff --git a/source/components/form/form.mjs b/source/components/form/form.mjs index c75adad72ef47e3a3d84fbdd6967502d86d6e18d..7d7f02ead3a7e4a5e91f4539fc8d3bdb1debb913 100644 --- a/source/components/form/form.mjs +++ b/source/components/form/form.mjs @@ -164,13 +164,6 @@ class Form extends DataSet { } function initDataSourceHandler() { - if (!this[datasourceLinkedElementSymbol]) { - return; - } -console.log(this[datasourceLinkedElementSymbol]); - this[datasourceLinkedElementSymbol].setOption("write.responseCallback", (response) => { - console.log("response!!!", response); - }) } diff --git a/source/data/datasource/server.mjs b/source/data/datasource/server.mjs index e062ed2bc854a1145a743a17efa3f5456812d9bf..d7dc4ca5e4be2df3f1e6dd9c218091d3348fa744 100644 --- a/source/data/datasource/server.mjs +++ b/source/data/datasource/server.mjs @@ -12,13 +12,21 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { internalSymbol, instanceSymbol } from "../../constants.mjs"; -import { isObject } from "../../types/is.mjs"; -import { Datasource } from "../datasource.mjs"; -import { Pathfinder } from "../pathfinder.mjs"; -import { Pipe } from "../pipe.mjs"; +import {instanceSymbol} from "../../constants.mjs"; +import {isArray, isFunction, isObject} from "../../types/is.mjs"; +import {Datasource} from "../datasource.mjs"; +import {diff} from "../diff.mjs"; +import {Pathfinder} from "../pathfinder.mjs"; +import {Pipe} from "../pipe.mjs"; -export { Server }; +export {Server}; + + +/** + * @private + * @type {symbol} + */ +const serverVersionSymbol = Symbol("serverVersion"); /** * Base class for all server data sources @@ -30,54 +38,82 @@ export { Server }; * @summary The Server class encapsulates the access to a server datasource */ class Server extends Datasource { - /** - * This method is called by the `instanceof` operator. - * @returns {symbol} - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/data/datasource/server"); - } - - /** - * This prepares the data that comes from the server. - * Should not be called directly. - * - * @private - * @param {Object} payload - * @returns {Object} - */ - transformServerPayload(payload) { - payload = doTransform.call(this, "read", payload); - - const dataPath = this.getOption("read.path"); - if (dataPath) { - payload = new Pathfinder(payload).getVia(dataPath); - } - - return payload; - } - - /** - * This prepares the data for writing and should not be called directly. - * - * @private - * @param {Object} payload - * @returns {Object} - */ - prepareServerPayload(payload) { - payload = doTransform.call(this, "write", payload); - - const sheathingObject = this.getOption("write.sheathing.object"); - const sheathingPath = this.getOption("write.sheathing.path"); - - if (sheathingObject && sheathingPath) { - const sub = payload; - payload = sheathingObject; - new Pathfinder(payload).setVia(sheathingPath, sub); - } - - return payload; - } + /** + * This method is called by the `instanceof` operator. + * @returns {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/data/datasource/server"); + } + + /** + * This prepares the data that comes from the server. + * Should not be called directly. + * + * @private + * @param {Object} payload + * @returns {Object} + */ + transformServerPayload(payload) { + payload = doTransform.call(this, "read", payload); + this[serverVersionSymbol] = payload; + + const dataPath = this.getOption("read.path"); + if (dataPath) { + payload = new Pathfinder(payload).getVia(dataPath); + } + + return payload; + } + + /** + * This prepares the data for writing and should not be called directly. + * + * @private + * @param {Object} payload + * @returns {Object} + */ + prepareServerPayload(payload) { + payload = doTransform.call(this, "write", payload); + payload = doDiff.call(this, payload); + + const sheathingObject = this.getOption("write.sheathing.object"); + const sheathingPath = this.getOption("write.sheathing.path"); + + if (sheathingObject && sheathingPath) { + const sub = payload; + payload = sheathingObject; + new Pathfinder(payload).setVia(sheathingPath, sub); + } + + return payload; + } +} + +/** + * + * @param obj + * @returns {*} + */ +function doDiff(obj) { + if (this[serverVersionSymbol] === null || this[serverVersionSymbol] === undefined) { + return obj; + } + + const callback = this.getOption("write.partial.callback"); + if (!isFunction(callback)) { + return obj; + } + + const results = diff(this[serverVersionSymbol], obj); + if (!results) { + return obj; + } + + obj = callback(obj, results); + this[serverVersionSymbol] = obj; + + return obj; } /** @@ -87,24 +123,32 @@ class Server extends Datasource { * @returns {Object} */ function doTransform(type, obj) { - const transformation = this.getOption(`${type}.mapping.transformer`); - if (transformation !== undefined && transformation !== null) { - const pipe = new Pipe(transformation); - const callbacks = this.getOption(`${type}.mapping.callbacks`); - - if (isObject(callbacks)) { - for (const key in callbacks) { - if ( - callbacks.hasOwnProperty(key) && - typeof callbacks[key] === "function" - ) { - pipe.setCallback(key, callbacks[key]); - } - } - } - - obj = pipe.run(obj); - } - - return obj; + const transformation = this.getOption(`${type}.mapping.transformer`); + if (transformation !== undefined && transformation !== null) { + const pipe = new Pipe(transformation); + const callbacks = this.getOption(`${type}.mapping.callbacks`); + + if (isArray(callbacks)) { + for (const callback of callbacks) { + if (typeof callback === "function") { + pipe.setCallback(callback); + } + } + } + + if (isObject(callbacks)) { + for (const key in callbacks) { + if ( + callbacks.hasOwnProperty(key) && + typeof callbacks[key] === "function" + ) { + pipe.setCallback(key, callbacks[key]); + } + } + } + + obj = pipe.run(obj); + } + + return obj; } diff --git a/source/data/datasource/server/restapi.mjs b/source/data/datasource/server/restapi.mjs index 04cab9ef51f0228d0ae391e49959472f8cec0381..d6f92235fec85b05e5b85b65657ea06b61c1a3f9 100644 --- a/source/data/datasource/server/restapi.mjs +++ b/source/data/datasource/server/restapi.mjs @@ -13,7 +13,8 @@ */ import {internalSymbol, instanceSymbol} from "../../../constants.mjs"; -import {isObject, isFunction} from "../../../types/is.mjs"; +import {isObject, isFunction, isArray} from "../../../types/is.mjs"; +import {diff} from "../../diff.mjs"; import {Server} from "../server.mjs"; import {WriteError} from "./restapi/writeerror.mjs"; import {DataFetchError} from "./restapi/data-fetch-error.mjs"; @@ -30,6 +31,7 @@ const rawDataSymbol = Symbol.for( "@schukai/monster/data/datasource/server/restapi/rawdata", ); + /** * The RestAPI is a class that enables a REST API server. * @@ -75,6 +77,8 @@ class RestAPI extends Server { * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing. * @property {Object} write.report * @property {String} write.report.path Path to validations + * @property {Object} write.partial + * @property {Function} write.partial.callback Callback function to be executed after the request has been completed. (obj, diffResult) => obj * @property {Object} write.sheathing * @property {Object} write.sheathing.object Object to be wrapped * @property {string} write.sheathing.path Path to the data @@ -107,6 +111,10 @@ class RestAPI extends Server { report: { path: undefined, }, + + partial: { + callback: null, + } }, read: { init: { @@ -135,10 +143,11 @@ class RestAPI extends Server { if (!init["method"]) init["method"] = "GET"; let callback = this.getOption("read.responseCallback"); - if (!callback) + if (!callback) { callback = (obj) => { this.set(this.transformServerPayload.call(this, obj)); }; + } return fetchData.call(this, init, "read", callback); } @@ -189,7 +198,7 @@ function fetchData(init, key, callback) { .then((resp) => { response = resp; - const acceptedStatus = this.getOption(`${key}.acceptedStatus`, [200]).map(Number); + const acceptedStatus = this.getOption(`${key}.acceptedStatus`, [200]).map(Number); if (acceptedStatus.indexOf(resp.status) === -1) { throw new DataFetchError( diff --git a/source/data/diff.mjs b/source/data/diff.mjs index 96bbd4398098c2472a5c4675977c0f655e116e33..99abde3badb44b4a52efd8bc1fcda700d6d2fa73 100644 --- a/source/data/diff.mjs +++ b/source/data/diff.mjs @@ -183,4 +183,4 @@ function getOperator(a, b) { } return operator; -} +} \ No newline at end of file diff --git a/test/cases/data/diff.mjs b/test/cases/data/diff.mjs index 5013de024de448b158950c6332c1d50dd25cc702..222e093ef41770f980ed2961da84ffbcf49f1368 100644 --- a/test/cases/data/diff.mjs +++ b/test/cases/data/diff.mjs @@ -6,6 +6,43 @@ import {Queue} from "../../../source/types/queue.mjs"; describe('Diff', function () { + describe('test to datasets', function () { + + var obj1, obj2; + + beforeEach(() => { + obj1 = [ + { + "id": 1, + "name": "test" + }, + { + "id": 2, + "name": "test2" + } + ] + + obj2 = [ + { + "id": 1, + "name": "test" + }, + { + "id": "3", + "name": "test2" + } + ] + + }); + + it('should return the difference between two datasets', function () { + let d = diff(obj1, obj2); + expect(JSON.stringify(d)).is.equal('[{"operator":"update","path":["1","id"],"first":{"value":2,"type":"number"},"second":{"value":"3","type":"string"}}]'); + }); + + + }) + describe('Diff special cases', function () { var obj1, obj2;