From b515e2f7322a2a3d7eb1ae2436eeec7e5f429ea1 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 5 Nov 2021 17:22:59 +0530 Subject: [PATCH 1/5] feat: add conditional `required`, fix formatting --- model/document.js | 62 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/model/document.js b/model/document.js index bfd3e758..fec4445c 100644 --- a/model/document.js +++ b/model/document.js @@ -99,7 +99,7 @@ module.exports = class BaseDocument extends Observable { this.roundFloats(); await this.trigger('change', { doc: this, - changed: fieldname + changed: fieldname, }); } @@ -194,15 +194,16 @@ module.exports = class BaseDocument extends Observable { } validateMandatory() { + const fieldValueMap = this.getFieldValueMap(); let checkForMandatory = [this]; - let tableFields = this.meta.fields.filter(df => df.fieldtype === 'Table'); - tableFields.map(df => { + let tableFields = this.meta.fields.filter((df) => df.fieldtype === 'Table'); + tableFields.map((df) => { let rows = this[df.fieldname]; checkForMandatory = [...checkForMandatory, ...rows]; }); let missingMandatory = checkForMandatory - .map(doc => getMissingMandatory(doc)) + .map((doc) => getMissingMandatory(doc)) .filter(Boolean); if (missingMandatory.length > 0) { @@ -212,16 +213,21 @@ module.exports = class BaseDocument extends Observable { } function getMissingMandatory(doc) { - let mandatoryFields = doc.meta.fields.filter(df => df.required); + let mandatoryFields = doc.meta.fields.filter((df) => { + if (df.required instanceof Function) { + return df.required(fieldValueMap); + } + return df.required; + }); let message = mandatoryFields - .filter(df => { + .filter((df) => { let value = doc[df.fieldname]; if (df.fieldtype === 'Table') { return value == null || value.length === 0; } return value == null || value === ''; }) - .map(df => { + .map((df) => { return `"${df.label}"`; }) .join(', '); @@ -277,7 +283,7 @@ module.exports = class BaseDocument extends Observable { if (!isValid) { throw new frappe.errors.ValidationError(`Invalid phone: ${value}`); } - } + }, }; return functions[validator.type]; @@ -288,7 +294,9 @@ module.exports = class BaseDocument extends Observable { for (let field of this.meta.getValidFields()) { let value = this[field.fieldname]; if (Array.isArray(value)) { - value = value.map(doc => (doc.getValidDict ? doc.getValidDict() : doc)); + value = value.map((doc) => + doc.getValidDict ? doc.getValidDict() : doc + ); } data[field.fieldname] = value; } @@ -341,7 +349,7 @@ module.exports = class BaseDocument extends Observable { async loadLinks() { this._links = {}; - let inlineLinks = this.meta.fields.filter(df => df.inline); + let inlineLinks = this.meta.fields.filter((df) => df.inline); for (let df of inlineLinks) { await this.loadLink(df.fieldname); } @@ -367,13 +375,13 @@ module.exports = class BaseDocument extends Observable { this.setValues(data); this._dirty = false; this.trigger('change', { - doc: this + doc: this, }); } clearValues() { let toClear = ['_dirty', '_notInserted'].concat( - this.meta.getValidFields().map(df => df.fieldname) + this.meta.getValidFields().map((df) => df.fieldname) ); for (let key of toClear) { this[key] = null; @@ -400,7 +408,7 @@ module.exports = class BaseDocument extends Observable { throw new frappe.errors.Conflict( frappe._('Document {0} {1} has been modified after loading', [ this.doctype, - this.name + this.name, ]) ); } @@ -412,7 +420,7 @@ module.exports = class BaseDocument extends Observable { } // set submit action flag - this.flags = {} + this.flags = {}; if (this.submitted && !currentDoc.submitted) { this.flags.submitAction = true; } @@ -506,7 +514,7 @@ module.exports = class BaseDocument extends Observable { } if (field.fieldtype === 'Table' && Array.isArray(value)) { - value = value.map(row => { + value = value.map((row) => { let doc = this._initChild(row, field.fieldname); doc.roundFloats(); return doc; @@ -519,7 +527,7 @@ module.exports = class BaseDocument extends Observable { roundFloats() { let fields = this.meta .getValidFields() - .filter(df => ['Float', 'Currency', 'Table'].includes(df.fieldtype)); + .filter((df) => ['Float', 'Currency', 'Table'].includes(df.fieldtype)); for (let df of fields) { let value = this[df.fieldname]; @@ -528,7 +536,7 @@ module.exports = class BaseDocument extends Observable { } // child if (Array.isArray(value)) { - value.map(row => row.roundFloats()); + value.map((row) => row.roundFloats()); continue; } // field @@ -643,7 +651,7 @@ module.exports = class BaseDocument extends Observable { // helper functions getSum(tablefield, childfield) { return (this[tablefield] || []) - .map(d => parseFloat(d[childfield], 10) || 0) + .map((d) => parseFloat(d[childfield], 10) || 0) .reduce((a, b) => a + b, 0); } @@ -666,4 +674,22 @@ module.exports = class BaseDocument extends Observable { isNew() { return this._notInserted; } + + getFieldValueMap() { + const fieldValueMap = this.meta.fields + .map(({ fieldname, fieldtype }) => [fieldname, fieldtype]) + .reduce((obj, [fieldname, fieldtype]) => { + let value = this[fieldname]; + + if (fieldtype == 'Table') { + value = value.map((childTableDoc) => + childTableDoc.getFieldValueMap() + ); + } + + obj[fieldname] = value; + return obj; + }, {}); + return Object.freeze(fieldValueMap); + } }; From 1b9aa921f9faed354ed716bf598461cf1935f5e1 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 5 Nov 2021 17:46:12 +0530 Subject: [PATCH 2/5] docs: update docs to mention conditional fields --- docs/models/index.md | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/models/index.md b/docs/models/index.md index e588ebee..c8107d23 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -6,14 +6,20 @@ Note: A model is called `DocType` in Frappe.js ### Fields -Every model must have a set of fields (these become database columns). All fields must have +Every model must have a set of fields (these become database columns). All fields may have the following properties: -- `fieldname`: Column name in database / property name -- `fieldtype`: Data type ([see details](fields.md)) -- `label`: Display label -- `required`: Is mandatory -- `hidden`: Is hidden -- `disabled`: Is disabled +- `fieldname`: Column name in database / property name. +- `fieldtype`: Data type ([see details](fields.md)). +- `label`: Display label. +- `required`: Is mandatory. +- `hidden`: Is hidden. +- `disabled`: Is disabled. + +### Conditional Fields + +The following fields: `hidden`, `required` can be conditional, depending on the values of other fields. +This is done by setting it's value to a function that receives an object with all the models fields and their values and returns a boolean. +See _"Posting Date"_ in the example below. ### Example @@ -28,7 +34,7 @@ module.exports = { "fieldname": "subject", "label": "Subject", "fieldtype": "Data", - "reqd": 1 + "required": 1 }, { "fieldname": "description", @@ -44,8 +50,14 @@ module.exports = { "Closed" ], "default": "Open", - "reqd": 1 - } + "required": 1 + }, + { + "fieldname": "postingDate", + "label": "Posting Date", + "fieldtype": "Date", + "required": (doc) => doc.status === "Closed" + }, ] } ``` From 9f3bd96053ca4bb073a69302cebadc26177d3d8f Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 5 Nov 2021 19:55:58 +0530 Subject: [PATCH 3/5] fix: apply required changes in database.js --- backends/database.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/database.js b/backends/database.js index f85c6268..59503c3d 100644 --- a/backends/database.js +++ b/backends/database.js @@ -126,7 +126,7 @@ module.exports = class Database extends Observable { } // required - if (field.required) { + if (field.required && !field.required instanceof Function) { column.notNullable(); } From 139065180381729629c93c83e2a3b183e698b1a6 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 8 Nov 2021 15:14:02 +0530 Subject: [PATCH 4/5] fix: fix the conditional, apply to default too, early exit in column creation --- backends/database.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backends/database.js b/backends/database.js index 59503c3d..fe339284 100644 --- a/backends/database.js +++ b/backends/database.js @@ -113,6 +113,12 @@ module.exports = class Database extends Observable { buildColumnForTable(table, field) { let columnType = this.getColumnType(field); + if (!columnType) { + // In case columnType is "Table" + // childTable links are handled using the childTable's "parent" field + return; + } + let column = table[columnType](field.fieldname); // primary key @@ -121,12 +127,12 @@ module.exports = class Database extends Observable { } // default value - if (field.default) { + if (!!field.default && !(field.default instanceof Function)) { column.defaultTo(field.default); } // required - if (field.required && !field.required instanceof Function) { + if (!!field.required && !(field.required instanceof Function)) { column.notNullable(); } From 3e833309dbefc4e40833e5fb9f7550dad1569f18 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 8 Nov 2021 19:35:37 +0530 Subject: [PATCH 5/5] fix: prevent error slipping because of asyncness --- model/document.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/model/document.js b/model/document.js index fec4445c..1c79e925 100644 --- a/model/document.js +++ b/model/document.js @@ -608,11 +608,11 @@ module.exports = class BaseDocument extends Observable { return this; } - insertOrUpdate() { + async insertOrUpdate() { if (this._notInserted) { - return this.insert(); + return await this.insert(); } else { - return this.update(); + return await this.update(); } } @@ -622,14 +622,23 @@ module.exports = class BaseDocument extends Observable { await this.trigger('afterDelete'); } + async submitOrRevert(isSubmit) { + const wasSubmitted = this.submitted; + this.submitted = isSubmit; + try { + await this.update(); + } catch (e) { + this.submitted = wasSubmitted; + throw e; + } + } + async submit() { - this.submitted = 1; - this.update(); + await this.submitOrRevert(1); } async revert() { - this.submitted = 0; - this.update(); + await this.submitOrRevert(0); } async rename(newName) {