2
0
mirror of https://github.com/frappe/books.git synced 2025-01-27 09:08:24 +00:00

Add tailwind, DocType based on DocType and much more (#108)

Add tailwind, DocType based on DocType and much more
This commit is contained in:
Faris Ansari 2019-10-23 12:06:45 +05:30 committed by GitHub
commit fa004db117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 5718 additions and 3319 deletions

1
.python-version Normal file
View File

@ -0,0 +1 @@
2.7.16

28
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,28 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Mocha Current File",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": [
"--timeout",
"999999",
"--colors",
"${file}"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"node_modules/**/*.js",
"lib/**/*.js",
"async_hooks.js",
"inspector_async_hook.js"
]
}
]
}

View File

@ -19,11 +19,12 @@ module.exports = class Database extends Observable {
for (let doctype in frappe.models) {
// check if controller module
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
if (!meta.isSingle) {
if (await this.tableExists(doctype)) {
await this.alterTable(doctype);
if (await this.tableExists(baseDoctype)) {
await this.alterTable(baseDoctype);
} else {
await this.createTable(doctype);
await this.createTable(baseDoctype);
}
}
}
@ -197,20 +198,27 @@ module.exports = class Database extends Observable {
triggerChange(doctype, name) {
this.trigger(`change:${doctype}`, { name }, 500);
this.trigger(`change`, { doctype, name }, 500);
// also trigger change for basedOn doctype
let meta = frappe.getMeta(doctype);
if (meta.basedOn) {
this.triggerChange(meta.basedOn, name);
}
}
async insert(doctype, doc) {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
doc = this.applyBaseDocTypeFilters(doctype, doc);
// insert parent
if (meta.isSingle) {
await this.updateSingle(meta, doc, doctype);
} else {
await this.insertOne(doctype, doc);
await this.insertOne(baseDoctype, doc);
}
// insert children
await this.insertChildren(meta, doc, doctype);
await this.insertChildren(meta, doc, baseDoctype);
this.triggerChange(doctype, doc.name);
@ -236,16 +244,18 @@ module.exports = class Database extends Observable {
async update(doctype, doc) {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
doc = this.applyBaseDocTypeFilters(doctype, doc);
// update parent
if (meta.isSingle) {
await this.updateSingle(meta, doc, doctype);
} else {
await this.updateOne(doctype, doc);
await this.updateOne(baseDoctype, doc);
}
// insert or update children
await this.updateChildren(meta, doc, doctype);
await this.updateChildren(meta, doc, baseDoctype);
this.triggerChange(doctype, doc.name);
@ -299,6 +309,10 @@ module.exports = class Database extends Observable {
// await frappe.db.run('delete from SingleValue where parent=?', name)
}
async rename(doctype, oldName, newName) {
// await frappe.db.run('update doctype set name = ? where name = ?', name)
}
prepareChild(parenttype, parent, child, field, idx) {
if (!child.name) {
child.name = frappe.getRandomString();
@ -339,6 +353,19 @@ module.exports = class Database extends Observable {
}
}
applyBaseDocTypeFilters(doctype, doc) {
let meta = frappe.getMeta(doctype);
if (meta.filters) {
for (let fieldname in meta.filters) {
let value = meta.filters[fieldname];
if (typeof value !== 'object') {
doc[fieldname] = value;
}
}
}
return doc;
}
async deleteMany(doctype, names) {
for (const name of names) {
await this.delete(doctype, name);
@ -346,7 +373,9 @@ module.exports = class Database extends Observable {
}
async delete(doctype, name) {
await this.deleteOne(doctype, name);
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
await this.deleteOne(baseDoctype, name);
// delete children
let tableFields = frappe.getMeta(doctype).getTableFields();
@ -370,12 +399,17 @@ module.exports = class Database extends Observable {
}
async getValue(doctype, filters, fieldname = 'name') {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
if (typeof filters === 'string') {
filters = { name: filters };
}
if (meta.filters) {
Object.assign(filters, meta.filters);
}
let row = await this.getAll({
doctype: doctype,
doctype: baseDoctype,
fields: [fieldname],
filters: filters,
start: 0,

View File

@ -74,17 +74,22 @@ module.exports = class sqliteDatabase extends Database {
columns.push(def);
if (field.fieldtype === 'Link' && field.target) {
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`);
let meta = frappe.getMeta(field.target);
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${meta.getBaseDocType()} ON UPDATE CASCADE ON DELETE RESTRICT`);
}
}
getColumnDefinition(field) {
let defaultValue = field.default;
if (typeof defaultValue === 'string') {
defaultValue = `'${defaultValue}'`
}
let def = [
field.fieldname,
this.typeMap[field.fieldtype],
field.fieldname === 'name' ? 'PRIMARY KEY NOT NULL' : '',
field.required ? 'NOT NULL' : '',
field.default ? `DEFAULT ${field.default}` : ''
field.default ? `DEFAULT ${defaultValue}` : ''
].join(' ');
return def;
@ -103,9 +108,11 @@ module.exports = class sqliteDatabase extends Database {
}
getOne(doctype, name, fields = '*') {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
fields = this.prepareFields(fields);
return new Promise((resolve, reject) => {
this.conn.get(`select ${fields} from ${doctype}
this.conn.get(`select ${fields} from ${baseDoctype}
where name = ?`, name,
(err, row) => {
resolve(row || {});
@ -159,8 +166,16 @@ module.exports = class sqliteDatabase extends Database {
await frappe.db.run('delete from SingleValue where parent=?', name)
}
async rename(doctype, oldName, newName) {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
await frappe.db.run(`update ${baseDoctype} set name = ? where name = ?`, [newName, oldName]);
await frappe.db.commit();
}
async setValues(doctype, name, fieldValuePair) {
const meta = frappe.getMeta(doctype);
const baseDoctype = meta.getBaseDocType();
const validFields = this.getKeys(doctype);
const validFieldnames = validFields.map(df => df.fieldname);
const fieldsToUpdate = Object.keys(fieldValuePair)
@ -179,22 +194,27 @@ module.exports = class sqliteDatabase extends Database {
// additional name for where clause
values.push(name);
return await this.run(`update ${doctype}
return await this.run(`update ${baseDoctype}
set ${assigns.join(', ')} where name=?`, values);
}
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
if (!fields) {
fields = frappe.getMeta(doctype).getKeywordFields();
fields = meta.getKeywordFields();
}
if (typeof fields === 'string') {
fields = [fields];
}
if (meta.filters) {
filters = Object.assign({}, filters, meta.filters);
}
return new Promise((resolve, reject) => {
let conditions = this.getFilterConditions(filters);
let query = `select ${fields.join(", ")}
from ${doctype}
from ${baseDoctype}
${conditions.conditions ? "where" : ""} ${conditions.conditions}
${groupBy ? ("group by " + groupBy.join(', ')) : ""}
${orderBy ? ("order by " + orderBy) : ""} ${orderBy ? (order || "asc") : ""}

View File

@ -46,31 +46,58 @@ module.exports = {
Object.assign(this.models, toAdd);
},
getDoctypeList(filters) {
let doctypeList = [];
if (filters && Object.keys(filters).length) {
for (let model in this.models) {
let doctypeName = model;
let doctype = this.models[doctypeName];
let matchedFields = 0;
for (let key in filters) {
let field = key;
let value = filters[field];
if (Boolean(doctype[field]) === Boolean(value)) {
matchedFields++;
}
}
if (matchedFields === Object.keys(filters).length)
doctypeList.push(doctypeName);
}
}
return doctypeList;
},
registerView(view, name, module) {
if (!this.views[view]) this.views[view] = {};
this.views[view][name] = module;
},
registerMethod({method, handler}) {
registerMethod({ method, handler }) {
this.methods[method] = handler;
if (this.app) {
// add to router if client-server
this.app.post(`/api/method/${method}`, this.asyncHandler(async function(request, response) {
this.app.post(
`/api/method/${method}`,
this.asyncHandler(async function(request, response) {
let data = await handler(request.body);
if (data === undefined) {
data = {}
data = {};
}
return response.json(data);
}));
})
);
}
},
async call({method, args}) {
async call({ method, args }) {
if (this.isServer) {
if (this.methods[method]) {
return await this.methods[method](args);
} else {
throw `${method} not found`;
throw new Error(`${method} not found`);
}
}
@ -78,7 +105,7 @@ module.exports = {
let response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(args || {})
@ -108,9 +135,22 @@ module.exports = {
}
},
removeFromCache(doctype, name) {
try {
delete this.docs[doctype][name];
} catch(e) {
console.warn(`Document ${doctype} ${name} does not exist`);
}
},
isDirty(doctype, name) {
return (this.docs && this.docs[doctype] && this.docs[doctype][name]
&& this.docs[doctype][name]._dirty) || false;
return (
(this.docs &&
this.docs[doctype] &&
this.docs[doctype][name] &&
this.docs[doctype][name]._dirty) ||
false
);
},
getDocFromCache(doctype, name) {
@ -123,7 +163,7 @@ module.exports = {
if (!this.metaCache[doctype]) {
let model = this.models[doctype];
if (!model) {
throw `${doctype} is not a registered doctype`;
throw new Error(`${doctype} is not a registered doctype`);
}
let metaClass = model.metaClass || this.BaseMeta;
this.metaCache[doctype] = new metaClass(model);
@ -135,7 +175,10 @@ module.exports = {
async getDoc(doctype, name) {
let doc = this.getDocFromCache(doctype, name);
if (!doc) {
doc = new (this.getDocumentClass(doctype))({doctype:doctype, name: name});
doc = new (this.getDocumentClass(doctype))({
doctype: doctype,
name: name
});
await doc.load();
this.addToCache(doc);
}
@ -169,13 +212,26 @@ module.exports = {
},
async getNewDoc(doctype) {
let doc = this.newDoc({doctype: doctype});
let doc = this.newDoc({ doctype: doctype });
doc._notInserted = true;
doc.name = this.getRandomString();
this.addToCache(doc);
return doc;
},
async newCustomDoc(fields) {
let doc = new this.BaseDocument({ isCustom: 1, fields });
doc._notInserted = true;
doc.name = this.getRandomString();
this.addToCache(doc);
return doc;
},
createMeta(fields) {
let meta = new this.BaseMeta({ isCustom: 1, fields });
return meta;
},
newDoc(data) {
let doc = new (this.getDocumentClass(data.doctype))(data);
doc.setDefaults();
@ -183,7 +239,7 @@ module.exports = {
},
async insert(data) {
return await (this.newDoc(data)).insert();
return await this.newDoc(data).insert();
},
async syncDoc(data) {
@ -203,14 +259,14 @@ module.exports = {
if (email === 'Administrator') {
this.session = {
user: 'Administrator'
}
};
return;
}
let response = await fetch(this.getServerURL() + '/api/login', {
method: 'POST',
headers: {
'Accept': 'application/json',
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
@ -222,7 +278,7 @@ module.exports = {
this.session = {
user: email,
token: res.token
}
};
return res;
}
@ -234,7 +290,7 @@ module.exports = {
let response = await fetch(this.getServerURL() + '/api/signup', {
method: 'POST',
headers: {
'Accept': 'application/json',
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, fullName, password })

View File

@ -8,17 +8,46 @@ module.exports = class BaseDocument extends Observable {
this.fetchValuesCache = {};
this.flags = {};
this.setup();
Object.assign(this, data);
this.setValues(data);
// clear fetch-values cache
frappe.db.on('change', (params) => this.fetchValuesCache[`${params.doctype}:${params.name}`] = {});
frappe.db.on(
'change',
params => (this.fetchValuesCache[`${params.doctype}:${params.name}`] = {})
);
}
setup() {
// add listeners
}
setValues(data) {
for (let fieldname in data) {
let value = data[fieldname];
if (fieldname.startsWith('_')) {
// private property
this[fieldname] = value;
} else if (Array.isArray(value)) {
for (let row of value) {
this.append(fieldname, row);
}
} else {
this[fieldname] = value;
}
}
// set unset fields as null
for (let field of this.meta.getValidFields()) {
// check for null or undefined
if (this[field.fieldname] == null) {
this[field.fieldname] = null;
}
}
}
get meta() {
if (this.isCustom) {
this._meta = frappe.createMeta(this.fields);
}
if (!this._meta) {
this._meta = frappe.getMeta(this.doctype);
}
@ -44,7 +73,14 @@ module.exports = class BaseDocument extends Observable {
if (this[fieldname] !== value) {
this._dirty = true;
if (Array.isArray(value)) {
this[fieldname] = [];
for (let row of value) {
this.append(fieldname, row);
}
} else {
this[fieldname] = await this.validateField(fieldname, value);
}
await this.applyChange(fieldname);
}
}
@ -53,21 +89,22 @@ module.exports = class BaseDocument extends Observable {
if (await this.applyFormula()) {
// multiple changes
await this.trigger('change', {
doc: this
doc: this,
changed: fieldname
});
} else {
// no other change, trigger control refresh
await this.trigger('change', {
doc: this,
fieldname: fieldname
fieldname: fieldname,
changed: fieldname
});
}
}
setDefaults() {
for (let field of this.meta.fields) {
if (this[field.fieldname] === null || this[field.fieldname] === undefined) {
if (this[field.fieldname] == null) {
let defaultValue = null;
if (field.fieldtype === 'Table') {
@ -80,6 +117,10 @@ module.exports = class BaseDocument extends Observable {
this[field.fieldname] = defaultValue;
}
}
if (this.meta.basedOn && this.meta.filters) {
this.setValues(this.meta.filters);
}
}
setKeywords() {
@ -90,18 +131,34 @@ module.exports = class BaseDocument extends Observable {
this.keywords = keywords.join(', ');
}
append(key, document) {
append(key, document = {}) {
if (!this[key]) {
this[key] = [];
}
this[key].push(this.initDoc(document));
this[key].push(this._initChild(document, key));
this._dirty = true;
this.applyChange(key);
}
initDoc(data) {
if (data.prototype instanceof Document) {
_initChild(data, key) {
if (data instanceof BaseDocument) {
return data;
} else {
return new Document(data);
data.doctype = this.meta.getField(key).childtype;
data.parent = this.name;
data.parenttype = this.doctype;
data.parentfield = key;
data.parentdoc = this;
if (!data.idx) {
data.idx = (this[key] || []).length;
}
if (!data.name) {
data.name = frappe.getRandomString();
}
return new BaseDocument(data);
}
}
@ -129,7 +186,7 @@ module.exports = class BaseDocument extends Observable {
setStandardValues() {
// set standard values on server-side only
if (frappe.isServer) {
let now = (new Date()).toISOString();
let now = new Date().toISOString();
if (!this.submitted) {
this.submitted = 0;
}
@ -157,13 +214,15 @@ module.exports = class BaseDocument extends Observable {
this.setDefaults();
}
} else {
throw new frappe.errors.NotFound(`Not Found: ${this.doctype} ${this.name}`);
throw new frappe.errors.NotFound(
`Not Found: ${this.doctype} ${this.name}`
);
}
}
syncValues(data) {
this.clearValues();
Object.assign(this, data);
this.setValues(data);
this._dirty = false;
this.trigger('change', {
doc: this
@ -195,11 +254,18 @@ module.exports = class BaseDocument extends Observable {
// check for conflict
if (currentDoc && this.modified != currentDoc.modified) {
throw new frappe.errors.Conflict(frappe._('Document {0} {1} has been modified after loading', [this.doctype, this.name]));
throw new frappe.errors.Conflict(
frappe._('Document {0} {1} has been modified after loading', [
this.doctype,
this.name
])
);
}
if (this.submitted && !this.meta.isSubmittable) {
throw new frappe.errors.ValidationError(frappe._('Document type {1} is not submittable', [this.doctype]));
throw new frappe.errors.ValidationError(
frappe._('Document type {1} is not submittable', [this.doctype])
);
}
// set submit action flag
@ -210,7 +276,6 @@ module.exports = class BaseDocument extends Observable {
if (currentDoc.submitted && !this.submitted) {
this.flags.revertAction = true;
}
}
}
@ -223,9 +288,10 @@ module.exports = class BaseDocument extends Observable {
// children
for (let tablefield of this.meta.getTableFields()) {
let formulaFields = frappe.getMeta(tablefield.childtype).getFormulaFields();
let formulaFields = frappe
.getMeta(tablefield.childtype)
.getFormulaFields();
if (formulaFields.length) {
// for each row
for (let row of this[tablefield.fieldname]) {
for (let field of formulaFields) {
@ -240,10 +306,15 @@ module.exports = class BaseDocument extends Observable {
}
}
// parent
// parent or child row
for (let field of this.meta.getFormulaFields()) {
if (shouldApplyFormula(field, doc)) {
const val = await field.formula(doc);
let val;
if (this.meta.isChild) {
val = await field.formula(doc, this.parentdoc);
} else {
val = await field.formula(doc);
}
if (val !== false && val !== undefined) {
doc[field.fieldname] = val;
}
@ -280,9 +351,14 @@ module.exports = class BaseDocument extends Observable {
await this.commit();
await this.trigger('beforeInsert');
let oldName = this.name;
const data = await frappe.db.insert(this.doctype, this.getValidDict());
this.syncValues(data);
if (oldName !== this.name) {
frappe.removeFromCache(this.doctype, oldName);
}
await this.trigger('afterInsert');
await this.trigger('afterSave');
@ -327,6 +403,13 @@ module.exports = class BaseDocument extends Observable {
this.update();
}
async rename(newName) {
await this.trigger('beforeRename');
await frappe.db.rename(this.doctype, this.name, newName);
this.name = newName;
await this.trigger('afterRename');
}
// trigger methods on the class if they match
// with the trigger name
async trigger(event, params) {
@ -338,12 +421,16 @@ module.exports = class BaseDocument extends Observable {
// helper functions
getSum(tablefield, childfield) {
return this[tablefield].map(d => (d[childfield] || 0)).reduce((a, b) => a + b, 0);
return this[tablefield]
.map(d => d[childfield] || 0)
.reduce((a, b) => a + b, 0);
}
async getFrom(doctype, name, fieldname) {
if (!name) return '';
let _values = this.fetchValuesCache[`${doctype}:${name}`] || (this.fetchValuesCache[`${doctype}:${name}`] = {});
let _values =
this.fetchValuesCache[`${doctype}:${name}`] ||
(this.fetchValuesCache[`${doctype}:${name}`] = {});
if (!_values[fieldname]) {
_values[fieldname] = await frappe.db.getValue(doctype, name, fieldname);
}

View File

@ -1,10 +1,18 @@
const BaseDocument = require('./document');
const frappe = require('frappejs');
const model = require('./index')
const model = require('./index');
const indicatorColor = require('frappejs/ui/constants/indicators');
module.exports = class BaseMeta extends BaseDocument {
constructor(data) {
if (data.basedOn) {
let config = frappe.models[data.basedOn];
Object.assign(data, config, {
name: data.name,
label: data.label,
filters: data.filters
});
}
super(data);
this.setDefaultIndicators();
if (this.setupMeta) {
@ -15,6 +23,20 @@ module.exports = class BaseMeta extends BaseDocument {
}
}
setValues(data) {
Object.assign(this, data);
if (!this.fields.find(df => df.fieldname === 'name')) {
this.fields = [
{
label: frappe._('Name'),
fieldname: 'name',
fieldtype: 'Data',
required: 1
}
].concat(this.fields);
}
}
hasField(fieldname) {
return this.getField(fieldname) ? true : false;
}
@ -49,25 +71,28 @@ module.exports = class BaseMeta extends BaseDocument {
}
getLabel(fieldname) {
return this.getField(fieldname).label;
let df = this.getField(fieldname);
return df.getLabel || df.label;
}
getTableFields() {
if (this._tableFields===undefined) {
this._tableFields = this.fields.filter(field => field.fieldtype === 'Table');
if (this._tableFields === undefined) {
this._tableFields = this.fields.filter(
field => field.fieldtype === 'Table'
);
}
return this._tableFields;
}
getFormulaFields() {
if (this._formulaFields===undefined) {
if (this._formulaFields === undefined) {
this._formulaFields = this.fields.filter(field => field.formula);
}
return this._formulaFields;
}
hasFormula() {
if (this._hasFormula===undefined) {
if (this._hasFormula === undefined) {
this._hasFormula = false;
if (this.getFormulaFields().length) {
this._hasFormula = true;
@ -83,6 +108,10 @@ module.exports = class BaseMeta extends BaseDocument {
return this._hasFormula;
}
getBaseDocType() {
return this.basedOn || this.name;
}
async set(fieldname, value) {
this[fieldname] = value;
await this.trigger(fieldname);
@ -94,39 +123,51 @@ module.exports = class BaseMeta extends BaseDocument {
getValidFields({ withChildren = true } = {}) {
if (!this._validFields) {
this._validFields = [];
this._validFieldsWithChildren = [];
const _add = (field) => {
const _add = field => {
this._validFields.push(field);
this._validFieldsWithChildren.push(field);
}
};
const doctype_fields = this.fields.map((field) => field.fieldname);
const doctype_fields = this.fields.map(field => field.fieldname);
// standard fields
for (let field of model.commonFields) {
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
if (
frappe.db.typeMap[field.fieldtype] &&
!doctype_fields.includes(field.fieldname)
) {
_add(field);
}
}
if (this.isSubmittable) {
_add({fieldtype:'Check', fieldname: 'submitted', label: frappe._('Submitted')})
_add({
fieldtype: 'Check',
fieldname: 'submitted',
label: frappe._('Submitted')
});
}
if (this.isChild) {
// child fields
for (let field of model.childFields) {
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
if (
frappe.db.typeMap[field.fieldtype] &&
!doctype_fields.includes(field.fieldname)
) {
_add(field);
}
}
} else {
// parent fields
for (let field of model.parentFields) {
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
if (
frappe.db.typeMap[field.fieldtype] &&
!doctype_fields.includes(field.fieldname)
) {
_add(field);
}
}
@ -135,7 +176,10 @@ module.exports = class BaseMeta extends BaseDocument {
if (this.isTree) {
// tree fields
for (let field of model.treeFields) {
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
if (
frappe.db.typeMap[field.fieldtype] &&
!doctype_fields.includes(field.fieldname)
) {
_add(field);
}
}
@ -167,15 +211,24 @@ module.exports = class BaseMeta extends BaseDocument {
if (!this._keywordFields) {
this._keywordFields = this.keywordFields;
if (!(this._keywordFields && this._keywordFields.length && this.fields)) {
this._keywordFields = this.fields.filter(field => field.fieldtype !== 'Table' && field.required).map(field => field.fieldname);
this._keywordFields = this.fields
.filter(field => field.fieldtype !== 'Table' && field.required)
.map(field => field.fieldname);
}
if (!(this._keywordFields && this._keywordFields.length)) {
this._keywordFields = ['name']
this._keywordFields = ['name'];
}
}
return this._keywordFields;
}
getQuickEditFields() {
if (this.quickEditFields) {
return this.quickEditFields.map(fieldname => this.getField(fieldname));
}
return this.getFieldsWith({ required: 1 });
}
validateSelect(field, value) {
let options = field.options;
if (!options) return;
@ -185,7 +238,9 @@ module.exports = class BaseMeta extends BaseDocument {
options = field.options.split('\n');
}
if (!options.includes(value)) {
throw new frappe.errors.ValueError(`${value} must be one of ${options.join(", ")}`);
throw new frappe.errors.ValueError(
`${value} must be one of ${options.join(', ')}`
);
}
return value;
}
@ -208,7 +263,7 @@ module.exports = class BaseMeta extends BaseDocument {
0: indicatorColor.GRAY,
1: indicatorColor.BLUE
}
}
};
}
}
}
@ -229,4 +284,4 @@ module.exports = class BaseMeta extends BaseDocument {
}
}
}
}
};

View File

@ -8,15 +8,16 @@
},
"scripts": {
"test": "NODE_ENV=test mocha --timeout 3000 tests",
"test-watch": "NODE_ENV=test mocha --timeout 3000 tests --watch --reporter=min",
"start": "nodemon app.js"
},
"dependencies": {
"awesomplete": "^1.1.2",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"bcrypt": "^2.0.1",
"bcrypt": "^3.0.6",
"body-parser": "^1.18.2",
"bootstrap": "^4.1.2",
"bootstrap": "^4.3.1",
"case-sensitive-paths-webpack-plugin": "^2.1.2",
"codemirror": "^5.35.0",
"commander": "^2.13.0",
@ -25,44 +26,50 @@
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"deepmerge": "^2.1.0",
"electron": "2.0.12",
"electron-builder": "^20.28.4",
"electron": "5.0.0",
"electron-builder": "^21.0.15",
"electron-debug": "^2.0.0",
"electron-devtools-installer": "^2.2.4",
"express": "^4.16.2",
"file-saver": "^2.0.2",
"feather-icons": "^4.7.3",
"file-loader": "^1.1.11",
"flatpickr": "^4.3.2",
"frappe-datatable": "^1.3.1",
"flatpickr": "^4.6.2",
"frappe-datatable": "^1.13.5",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"jquery": "^3.3.1",
"jwt-simple": "^0.5.1",
"luxon": "^1.0.0",
"popper.js": "^1.14.3",
"mkdirp": "^0.5.1",
"morgan": "^1.9.0",
"multer": "^1.3.1",
"mysql": "^2.15.0",
"node-fetch": "^1.7.3",
"node-sass": "^4.7.2",
"node-sass": "^4.12.0",
"nunjucks": "^3.1.0",
"octicons": "^7.2.0",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"puppeteer": "^1.2.0",
"puppeteer-core": "^1.19.0",
"sass-loader": "^7.0.3",
"sharp": "^0.20.8",
"sharp": "^0.23.0",
"showdown": "^1.8.6",
"socket.io": "^2.0.4",
"sqlite3": "^4.0.2",
"vue": "^2.5.16",
"vue-flatpickr-component": "^7.0.4",
"sqlite3": "^4.0.9",
"vue": "^2.6.10",
"vue-flatpickr-component": "^8.1.2",
"vue-loader": "^15.2.6",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.16",
"vue-router": "^3.0.7",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.16.1",
"webpack-dev-server": "^3.1.4",
"webpack-hot-middleware": "^2.22.3"
"webpack-hot-middleware": "^2.22.3",
"csvjson-csv2json": "5.0.6",
"postcss-loader": "3.0.0",
"tailwindcss": "1.1.1",
"autoprefixer": "9.6.1"
},
"repository": {
"type": "git",

View File

@ -1,5 +1,5 @@
const frappe = require('frappejs');
const puppeteer = require('puppeteer');
const puppeteer = require('puppeteer-core');
const fs = require('fs');
const path = require('path');
const { getTmpDir } = require('frappejs/server/utils');
@ -11,8 +11,9 @@ async function makePDF(html, filepath) {
const page = await browser.newPage();
await page.setContent(html);
await page.addStyleTag({
url: 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css'
})
url:
'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css'
});
await page.pdf({
path: filepath,
format: 'A4'
@ -23,15 +24,27 @@ async function makePDF(html, filepath) {
async function getPDFForElectron(doctype, name, destination, htmlContent) {
const { remote, shell } = require('electron');
const { BrowserWindow } = remote;
const html = htmlContent || await getHTML(doctype, name);
const filepath = path.join(destination, name + '.pdf');
const html = htmlContent || (await getHTML(doctype, name));
if (!destination) {
destination =
process.env.NODE_ENV === 'development'
? path.resolve('.')
: remote.getGlobal('documentsPath');
}
const fs = require('fs')
const filepath = path.resolve(
path.join(destination, '/frappe-accounting/' + name + '.pdf')
);
const fs = require('fs');
let printWindow = new BrowserWindow({
width: 600,
height: 800,
show: false
})
});
const __static = remote.getGlobal('__static') || __static;
printWindow.loadURL(`file://${path.join(__static, 'print.html')}`);
printWindow.on('closed', () => {
@ -46,20 +59,23 @@ async function getPDFForElectron(doctype, name, destination, htmlContent) {
const printPromise = new Promise(resolve => {
printWindow.webContents.on('did-finish-load', () => {
printWindow.webContents.printToPDF({
printWindow.webContents.printToPDF(
{
marginsType: 1, // no margin
pageSize: 'A4',
printBackground: true
}, (error, data) => {
if (error) throw error
},
(error, data) => {
if (error) throw error;
printWindow.close();
fs.writeFile(filepath, data, (error) => {
if (error) throw error
fs.writeFile(filepath, data, error => {
if (error) throw error;
resolve(shell.openItem(filepath));
})
})
})
})
});
}
);
});
});
await printPromise;
// await makePDF(html, filepath);
@ -75,14 +91,20 @@ async function handlePDFRequest(req, res) {
const { doctype, name } = args;
const html = await getHTML(doctype, name);
const filepath = path.join(getTmpDir(), `frappe-pdf-${getRandomString()}.pdf`);
const filepath = path.join(
getTmpDir(),
`frappe-pdf-${getRandomString()}.pdf`
);
await makePDF(html, filepath);
const file = fs.createReadStream(filepath);
const stat = fs.statSync(filepath);
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename=${path.basename(filepath)}`);
res.setHeader(
'Content-Disposition',
`attachment; filename=${path.basename(filepath)}`
);
file.pipe(res);
}
@ -90,4 +112,4 @@ module.exports = {
makePDF,
setupExpressRoute,
getPDFForElectron
}
};

View File

@ -1,6 +1,7 @@
const assert = require('assert');
const frappe = require('frappejs');
const helpers = require('./helpers');
const BaseDocument = require('frappejs/model/document');
describe('Document', () => {
before(async function() {
@ -118,6 +119,46 @@ describe('Document', () => {
assert.equal(user.roles[0].parentfield, 'roles');
});
it('should convert children objects to BaseDocument', async () => {
if (!await frappe.db.exists('Role', 'Test Role 1')) {
await frappe.insert({doctype: 'Role', name: 'Test Role'});
await frappe.insert({doctype: 'Role', name: 'Test Role 1'});
}
let user = frappe.newDoc({
doctype: 'User',
name: frappe.getRandomString(),
fullName: 'Test User',
password: frappe.getRandomString(),
roles: [
{
role: 'Test Role'
}
]
});
await user.insert();
assert.ok(user.roles[0] instanceof BaseDocument);
assert.equal(user.roles[0].parent, user.name);
assert.equal(user.roles[0].parenttype, 'User');
assert.equal(user.roles[0].parentfield, 'roles');
assert.equal(user.roles[0].idx, 0);
user.append('roles', { role: 'Test Role 1'});
assert.equal(user.roles[1].role, 'Test Role 1');
assert.ok(user.roles[1] instanceof BaseDocument);
assert.equal(user.roles[1].parent, user.name);
assert.equal(user.roles[1].parenttype, 'User');
assert.equal(user.roles[1].parentfield, 'roles');
assert.equal(user.roles[1].idx, 1);
user.set('roles', [{ role: 'Test Role' }]);
assert.equal(user.roles.length, 1);
assert.ok(user.roles[0] instanceof BaseDocument);
assert.equal(user.roles[0].parent, user.name);
assert.equal(user.roles[0].parenttype, 'User');
assert.equal(user.roles[0].parentfield, 'roles');
assert.equal(user.roles[0].idx, 0);
});
});
function test_doc() {

View File

@ -12,7 +12,7 @@ const common = require('frappejs/common');
var test_server;
describe.only('REST', () => {
describe('REST', () => {
before(async function() {
test_server = spawn('node', ['tests/test_server.js'], { stdio: 'inherit' });

View File

@ -40,7 +40,8 @@ export default {
notFound: false,
invalid: false,
invalidFields: [],
links: []
links: [],
defaults: this.defaultValues
};
},
computed: {
@ -52,25 +53,36 @@ export default {
}
},
async created() {
if (!this.defaults) {
this.defaults = {};
}
this.meta.fields.forEach(field => {
if (field.defaultValue)
this.defaults[field.fieldname] = field.defaultValue;
});
if (!this.name) return;
try {
this.doc = await frappe.getDoc(this.doctype, this.name);
if (this.doc.isNew() && this.meta.fields.map(df => df.fieldname).includes('name')) {
if (
this.doc.isNew() &&
this.meta.fields.map(df => df.fieldname).includes('name')
) {
// For a user editable name field,
// it should be unset since it is autogenerated
this.doc.set('name', '');
}
if (this.defaultValues) {
for (let fieldname in this.defaultValues) {
const value = this.defaultValues[fieldname];
this.doc.set(fieldname, value);
if (this.doc.isNew() && this.defaults) {
for (let fieldname in this.defaults) {
const value = this.defaults[fieldname];
await this.doc.set(fieldname, value);
}
}
this.docLoaded = true;
} catch (e) {
console.error(e);
this.notFound = true;
}
this.setLinks();
@ -89,7 +101,6 @@ export default {
}
this.$emit('save', this.doc);
} catch (e) {
console.error(e);
return;
@ -112,13 +123,23 @@ export default {
},
async submit() {
this.doc.set('submitted', 1);
await this.doc.set('submitted', 1);
try {
await this.save();
} catch (e) {
await this.doc.set('submitted', 0);
await this.doc.set('_dirty', false);
}
},
async revert() {
this.doc.set('submitted', 0);
await this.doc.set('submitted', 0);
try {
await this.save();
} catch (e) {
await this.doc.set('submitted', 1);
await this.doc.set('_dirty', false);
}
},
print() {

View File

@ -2,7 +2,12 @@
<div class="frappe-form-actions d-flex justify-content-between align-items-center">
<h5 class="m-0">{{ title }}</h5>
<div class="d-flex">
<f-button primary v-if="showSave" :disabled="disableSave" @click="$emit('save')">{{ _('Save') }}</f-button>
<f-button
primary
v-if="showSave"
:disabled="disableSave"
@click="$emit('save')"
>{{ _('Save') }}</f-button>
<f-button primary v-if="showSubmit" @click="$emit('submit')">{{ _('Submit') }}</f-button>
<f-button secondary v-if="showRevert" @click="$emit('revert')">{{ _('Revert') }}</f-button>
<div class="ml-2" v-if="showPrint">
@ -31,7 +36,7 @@ export default {
showNextAction: false,
showPrint: false,
disableSave: false
}
};
},
created() {
this.doc.on('change', () => {
@ -44,36 +49,45 @@ export default {
this.isDirty = this.doc._dirty;
this.showSubmit =
this.meta.isSubmittable
&& !this.isDirty
&& !this.doc.isNew()
&& this.doc.submitted === 0;
this.meta.isSubmittable &&
!this.isDirty &&
!this.doc.isNew() &&
this.doc.submitted === 0;
this.showRevert =
this.meta.isSubmittable
&& !this.isDirty
&& !this.doc.isNew()
&& this.doc.submitted === 1;
this.meta.isSubmittable &&
!this.isDirty &&
!this.doc.isNew() &&
this.doc.submitted === 1;
this.showNextAction = 1
this.showNextAction = 1;
this.showNextAction =
!this.doc.isNew()
&& this.links.length;
this.showNextAction = !this.doc.isNew() && this.links.length;
this.showPrint =
this.doc.submitted === 1
&& this.meta.print
this.showPrint = this.doc.submitted === 1 && this.meta.print;
this.showSave =
this.doc.isNew() ?
true :
this.meta.isSubmittable ?
(this.isDirty ? true : false) :
true;
this.showSave = this.doc.isNew()
? true
: this.meta.isSubmittable
? this.isDirty
? true
: false
: true;
this.disableSave =
this.doc.isNew() ? false : !this.isDirty;
this.disableSave = this.doc.isNew() ? false : !this.isDirty;
},
getFormTitle() {
const _ = this._;
try {
return _(
this.meta.getFormTitle(this.doc) ||
this.meta.label ||
this.doc.doctype
);
} catch (e) {
return _(this.meta.label || this.doc.doctype);
}
}
},
computed: {
@ -84,12 +98,12 @@ export default {
const _ = this._;
if (this.doc.isNew()) {
return _('New {0}', _(this.doc.doctype));
return _('New {0}', this.getFormTitle());
}
const titleField = this.meta.titleField || 'name';
return this.doc[titleField];
}
}
}
};
</script>

View File

@ -1,17 +1,21 @@
<template>
<form :class="['frappe-form-layout', { 'was-validated': invalid }]">
<div class="form-row" v-if="layoutConfig"
v-for="(section, i) in layoutConfig.sections" :key="i"
v-show="showSection(i)"
<div
class="form-row"
v-if="layoutConfig && showSection(i)"
v-for="(section, i) in layoutConfig.sections"
:key="i"
>
<div class="col" v-for="(column, j) in section.columns" :key="j">
<frappe-control
v-for="fieldname in column.fields"
ref="frappe-control"
v-for="(fieldname, k) in column.fields"
v-if="shouldRenderField(fieldname)"
:key="fieldname"
:key="getDocField(fieldname).label"
:docfield="getDocField(fieldname)"
:value="$data[fieldname]"
:doc="doc"
:autofocus="doc.isNew() && (i === currentSection || i === 0) && j === 0 && k === 0 && !$data[fieldname]"
@change="value => updateDoc(fieldname, value)"
/>
</div>
@ -45,14 +49,27 @@ export default {
this[df.fieldname] = doc[df.fieldname];
});
}
this.updateLabels();
});
},
methods: {
updateLabels() {
this.$refs['frappe-control'].forEach(control => {
control.docfield.label = control.docfield.getLabel
? control.docfield.getLabel(this.doc)
: control.docfield.label;
});
},
getDocField(fieldname) {
return this.fields.find(df => df.fieldname === fieldname);
},
shouldRenderField(fieldname) {
const hidden = Boolean(this.getDocField(fieldname).hidden);
let hidden;
try {
hidden = Boolean(this.getDocField(fieldname).hidden(this.doc));
} catch (e) {
hidden = Boolean(this.getDocField(fieldname).hidden) || false;
}
if (hidden) {
return false;
@ -84,15 +101,17 @@ export default {
if (!layout) {
const fields = this.fields.map(df => df.fieldname);
layout = [{
layout = [
{
columns: [{ fields }]
}];
}
];
}
if (Array.isArray(layout)) {
layout = {
sections: layout
}
};
}
return layout;
}

View File

@ -7,7 +7,9 @@
@delete="deleteCheckedItems"
/>
<ul class="list-group">
<list-item v-for="doc of data" :key="doc.name"
<list-item
v-for="doc of data"
:key="doc.name"
:id="doc.name"
:isActive="doc.name === $route.params.name"
:isChecked="isChecked(doc.name)"
@ -15,9 +17,7 @@
@checkItem="toggleCheck(doc.name)"
>
<indicator v-if="hasIndicator" :color="getIndicatorColor(doc)" />
<span class="d-inline-block ml-2">
{{ doc[meta.titleField || 'name'] }}
</span>
<span class="d-inline-block ml-2">{{ doc[meta.titleField || 'name'] }}</span>
</list-item>
</ul>
</div>

View File

@ -1,8 +1,8 @@
import ModalContainer from './ModalContainer';
import frappe from 'frappejs';
const Plugin = {
install (Vue) {
install(Vue) {
this.event = new Vue();
Vue.prototype.$modal = {
@ -13,13 +13,15 @@ const Plugin = {
hide(id) {
Plugin.event.$emit('hide', id);
}
}
};
frappe.showModal = Vue.prototype.$modal.show;
// create modal container
const div = document.createElement('div');
document.body.appendChild(div);
new Vue({ render: h => h(ModalContainer) }).$mount(div);
}
}
};
export default Plugin;

View File

@ -6,6 +6,11 @@ export default {
}
return this.getWrapperElement(h);
},
data() {
return {
label: this.docfield.label
};
},
props: {
docfield: Object,
value: [String, Number, Array, FileList],
@ -14,7 +19,8 @@ export default {
default: false
},
disabled: Boolean,
autofocus: Boolean
autofocus: Boolean,
doc: Object
},
mounted() {
if (this.autofocus) {
@ -23,8 +29,13 @@ export default {
},
computed: {
id() {
return this.docfield.fieldname + '-'
+ document.querySelectorAll(`[data-fieldname="${this.docfield.fieldname}"]`).length;
return (
this.docfield.fieldname +
'-' +
document.querySelectorAll(
`[data-fieldname="${this.docfield.fieldname}"]`
).length
);
},
inputClass() {
return [];
@ -38,16 +49,28 @@ export default {
},
methods: {
getWrapperElement(h) {
return h('div', {
class: ['form-group', this.onlyInput ? 'mb-0' : '', ...this.wrapperClass],
return h(
'div',
{
class: [
'form-group',
this.onlyInput ? 'mb-0' : '',
...this.wrapperClass
],
attrs: {
'data-fieldname': this.docfield.fieldname,
'data-fieldtype': this.docfield.fieldtype
}
}, this.getChildrenElement(h));
},
this.getChildrenElement(h)
);
},
getChildrenElement(h) {
return [this.getLabelElement(h), this.getInputElement(h)]
return [
this.getLabelElement(h),
this.getInputElement(h),
this.getDescriptionElement(h)
];
},
getLabelElement(h) {
return h('label', {
@ -56,38 +79,57 @@ export default {
for: this.id
},
domProps: {
textContent: this.docfield.label
textContent: this.label
}
});
},
getDescriptionElement(h) {
return h('small', {
class: ['form-text', 'text-muted'],
domProps: {
textContent: this.docfield.description || ''
}
});
},
getInputElement(h) {
return h(this.getInputTag(), {
return h(
this.getInputTag(),
{
class: this.getInputClass(),
attrs: this.getInputAttrs(),
on: this.getInputListeners(),
domProps: this.getDomProps(),
ref: 'input'
}, this.getInputChildren(h));
},
this.getInputChildren(h)
);
},
getInputTag() {
return 'input';
},
getFormControlSize() {
return this.docfield.size === 'small'
? 'form-control-sm'
: this.size === 'large'
? 'form-control-lg'
: '';
},
getInputClass() {
return ['form-control', ...this.inputClass];
return ['form-control', this.getFormControlSize(), ...this.inputClass];
},
getInputAttrs() {
return {
id: this.id,
type: 'text',
placeholder: '',
placeholder: this.docfield.placeholder || '',
value: this.value,
required: this.docfield.required,
disabled: this.disabled
}
};
},
getInputListeners() {
return {
change: (e) => {
change: e => {
this.handleChange(e.target.value);
}
};
@ -104,9 +146,7 @@ export default {
this.$refs.input.setCustomValidity(isValid === false ? 'error' : '');
this.$emit('change', value);
},
getValueFromInput(e) {
},
getValueFromInput(e) {},
validate() {
return true;
},
@ -114,5 +154,5 @@ export default {
return value;
}
}
}
};
</script>

View File

@ -3,10 +3,12 @@
<label v-if="!onlyInput">{{ docfield.label }}</label>
<flat-pickr
class="form-control"
:class="getFormControlSize()"
:placeholder="docfield.placeholder"
:value="value"
:config="config"
@on-change="emitChange">
</flat-pickr>
@on-change="emitChange"
></flat-pickr>
</div>
</template>
<script>
@ -27,5 +29,8 @@ export default {
};
</script>
<style lang="scss">
@import "~flatpickr/dist/flatpickr.css";
@import '~flatpickr/dist/flatpickr.css';
.flat-pickr-input {
background-color: #fff;
}
</style>

View File

@ -1,6 +1,14 @@
<script>
import Float from './Float';
export default {
extends: Float
}
extends: Float,
methods: {
parse(value) {
return frappe.format(value, 'Currency');
},
validate(value) {
return !isNaN(frappe.parseNumber(value));
}
}
};
</script>

View File

@ -6,7 +6,21 @@ export default {
props: {
config: {
type: Object,
default: () => ({})
default: () => {
let dateFormat = {
'dd/MM/yyyy': 'd/m/Y',
'MM/dd/yyyy': 'm/d/Y',
'dd-MM-yyyy': 'd-m-Y',
'MM-dd-yyyy': 'm-d-Y',
'yyyy-MM-dd': 'Y-m-d'
};
let altFormat = dateFormat[frappe.SystemSettings.dateFormat];
return {
altInput: true,
dateFormat: 'Y-m-d',
altFormat: altFormat
};
}
}
}
};

View File

@ -6,10 +6,12 @@
:onlyInput="onlyInput"
:disabled="isDisabled"
:autofocus="autofocus"
:doc="doc"
@change="$emit('change', $event)"
/>
</template>
<script>
import frappe from 'frappejs';
import Base from './Base';
import Autocomplete from './Autocomplete';
import Check from './Check';
@ -77,7 +79,7 @@ export default {
return this.doc[reference];
}
};
},
}
};
</script>
<style scoped>

View File

@ -10,9 +10,9 @@ export default {
extends: Autocomplete,
methods: {
async getList(query) {
let filters = this.docfield.getFilters ?
this.docfield.getFilters(query) :
null;
let filters = this.docfield.getFilters
? this.docfield.getFilters(query, this.doc)
: null;
if (query) {
if (!filters) filters = {};
@ -42,6 +42,7 @@ export default {
}))
.concat({
label: plusIcon + ' New ' + this.getTarget(),
filters,
value: '__newItem'
});
},
@ -138,18 +139,23 @@ export default {
},
onItemClick(item) {
if (item.value === '__newItem') {
this.openFormModal();
this.openFormModal(item.filters);
} else {
this.handleChange(item.value);
}
},
async openFormModal() {
async openFormModal(filters) {
const input = this.$refs.input;
const newDoc = await frappe.getNewDoc(this.getTarget());
let defaultValues = {};
if (filters) {
for (let key of Object.keys(filters)) {
defaultValues[key] = filters[key];
}
}
defaultValues.name = input.value !== '__newItem' ? input.value : null;
this.$formModal.open(newDoc, {
defaultValues: {
name: input.value !== '__newItem' ? input.value : null
},
defaultValues,
onClose: () => {
// if new doc was not created
// then reset the input value

View File

@ -18,7 +18,9 @@ export default {
h('option', {
attrs: {
key: option,
selected: option === this.value
value: option,
disabled: option.indexOf('...') > -1,
selected: option.indexOf('...') > -1 || option === this.value
},
domProps: {
textContent: option
@ -27,5 +29,5 @@ export default {
);
}
}
}
};
</script>

View File

@ -4,12 +4,10 @@
<thead>
<tr>
<th scope="col" width="60">
<input class="mr-2" type="checkbox" @change="toggleCheckAll">
<input class="mr-2" type="checkbox" @change="toggleCheckAll" />
<span>#</span>
</th>
<th scope="col" v-for="column in columns" :key="column.fieldname">
{{ column.label }}
</th>
<th scope="col" v-for="column in columns" :key="column.fieldname">{{ column.label }}</th>
</tr>
</thead>
<tbody v-if="rows.length">
@ -20,10 +18,12 @@
type="checkbox"
:checked="checkedRows.includes(i)"
@change="e => onCheck(e, i)"
>
/>
<span>{{ i + 1 }}</span>
</th>
<td v-for="column in columns" :key="column.fieldname"
<td
v-for="column in columns"
:key="column.fieldname"
tabindex="1"
:ref="column.fieldname + i"
@click="activateFocus(i, column.fieldname)"
@ -37,7 +37,10 @@
@keydown.down="focusBelowCell(i, column.fieldname)"
@keydown.esc="escOnCell(i, column.fieldname)"
>
<div class="table-cell" :class="{'active': isFocused(i, column.fieldname)}">
<div
class="table-cell"
:class="{'active': isFocused(i, column.fieldname),'p-1': isEditing(i, column.fieldname)}"
>
<frappe-control
v-if="isEditing(i, column.fieldname)"
:docfield="getDocfield(column.fieldname)"
@ -47,9 +50,11 @@
:autofocus="true"
@change="onCellChange(i, column.fieldname, $event)"
/>
<div class="text-truncate" v-else>
{{ row[column.fieldname] || '&nbsp;' }}
</div>
<div
class="text-truncate"
:data-fieldtype="column.fieldtype"
v-else
>{{ row[column.fieldname] || '&nbsp;' }}</div>
</div>
</td>
</tr>
@ -57,16 +62,14 @@
<tbody v-else>
<tr>
<td :colspan="columns.length + 1" class="text-center">
<div class="table-cell">
No Data
</div>
<div class="table-cell">No Data</div>
</td>
</tr>
</tbody>
</table>
<div class="table-actions" v-if="!disabled">
<f-button danger @click="removeCheckedRows" v-if="checkedRows.length">Remove</f-button>
<f-button light @click="addRow" v-if="!checkedRows.length">Add Row</f-button>
<f-button secondary @click="addRow" v-if="!checkedRows.length">Add Row</f-button>
</div>
</div>
</template>
@ -96,7 +99,12 @@ export default {
enterPressOnCell() {
const { index, fieldname } = this.currentlyFocused;
if (this.isEditing(index, fieldname)) {
// FIX: enter pressing on a cell with a value throws error.
// Problem: input gets undefined on deactivating
setTimeout(() => {
this.deactivateEditing();
}, 300);
this.activateFocus(index, fieldname);
} else {
this.activateEditing(index, fieldname);
@ -104,7 +112,10 @@ export default {
},
focusPreviousCell() {
let { index, fieldname } = this.currentlyFocused;
if (this.isFocused(index, fieldname) && !this.isEditing(index, fieldname)) {
if (
this.isFocused(index, fieldname) &&
!this.isEditing(index, fieldname)
) {
let pos = this._getColumnIndex(fieldname);
pos -= 1;
if (pos < 0) {
@ -120,8 +131,10 @@ export default {
},
focusNextCell() {
let { index, fieldname } = this.currentlyFocused;
if (this.isFocused(index, fieldname) && !this.isEditing(index, fieldname)) {
if (
this.isFocused(index, fieldname) &&
!this.isEditing(index, fieldname)
) {
let pos = this._getColumnIndex(fieldname);
pos += 1;
if (pos > this.columns.length - 1) {
@ -204,7 +217,10 @@ export default {
};
},
activateFocus(i, fieldname) {
this.deactivateEditing();
if (this.isFocused(i, fieldname) && this.isEditing(i, fieldname)) {
return;
}
// this.deactivateEditing();
const docfield = this.columns.find(c => c.fieldname === fieldname);
this.currentlyFocused = {
index: i,
@ -224,14 +240,13 @@ export default {
this.currentlyFocused = {};
}
},
addRow() {
async addRow() {
const rows = this.rows.slice();
const newRow = {
idx: rows.length
};
const newRow = { idx: rows.length };
for (let column of this.columns) {
newRow[column.fieldname] = null;
if (column.defaultValue) newRow[column.fieldname] = column.defaultValue;
else newRow[column.fieldname] = null;
}
rows.push(newRow);
@ -292,7 +307,7 @@ export default {
</script>
<style lang="scss" scoped>
td {
padding: 0;
padding: 0rem;
outline: none;
}
@ -322,4 +337,9 @@ td {
[data-fieldtype='Link'] .input-group-append {
display: none;
}
[data-fieldtype='Currency'],
[data-fieldtype='Float'] {
text-align: right !important;
}
</style>

View File

@ -10,15 +10,15 @@ export default {
return {
id: this.id,
required: this.docfield.required,
rows: 3,
rows: this.docfield.rows || 3,
disabled: this.disabled
};
},
getDomProps() {
return {
value: this.value
};
}
}
}
}
};
</script>

View File

@ -1,22 +1,26 @@
<template>
<div class="row pb-4">
<frappe-control class="col-4"
v-for="docfield in filters"
:key="docfield.fieldname"
<div class="d-flex px-1">
<div class="col-3" v-for="docfield in filterFields" :key="docfield.fieldname">
<frappe-control
v-if="shouldRenderField(docfield)"
:docfield="docfield"
:value="$data.filterValues[docfield.fieldname]"
:doc="$data.filterValues"
@change="updateValue(docfield.fieldname, $event)"/>
:onlyInput="true"
:doc="filterDoc"
@change="updateValue(docfield.fieldname, $event)"
class="mb-4"
/>
</div>
</div>
</template>
<script>
import FrappeControl from 'frappejs/ui/components/controls/FrappeControl';
export default {
props: ['filters', 'filterDefaults'],
props: ['filterFields', 'filterDoc', 'filterDefaults'],
data() {
const filterValues = {};
for (let filter of this.filters) {
for (let filter of this.filterFields) {
filterValues[filter.fieldname] =
this.filterDefaults[filter.fieldname] || null;
}
@ -32,7 +36,22 @@ export default {
}
},
methods: {
shouldRenderField(field) {
let hidden;
try {
hidden = Boolean(field.hidden(this.filterDoc));
} catch (e) {
hidden = Boolean(field.hidden) || false;
}
if (hidden) {
return false;
}
return true;
},
updateValue(fieldname, value) {
this.filterDoc.set(fieldname, value);
this.filterValues[fieldname] = value;
this.$emit('change', this.filterValues);
}

View File

@ -0,0 +1,16 @@
<template>
<div class="page-header pr-4 ml-0 pl-0 py-2 d-flex align-items-center border-bottom bg-white">
<div class="text-right ml-2" v-for="link of links" :key="link.label">
<f-button secondary @click="link.handler">{{ link.label }}</f-button>
</div>
</div>
</template>
<script>
export default {
props: ['links']
};
</script>
<style>
</style>

View File

@ -1,9 +1,20 @@
<template>
<div>
<div class="p-4">
<h4 class="pb-2">{{ reportConfig.title }}</h4>
<report-filters v-if="filtersExists" :filters="reportConfig.filterFields" :filterDefaults="filters" @change="getReportData"></report-filters>
<div class="pt-2" ref="datatable" v-once></div>
<div class="row pb-4">
<h4 class="col-6 d-flex">{{ reportConfig.title }}</h4>
<report-links class="col-6 d-flex pr-0 flex-row-reverse" v-if="linksExists" :links="links"></report-links>
</div>
<div class="row pb-4">
<report-filters
class="col-12 pr-0"
v-if="filtersExists"
:filters="reportConfig.filterFields"
:filterDefaults="filters"
@change="getReportData"
></report-filters>
</div>
<div class="pt-2 pr-3" ref="datatable" v-once></div>
</div>
<not-found v-if="!reportConfig" />
</div>
@ -11,7 +22,8 @@
<script>
import DataTable from 'frappe-datatable';
import frappe from 'frappejs';
import ReportFilters from './ReportFilters';
import ReportFilters from 'frappejs/ui/pages/Report/ReportFilters';
import ReportLinks from 'frappejs/ui/pages/Report/ReportLinks';
import utils from 'frappejs/client/ui/utils';
export default {
@ -20,8 +32,25 @@ export default {
computed: {
filtersExists() {
return (this.reportConfig.filterFields || []).length;
},
linksExists() {
return (this.reportConfig.linkFields || []).length;
}
},
watch: {
reportName() {
//FIX: Report's data forwards to next consecutively changed report
this.getReportData(this.filters);
}
},
data() {
return {
links: []
};
},
async created() {
this.setLinks();
},
methods: {
async getReportData(filters) {
let data = await frappe.call({
@ -48,7 +77,7 @@ export default {
columns = this.getColumns();
}
for(let column of columns) {
for (let column of columns) {
column.editable = false;
}
@ -57,9 +86,26 @@ export default {
} else {
this.datatable = new DataTable(this.$refs.datatable, {
columns: columns,
data: rows
data: rows,
treeView: this.reportConfig.treeView || false,
cellHeight: 35
});
}
return [rows, columns];
},
setLinks() {
if (this.linksExists) {
let links = [];
for (let link of this.reportConfig.linkFields) {
links.push({
label: link.label,
handler: () => {
link.action(this);
}
});
}
this.links = links;
}
},
getColumns(data) {
const columns = this.reportConfig.getColumns(data);
@ -67,9 +113,13 @@ export default {
}
},
components: {
ReportFilters
ReportFilters,
ReportLinks
}
};
</script>
<style>
.datatable {
font-size: 12px;
}
</style>

View File

@ -8,13 +8,10 @@ module.exports = {
if (typeof field === 'string') {
field = { fieldtype: field };
}
if (field.fieldtype === 'Currency') {
value = numberFormat.formatNumber(value);
} else if (field.fieldtype === 'Text') {
// value = markdown.makeHtml(value || '');
} else if (field.fieldtype === 'Date') {
let dateFormat;
if (!frappe.SystemSettings) {
@ -24,7 +21,13 @@ module.exports = {
}
value = luxon.DateTime.fromISO(value).toFormat(dateFormat);
if (value === 'Invalid DateTime') {
value = '';
}
} else if (field.fieldtype === 'Check') {
typeof parseInt(value) === 'number'
? (value = parseInt(value))
: (value = Boolean(value));
} else {
if (value === null || value === undefined) {
value = '';
@ -34,4 +37,4 @@ module.exports = {
}
return value;
}
}
};

View File

@ -1,15 +1,15 @@
const numberFormats = {
"#,###.##": { fractionSep: ".", groupSep: ",", precision: 2 },
"#.###,##": { fractionSep: ",", groupSep: ".", precision: 2 },
"# ###.##": { fractionSep: ".", groupSep: " ", precision: 2 },
"# ###,##": { fractionSep: ",", groupSep: " ", precision: 2 },
"#'###.##": { fractionSep: ".", groupSep: "'", precision: 2 },
"#, ###.##": { fractionSep: ".", groupSep: ", ", precision: 2 },
"#,##,###.##": { fractionSep: ".", groupSep: ",", precision: 2 },
"#,###.###": { fractionSep: ".", groupSep: ",", precision: 3 },
"#.###": { fractionSep: "", groupSep: ".", precision: 0 },
"#,###": { fractionSep: "", groupSep: ",", precision: 0 },
}
'#,###.##': { fractionSep: '.', groupSep: ',', precision: 2 },
'#.###,##': { fractionSep: ',', groupSep: '.', precision: 2 },
'# ###.##': { fractionSep: '.', groupSep: ' ', precision: 2 },
'# ###,##': { fractionSep: ',', groupSep: ' ', precision: 2 },
"#'###.##": { fractionSep: '.', groupSep: "'", precision: 2 },
'#, ###.##': { fractionSep: '.', groupSep: ', ', precision: 2 },
'#,##,###.##': { fractionSep: '.', groupSep: ',', precision: 2 },
'#,###.###': { fractionSep: '.', groupSep: ',', precision: 3 },
'#.###': { fractionSep: '', groupSep: '.', precision: 0 },
'#,###': { fractionSep: '', groupSep: ',', precision: 0 }
};
module.exports = {
// parse a formatted number string
@ -53,7 +53,8 @@ module.exports = {
for (var i = integer.length; i >= 0; i--) {
var l = this.removeSeparator(str, info.groupSep).length;
if (format == "#,##,###.##" && str.indexOf(",") != -1) { // INR
if (format == '#,##,###.##' && str.indexOf(',') != -1) {
// INR
group_position = 2;
l += 1;
}
@ -64,24 +65,27 @@ module.exports = {
str += info.groupSep;
}
}
parts[0] = str.split("").reverse().join("");
parts[0] = str
.split('')
.reverse()
.join('');
}
if (parts[0] + "" == "") {
parts[0] = "0";
if (parts[0] + '' == '') {
parts[0] = '0';
}
// join decimal
parts[1] = (parts[1] && info.fractionSep) ? (info.fractionSep + parts[1]) : "";
parts[1] = parts[1] && info.fractionSep ? info.fractionSep + parts[1] : '';
// join
return (is_negative ? "-" : "") + parts[0] + parts[1];
return (is_negative ? '-' : '') + parts[0] + parts[1];
},
getFormatInfo(format) {
let format_info = numberFormats[format];
if (!format_info) {
throw `Unknown number format "${format}"`;
throw new Error(`Unknown number format "${format}"`);
}
return format_info;
@ -92,13 +96,14 @@ module.exports = {
var d = parseInt(precision || 0);
var m = Math.pow(10, d);
var n = +(d ? Math.abs(num) * m : Math.abs(num)).toFixed(8); // Avoid rounding errors
var i = Math.floor(n), f = n - i;
var r = ((!precision && f == 0.5) ? ((i % 2 == 0) ? i : i + 1) : Math.round(n));
var i = Math.floor(n),
f = n - i;
var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n);
r = d ? r / m : r;
return is_negative ? -r : r;
},
removeSeparator(text, sep) {
return text.replace(new RegExp(sep === "." ? "\\." : sep, "g"), '');
return text.replace(new RegExp(sep === '.' ? '\\.' : sep, 'g'), '');
}
};

View File

@ -21,7 +21,9 @@ function makeConfig() {
const whiteListedModules = ['vue'];
const allDependencies = Object.assign(frappeDependencies, appDependencies);
const externals = Object.keys(allDependencies).filter(d => !whiteListedModules.includes(d));
const externals = Object.keys(allDependencies).filter(
d => !whiteListedModules.includes(d)
);
getConfig = function getConfig() {
const config = {
@ -31,7 +33,9 @@ function makeConfig() {
externals: isElectron ? externals : undefined,
target: isElectron ? 'electron-renderer' : 'web',
output: {
path: isElectron ? resolveAppDir('./dist/electron') : resolveAppDir('./dist'),
path: isElectron
? resolveAppDir('./dist/electron')
: resolveAppDir('./dist'),
filename: '[name].js',
// publicPath: appConfig.dev.assetsPublicPath,
libraryTarget: isElectron ? 'commonjs2' : undefined
@ -46,10 +50,8 @@ function makeConfig() {
{
test: /\.js$/,
loader: 'babel-loader',
exclude: file => (
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
)
exclude: file =>
/node_modules/.test(file) && !/\.vue\.js/.test(file)
},
{
test: /\.node$/,
@ -57,48 +59,62 @@ function makeConfig() {
},
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
use: ['vue-style-loader', 'css-loader', {
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
}]
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
use: ['vue-style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
test: /\.(png|svg|jpg|woff|woff2|gif)$/,
use: ['file-loader']
}
]
},
resolve: {
extensions: ['.js', '.vue', '.json', '.css', '.node'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'deepmerge$': 'deepmerge/dist/umd.js',
vue$: 'vue/dist/vue.esm.js',
deepmerge$: 'deepmerge/dist/umd.js',
'@': appConfig.dev.srcDir ? resolveAppDir(appConfig.dev.srcDir) : null
}
},
plugins: [
new webpack.DefinePlugin(Object.assign({
new webpack.DefinePlugin(
Object.assign(
{
'process.env': appConfig.dev.env,
'process.env.NODE_ENV': isProduction ? '"production"' : '"development"',
'process.env.NODE_ENV': isProduction
? '"production"'
: '"development"',
'process.env.ELECTRON': JSON.stringify(process.env.ELECTRON)
}, !isProduction ? {
'__static': `"${resolveAppDir(appConfig.staticPath).replace(/\\/g, '\\\\')}"`
} : {})),
},
!isProduction
? {
__static: `"${resolveAppDir(appConfig.staticPath).replace(
/\\/g,
'\\\\'
)}"`
}
: {}
)
),
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: resolveAppDir(appConfig.dev.entryHtml),
nodeModules: !isProduction
? isMonoRepo ? resolveAppDir('../../node_modules') : resolveAppDir('./node_modules')
? isMonoRepo
? resolveAppDir('../../node_modules')
: resolveAppDir('./node_modules')
: false
}),
new CaseSensitivePathsWebpackPlugin(),
@ -106,17 +122,23 @@ function makeConfig() {
new webpack.HotModuleReplacementPlugin(),
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [`FrappeJS server started at http://${appConfig.dev.devServerHost}:${appConfig.dev.devServerPort}`],
},
messages: [
`FrappeJS server started at http://${
appConfig.dev.devServerHost
}:${appConfig.dev.devServerPort}`
]
}
}),
new webpack.ProgressPlugin(),
isProduction ? new CopyWebpackPlugin([
isProduction
? new CopyWebpackPlugin([
{
from: resolveAppDir(appConfig.staticPath),
to: resolveAppDir('./dist/electron/static'),
ignore: ['.*']
}
]) : null,
])
: null
// isProduction ? new BabiliWebpackPlugin() : null,
// isProduction ? new webpack.LoaderOptionsPlugin({ minimize: true }) : null,
].filter(Boolean),
@ -143,10 +165,10 @@ function makeConfig() {
tls: 'empty',
child_process: 'empty'
}
}
};
return config;
}
};
getElectronMainConfig = function getElectronMainConfig() {
return {
@ -179,7 +201,8 @@ function makeConfig() {
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
// isProduction && new BabiliWebpackPlugin(),
isProduction && new webpack.DefinePlugin({
isProduction &&
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
})
].filter(Boolean),
@ -187,8 +210,8 @@ function makeConfig() {
extensions: ['.js', '.json', '.node']
},
target: 'electron-main'
}
}
};
};
}
makeConfig();

5859
yarn.lock

File diff suppressed because it is too large Load Diff