diff --git a/AppBuilder/core b/AppBuilder/core index 9c645df2..c66267ca 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 9c645df2f06fb745e3ea0e43e7fd3764da9cd82b +Subproject commit c66267ca1afa394d1751a7f77a4eb9b0781f8464 diff --git a/AppBuilder/platform/plugins/included/index.js b/AppBuilder/platform/plugins/included/index.js index 2f45b217..c2a62276 100644 --- a/AppBuilder/platform/plugins/included/index.js +++ b/AppBuilder/platform/plugins/included/index.js @@ -1,5 +1,6 @@ import viewList from "./view_list/FNAbviewlist.js"; import viewTab from "./view_tab/FNAbviewtab.js"; +import viewDetail from "./view_detail/FNAbviewdetail.js"; import viewText from "./view_text/FNAbviewtext.js"; import viewImage from "./view_image/FNAbviewimage.js"; import viewDataSelect from "./view_data-select/FNAbviewdataselect.js"; @@ -8,6 +9,7 @@ import viewPdfImporter from "./view_pdfImporter/FNAbviewpdfimporter.js"; const AllPlugins = [ viewTab, viewList, + viewDetail, viewText, viewImage, viewDataSelect, diff --git a/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetail.js b/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetail.js new file mode 100644 index 00000000..a88a8f53 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetail.js @@ -0,0 +1,140 @@ +import FNAbviewdetailComponent from "./FNAbviewdetailComponent.js"; + +// Detail view plugin: replaces the original ABViewDetail / ABViewDetailCore. +// All logic from both Core and platform is contained in this file. +export default function FNAbviewdetail({ + ABViewContainer, + ABViewContainerComponent, + ABViewComponentPlugin, +}) { + const ABViewDetailComponent = FNAbviewdetailComponent({ + ABViewContainerComponent, + ABViewComponentPlugin, + }); + + const ABViewDetailDefaults = { + key: "detail", + icon: "file-text-o", + labelKey: "Detail(plugin)", + }; + + const ABViewDetailPropertyComponentDefaults = { + dataviewID: null, + showLabel: true, + labelPosition: "left", + labelWidth: 120, + height: 0, + }; + + return class ABViewDetailPlugin extends ABViewContainer { + /** + * @param {obj} values key=>value hash of ABView values + * @param {ABApplication} application the application object this view is under + * @param {ABView} parent the ABView this view is a child of. (can be null) + */ + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues ?? ABViewDetailDefaults + ); + } + + static getPluginType() { + return "view"; + } + + static getPluginKey() { + return this.common().key; + } + + static common() { + return ABViewDetailDefaults; + } + + static defaultValues() { + return ABViewDetailPropertyComponentDefaults; + } + + /** + * @method fromValues() + * Initialize this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + this.settings.labelPosition = + this.settings.labelPosition || + ABViewDetailPropertyComponentDefaults.labelPosition; + + this.settings.showLabel = JSON.parse( + this.settings.showLabel != null + ? this.settings.showLabel + : ABViewDetailPropertyComponentDefaults.showLabel + ); + + this.settings.labelWidth = parseInt( + this.settings.labelWidth || + ABViewDetailPropertyComponentDefaults.labelWidth + ); + this.settings.height = parseInt( + this.settings.height ?? + ABViewDetailPropertyComponentDefaults.height + ); + } + + /** + * @method componentList + * Return the list of components available on this view to display in the editor. + */ + componentList() { + const viewsToAllow = ["label", "text"]; + const allComponents = this.application.viewAll(); + return allComponents.filter((c) => + viewsToAllow.includes(c.common().key) + ); + } + + addFieldToDetail(field, yPosition) { + if (field == null) return; + + const newView = field + .detailComponent() + .newInstance(this.application, this); + if (newView == null) return; + + newView.settings = newView.settings ?? {}; + newView.settings.fieldId = field.id; + newView.settings.labelWidth = + this.settings.labelWidth || + ABViewDetailPropertyComponentDefaults.labelWidth; + newView.settings.alias = field.alias; + newView.position.y = yPosition; + + this._views.push(newView); + return newView; + } + + /** + * @method component() + * Return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABViewDetailComponent(this); + } + + warningsEval() { + super.warningsEval(); + + const DC = this.datacollection; + if (!DC) { + this.warningsMessage( + `can't resolve it's datacollection[${this.settings.dataviewID}]` + ); + } + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetailComponent.js b/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetailComponent.js new file mode 100644 index 00000000..3ed2166a --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetailComponent.js @@ -0,0 +1,179 @@ +export default function FNAbviewdetailComponent({ + ABViewContainerComponent, + ABViewComponentPlugin, +}) { + const ContainerComponent = + ABViewContainerComponent?.default ?? ABViewContainerComponent; + const Base = ContainerComponent ?? ABViewComponentPlugin; + if (!Base) { + return class ABAbviewdetailComponent {}; + } + + return class ABAbviewdetailComponent extends Base { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewDetail_${baseView.id}`, + Object.assign({ detail: "" }, ids) + ); + this.idBase = idBase || `ABViewDetail_${baseView.id}`; + } + + ui() { + if (!ContainerComponent) { + return this._uiDataviewFallback(); + } + const _ui = super.ui(); + return { + type: "form", + id: this.ids.component, + borderless: true, + rows: [{ body: _ui }], + }; + } + + _uiDataviewFallback() { + const settings = this.settings; + const _uiDetail = { + id: this.ids.detail, + view: "dataview", + type: { width: 1000, height: 30 }, + template: (item) => (item ? JSON.stringify(item) : ""), + }; + if (settings.height !== 0) _uiDetail.height = settings.height; + else _uiDetail.autoHeight = true; + const _ui = super.ui([_uiDetail]); + delete _ui.type; + return _ui; + } + + onShow() { + const baseView = this.view; + try { + const dataCy = `Detail ${baseView.name?.split(".")[0]} ${baseView.id}`; + $$(this.ids.component)?.$view?.setAttribute("data-cy", dataCy); + } catch (e) { + console.warn("Problem setting data-cy", e); + } + + const dv = this.datacollection; + if (dv) { + const currData = dv.getCursor(); + if (currData) this.displayData(currData); + + ["changeCursor", "cursorStale", "collectionEmpty"].forEach((key) => { + this.eventAdd({ + emitter: dv, + eventName: key, + listener: (...p) => this.displayData(...p), + }); + }); + this.eventAdd({ + emitter: dv, + eventName: "create", + listener: (createdRow) => { + if (dv.getCursor()?.id === createdRow.id) + this.displayData(createdRow); + }, + }); + this.eventAdd({ + emitter: dv, + eventName: "update", + listener: (updatedRow) => { + if (dv.getCursor()?.id === updatedRow.id) + this.displayData(updatedRow); + }, + }); + } + + super.onShow?.(); + } + + displayData(rowData = {}) { + if (!ContainerComponent) return; + if (rowData == null && this.datacollection) + rowData = this.datacollection.getCursor() ?? {}; + + const views = (this.view.views() || []).sort((a, b) => { + if (!a?.field?.() || !b?.field?.()) return 0; + if (a.field().key === "formula" && b.field().key === "calculate") + return -1; + if (a.field().key === "calculate" && b.field().key === "formula") + return 1; + return 0; + }); + + views.forEach((f) => { + let val; + if (f.field) { + const field = f.field(); + if (!field) return; + + switch (field.key) { + case "connectObject": + val = field.pullRelationValues(rowData); + break; + case "list": + val = rowData?.[field.columnName]; + if (!val || (Array.isArray(val) && val.length === 0)) { + val = ""; + break; + } + if (field.settings.isMultiple === 0) { + let myVal = ""; + (field.settings.options || []).forEach((opt) => { + if (opt.id === val) myVal = opt.text; + }); + if (field.settings.hasColors) { + let hasCustomColor = ""; + (field.settings.options || []).forEach((h) => { + if (h.text === myVal) { + hasCustomColor = "hascustomcolor"; + } + }); + const hex = (field.settings.options || []).find( + (o) => o.text === myVal + )?.hex ?? "#66666"; + myVal = `${myVal}`; + } + val = myVal; + } else { + const items = val.map((value) => { + let myVal = ""; + (field.settings.options || []).forEach((opt) => { + if (opt.id === value.id) myVal = opt.text; + }); + const optionHex = + field.settings.hasColors && value.hex + ? `background: ${value.hex};` + : ""; + const hasCustomColor = + field.settings.hasColors && value.hex + ? "hascustomcolor" + : ""; + return `${myVal}`; + }); + val = items.join(""); + } + break; + case "user": + val = field.pullRelationValues(rowData); + break; + case "file": + val = rowData?.[field.columnName] ?? ""; + break; + case "formula": + val = rowData ? field.format(rowData, false) : ""; + break; + default: + val = field.format(rowData); + } + } + + const vComponent = f.component(this.idBase); + vComponent?.setValue?.(val); + vComponent?.displayText?.(rowData); + }); + } + }; +} diff --git a/test/AppBuilder/platform/views/ABViewDetail.test.js b/test/AppBuilder/platform/views/ABViewDetail.test.js deleted file mode 100644 index a3aed430..00000000 --- a/test/AppBuilder/platform/views/ABViewDetail.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import assert from "assert"; -import ABFactory from "../../../../AppBuilder/ABFactory"; -import ABViewDetail from "../../../../AppBuilder/platform/views/ABViewDetail"; -import ABViewDetailComponent from "../../../../AppBuilder/platform/views/viewComponent/ABViewDetailComponent"; - -function getTarget() { - const AB = new ABFactory(); - const application = AB.applicationNew({}); - return new ABViewDetail({}, application); -} - -describe("ABViewDetail widget", function () { - it(".component - should return a instance of ABViewDetailComponent", function () { - const target = getTarget(); - - const result = target.component(); - - assert.equal(true, result instanceof ABViewDetailComponent); - }); -});