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);
- });
-});