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