2
0
mirror of https://github.com/frappe/books.git synced 2025-01-26 16:48:28 +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") : ""}

558
index.js
View File

@ -1,261 +1,317 @@
const Observable = require('./utils/observable');
module.exports = {
async init() {
if (this._initialized) return;
this.initConfig();
this.initGlobals();
this.docs = new Observable();
this.events = new Observable();
this._initialized = true;
},
async init() {
if (this._initialized) return;
this.initConfig();
this.initGlobals();
this.docs = new Observable();
this.events = new Observable();
this._initialized = true;
},
initConfig() {
this.config = {
serverURL: '',
backend: 'sqlite',
port: 8000
};
},
initConfig() {
this.config = {
serverURL: '',
backend: 'sqlite',
port: 8000
};
},
initGlobals() {
this.metaCache = {};
this.models = {};
this.forms = {};
this.views = {};
this.flags = {};
this.methods = {};
// temp params while calling routes
this.params = {};
},
initGlobals() {
this.metaCache = {};
this.models = {};
this.forms = {};
this.views = {};
this.flags = {};
this.methods = {};
// temp params while calling routes
this.params = {};
},
registerLibs(common) {
// add standard libs and utils to frappe
common.initLibs(this);
},
registerLibs(common) {
// add standard libs and utils to frappe
common.initLibs(this);
},
registerModels(models, type) {
// register models from app/models/index.js
const toAdd = Object.assign({}, models.models);
registerModels(models, type) {
// register models from app/models/index.js
const toAdd = Object.assign({}, models.models);
// post process based on type
if (models[type]) {
models[type](toAdd);
}
Object.assign(this.models, toAdd);
},
registerView(view, name, module) {
if (!this.views[view]) this.views[view] = {};
this.views[view][name] = module;
},
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) {
let data = await handler(request.body);
if (data === undefined) {
data = {}
}
return response.json(data);
}));
}
},
async call({method, args}) {
if (this.isServer) {
if (this.methods[method]) {
return await this.methods[method](args);
} else {
throw `${method} not found`;
}
}
let url = `/api/method/${method}`;
let response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(args || {})
});
return await response.json();
},
addToCache(doc) {
if (!this.docs) return;
// add to `docs` cache
if (doc.doctype && doc.name) {
if (!this.docs[doc.doctype]) {
this.docs[doc.doctype] = {};
}
this.docs[doc.doctype][doc.name] = doc;
// singles available as first level objects too
if (doc.doctype === doc.name) {
this[doc.name] = doc;
}
// propogate change to `docs`
doc.on('change', params => {
this.docs.trigger('change', params);
});
}
},
isDirty(doctype, name) {
return (this.docs && this.docs[doctype] && this.docs[doctype][name]
&& this.docs[doctype][name]._dirty) || false;
},
getDocFromCache(doctype, name) {
if (this.docs && this.docs[doctype] && this.docs[doctype][name]) {
return this.docs[doctype][name];
}
},
getMeta(doctype) {
if (!this.metaCache[doctype]) {
let model = this.models[doctype];
if (!model) {
throw `${doctype} is not a registered doctype`;
}
let metaClass = model.metaClass || this.BaseMeta;
this.metaCache[doctype] = new metaClass(model);
}
return this.metaCache[doctype];
},
async getDoc(doctype, name) {
let doc = this.getDocFromCache(doctype, name);
if (!doc) {
doc = new (this.getDocumentClass(doctype))({doctype:doctype, name: name});
await doc.load();
this.addToCache(doc);
}
return doc;
},
getDocumentClass(doctype) {
const meta = this.getMeta(doctype);
return meta.documentClass || this.BaseDocument;
},
async getSingle(doctype) {
return await this.getDoc(doctype, doctype);
},
async getDuplicate(doc) {
const newDoc = await this.getNewDoc(doc.doctype);
for (let field of this.getMeta(doc.doctype).getValidFields()) {
if (['name', 'submitted'].includes(field.fieldname)) continue;
if (field.fieldtype === 'Table') {
newDoc[field.fieldname] = (doc[field.fieldname] || []).map(d => {
let newd = Object.assign({}, d);
newd.name = '';
return newd;
});
} else {
newDoc[field.fieldname] = doc[field.fieldname];
}
}
return newDoc;
},
async getNewDoc(doctype) {
let doc = this.newDoc({doctype: doctype});
doc._notInserted = true;
doc.name = this.getRandomString();
this.addToCache(doc);
return doc;
},
newDoc(data) {
let doc = new (this.getDocumentClass(data.doctype))(data);
doc.setDefaults();
return doc;
},
async insert(data) {
return await (this.newDoc(data)).insert();
},
async syncDoc(data) {
let doc;
if (await this.db.exists(data.doctype, data.name)) {
doc = await this.getDoc(data.doctype, data.name);
Object.assign(doc, data);
await doc.update();
} else {
doc = this.newDoc(data);
await doc.insert();
}
},
// only for client side
async login(email, password) {
if (email === 'Administrator') {
this.session = {
user: 'Administrator'
}
return;
}
let response = await fetch(this.getServerURL() + '/api/login', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (response.status === 200) {
const res = await response.json();
this.session = {
user: email,
token: res.token
}
return res;
}
return response;
},
async signup(email, fullName, password) {
let response = await fetch(this.getServerURL() + '/api/signup', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, fullName, password })
});
if (response.status === 200) {
return await response.json();
}
return response;
},
getServerURL() {
return this.config.serverURL || '';
},
close() {
this.db.close();
if (this.server) {
this.server.close();
}
// post process based on type
if (models[type]) {
models[type](toAdd);
}
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 }) {
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) {
let data = await handler(request.body);
if (data === undefined) {
data = {};
}
return response.json(data);
})
);
}
},
async call({ method, args }) {
if (this.isServer) {
if (this.methods[method]) {
return await this.methods[method](args);
} else {
throw new Error(`${method} not found`);
}
}
let url = `/api/method/${method}`;
let response = await fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(args || {})
});
return await response.json();
},
addToCache(doc) {
if (!this.docs) return;
// add to `docs` cache
if (doc.doctype && doc.name) {
if (!this.docs[doc.doctype]) {
this.docs[doc.doctype] = {};
}
this.docs[doc.doctype][doc.name] = doc;
// singles available as first level objects too
if (doc.doctype === doc.name) {
this[doc.name] = doc;
}
// propogate change to `docs`
doc.on('change', params => {
this.docs.trigger('change', params);
});
}
},
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
);
},
getDocFromCache(doctype, name) {
if (this.docs && this.docs[doctype] && this.docs[doctype][name]) {
return this.docs[doctype][name];
}
},
getMeta(doctype) {
if (!this.metaCache[doctype]) {
let model = this.models[doctype];
if (!model) {
throw new Error(`${doctype} is not a registered doctype`);
}
let metaClass = model.metaClass || this.BaseMeta;
this.metaCache[doctype] = new metaClass(model);
}
return this.metaCache[doctype];
},
async getDoc(doctype, name) {
let doc = this.getDocFromCache(doctype, name);
if (!doc) {
doc = new (this.getDocumentClass(doctype))({
doctype: doctype,
name: name
});
await doc.load();
this.addToCache(doc);
}
return doc;
},
getDocumentClass(doctype) {
const meta = this.getMeta(doctype);
return meta.documentClass || this.BaseDocument;
},
async getSingle(doctype) {
return await this.getDoc(doctype, doctype);
},
async getDuplicate(doc) {
const newDoc = await this.getNewDoc(doc.doctype);
for (let field of this.getMeta(doc.doctype).getValidFields()) {
if (['name', 'submitted'].includes(field.fieldname)) continue;
if (field.fieldtype === 'Table') {
newDoc[field.fieldname] = (doc[field.fieldname] || []).map(d => {
let newd = Object.assign({}, d);
newd.name = '';
return newd;
});
} else {
newDoc[field.fieldname] = doc[field.fieldname];
}
}
return newDoc;
},
async getNewDoc(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();
return doc;
},
async insert(data) {
return await this.newDoc(data).insert();
},
async syncDoc(data) {
let doc;
if (await this.db.exists(data.doctype, data.name)) {
doc = await this.getDoc(data.doctype, data.name);
Object.assign(doc, data);
await doc.update();
} else {
doc = this.newDoc(data);
await doc.insert();
}
},
// only for client side
async login(email, password) {
if (email === 'Administrator') {
this.session = {
user: 'Administrator'
};
return;
}
let response = await fetch(this.getServerURL() + '/api/login', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (response.status === 200) {
const res = await response.json();
this.session = {
user: email,
token: res.token
};
return res;
}
return response;
},
async signup(email, fullName, password) {
let response = await fetch(this.getServerURL() + '/api/signup', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, fullName, password })
});
if (response.status === 200) {
return await response.json();
}
return response;
},
getServerURL() {
return this.config.serverURL || '';
},
close() {
this.db.close();
if (this.server) {
this.server.close();
}
}
};

View File

@ -3,354 +3,441 @@ const Observable = require('frappejs/utils/observable');
const naming = require('./naming');
module.exports = class BaseDocument extends Observable {
constructor(data) {
super();
this.fetchValuesCache = {};
this.flags = {};
this.setup();
Object.assign(this, data);
constructor(data) {
super();
this.fetchValuesCache = {};
this.flags = {};
this.setup();
this.setValues(data);
// clear fetch-values cache
frappe.db.on('change', (params) => this.fetchValuesCache[`${params.doctype}:${params.name}`] = {});
}
// clear fetch-values cache
frappe.db.on(
'change',
params => (this.fetchValuesCache[`${params.doctype}:${params.name}`] = {})
);
}
setup() {
// add listeners
}
setup() {
// add listeners
}
get meta() {
if (!this._meta) {
this._meta = frappe.getMeta(this.doctype);
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);
}
return this._meta;
} 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);
}
return this._meta;
}
async getSettings() {
if (!this._settings) {
this._settings = await frappe.getSingle(this.meta.settings);
}
return this._settings;
}
// set value and trigger change
async set(fieldname, value) {
if (typeof fieldname === 'object') {
const valueDict = fieldname;
for (let fieldname in valueDict) {
await this.set(fieldname, valueDict[fieldname]);
}
return;
}
async getSettings() {
if (!this._settings) {
this._settings = await frappe.getSingle(this.meta.settings);
if (this[fieldname] !== value) {
this._dirty = true;
if (Array.isArray(value)) {
this[fieldname] = [];
for (let row of value) {
this.append(fieldname, row);
}
return this._settings;
} else {
this[fieldname] = await this.validateField(fieldname, value);
}
await this.applyChange(fieldname);
}
}
// set value and trigger change
async set(fieldname, value) {
if (typeof fieldname === 'object') {
const valueDict = fieldname;
for (let fieldname in valueDict) {
await this.set(fieldname, valueDict[fieldname]);
}
return;
async applyChange(fieldname) {
if (await this.applyFormula()) {
// multiple changes
await this.trigger('change', {
doc: this,
changed: fieldname
});
} else {
// no other change, trigger control refresh
await this.trigger('change', {
doc: this,
fieldname: fieldname,
changed: fieldname
});
}
}
setDefaults() {
for (let field of this.meta.fields) {
if (this[field.fieldname] == null) {
let defaultValue = null;
if (field.fieldtype === 'Table') {
defaultValue = [];
}
if (field.default) {
defaultValue = field.default;
}
if (this[fieldname] !== value) {
this._dirty = true;
this[fieldname] = await this.validateField(fieldname, value);
await this.applyChange(fieldname);
}
this[field.fieldname] = defaultValue;
}
}
async applyChange(fieldname) {
if (await this.applyFormula()) {
// multiple changes
await this.trigger('change', {
doc: this
});
} else {
// no other change, trigger control refresh
await this.trigger('change', {
doc: this,
fieldname: fieldname
});
}
if (this.meta.basedOn && this.meta.filters) {
this.setValues(this.meta.filters);
}
}
setDefaults() {
for (let field of this.meta.fields) {
if (this[field.fieldname] === null || this[field.fieldname] === undefined) {
let defaultValue = null;
if (field.fieldtype === 'Table') {
defaultValue = [];
}
if (field.default) {
defaultValue = field.default;
}
this[field.fieldname] = defaultValue;
}
}
setKeywords() {
let keywords = [];
for (let fieldname of this.meta.getKeywordFields()) {
keywords.push(this[fieldname]);
}
this.keywords = keywords.join(', ');
}
setKeywords() {
let keywords = [];
for (let fieldname of this.meta.getKeywordFields()) {
keywords.push(this[fieldname]);
}
this.keywords = keywords.join(', ');
append(key, document = {}) {
if (!this[key]) {
this[key] = [];
}
this[key].push(this._initChild(document, key));
this._dirty = true;
this.applyChange(key);
}
append(key, document) {
if (!this[key]) {
this[key] = [];
}
this[key].push(this.initDoc(document));
_initChild(data, key) {
if (data instanceof BaseDocument) {
return data;
} else {
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);
}
}
initDoc(data) {
if (data.prototype instanceof Document) {
return data;
} else {
return new Document(data);
}
async validateField(key, value) {
let field = this.meta.getField(key);
if (field && field.fieldtype == 'Select') {
return this.meta.validateSelect(field, value);
}
return value;
}
async validateField(key, value) {
let field = this.meta.getField(key);
if (field && field.fieldtype == 'Select') {
return this.meta.validateSelect(field, value);
}
return value;
getValidDict() {
let data = {};
for (let field of this.meta.getValidFields()) {
data[field.fieldname] = this[field.fieldname];
}
return data;
}
getValidDict() {
let data = {};
for (let field of this.meta.getValidFields()) {
data[field.fieldname] = this[field.fieldname];
}
return data;
}
getFullDict() {
let data = this.getValidDict();
return data;
}
getFullDict() {
let data = this.getValidDict();
return data;
}
setStandardValues() {
// set standard values on server-side only
if (frappe.isServer) {
let now = (new Date()).toISOString();
if (!this.submitted) {
this.submitted = 0;
}
if (!this.owner) {
this.owner = frappe.session.user;
}
if (!this.creation) {
this.creation = now;
}
if (!this.modifiedBy) {
this.modifiedBy = frappe.session.user;
}
this.modified = now;
}
}
async load() {
let data = await frappe.db.get(this.doctype, this.name);
if (data.name) {
this.syncValues(data);
if (this.meta.isSingle) {
this.setDefaults();
}
} else {
throw new frappe.errors.NotFound(`Not Found: ${this.doctype} ${this.name}`);
}
}
syncValues(data) {
this.clearValues();
Object.assign(this, data);
this._dirty = false;
this.trigger('change', {
doc: this
});
}
clearValues() {
for (let field of this.meta.getValidFields()) {
if (this[field.fieldname]) {
delete this[field.fieldname];
}
}
}
setChildIdx() {
// renumber children
for (let field of this.meta.getValidFields()) {
if (field.fieldtype === 'Table') {
for (let i = 0; i < (this[field.fieldname] || []).length; i++) {
this[field.fieldname][i].idx = i;
}
}
}
}
async compareWithCurrentDoc() {
if (frappe.isServer && !this.isNew()) {
let currentDoc = await frappe.db.get(this.doctype, this.name);
// 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]));
}
if (this.submitted && !this.meta.isSubmittable) {
throw new frappe.errors.ValidationError(frappe._('Document type {1} is not submittable', [this.doctype]));
}
// set submit action flag
if (this.submitted && !currentDoc.submitted) {
this.flags.submitAction = true;
}
if (currentDoc.submitted && !this.submitted) {
this.flags.revertAction = true;
}
}
}
async applyFormula() {
if (!this.meta.hasFormula()) {
return false;
}
let doc = this;
// children
for (let tablefield of this.meta.getTableFields()) {
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) {
if (shouldApplyFormula(field, row)) {
const val = await field.formula(row, doc);
if (val !== false && val !== undefined) {
row[field.fieldname] = val;
}
}
}
}
}
}
// parent
for (let field of this.meta.getFormulaFields()) {
if (shouldApplyFormula(field, doc)) {
const val = await field.formula(doc);
if (val !== false && val !== undefined) {
doc[field.fieldname] = val;
}
}
}
return true;
function shouldApplyFormula(field, doc) {
if (field.readOnly) {
return true;
}
if (!frappe.isServer || frappe.isElectron) {
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
return true;
}
}
return false;
}
}
async commit() {
// re-run triggers
this.setStandardValues();
this.setKeywords();
this.setChildIdx();
await this.applyFormula();
await this.trigger('validate');
}
async insert() {
await naming.setName(this);
await this.commit();
await this.trigger('beforeInsert');
const data = await frappe.db.insert(this.doctype, this.getValidDict());
this.syncValues(data);
await this.trigger('afterInsert');
await this.trigger('afterSave');
return this;
}
async update() {
await this.compareWithCurrentDoc();
await this.commit();
await this.trigger('beforeUpdate');
// before submit
if (this.flags.submitAction) await this.trigger('beforeSubmit');
if (this.flags.revertAction) await this.trigger('beforeRevert');
const data = await frappe.db.update(this.doctype, this.getValidDict());
this.syncValues(data);
await this.trigger('afterUpdate');
await this.trigger('afterSave');
// after submit
if (this.flags.submitAction) await this.trigger('afterSubmit');
if (this.flags.revertAction) await this.trigger('afterRevert');
return this;
}
async delete() {
await this.trigger('beforeDelete');
await frappe.db.delete(this.doctype, this.name);
await this.trigger('afterDelete');
}
async submit() {
this.submitted = 1;
this.update();
}
async revert() {
setStandardValues() {
// set standard values on server-side only
if (frappe.isServer) {
let now = new Date().toISOString();
if (!this.submitted) {
this.submitted = 0;
this.update();
}
}
// trigger methods on the class if they match
// with the trigger name
async trigger(event, params) {
if (this[event]) {
await this[event](params);
if (!this.owner) {
this.owner = frappe.session.user;
}
if (!this.creation) {
this.creation = now;
}
if (!this.modifiedBy) {
this.modifiedBy = frappe.session.user;
}
this.modified = now;
}
}
async load() {
let data = await frappe.db.get(this.doctype, this.name);
if (data.name) {
this.syncValues(data);
if (this.meta.isSingle) {
this.setDefaults();
}
} else {
throw new frappe.errors.NotFound(
`Not Found: ${this.doctype} ${this.name}`
);
}
}
syncValues(data) {
this.clearValues();
this.setValues(data);
this._dirty = false;
this.trigger('change', {
doc: this
});
}
clearValues() {
for (let field of this.meta.getValidFields()) {
if (this[field.fieldname]) {
delete this[field.fieldname];
}
}
}
setChildIdx() {
// renumber children
for (let field of this.meta.getValidFields()) {
if (field.fieldtype === 'Table') {
for (let i = 0; i < (this[field.fieldname] || []).length; i++) {
this[field.fieldname][i].idx = i;
}
await super.trigger(event, params);
}
}
}
async compareWithCurrentDoc() {
if (frappe.isServer && !this.isNew()) {
let currentDoc = await frappe.db.get(this.doctype, this.name);
// 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
])
);
}
if (this.submitted && !this.meta.isSubmittable) {
throw new frappe.errors.ValidationError(
frappe._('Document type {1} is not submittable', [this.doctype])
);
}
// set submit action flag
if (this.submitted && !currentDoc.submitted) {
this.flags.submitAction = true;
}
if (currentDoc.submitted && !this.submitted) {
this.flags.revertAction = true;
}
}
}
async applyFormula() {
if (!this.meta.hasFormula()) {
return false;
}
// helper functions
getSum(tablefield, childfield) {
return this[tablefield].map(d => (d[childfield] || 0)).reduce((a, b) => a + b, 0);
}
let doc = this;
async getFrom(doctype, name, fieldname) {
if (!name) return '';
let _values = this.fetchValuesCache[`${doctype}:${name}`] || (this.fetchValuesCache[`${doctype}:${name}`] = {});
if (!_values[fieldname]) {
_values[fieldname] = await frappe.db.getValue(doctype, name, fieldname);
// children
for (let tablefield of this.meta.getTableFields()) {
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) {
if (shouldApplyFormula(field, row)) {
const val = await field.formula(row, doc);
if (val !== false && val !== undefined) {
row[field.fieldname] = val;
}
}
}
}
return _values[fieldname];
}
}
isNew() {
return this._notInserted;
// parent or child row
for (let field of this.meta.getFormulaFields()) {
if (shouldApplyFormula(field, 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;
}
}
}
};
return true;
function shouldApplyFormula(field, doc) {
if (field.readOnly) {
return true;
}
if (!frappe.isServer || frappe.isElectron) {
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
return true;
}
}
return false;
}
}
async commit() {
// re-run triggers
this.setStandardValues();
this.setKeywords();
this.setChildIdx();
await this.applyFormula();
await this.trigger('validate');
}
async insert() {
await naming.setName(this);
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');
return this;
}
async update() {
await this.compareWithCurrentDoc();
await this.commit();
await this.trigger('beforeUpdate');
// before submit
if (this.flags.submitAction) await this.trigger('beforeSubmit');
if (this.flags.revertAction) await this.trigger('beforeRevert');
const data = await frappe.db.update(this.doctype, this.getValidDict());
this.syncValues(data);
await this.trigger('afterUpdate');
await this.trigger('afterSave');
// after submit
if (this.flags.submitAction) await this.trigger('afterSubmit');
if (this.flags.revertAction) await this.trigger('afterRevert');
return this;
}
async delete() {
await this.trigger('beforeDelete');
await frappe.db.delete(this.doctype, this.name);
await this.trigger('afterDelete');
}
async submit() {
this.submitted = 1;
this.update();
}
async revert() {
this.submitted = 0;
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) {
if (this[event]) {
await this[event](params);
}
await super.trigger(event, params);
}
// helper functions
getSum(tablefield, childfield) {
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}`] = {});
if (!_values[fieldname]) {
_values[fieldname] = await frappe.db.getValue(doctype, name, fieldname);
}
return _values[fieldname];
}
isNew() {
return this._notInserted;
}
};

View File

@ -1,232 +1,287 @@
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) {
super(data);
this.setDefaultIndicators();
if (this.setupMeta) {
this.setupMeta();
}
if (!this.titleField) {
this.titleField = 'name';
}
}
hasField(fieldname) {
return this.getField(fieldname) ? true : false;
}
getField(fieldname) {
if (!this._field_map) {
this._field_map = {};
for (let field of this.fields) {
this._field_map[field.fieldname] = field;
}
}
return this._field_map[fieldname];
}
/**
* Get fields filtered by filters
* @param {Object} filters
*
* Usage:
* meta = frappe.getMeta('ToDo')
* dataFields = meta.getFieldsWith({ fieldtype: 'Data' })
*/
getFieldsWith(filters) {
return this.fields.filter(df => {
let match = true;
for (const key in filters) {
const value = filters[key];
match = df[key] === value;
}
return match;
constructor(data) {
if (data.basedOn) {
let config = frappe.models[data.basedOn];
Object.assign(data, config, {
name: data.name,
label: data.label,
filters: data.filters
});
}
getLabel(fieldname) {
return this.getField(fieldname).label;
super(data);
this.setDefaultIndicators();
if (this.setupMeta) {
this.setupMeta();
}
if (!this.titleField) {
this.titleField = 'name';
}
}
getTableFields() {
if (this._tableFields===undefined) {
this._tableFields = this.fields.filter(field => field.fieldtype === 'Table');
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
}
return this._tableFields;
].concat(this.fields);
}
}
getFormulaFields() {
if (this._formulaFields===undefined) {
this._formulaFields = this.fields.filter(field => field.formula);
hasField(fieldname) {
return this.getField(fieldname) ? true : false;
}
getField(fieldname) {
if (!this._field_map) {
this._field_map = {};
for (let field of this.fields) {
this._field_map[field.fieldname] = field;
}
}
return this._field_map[fieldname];
}
/**
* Get fields filtered by filters
* @param {Object} filters
*
* Usage:
* meta = frappe.getMeta('ToDo')
* dataFields = meta.getFieldsWith({ fieldtype: 'Data' })
*/
getFieldsWith(filters) {
return this.fields.filter(df => {
let match = true;
for (const key in filters) {
const value = filters[key];
match = df[key] === value;
}
return match;
});
}
getLabel(fieldname) {
let df = this.getField(fieldname);
return df.getLabel || df.label;
}
getTableFields() {
if (this._tableFields === undefined) {
this._tableFields = this.fields.filter(
field => field.fieldtype === 'Table'
);
}
return this._tableFields;
}
getFormulaFields() {
if (this._formulaFields === undefined) {
this._formulaFields = this.fields.filter(field => field.formula);
}
return this._formulaFields;
}
hasFormula() {
if (this._hasFormula === undefined) {
this._hasFormula = false;
if (this.getFormulaFields().length) {
this._hasFormula = true;
} else {
for (let tablefield of this.getTableFields()) {
if (frappe.getMeta(tablefield.childtype).getFormulaFields().length) {
this._hasFormula = true;
break;
}
}
return this._formulaFields;
}
}
return this._hasFormula;
}
hasFormula() {
if (this._hasFormula===undefined) {
this._hasFormula = false;
if (this.getFormulaFields().length) {
this._hasFormula = true;
} else {
for (let tablefield of this.getTableFields()) {
if (frappe.getMeta(tablefield.childtype).getFormulaFields().length) {
this._hasFormula = true;
break;
}
}
}
getBaseDocType() {
return this.basedOn || this.name;
}
async set(fieldname, value) {
this[fieldname] = value;
await this.trigger(fieldname);
}
get(fieldname) {
return this[fieldname];
}
getValidFields({ withChildren = true } = {}) {
if (!this._validFields) {
this._validFields = [];
this._validFieldsWithChildren = [];
const _add = field => {
this._validFields.push(field);
this._validFieldsWithChildren.push(field);
};
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)
) {
_add(field);
}
return this._hasFormula;
}
}
async set(fieldname, value) {
this[fieldname] = value;
await this.trigger(fieldname);
}
get(fieldname) {
return this[fieldname];
}
getValidFields({ withChildren = true } = {}) {
if (!this._validFields) {
this._validFields = [];
this._validFieldsWithChildren = [];
const _add = (field) => {
this._validFields.push(field);
this._validFieldsWithChildren.push(field);
}
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)) {
_add(field);
}
}
if (this.isSubmittable) {
_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)) {
_add(field);
}
}
} else {
// parent fields
for (let field of model.parentFields) {
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
_add(field);
}
}
}
if (this.isTree) {
// tree fields
for (let field of model.treeFields) {
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
_add(field);
}
}
}
// doctype fields
for (let field of this.fields) {
let include = frappe.db.typeMap[field.fieldtype];
if (include) {
_add(field);
}
// include tables if (withChildren = True)
if (!include && field.fieldtype === 'Table') {
this._validFieldsWithChildren.push(field);
}
}
}
if (withChildren) {
return this._validFieldsWithChildren;
} else {
return this._validFields;
}
}
getKeywordFields() {
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);
}
if (!(this._keywordFields && this._keywordFields.length)) {
this._keywordFields = ['name']
}
}
return this._keywordFields;
}
validateSelect(field, value) {
let options = field.options;
if (!options) return;
if (typeof options === 'string') {
// values given as string
options = field.options.split('\n');
}
if (!options.includes(value)) {
throw new frappe.errors.ValueError(`${value} must be one of ${options.join(", ")}`);
}
return value;
}
async trigger(event, params = {}) {
Object.assign(params, {
doc: this,
name: event
if (this.isSubmittable) {
_add({
fieldtype: 'Check',
fieldname: 'submitted',
label: frappe._('Submitted')
});
}
await super.trigger(event, params);
}
setDefaultIndicators() {
if (!this.indicators) {
if (this.isSubmittable) {
this.indicators = {
key: 'submitted',
colors: {
0: indicatorColor.GRAY,
1: indicatorColor.BLUE
}
}
}
if (this.isChild) {
// child fields
for (let field of model.childFields) {
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)
) {
_add(field);
}
}
}
if (this.isTree) {
// tree fields
for (let field of model.treeFields) {
if (
frappe.db.typeMap[field.fieldtype] &&
!doctype_fields.includes(field.fieldname)
) {
_add(field);
}
}
}
// doctype fields
for (let field of this.fields) {
let include = frappe.db.typeMap[field.fieldtype];
if (include) {
_add(field);
}
// include tables if (withChildren = True)
if (!include && field.fieldtype === 'Table') {
this._validFieldsWithChildren.push(field);
}
}
}
getIndicatorColor(doc) {
if (frappe.isDirty(this.name, doc.name)) {
return indicatorColor.ORANGE;
if (withChildren) {
return this._validFieldsWithChildren;
} else {
return this._validFields;
}
}
getKeywordFields() {
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);
}
if (!(this._keywordFields && this._keywordFields.length)) {
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;
if (typeof options === 'string') {
// values given as string
options = field.options.split('\n');
}
if (!options.includes(value)) {
throw new frappe.errors.ValueError(
`${value} must be one of ${options.join(', ')}`
);
}
return value;
}
async trigger(event, params = {}) {
Object.assign(params, {
doc: this,
name: event
});
await super.trigger(event, params);
}
setDefaultIndicators() {
if (!this.indicators) {
if (this.isSubmittable) {
this.indicators = {
key: 'submitted',
colors: {
0: indicatorColor.GRAY,
1: indicatorColor.BLUE
}
};
}
}
}
getIndicatorColor(doc) {
if (frappe.isDirty(this.name, doc.name)) {
return indicatorColor.ORANGE;
} else {
if (this.indicators) {
let value = doc[this.indicators.key];
if (value) {
return this.indicators.colors[value] || indicatorColor.GRAY;
} else {
if (this.indicators) {
let value = doc[this.indicators.key];
if (value) {
return this.indicators.colors[value] || indicatorColor.GRAY;
} else {
return indicatorColor.GRAY;
}
} else {
return indicatorColor.GRAY;
}
return indicatorColor.GRAY;
}
} else {
return indicatorColor.GRAY;
}
}
}
}
};

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');
@ -7,87 +7,109 @@ const { getHTML } = require('frappejs/common/print');
const { getRandomString } = require('frappejs/utils');
async function makePDF(html, filepath) {
const browser = await puppeteer.launch();
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'
})
await page.pdf({
path: filepath,
format: 'A4'
});
await browser.close();
const browser = await puppeteer.launch();
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'
});
await page.pdf({
path: filepath,
format: 'A4'
});
await browser.close();
}
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 { remote, shell } = require('electron');
const { BrowserWindow } = remote;
const html = htmlContent || (await getHTML(doctype, name));
if (!destination) {
destination =
process.env.NODE_ENV === 'development'
? path.resolve('.')
: remote.getGlobal('documentsPath');
}
const fs = require('fs')
let printWindow = new BrowserWindow({
width: 600,
height: 800,
show: false
})
printWindow.loadURL(`file://${path.join(__static, 'print.html')}`);
const filepath = path.resolve(
path.join(destination, '/frappe-accounting/' + name + '.pdf')
);
printWindow.on('closed', () => {
printWindow = null;
});
const fs = require('fs');
let printWindow = new BrowserWindow({
width: 600,
height: 800,
show: false
});
const code = `
const __static = remote.getGlobal('__static') || __static;
printWindow.loadURL(`file://${path.join(__static, 'print.html')}`);
printWindow.on('closed', () => {
printWindow = null;
});
const code = `
document.body.innerHTML = \`${html}\`;
`;
printWindow.webContents.executeJavaScript(code);
printWindow.webContents.executeJavaScript(code);
const printPromise = new Promise(resolve => {
printWindow.webContents.on('did-finish-load', () => {
printWindow.webContents.printToPDF({
const printPromise = new Promise(resolve => {
printWindow.webContents.on('did-finish-load', () => {
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);
await printPromise;
// await makePDF(html, filepath);
}
function setupExpressRoute() {
if (!frappe.app) return;
frappe.app.post('/api/method/pdf', frappe.asyncHandler(handlePDFRequest));
if (!frappe.app) return;
frappe.app.post('/api/method/pdf', frappe.asyncHandler(handlePDFRequest));
}
async function handlePDFRequest(req, res) {
const args = req.body;
const { doctype, name } = args;
const html = await getHTML(doctype, name);
const args = req.body;
const { doctype, name } = args;
const html = await getHTML(doctype, name);
const filepath = path.join(getTmpDir(), `frappe-pdf-${getRandomString()}.pdf`);
await makePDF(html, filepath);
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)}`);
file.pipe(res);
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)}`
);
file.pipe(res);
}
module.exports = {
makePDF,
setupExpressRoute,
getPDFForElectron
}
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() {
@ -127,4 +168,4 @@ function test_doc() {
subject: 'testing 1',
description: 'test description 1'
});
}
}

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' });
@ -88,4 +88,4 @@ describe.only('REST', () => {
assert.equal(await frappe.db.exists(doc2.doctype, doc2.name), false);
});
});
});

View File

@ -1,25 +1,25 @@
<template>
<div class="frappe-form">
<form-actions
class="p-3 border-bottom"
v-if="shouldRenderForm"
:doc="doc"
:links="links"
@save="save"
@submit="submit"
@revert="revert"
@print="print"
/>
<form-layout
class="p-3"
v-if="shouldRenderForm"
:doc="doc"
:fields="meta.fields"
:layout="meta.layout"
:invalid="invalid"
/>
<not-found v-if="notFound" />
</div>
<div class="frappe-form">
<form-actions
class="p-3 border-bottom"
v-if="shouldRenderForm"
:doc="doc"
:links="links"
@save="save"
@submit="submit"
@revert="revert"
@print="print"
/>
<form-layout
class="p-3"
v-if="shouldRenderForm"
:doc="doc"
:fields="meta.fields"
:layout="meta.layout"
:invalid="invalid"
/>
<not-found v-if="notFound" />
</div>
</template>
<script>
import frappe from 'frappejs';
@ -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.save();
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.save();
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 = [{
columns: [{ fields }]
}];
layout = [
{
columns: [{ fields }]
}
];
}
if (Array.isArray(layout)) {
layout = {
sections: layout
}
};
}
return layout;
}

View File

@ -1,26 +1,26 @@
<template>
<div class="frappe-list">
<list-actions
:doctype="doctype"
:showDelete="checkList.length"
@new="$emit('newDoc')"
@delete="deleteCheckedItems"
/>
<ul class="list-group">
<list-item v-for="doc of data" :key="doc.name"
:id="doc.name"
:isActive="doc.name === $route.params.name"
:isChecked="isChecked(doc.name)"
@clickItem="openForm(doc.name)"
@checkItem="toggleCheck(doc.name)"
>
<indicator v-if="hasIndicator" :color="getIndicatorColor(doc)" />
<span class="d-inline-block ml-2">
{{ doc[meta.titleField || 'name'] }}
</span>
</list-item>
</ul>
</div>
<div class="frappe-list">
<list-actions
:doctype="doctype"
:showDelete="checkList.length"
@new="$emit('newDoc')"
@delete="deleteCheckedItems"
/>
<ul class="list-group">
<list-item
v-for="doc of data"
:key="doc.name"
:id="doc.name"
:isActive="doc.name === $route.params.name"
:isChecked="isChecked(doc.name)"
@clickItem="openForm(doc.name)"
@checkItem="toggleCheck(doc.name)"
>
<indicator v-if="hasIndicator" :color="getIndicatorColor(doc)" />
<span class="d-inline-block ml-2">{{ doc[meta.titleField || 'name'] }}</span>
</list-item>
</ul>
</div>
</template>
<script>
import frappe from 'frappejs';

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;
export default Plugin;

View File

@ -1,118 +1,158 @@
<script>
export default {
render(h) {
if (this.onlyInput) {
return this.getInputElement(h);
}
return this.getWrapperElement(h);
},
props: {
docfield: Object,
value: [String, Number, Array, FileList],
onlyInput: {
type: Boolean,
default: false
},
disabled: Boolean,
autofocus: Boolean
},
mounted() {
if (this.autofocus) {
this.$refs.input.focus();
}
},
computed: {
id() {
return this.docfield.fieldname + '-'
+ document.querySelectorAll(`[data-fieldname="${this.docfield.fieldname}"]`).length;
},
inputClass() {
return [];
},
wrapperClass() {
return [];
},
labelClass() {
return [];
}
},
methods: {
getWrapperElement(h) {
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));
},
getChildrenElement(h) {
return [this.getLabelElement(h), this.getInputElement(h)]
},
getLabelElement(h) {
return h('label', {
class: [this.labelClass, 'text-muted'],
attrs: {
for: this.id
},
domProps: {
textContent: this.docfield.label
}
});
},
getInputElement(h) {
return h(this.getInputTag(), {
class: this.getInputClass(),
attrs: this.getInputAttrs(),
on: this.getInputListeners(),
domProps: this.getDomProps(),
ref: 'input'
}, this.getInputChildren(h));
},
getInputTag() {
return 'input';
},
getInputClass() {
return ['form-control', ...this.inputClass];
},
getInputAttrs() {
return {
id: this.id,
type: 'text',
placeholder: '',
value: this.value,
required: this.docfield.required,
disabled: this.disabled
}
},
getInputListeners() {
return {
change: (e) => {
this.handleChange(e.target.value);
}
};
},
getInputChildren() {
return null;
},
getDomProps() {
return null;
},
async handleChange(value) {
value = this.parse(value);
const isValid = await this.validate(value);
this.$refs.input.setCustomValidity(isValid === false ? 'error' : '');
this.$emit('change', value);
},
getValueFromInput(e) {
},
validate() {
return true;
},
parse(value) {
return value;
}
render(h) {
if (this.onlyInput) {
return this.getInputElement(h);
}
}
return this.getWrapperElement(h);
},
data() {
return {
label: this.docfield.label
};
},
props: {
docfield: Object,
value: [String, Number, Array, FileList],
onlyInput: {
type: Boolean,
default: false
},
disabled: Boolean,
autofocus: Boolean,
doc: Object
},
mounted() {
if (this.autofocus) {
this.$refs.input.focus();
}
},
computed: {
id() {
return (
this.docfield.fieldname +
'-' +
document.querySelectorAll(
`[data-fieldname="${this.docfield.fieldname}"]`
).length
);
},
inputClass() {
return [];
},
wrapperClass() {
return [];
},
labelClass() {
return [];
}
},
methods: {
getWrapperElement(h) {
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)
);
},
getChildrenElement(h) {
return [
this.getLabelElement(h),
this.getInputElement(h),
this.getDescriptionElement(h)
];
},
getLabelElement(h) {
return h('label', {
class: [this.labelClass, 'text-muted'],
attrs: {
for: this.id
},
domProps: {
textContent: this.label
}
});
},
getDescriptionElement(h) {
return h('small', {
class: ['form-text', 'text-muted'],
domProps: {
textContent: this.docfield.description || ''
}
});
},
getInputElement(h) {
return h(
this.getInputTag(),
{
class: this.getInputClass(),
attrs: this.getInputAttrs(),
on: this.getInputListeners(),
domProps: this.getDomProps(),
ref: 'input'
},
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.getFormControlSize(), ...this.inputClass];
},
getInputAttrs() {
return {
id: this.id,
type: 'text',
placeholder: this.docfield.placeholder || '',
value: this.value,
required: this.docfield.required,
disabled: this.disabled
};
},
getInputListeners() {
return {
change: e => {
this.handleChange(e.target.value);
}
};
},
getInputChildren() {
return null;
},
getDomProps() {
return null;
},
async handleChange(value) {
value = this.parse(value);
const isValid = await this.validate(value);
this.$refs.input.setCustomValidity(isValid === false ? 'error' : '');
this.$emit('change', value);
},
getValueFromInput(e) {},
validate() {
return true;
},
parse(value) {
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

@ -1,31 +1,33 @@
<script>
import Base from './Base';
export default {
extends: Base,
methods: {
getInputTag() {
return 'select';
},
getInputAttrs() {
return {
id: this.id,
required: this.docfield.required,
disabled: this.disabled
};
},
getInputChildren(h) {
return this.docfield.options.map(option =>
h('option', {
attrs: {
key: option,
selected: option === this.value
},
domProps: {
textContent: option
}
})
);
}
extends: Base,
methods: {
getInputTag() {
return 'select';
},
getInputAttrs() {
return {
id: this.id,
required: this.docfield.required,
disabled: this.disabled
};
},
getInputChildren(h) {
return this.docfield.options.map(option =>
h('option', {
attrs: {
key: option,
value: option,
disabled: option.indexOf('...') > -1,
selected: option.indexOf('...') > -1 || option === this.value
},
domProps: {
textContent: option
}
})
);
}
}
}
};
</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)) {
this.deactivateEditing();
// 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

@ -1,24 +1,24 @@
<script>
import Base from './Base';
export default {
extends: Base,
methods: {
getInputTag() {
return 'textarea';
},
getInputAttrs() {
return {
id: this.id,
required: this.docfield.required,
rows: 3,
disabled: this.disabled
};
},
getDomProps() {
return {
value: this.value
}
}
extends: Base,
methods: {
getInputTag() {
return 'textarea';
},
getInputAttrs() {
return {
id: this.id,
required: this.docfield.required,
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"
:docfield="docfield"
:value="$data.filterValues[docfield.fieldname]"
:doc="$data.filterValues"
@change="updateValue(docfield.fieldname, $event)"/>
<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]"
: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,17 +1,29 @@
<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>
<not-found v-if="!reportConfig" />
<div>
<div class="p-4">
<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>
</template>
<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,13 +32,30 @@ 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({
method: this.reportConfig.method,
args: filters
method: this.reportConfig.method,
args: filters
});
let rows, columns;
@ -48,8 +77,8 @@ export default {
columns = this.getColumns();
}
for(let column of columns) {
column.editable = false;
for (let column of columns) {
column.editable = false;
}
if (this.datatable) {
@ -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

@ -4,34 +4,37 @@ const luxon = require('luxon');
const frappe = require('frappejs');
module.exports = {
format(value, field) {
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) {
dateFormat = 'yyyy-MM-dd';
} else {
dateFormat = frappe.SystemSettings.dateFormat;
}
value = luxon.DateTime.fromISO(value).toFormat(dateFormat);
} else {
if (value === null || value === undefined) {
value = '';
} else {
value = value + '';
}
}
return value;
format(value, field) {
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) {
dateFormat = 'yyyy-MM-dd';
} else {
dateFormat = frappe.SystemSettings.dateFormat;
}
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 = '';
} else {
value = value + '';
}
}
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({
'process.env': appConfig.dev.env,
'process.env.NODE_ENV': isProduction ? '"production"' : '"development"',
'process.env.ELECTRON': JSON.stringify(process.env.ELECTRON)
}, !isProduction ? {
'__static': `"${resolveAppDir(appConfig.staticPath).replace(/\\/g, '\\\\')}"`
} : {})),
new webpack.DefinePlugin(
Object.assign(
{
'process.env': appConfig.dev.env,
'process.env.NODE_ENV': isProduction
? '"production"'
: '"development"',
'process.env.ELECTRON': JSON.stringify(process.env.ELECTRON)
},
!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([
{
from: resolveAppDir(appConfig.staticPath),
to: resolveAppDir('./dist/electron/static'),
ignore: ['.*']
}
]) : null,
isProduction
? new CopyWebpackPlugin([
{
from: resolveAppDir(appConfig.staticPath),
to: resolveAppDir('./dist/electron/static'),
ignore: ['.*']
}
])
: 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,16 +201,17 @@ function makeConfig() {
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
// isProduction && new BabiliWebpackPlugin(),
isProduction && new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
})
isProduction &&
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
})
].filter(Boolean),
resolve: {
extensions: ['.js', '.json', '.node']
},
target: 'electron-main'
}
}
};
};
}
makeConfig();

5859
yarn.lock

File diff suppressed because it is too large Load Diff