diff --git a/AppBuilder/core b/AppBuilder/core index 692e247a..e3784f40 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 692e247a484ef0ae1275b6889a2ab5b417747b59 +Subproject commit e3784f40e6e78211ea962470d16c7e638c91760b diff --git a/AppBuilder/platform/plugins/included/index.js b/AppBuilder/platform/plugins/included/index.js index bd2fbc35..980817bd 100644 --- a/AppBuilder/platform/plugins/included/index.js +++ b/AppBuilder/platform/plugins/included/index.js @@ -1,11 +1,12 @@ -import viewDataview from "./view_dataview/FNAbviewdataview.js"; import viewCarousel from "./view_carousel/FNAbviewcarousel.js"; import viewComment from "./view_comment/FNAbviewcomment.js"; import viewCsvExporter from "./view_csvExporter/FNAbviewcsvexporter.js"; import viewCsvImporter from "./view_csvImporter/FNAbviewcsvimporter.js"; import viewDataSelect from "./view_data-select/FNAbviewdataselect.js"; +import viewDataview from "./view_dataview/FNAbviewdataview.js"; import viewDetail from "./view_detail/FNAbviewdetail.js"; import viewImage from "./view_image/FNAbviewimage.js"; +import viewKanban from "./view_kanban/FNAbviewkanban.js"; import viewLabel from "./view_label/FNAbviewlabel.js"; import viewLayout from "./view_layout/FNAbviewlayout.js"; import viewList from "./view_list/FNAbviewlist.js"; @@ -18,17 +19,18 @@ const AllPlugins = [ viewComment, viewCsvExporter, viewCsvImporter, - viewCsvImporter, viewDataSelect, + viewDataview, viewDetail, viewImage, + viewKanban, viewLabel, viewLayout, viewList, viewPdfImporter, viewTab, viewText, -, viewDataview]; +]; export default { load: (AB) => { diff --git a/AppBuilder/platform/plugins/included/view_kanban/FNAbviewKanbanForm.js b/AppBuilder/platform/plugins/included/view_kanban/FNAbviewKanbanForm.js new file mode 100644 index 00000000..5bfba3bc --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_kanban/FNAbviewKanbanForm.js @@ -0,0 +1,297 @@ +/** + * Kanban sidebar detached form: save button only. + * No cross-folder imports — bases come from pluginAPI (see ABClassManager.getPluginAPI). + */ + +export default function createABViewKanbanDetachedFormSave({ + AB, + ABViewPlugin, + ABViewComponentPlugin, +}) { + + const ABViewFormButtonPropertyComponentDefaults = { + includeSave: true, + saveLabel: "", + includeCancel: false, + cancelLabel: "", + includeReset: false, + resetLabel: "", + includeDelete: false, + deleteLabel: "", + afterCancel: null, + alignment: "right", + isDefault: false, // mark default button of form widget + }; + + + class ABViewFormButtonCore extends ABViewPlugin { + static common() { + return { + key: "button", + // {string} unique key for this view + + icon: "square", + // {string} fa-[icon] reference for this view + + labelKey: "ab.components.button", + // {string} the multilingual label key for the class label + }; + } + constructor(values, application, parent, defaultValues) { + const ABViewFormButtonDefaults = { + key: "button", + // {string} unique key for this view + + icon: "square", + // {string} fa-[icon] reference for this view + + labelKey: "ab.components.button", + // {string} the multilingual label key for the class label + }; + super( + values, + application, + parent, + defaultValues || ABViewFormButtonDefaults + ) + } + + + static defaultValues() { + return ABViewFormButtonPropertyComponentDefaults; + } + + /// + /// Instance Methods + /// + + toObj() { + // labels are multilingual values: + let labels = []; + + if (this.settings.saveLabel) labels.push("saveLabel"); + + if (this.settings.cancelLabel) labels.push("cancelLabel"); + + if (this.settings.resetLabel) labels.push("resetLabel"); + + if (this.settings.deleteLabel) labels.push("deleteLabel"); + + this.unTranslate(this.settings, this.settings, labels); + + let result = super.toObj(); + + return result; + } + + /** + * @property datacollection + * return data source + * NOTE: this view doesn't track a DataCollection. + * @return {ABDataCollection} + */ + get datacollection() { + return null; + } + + fromValues(values) { + super.fromValues(values); + + // labels are multilingual values: + let labels = []; + + if (this.settings.saveLabel) labels.push("saveLabel"); + + if (this.settings.cancelLabel) labels.push("cancelLabel"); + + if (this.settings.resetLabel) labels.push("resetLabel"); + + if (this.settings.deleteLabel) labels.push("deleteLabel"); + + this.unTranslate(this.settings, this.settings, labels); + + // this.settings.includeSave = JSON.parse( + // (this.settings?.includeSave ?? true) && + // ABViewFormButtonPropertyComponentDefaults.includeSave + // ); + // this.settings.includeCancel = JSON.parse( + // this.settings.includeCancel || + // ABViewFormButtonPropertyComponentDefaults.includeCancel + // ); + // this.settings.includeReset = JSON.parse( + // this.settings.includeReset || + // ABViewFormButtonPropertyComponentDefaults.includeReset + // ); + // this.settings.includeDelete = JSON.parse( + // this.settings.includeDelete || + // ABViewFormButtonPropertyComponentDefaults.includeDelete + // ); + + // this.settings.isDefault = JSON.parse( + // this.settings.isDefault || + // ABViewFormButtonPropertyComponentDefaults.isDefault + // ); + } + + /** + * @method componentList + * return the list of components available on this view to display in the editor. + */ + componentList() { + return []; + } + } + + class formComponent extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewFormItem_${baseView.id}`, + Object.assign({ formItem: "" }, ids) + ); + } + + ui(uiFormItemComponent = {}) { + // setup 'label' of the element + const baseView = this.view; + const form = baseView.parentFormComponent(), + field = baseView.field?.() || null, + label = ""; + const settings = form?.settings || {}; + const _uiFormItem = { + id: this.ids.formItem, + labelPosition: settings.labelPosition, + labelWidth: settings.labelWidth, + label, + }; + + if (field) { + _uiFormItem.name = field.columnName; + + // default value + const data = {}; + + field.defaultValue(data); + + if (data[field.columnName]) _uiFormItem.value = data[field.columnName]; + + if (settings.showLabel) _uiFormItem.label = field.label; + + if (field.settings.required || baseView.settings?.required) + _uiFormItem.required = 1; + + if (baseView.settings?.disable === 1) _uiFormItem.disabled = true; + + // this may be needed if we want to format data at this point + // if (field.format) data = field.format(data); + + _uiFormItem.validate = (val, data, colName) => { + const validator = AB.Validation.validator(); + + field.isValidData(data, validator); + + return validator.pass(); + }; + } + + const _ui = super.ui([ + Object.assign({}, _uiFormItem, uiFormItemComponent), + ]); + + delete _ui.type; + + return _ui; + } + }; + + + class ABViewKanbanDetachedFormSaveComponent extends formComponent { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewKanbanDetachedFormSave_${baseView.id}`, + ids + ); + } + + ui() { + const self = this; + const baseView = this.view; + const form = baseView.parentFormComponent(); + const settings = baseView.settings ?? {}; + const alignment = + settings.alignment || + baseView.constructor.defaultValues().alignment; + + const _ui = { cols: [] }; + + if (alignment === "center" || alignment === "right") { + _ui.cols.push({}); + } + + if (settings.includeSave) { + _ui.cols.push({ + view: "button", + type: "form", + css: "webix_primary", + autowidth: true, + value: settings.saveLabel || this.label("Save"), + click: function () { + self.onSave(this); + }, + on: { + onAfterRender: function () { + this.getInputNode().setAttribute( + "data-cy", + `button save ${form.id}` + ); + }, + }, + }); + } + + if (alignment === "center" || alignment === "left") { + _ui.cols.push({}); + } + + return super.ui(_ui); + } + + onSave(saveButton) { + if (!saveButton) { + console.error("Require the button element"); + return; + } + const form = this.view.parentFormComponent(); + const formView = saveButton.getFormView(); + + saveButton.disable?.(); + + form + .saveData(formView) + .then(() => { + saveButton.enable?.(); + form.focusOnFirst(); + }) + .catch((err) => { + console.error(err); + try { + saveButton.enable?.(); + } catch (e) { + AB.notify.developer(e, { + context: + "ABViewKanbanDetachedFormSave.onSave > saveButton.enable()", + buttonID: this?.view?.id, + formID: this?.view?.parent?.id, + }); + } + }); + } + } + + return class ABViewKanbanDetachedFormSave extends ABViewFormButtonCore { + component() { + return new ABViewKanbanDetachedFormSaveComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_kanban/FNAbviewKanbanFormSidePanel.js b/AppBuilder/platform/plugins/included/view_kanban/FNAbviewKanbanFormSidePanel.js new file mode 100644 index 00000000..e0bbd15f --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_kanban/FNAbviewKanbanFormSidePanel.js @@ -0,0 +1,201 @@ +/* + * FNAbviewKanbanFormSidePanel + * + * Form area for editing Kanban cards (included plugin; ESM). + */ + +export default function FNAbviewKanbanFormSidePanel({ + ABViewComponentPlugin, + ABViewKanbanDetachedFormSave, +}) { + return class FNAbviewKanbanFormSidePanel extends ABViewComponentPlugin { + constructor(comKanBan, idBase, editFields) { + super(comKanBan, idBase || `${comKanBan.view?.id}_formSidePanel`, { + form: "", + }); + + this.editFields = editFields; + + this._mockApp = this.AB.applicationNew({}); + } + + ui() { + const ids = this.ids; + const L = (...params) => this.AB.Multilingual.label(...params); + + return { + id: ids.component, + width: 300, + hidden: true, + rows: [ + { + view: "toolbar", + css: "webix_dark", + cols: [ + { + view: "label", + label: L("Edit Record"), + }, + { + view: "icon", + icon: "wxi-close", + align: "right", + click: () => { + this.hide(); + }, + }, + ], + }, + { + view: "scrollview", + body: { + rows: [ + { + id: ids.form, + view: "form", + type: "clean", + borderless: true, + rows: [], + }, + ], + }, + }, + ], + }; + } + + hide() { + $$(this.ids.component)?.hide(); + + this.emit("close"); + } + + show(data) { + $$(this.ids.component)?.show(); + + this.refreshForm(data); + } + + isVisible() { + return $$(this.ids.component)?.isVisible() ?? false; + } + + refreshForm(data) { + const ids = this.ids; + const $formView = $$(ids.form); + const CurrentObject = this.CurrentObject; + + if (!CurrentObject || !$formView) return; + + data = data || {}; + + const formAttrs = { + id: `${this.ids.component}_sideform`, + key: "form", + settings: { + columns: 1, + labelPosition: "top", + showLabel: 1, + clearOnLoad: 0, + clearOnSave: 0, + labelWidth: 120, + height: 0, + }, + }; + + const form = this.AB.viewNewDetatched(formAttrs); + + form.objectLoad(CurrentObject); + + CurrentObject.fields().forEach((f, index) => { + if (!this.editFields || this.editFields.indexOf(f.id) > -1) { + form.addFieldToForm(f, index); + } + }); + + form._views.push( + new ABViewKanbanDetachedFormSave( + { + settings: { + includeSave: true, + includeCancel: false, + includeReset: false, + }, + position: { + y: CurrentObject.fields().length, + }, + }, + this._mockApp, + form + ) + ); + + form._views.forEach( + (v, index) => (v.id = `${form.id}_${v.key}_${index}`) + ); + + const formCom = form.component(this.AB._App); + + webix.ui(formCom.ui().rows.concat({}), $formView); + webix.extend($formView, webix.ProgressBar); + + formCom.init( + this.AB, + 2, + { + onBeforeSaveData: () => { + const formVals = form.getFormValues($formView, CurrentObject); + + if (!form.validateData($formView, CurrentObject, formVals)) + return false; + + $formView?.showProgress({ type: "icon" }); + + if (formVals.id) { + CurrentObject.model() + .update(formVals.id, formVals) + .then((updateVals) => { + this.emit("update", updateVals); + + $formView?.hideProgress({ type: "icon" }); + }) + .catch((err) => { + this.AB.notify.developer(err, { + context: + "ABViewKanbanFormSidePanel:onBeforeSaveData():update(): Error updating value", + formVals, + }); + $formView?.hideProgress({ type: "icon" }); + }); + } else { + CurrentObject.model() + .create(formVals) + .then((newVals) => { + this.emit("add", newVals); + + $formView?.hideProgress({ type: "icon" }); + }) + .catch((err) => { + this.AB.notify.developer(err, { + context: + "ABViewKanbanFormSidePanel:onBeforeSaveData():.create(): Error creating value", + formVals, + }); + + $formView?.hideProgress({ type: "icon" }); + }); + } + + return false; + }, + }, + 2 + ); + + $formView.clear(); + $formView.parse(data); + + formCom.onShow(data); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_kanban/FNAbviewkanban.js b/AppBuilder/platform/plugins/included/view_kanban/FNAbviewkanban.js new file mode 100644 index 00000000..29de9b32 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_kanban/FNAbviewkanban.js @@ -0,0 +1,136 @@ +import FNAbviewkanbanComponent from "./FNAbviewkanbanComponent.js"; +import FNAbviewKanbanDetachedFormSave from "./FNAbviewKanbanForm.js"; +import FNAbviewKanbanFormSidePanel from "./FNAbviewkanbanFormSidePanel.js"; + +// FNAbviewkanban Web +// A web side import for an ABView. +// +export default function FNAbviewkanban({ + AB, + ABViewWidgetPlugin, + ABViewComponentPlugin, + ABViewPropertyLinkPage, + ABViewPlugin, +}) { + const ABViewKanbanDetachedFormSave = FNAbviewKanbanDetachedFormSave({ + AB, + ABViewPlugin, + ABViewComponentPlugin, + }); + const KanbanFormSidePanel = FNAbviewKanbanFormSidePanel({ + ABViewComponentPlugin, + ABViewKanbanDetachedFormSave, + }); + const ABAbviewkanbanComponent = FNAbviewkanbanComponent({ + AB, + ABViewComponentPlugin, + FNAbviewKanbanFormSidePanel: KanbanFormSidePanel, + }); + + const ABViewKanbanPropertyComponentDefaults = { + dataviewID: null, // uuid ABDataCollection; DC resolves ABObject + editFields: [], // ABField.id[] fields shown in editor + verticalGroupingField: "", // ABField.id vertical lanes + horizontalGroupingField: "", // ABField.id optional horizontal grouping + ownerField: "", // ABFieldUser.id card owner + template: "", // json ABViewText card body; placeholders {field.id} + }; + + const ABViewDefaults = { + key: "kanban", // {string} unique view key + icon: "columns", // {string} font-awesome (no fa- prefix) + labelKey: "Kanban", // {string} multilingual label key → L(labelKey) + }; + + class ABViewKanbanCore extends ABViewWidgetPlugin { + constructor(values, application, parent, defaultValues) { + super(values, application, parent, defaultValues || ABViewDefaults); + } + + /// + /// Instance Methods + /// + + /** + * @method componentList + * return the list of components available on this view to display in the editor. + */ + componentList() { + return []; + } + + fromValues(values) { + super.fromValues(values); + + // set a default .template value + if (!this.settings.template) { + this.settings.template = { id: `${this.id}_template`, key: "text" }; + this.settings.template.text = this.settings.textTemplate; + } + + this.TextTemplate = AB.viewNewDetatched(this.settings.template); + } + + toObj() { + var obj = super.toObj(); + obj.settings.template = this.TextTemplate.toObj(); + // NOTE: this corrects the initial save where this.id == undefined + // all the rest will set the .id correctly. + obj.settings.template.id = `${this.id}_template`; + return obj; + } + + static common() { + return ABViewDefaults; + } + + static defaultValues() { + return ABViewKanbanPropertyComponentDefaults; + } + } + + return class ABViewKanban extends ABViewKanbanCore { + /** + * @method getPluginKey + * return the plugin key for this view. + * @return {string} plugin key + */ + static getPluginKey() { + return this.common().key; + } + get linkPageHelper() { + if (this.__linkPageHelper == null) + this.__linkPageHelper = new ABViewPropertyLinkPage(); + + return this.__linkPageHelper; + } + + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component(parentId) { + return new ABAbviewkanbanComponent(this, parentId); + } + + // + // Editor Related + // + + get linkPageHelper() { + return (this.__linkPageHelper = + this.__linkPageHelper || new ABViewPropertyLinkPage()); + } + + warningsEval() { + super.warningsEval(); + let DC = this.datacollection; + if (!DC) { + this.warningsMessage( + `can't resolve it's datacollection[${this.settings.dataviewID}]` + ); + } + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_kanban/FNAbviewkanbanComponent.js b/AppBuilder/platform/plugins/included/view_kanban/FNAbviewkanbanComponent.js new file mode 100644 index 00000000..5c211c22 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_kanban/FNAbviewkanbanComponent.js @@ -0,0 +1,585 @@ +export default function FNAbviewkanbanComponent({ + AB, + ABViewComponentPlugin, + FNAbviewKanbanFormSidePanel, +}) { + return class ABAbviewkanbanComponent extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewKanban_${baseView.id}`, + Object.assign( + { + kanbanView: "", + + kanban: "", + resizer: "", + formSidePanel: "", + }, + ids + ) + ); + + this.FormSide = new FNAbviewKanbanFormSidePanel( + this, + this.ids.formSidePanel, + this.settings.editFields + ); + + this.CurrentVerticalField = null; + this.CurrentHorizontalField = null; + this.CurrentOwnerField = null; + + this.TextTemplate = baseView.TextTemplate; + + this._updatingOwnerRowId = null; + this._ABFieldConnect = null; + this._ABFieldUser = null; + this._ABFieldList = null; + } + + get ABFieldConnect() { + return (this._ABFieldConnect = + this._ABFieldConnect || + AB.Class.ABFieldManager.fieldByKey("connectObject")); + } + + get ABFieldUser() { + return (this._ABFieldUser = + this._ABFieldUser || AB.Class.ABFieldManager.fieldByKey("user")); + } + + get ABFieldList() { + return (this._ABFieldList = + this._ABFieldList || AB.Class.ABFieldManager.fieldByKey("list")); + } + + ui() { + const ids = this.ids; + const baseView = this.view; + const self = this; + this.linkPage = baseView.linkPageHelper.component(); + + const _ui = super.ui([ + { + id: ids.kanbanView, + cols: [ + { + id: ids.kanban, + view: "kanban", + cols: [], + userList: { + view: "menu", + // yCount: 8, + // scroll: false, + template: ' #value#', + width: 150, + on: { + onSelectChange: function () { + // get this row id from onAvatarClick event + if (!self._updatingOwnerRowId) return; + + const userId = this.getSelectedId(false); + if (!userId) return; + + self.updateOwner(self._updatingOwnerRowId, userId); + }, + }, + }, + editor: false, // we use side bar + users: [], + tags: [], + data: [], + on: { + onListAfterSelect: (itemId, list) => { + this.CurrentDatacollection?.setCursor(itemId); + this.emit("select", itemId); + + // link pages events + const editPage = this.settings.editPage; + if (editPage) + this.linkPage.changePage(editPage, itemId); + + const detailsPage = this.settings.detailsPage; + if (detailsPage) + this.linkPage.changePage(detailsPage, itemId); + }, + onAfterStatusChange: (rowId, status /*, list */) => { + this.updateStatus(rowId, status); + }, + onAvatarClick: (rowId /*, ev, node, list */) => { + // keep this row id for update owner data in .userList + this._updatingOwnerRowId = rowId; + }, + }, + }, + { + id: ids.resizer, + view: "resizer", + css: "bg_gray", + width: 11, + hidden: true, + }, + this.FormSide.ui(), + ], + }, + ]); + + delete _ui.type; + + return _ui; + } + + async init(AB) { + await super.init(AB); + + const abWebix = AB.Webix; + const baseView = this.view; + + if (this.$kb) abWebix.extend(this.$kb, abWebix.ProgressBar); + + this.FormSide.init(AB); + this.FormSide.on("add", (newVals) => { + this.saveData(newVals); + }); + this.FormSide.on("update", (updateVals) => { + this.saveData(updateVals); + }); + + let dc = baseView.datacollection; + if (dc) this.datacollectionLoad(dc); + + this.linkPage.init({ + view: baseView, + datacollection: dc, + }); + + this.show(); + } + + get $kb() { + return (this._kb = this._kb || $$(this.ids.kanban)); + } + + kanbanListTemplate() { + return { + icons: [ + // { icon: "mdi mdi-comment", show: function (obj) { return !!obj.comments }, template: "#comments.length#" }, + { + icon: "fa fa-trash-o", + click: (rowId /*, e */) => { + this.removeCard(rowId); + }, + }, + ], + // avatar template + templateAvatar: (obj) => { + if ( + this.CurrentOwnerField && + obj[this.CurrentOwnerField.columnName] + ) + return this.CurrentOwnerField.format(obj); + else return ""; + }, + // template for item body + // show item image and text + templateBody: (data) => { + // if (!this.settings.template) + if (!this.TextTemplate.text) + return this.CurrentObject?.displayData(data); + + // return our default text template + return this.TextTemplate.displayText(data); + }, + }; + } + + /** + * @function hide() + * + * hide this component. + */ + hide() { + $$(this.ids.kanbanView)?.hide(); + } + + /** + * @function show() + * Show this component. + */ + async show() { + const ids = this.ids; + + $$(ids.kanbanView)?.show(); + + this.FormSide.hide(); + + $$(ids.resizer)?.hide(); + + var CurrentObject = this.CurrentObject; + if (!CurrentObject) { + CurrentObject = this.datacollection?.datasource; + } + if (!CurrentObject) return; + + // Get vertical grouping field and populate to kanban list + // NOTE: this field should be the select list type + const CurrentVerticalField = CurrentObject.fieldByID( + this.settings.verticalGroupingField + ); + if (!CurrentVerticalField) return; + + this.CurrentVerticalField = CurrentVerticalField; + + let horizontalOptions = []; + + const CurrentHorizontalField = CurrentObject.fieldByID( + this.settings.horizontalGroupingField + ); + + this.CurrentHorizontalField = CurrentHorizontalField; + + if ( + CurrentHorizontalField && + CurrentHorizontalField instanceof this.ABFieldConnect + ) + // Pull horizontal options + horizontalOptions = await CurrentHorizontalField.getOptions(); + + // Option format - { id: "1543563751920", text: "Normal", hex: "#4CAF50" } + const verticalOptions = (CurrentVerticalField.settings.options || []).map( + (opt) => { + // Vertical & Horizontal fields + if (CurrentVerticalField && CurrentHorizontalField) { + let rows = [], + // [{ + // id: '', + // text: '' + // }] + horizontalVals = []; + + // pull options of the Horizontal field + if (CurrentHorizontalField instanceof this.ABFieldList) { + // make a copy of the settings. + horizontalVals = ( + CurrentHorizontalField.settings.options || [] + ).map((o) => o); + } else if (CurrentHorizontalField instanceof this.ABFieldUser) { + horizontalVals = CurrentHorizontalField.getUsers().map( + (u) => { + return { + id: u.id, + text: u.text || u.value, + }; + } + ); + } else if (CurrentHorizontalField instanceof this.ABFieldConnect) + horizontalVals = horizontalOptions.map(({ id, text }) => ({ + id, + text, + })); + + horizontalVals.push({ + id: null, + text: this.label("Other"), + }); + + horizontalVals.forEach((val) => { + const statusOps = {}; + + statusOps[CurrentVerticalField.columnName] = opt.id; + statusOps[CurrentHorizontalField.columnName] = val.id; + + // Header + rows.push({ + template: val.text, + height: 20, + css: "progress_header", + }); + + // Kanban list + rows.push({ + view: "kanbanlist", + status: statusOps, + type: this.kanbanListTemplate(), + }); + }); + + return { + header: opt.text, + body: { + margin: 0, + rows: rows, + }, + }; + } + // Vertical field only + else if (CurrentVerticalField) { + const statusOps = {}; + + statusOps[CurrentVerticalField.columnName] = opt.id; + + return { + header: opt.text, + body: { + view: "kanbanlist", + status: statusOps, + type: this.kanbanListTemplate(), + }, + }; + } + } + ); + + const ab = AB; + const abWebix = ab.Webix; + + // Rebuild kanban that contains options + // NOTE: webix kanban does not support dynamic vertical list + abWebix.ui(verticalOptions, $$(ids.kanban)); + $$(ids.kanban).reconstruct(); + + // Owner field + const CurrentOwnerField = CurrentObject.fieldByID( + this.settings.ownerField + ); + + this.CurrentOwnerField = CurrentOwnerField; + + if (CurrentOwnerField) { + const $menuUser = $$(ids.kanban).getUserList(); + + $menuUser.clearAll(); + + if (CurrentOwnerField instanceof this.ABFieldUser) { + const users = ab.Account.userList().map((u) => { + return { + id: u.username, + value: u.username, + }; + }); + + $menuUser.parse(users); + } else if (CurrentOwnerField instanceof this.ABFieldConnect) { + const options = await CurrentOwnerField.getOptions(); + + try { + $menuUser.parse( + options.map((opt) => { + return { + id: opt.id, + value: opt.text, + }; + }) + ); + } catch (e) { + // TODO: remove this. Trying to catch a random webix error: + // Cannot read properties of null (reading 'driver') + console.error(e); + console.warn(options); + } + } + } + } + + busy() { + this.$kb?.showProgress?.({ type: "icon" }); + } + + ready() { + this.$kb?.hideProgress?.(); + } + + objectLoad(object) { + super.objectLoad(object); + + this.TextTemplate.objectLoad(object); + this.FormSide.objectLoad(object); + } + + /** + * @method datacollectionLoad + * + * @param datacollection {ABDatacollection} + */ + datacollectionLoad(datacollection) { + super.datacollectionLoad(datacollection); + + const DC = this.CurrentDatacollection || datacollection; + + if (DC) { + DC.bind(this.$kb); + + const obj = DC.datasource; + + if (obj) this.objectLoad(obj); + + return; + } + + this.$kb.unbind(); + } + + async updateStatus(rowId, status) { + if (!this.CurrentVerticalField) return; + + // Show loading cursor + this.busy(); + + let patch = {}; + + // update multi-values + if (status instanceof Object) patch = status; + // update single value + else patch[this.CurrentVerticalField.columnName] = status; + + // update empty value + let needRefresh = false; + + for (const key in patch) + if (!patch[key]) { + patch[key] = ""; + + // WORKAROUND: if update data is empty, then it will need to refresh + // the kanban after update + needRefresh = true; + } + + try { + await this.CurrentObject?.model().update(rowId, patch); + + this.ready(); + + if (needRefresh) this.show(); + + // update form data + if (this.FormSide.isVisible()) { + const data = $$(this.ids.kanban).getItem(rowId); + + this.FormSide.refresh(data); + } + } catch (err) { + AB.notify.developer(err, { + context: "ABViewKanban:updateStatus(): Error saving item:", + rowId, + patch, + }); + } + } + + async updateOwner(rowId, val) { + if (!this.CurrentOwnerField) return; + + // Show loading cursor + this.busy(); + + const patch = {}; + + patch[this.CurrentOwnerField.columnName] = val; + + try { + const updatedRow = await this.CurrentObject?.model().update( + rowId, + patch + ); + + // update card + this.$kb?.updateItem(rowId, updatedRow); + + // update form data + if (this.FormSide.isVisible()) { + const data = this.$kb.getItem(rowId); + + this.FormSide.refresh(data); + } + + this.ready(); + } catch (err) { + AB.notify.developer(err, { + context: "ABViewKanban:updateOwner(): Error saving item:", + rowId, + val, + }); + + this.ready(); + } + } + + saveData(data) { + // update + if (data.id && this.$kb.exists(data.id)) + this.$kb.updateItem(data.id, data); + // insert + else this.$kb.add(data); + } + + unselect() { + if (this.$kb) + this.$kb.eachList((list /*, status*/) => { + list?.unselect?.(); + }); + } + + addCard() { + this.unselect(); + + // show the side form + this.FormSide.show(); + $$(this.ids.resizer).show(); + } + + async removeCard(rowId) { + const ab = AB; + const abWebix = ab.Webix; + + abWebix.confirm({ + title: this.label("Remove card"), + text: this.label("Do you want to delete this card?"), + callback: async (result) => { + if (!result) return; + + this.busy(); + + try { + const response = await this.CurrentObject?.model().delete(rowId); + + if (response.numRows > 0) { + this.$kb.remove(rowId); + } else { + abWebix.alert({ + text: this.label( + "No rows were effected. This does not seem right." + ), + }); + } + } catch (err) { + ab.notify.developer(err, { + message: "ABViewKanban:removeCard(): Error deleting item:", + rowId, + }); + } + + this.ready(); + }, + }); + } + + /** + * @method setFields() + * Save the current view options. + * @param options - { + * verticalGrouping: {ABField} - required + * horizontalGrouping: {ABField} - optional + * ownerField: {ABField} - optional + * } + */ + setFields(options) { + this.CurrentVerticalField = options.verticalGrouping; + this.CurrentHorizontalField = options.horizontalGrouping; + this.CurrentOwnerField = options.ownerField; + } + + + }; + +} diff --git a/AppBuilder/platform/plugins/included/view_kanban/FNaBviewKanbanFormSidePanel.js b/AppBuilder/platform/plugins/included/view_kanban/FNaBviewKanbanFormSidePanel.js new file mode 100644 index 00000000..e0bbd15f --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_kanban/FNaBviewKanbanFormSidePanel.js @@ -0,0 +1,201 @@ +/* + * FNAbviewKanbanFormSidePanel + * + * Form area for editing Kanban cards (included plugin; ESM). + */ + +export default function FNAbviewKanbanFormSidePanel({ + ABViewComponentPlugin, + ABViewKanbanDetachedFormSave, +}) { + return class FNAbviewKanbanFormSidePanel extends ABViewComponentPlugin { + constructor(comKanBan, idBase, editFields) { + super(comKanBan, idBase || `${comKanBan.view?.id}_formSidePanel`, { + form: "", + }); + + this.editFields = editFields; + + this._mockApp = this.AB.applicationNew({}); + } + + ui() { + const ids = this.ids; + const L = (...params) => this.AB.Multilingual.label(...params); + + return { + id: ids.component, + width: 300, + hidden: true, + rows: [ + { + view: "toolbar", + css: "webix_dark", + cols: [ + { + view: "label", + label: L("Edit Record"), + }, + { + view: "icon", + icon: "wxi-close", + align: "right", + click: () => { + this.hide(); + }, + }, + ], + }, + { + view: "scrollview", + body: { + rows: [ + { + id: ids.form, + view: "form", + type: "clean", + borderless: true, + rows: [], + }, + ], + }, + }, + ], + }; + } + + hide() { + $$(this.ids.component)?.hide(); + + this.emit("close"); + } + + show(data) { + $$(this.ids.component)?.show(); + + this.refreshForm(data); + } + + isVisible() { + return $$(this.ids.component)?.isVisible() ?? false; + } + + refreshForm(data) { + const ids = this.ids; + const $formView = $$(ids.form); + const CurrentObject = this.CurrentObject; + + if (!CurrentObject || !$formView) return; + + data = data || {}; + + const formAttrs = { + id: `${this.ids.component}_sideform`, + key: "form", + settings: { + columns: 1, + labelPosition: "top", + showLabel: 1, + clearOnLoad: 0, + clearOnSave: 0, + labelWidth: 120, + height: 0, + }, + }; + + const form = this.AB.viewNewDetatched(formAttrs); + + form.objectLoad(CurrentObject); + + CurrentObject.fields().forEach((f, index) => { + if (!this.editFields || this.editFields.indexOf(f.id) > -1) { + form.addFieldToForm(f, index); + } + }); + + form._views.push( + new ABViewKanbanDetachedFormSave( + { + settings: { + includeSave: true, + includeCancel: false, + includeReset: false, + }, + position: { + y: CurrentObject.fields().length, + }, + }, + this._mockApp, + form + ) + ); + + form._views.forEach( + (v, index) => (v.id = `${form.id}_${v.key}_${index}`) + ); + + const formCom = form.component(this.AB._App); + + webix.ui(formCom.ui().rows.concat({}), $formView); + webix.extend($formView, webix.ProgressBar); + + formCom.init( + this.AB, + 2, + { + onBeforeSaveData: () => { + const formVals = form.getFormValues($formView, CurrentObject); + + if (!form.validateData($formView, CurrentObject, formVals)) + return false; + + $formView?.showProgress({ type: "icon" }); + + if (formVals.id) { + CurrentObject.model() + .update(formVals.id, formVals) + .then((updateVals) => { + this.emit("update", updateVals); + + $formView?.hideProgress({ type: "icon" }); + }) + .catch((err) => { + this.AB.notify.developer(err, { + context: + "ABViewKanbanFormSidePanel:onBeforeSaveData():update(): Error updating value", + formVals, + }); + $formView?.hideProgress({ type: "icon" }); + }); + } else { + CurrentObject.model() + .create(formVals) + .then((newVals) => { + this.emit("add", newVals); + + $formView?.hideProgress({ type: "icon" }); + }) + .catch((err) => { + this.AB.notify.developer(err, { + context: + "ABViewKanbanFormSidePanel:onBeforeSaveData():.create(): Error creating value", + formVals, + }); + + $formView?.hideProgress({ type: "icon" }); + }); + } + + return false; + }, + }, + 2 + ); + + $formView.clear(); + $formView.parse(data); + + formCom.onShow(data); + } + }; +}