diff --git a/src/rootPages/Designer/properties/PropertyManager.js b/src/rootPages/Designer/properties/PropertyManager.js
index 1221e71d..2ecaf3ed 100644
--- a/src/rootPages/Designer/properties/PropertyManager.js
+++ b/src/rootPages/Designer/properties/PropertyManager.js
@@ -148,6 +148,7 @@ export default function (AB) {
require("./mobile/ABMobileViewFormTextbox"),
require("./mobile/ABMobileViewLabel"),
require("./mobile/ABMobileViewList"),
+ require("./mobile/ABMobileViewTimeline"),
].forEach((V) => {
MobileViews.push(V.default(AB));
});
diff --git a/src/rootPages/Designer/properties/mobile/ABMobileViewForm.js b/src/rootPages/Designer/properties/mobile/ABMobileViewForm.js
index af617cd4..5ab6cda0 100644
--- a/src/rootPages/Designer/properties/mobile/ABMobileViewForm.js
+++ b/src/rootPages/Designer/properties/mobile/ABMobileViewForm.js
@@ -685,15 +685,15 @@ export default function (AB) {
yPosition
);
// @TODO: filter out unknown mobile-view
+ if (!newFieldView) return;
if (newFieldView.defaults.key == "mobile-view") return;
- if (newFieldView) {
- newFieldView.once("destroyed", () =>
- this.populate(currView)
- );
-
- // // Call save API
- saveTasks.push(newFieldView.save());
- }
+
+ newFieldView.once("destroyed", () =>
+ this.populate(currView)
+ );
+
+ // // Call save API
+ saveTasks.push(newFieldView.save());
// update item to UI list
f.selected = 1;
diff --git a/src/rootPages/Designer/properties/mobile/ABMobileViewTimeline.js b/src/rootPages/Designer/properties/mobile/ABMobileViewTimeline.js
new file mode 100644
index 00000000..b462c89c
--- /dev/null
+++ b/src/rootPages/Designer/properties/mobile/ABMobileViewTimeline.js
@@ -0,0 +1,386 @@
+/*
+ * ABMobileViewTimeline
+ * A Property manager for our ABMobileViewTimeline definitions
+ */
+
+import FABMobileView from "./ABMobileView";
+import FLabelTemplate from "../../ui_common_label_template";
+
+export default function (AB) {
+ const BASE_ID = "properties_abmobileview_timeline";
+
+ const ABMobileView = FABMobileView(AB);
+ const LabelTemplate = FLabelTemplate(AB);
+
+ const L = ABMobileView.L();
+ const uiConfig = AB.UISettings.config();
+
+ class ABViewListProperty extends ABMobileView {
+ constructor() {
+ super(BASE_ID, {
+ datacollection: "",
+ dateField: "",
+ // height: "",
+ hideTitle: "",
+ linkPageAdd: "",
+ linkPageDetail: "",
+ });
+ }
+
+ static get key() {
+ return "mobile-timeline";
+ }
+
+ ui() {
+ // const defaultValues = this.defaultValues();
+ const ids = this.ids;
+
+ return super.ui([
+ {
+ id: ids.datacollection,
+ name: "dataviewID",
+ view: "richselect",
+ label: L("Data Source"),
+ labelWidth: uiConfig.labelWidthLarge,
+ on: {
+ onChange: (dcId, oldDcId) => {
+ if (dcId == oldDcId) return;
+
+ // Update field options in property
+ this.onChangeDC(dcId);
+ this.onChange();
+ },
+ },
+ },
+ {
+ id: ids.dateField,
+ name: "dateField",
+ view: "richselect",
+ label: L("Date Field"),
+ labelWidth: uiConfig.labelWidthLarge,
+ on: {
+ onChange: () => {
+ this.onChange();
+ },
+ },
+ },
+ // {
+ // id: ids.height,
+ // view: "counter",
+ // name: "height",
+ // label: L("Height:"),
+ // labelWidth: uiConfig.labelWidthLarge,
+ // on: {
+ // onChange: () => {
+ // this.onChange();
+ // },
+ // },
+ // },
+ {
+ id: ids.hideTitle,
+ view: "checkbox",
+ name: "hideTitle",
+ label: L("Hide Title:"),
+ labelWidth: uiConfig.labelWidthLarge,
+ on: {
+ onChange: () => {
+ this.onChange();
+ },
+ },
+ },
+ {
+ id: ids.itemTemplate,
+ view: "button",
+ type: "icon",
+ icon: "fa-regular fa-pen-to-square",
+ label: L("Item Template"),
+ click: function (/* id, event*/) {
+ LabelTemplate.show(this.$view);
+ },
+ },
+ {
+ view: "fieldset",
+ label: L("Linked Pages:"),
+ labelWidth: uiConfig.labelWidthLarge,
+ body: {
+ type: "clean",
+ padding: 10,
+ rows: [
+ {
+ id: ids.linkPageAdd,
+ view: "combo",
+ clear: true,
+ placeholder: L("No linked view"),
+ name: "linkPageAdd",
+ label: L("Add Page:"),
+ labelWidth: uiConfig.labelWidthLarge,
+ options: [],
+ on: {
+ onChange: () => this.onChange(),
+ },
+ },
+ {
+ id: ids.linkPageDetail,
+ view: "combo",
+ clear: true,
+ placeholder: L("No linked view"),
+ name: "linkPageDetail",
+ label: L("Edit/Detail Page:"),
+ labelWidth: uiConfig.labelWidthLarge,
+ options: [],
+ on: {
+ onChange: () => this.onChange(),
+ },
+ },
+ /*
+ // See if we need a separate Edit option:
+ {
+ id: ids.linkPageEdit,
+ view: "combo",
+ clear: true,
+ placeholder: L("No linked form"),
+ name: "linkPageEdit",
+ label: L("Edit Form:"),
+ labelWidth: uiConfig.labelWidthLarge,
+ options: [],
+ on: {
+ onChange: () => this.onChange(),
+ },
+ },
+ */
+ ],
+ },
+ },
+ ]);
+ }
+
+ async init(AB) {
+ this.AB = AB;
+
+ LabelTemplate.init(AB);
+ LabelTemplate.on("save", (labelTemplate) => {
+ this.onChange();
+ });
+
+ await super.init(AB);
+ }
+
+ onChangeDC(dcId) {
+ var datacollection = this.AB.datacollectionByID(dcId);
+ var object = datacollection ? datacollection.datasource : null;
+ if (object) {
+ LabelTemplate.objectLoad(object);
+ }
+
+ this.propertyUpdateFieldOptions(dcId);
+ }
+
+ /**
+ * @method propertyUpdateFieldOptions
+ * Populate fields of object to select list in property
+ * @param {string} dcId - id of ABDatacollection
+ */
+ propertyUpdateFieldOptions(dcId) {
+ var datacollection = this.AB.datacollectionByID(dcId);
+ var object = datacollection ? datacollection.datasource : null;
+
+ // Pull field list
+ var fieldOptions = [];
+ if (object != null) {
+ fieldOptions = object.fields().map((f) => {
+ return {
+ id: f.id,
+ value: f.label,
+ };
+ });
+ }
+
+ const ids = this.ids;
+ $$(ids.dateField).define("options", fieldOptions);
+ $$(ids.dateField).refresh();
+ }
+
+ populate(view) {
+ super.populate(view);
+
+ const ids = this.ids;
+
+ var dcID = view.settings.dataviewID ? view.settings.dataviewID : null;
+ var $dc = $$(ids.datacollection);
+
+ // Pull data collections to options
+ var dcOptions = view.application.datacollectionsIncluded().map((d) => {
+ return {
+ id: d.id,
+ value: d.label,
+ icon:
+ d.sourceType == "query" ? "fa fa-filter" : "fa fa-database",
+ };
+ });
+ $dc.define("options", dcOptions);
+ $dc.define("value", dcID);
+ $dc.refresh();
+
+ this.propertyUpdateFieldOptions(dcID);
+
+ $$(ids.dateField).setValue(view.settings.dateField);
+
+ let $hideTitle = $$(ids.hideTitle);
+ if (view.settings.hideTitle) {
+ $hideTitle.define("value", 1);
+ } else {
+ $hideTitle.define("value", 0);
+ }
+ $hideTitle.refresh();
+
+ // $$(ids.height).setValue(view.settings.height);
+
+ let obj = this.AB.datacollectionByID(dcID)?.datasource;
+ if (obj) {
+ LabelTemplate.objectLoad(obj);
+ }
+
+ // offer a suggestion if label format is not set:
+ if (!view.settings.templateItem) {
+ view.settings.templateItem = `
+
+
+ {Title}
+
+
+
+
+ {Secondary Item}
+
`;
+ }
+ LabelTemplate.setLabelFormat(view.settings.templateItem);
+
+ ////
+ //// Page Lists
+
+ // Regather the current Page lists so they are always up to date:
+ let pagesWithForms = this.pagesRelevant(view, "mobile-form", dcID);
+
+ // include an option we will use to remove the value:
+ // pagesWithForms.unshift({
+ // id: "noLinkedView",
+ // value: L("No linked view"),
+ // });
+
+ let $lpAdd = $$(ids.linkPageAdd);
+ $lpAdd.define("options", pagesWithForms);
+ $lpAdd.define("value", view.settings.linkPageAdd);
+ $lpAdd.refresh();
+
+ let $lpDetail = $$(ids.linkPageDetail);
+ $lpDetail.define("options", pagesWithForms);
+ $lpDetail.define("value", view.settings.linkPageDetail);
+ $lpDetail.refresh();
+ }
+
+ _filterWidgetAndDC(v, widgetKey, dcID) {
+ // valid options have .widgetKey AND are tied to our
+ // datacollection
+
+ let vDC = v.datacollection;
+ return (
+ v.key == widgetKey &&
+ (vDC?.id == dcID || vDC?.datacollectionFollow?.id == dcID)
+ );
+ }
+
+ /**
+ * @method pagesRelevant()
+ * search our possible Pages for ones that might work as one of our
+ * link pages.
+ * @param {string} key
+ * A matching page must contain a widget that is tied to our
+ * datacollection. This is the key of a widget we are searching
+ * for.
+ * @return {array[ABMobilePage]}
+ */
+ pagesRelevant(view, key, dcID) {
+ let relevantPages = [];
+
+ // First gather Pages that are UNDER the page this current View is on
+ let parent = view.parent;
+ if (parent) {
+ relevantPages = relevantPages.concat(
+ parent
+ .pages((p) => {
+ return p.views((v) => {
+ return this._filterWidgetAndDC(v, key, dcID);
+ }, true).length;
+ }, true)
+ .map((p) => {
+ return {
+ id: p.id,
+ value: p.label,
+ };
+ })
+ );
+ }
+
+ // NOW Add in possible pages from ALL the available Pages:
+ // I mean, who knows how our designer is laying things out:
+
+ relevantPages = relevantPages.concat(
+ view
+ .pageRoot()
+ .pages((p) => {
+ return p.views((v) => {
+ return this._filterWidgetAndDC(v, key, dcID);
+ }, true).length;
+ }, true)
+ // now remove any pages that were already in relevantPages
+ .filter((p) => {
+ return !relevantPages.find((rp) => p.id == rp.id);
+ })
+ .map((p) => {
+ let pParent = p.parent;
+ return {
+ id: p.id,
+ value: pParent
+ ? `${pParent.label} -> ${p.label}`
+ : p.label,
+ };
+ })
+ );
+
+ return relevantPages;
+ }
+
+ defaultValues() {
+ const ViewClass = this.ViewClass();
+
+ let values = null;
+
+ if (ViewClass) {
+ values = ViewClass.defaultValues();
+ }
+
+ return values;
+ }
+
+ /**
+ * @method values
+ * return the values for this form.
+ * @return {obj}
+ */
+ values() {
+ const ids = this.ids;
+
+ const $component = $$(ids.component);
+
+ const values = super.values();
+
+ values.settings = $component.getValues();
+ values.settings.templateItem = LabelTemplate.labelFormat;
+
+ return values;
+ }
+ }
+
+ return ABViewListProperty;
+}
diff --git a/src/rootPages/Designer/properties/rules/ruleActions/ABViewRuleActionObjectUpdater.js b/src/rootPages/Designer/properties/rules/ruleActions/ABViewRuleActionObjectUpdater.js
index 6a589353..bac4af74 100644
--- a/src/rootPages/Designer/properties/rules/ruleActions/ABViewRuleActionObjectUpdater.js
+++ b/src/rootPages/Designer/properties/rules/ruleActions/ABViewRuleActionObjectUpdater.js
@@ -451,7 +451,11 @@ export default function (AB) {
if (!field) return;
const fieldComponent = field.formComponent(),
- abView = fieldComponent.newInstance(this.Rule.CurrentApplication);
+ abView = fieldComponent.newInstance(
+ this.Rule.CurrentApplication.isWebApp
+ ? this.Rule.CurrentApplication
+ : null
+ );
// let formFieldComponent = abView.component(this.AB._App);
let formFieldComponent = abView.component();
let $componentView, $inputView;
diff --git a/src/rootPages/Designer/ui_common_label_template.js b/src/rootPages/Designer/ui_common_label_template.js
new file mode 100644
index 00000000..aea02f2d
--- /dev/null
+++ b/src/rootPages/Designer/ui_common_label_template.js
@@ -0,0 +1,245 @@
+/*
+ * UI_Common_Label_Template
+ *
+ * A common Label Template builder for our various elements.
+ *
+ */
+import UI_Class from "./ui_class";
+export default function (AB, ibase) {
+ ibase = ibase || "ui_common_label_template" + AB.jobID();
+ const UIClass = UI_Class(AB);
+ var L = UIClass.L();
+
+ class UI_Common_Label_Template extends UIClass {
+ constructor(base) {
+ super(base, {
+ // component: idBase,
+ format: "",
+ list: "",
+ buttonSave: "",
+ });
+
+ this.labelFormat = "";
+ }
+
+ ui() {
+ let ids = this.ids;
+
+ // webix UI definition:
+ return {
+ view: "popup",
+ id: ids.component,
+ modal: true,
+ autoheight: true,
+ // maxHeight: 420,
+ width: 500,
+ body: {
+ rows: [
+ {
+ view: "label",
+ label: L("Label format"),
+ },
+ {
+ view: "textarea",
+ id: ids.format,
+ height: 100,
+ },
+ {
+ view: "label",
+ label: L("Select field item to generate format."),
+ },
+ {
+ view: "label",
+ label: L("Fields"),
+ },
+ {
+ view: "list",
+ name: "columnList",
+ id: ids.list,
+ width: 500,
+ height: 180,
+ maxHeight: 180,
+ select: false,
+ template: "#label#",
+ on: {
+ onItemClick: (id, e, node) => {
+ this.onItemClick(id, e, node);
+ },
+ },
+ },
+ {
+ height: 10,
+ },
+ {
+ cols: [
+ { fillspace: true },
+ {
+ view: "button",
+ name: "cancel",
+ value: L("Cancel"),
+ css: "ab-cancel-button",
+ autowidth: true,
+ click: () => {
+ this.buttonCancel();
+ },
+ },
+ {
+ view: "button",
+ css: "webix_primary",
+ name: "save",
+ id: ids.buttonSave,
+ label: L("Save"),
+ type: "form",
+ autowidth: true,
+ click: () => {
+ this.buttonSave();
+ },
+ },
+ ],
+ },
+ ],
+ },
+ on: {
+ onShow: () => {
+ this.onShow();
+ },
+ },
+ };
+ }
+
+ // for setting up UI
+ init(AB) {
+ this.AB = AB;
+
+ webix.ui(this.ui());
+
+ webix.extend($$(this.ids.list), webix.ProgressBar);
+ }
+
+ // changed() {
+ // this.emit("changed", this._settings);
+ // }
+
+ buttonCancel() {
+ $$(this.ids.component).hide();
+ }
+
+ async buttonSave() {
+ var ids = this.ids;
+
+ // disable our save button
+ var ButtonSave = $$(ids.buttonSave);
+ ButtonSave.disable();
+
+ // get our current labelFormt
+ var labelFormat = $$(ids.format).getValue();
+
+ // start our spinner
+ var List = $$(ids.list);
+ List.showProgress({ type: "icon" });
+
+ // convert from our User Friendly {Label} format to our
+ // object friendly {Name} format
+ List.data.each(function (d) {
+ labelFormat = labelFormat.replace(
+ new RegExp("{" + d.label + "}", "g"),
+ "{" + d.id + "}"
+ );
+ });
+
+ this.labelFormat = labelFormat;
+ this.emit("save", labelFormat);
+
+ List.hideProgress(); // hide the spinner
+ ButtonSave.enable(); // enable the save button
+ this.hide();
+ }
+
+ hide() {
+ $$(this.ids.component).hide();
+ }
+
+ setLabelFormat(labelFormat) {
+ this.labelFormat = labelFormat;
+ }
+
+ objectLoad(object) {
+ super.objectLoad(object);
+
+ // clear our list
+ var List = $$(this.ids.list);
+ List.clearAll();
+
+ // refresh list with new set of fields
+ var listFields = object
+ .fields((f) => {
+ return f.fieldUseAsLabel();
+ })
+ .map((f) => {
+ return {
+ id: f.id,
+ label: f.label,
+ };
+ });
+
+ List.parse(listFields);
+ List.refresh();
+ }
+
+ onItemClick(id /*, e, node */) {
+ var ids = this.ids;
+ var selectedItem = $$(ids.list).getItem(id);
+ var labelFormat = $$(ids.format).getValue();
+ labelFormat += `{${selectedItem.label}}`;
+ $$(ids.format).setValue(labelFormat);
+ }
+
+ onShow() {
+ var ids = this.ids;
+
+ var labelFormat = this.labelFormat;
+ var Format = $$(ids.format);
+ var List = $$(ids.list);
+
+ Format.setValue("");
+ Format.enable();
+ List.enable();
+ $$(ids.buttonSave).enable();
+
+ // our labelFormat should be in a computer friendly {name} format
+ // here we want to convert it to a user friendly {label} format
+ // to use in our popup:
+ if (labelFormat) {
+ if (List.data?.count() > 0) {
+ List.data.each(function (d) {
+ labelFormat = labelFormat.replace(
+ new RegExp(`{${d.id}}`, "g"),
+ `{${d.label}}`
+ );
+ });
+ }
+ } else {
+ // no label format:
+ // Default to first field
+ if (List.data?.count() > 0) {
+ var field = List.getItem(List.getFirstId());
+ labelFormat = `{${field.label}}`;
+ }
+ }
+
+ Format.setValue(labelFormat || "");
+ }
+
+ /**
+ * @function show()
+ *
+ * Show this component.
+ * @param {obj} $view the webix.$view to hover the popup around.
+ */
+ show($view) {
+ $$(this.ids.component).show($view);
+ }
+ }
+
+ return new UI_Common_Label_Template(ibase);
+}