2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 19:39:07 +00:00
File doctype

- Upload files using multer
- uploadFiles API in http.js
- Link File to doctype with name and field
- Refactor File component for fullpaths
- frappe.db.setValue(s) API
This commit is contained in:
Faris Ansari 2018-08-18 21:24:17 +05:30 committed by GitHub
parent 1aabf7ef40
commit 375325d917
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1166 additions and 906 deletions

View File

@ -2,499 +2,513 @@ const frappe = require('frappejs');
const Observable = require('frappejs/utils/observable'); const Observable = require('frappejs/utils/observable');
module.exports = class Database extends Observable { module.exports = class Database extends Observable {
constructor() { constructor() {
super(); super();
this.initTypeMap(); this.initTypeMap();
} }
async connect() { async connect() {
// this.conn // this.conn
} }
close() { close() {
this.conn.close(); this.conn.close();
} }
async migrate() { async migrate() {
for (let doctype in frappe.models) { for (let doctype in frappe.models) {
// check if controller module // check if controller module
let meta = frappe.getMeta(doctype); let meta = frappe.getMeta(doctype);
if (!meta.isSingle) { if (!meta.isSingle) {
if (await this.tableExists(doctype)) { if (await this.tableExists(doctype)) {
await this.alterTable(doctype); await this.alterTable(doctype);
} else {
await this.createTable(doctype);
}
}
}
await this.commit();
}
async createTable(doctype, newName=null) {
let meta = frappe.getMeta(doctype);
let columns = [];
let indexes = [];
for (let field of meta.getValidFields({ withChildren: false })) {
if (this.typeMap[field.fieldtype]) {
this.updateColumnDefinition(field, columns, indexes);
}
}
return await this.runCreateTableQuery(newName || doctype, columns, indexes);
}
async tableExists(table) {
// return true if table exists
}
async runCreateTableQuery(doctype, columns, indexes) {
// override
}
updateColumnDefinition(field, columns, indexes) {
// return `${df.fieldname} ${this.typeMap[df.fieldtype]} ${ ? "PRIMARY KEY" : ""} ${df.required && !df.default ? "NOT NULL" : ""} ${df.default ? `DEFAULT ${df.default}` : ""}`
}
async alterTable(doctype) {
// get columns
let diff = await this.getColumnDiff(doctype);
let newForeignKeys = await this.getNewForeignKeys(doctype);
if (diff.added.length) {
await this.addColumns(doctype, diff.added);
}
if (diff.removed.length) {
await this.removeColumns(doctype, diff.removed);
}
if (newForeignKeys.length) {
await this.addForeignKeys(doctype, newForeignKeys);
}
}
async getColumnDiff(doctype) {
const tableColumns = await this.getTableColumns(doctype);
const validFields = frappe.getMeta(doctype).getValidFields({ withChildren: false });
const diff = { added: [], removed: [] };
for (let field of validFields) {
if (!tableColumns.includes(field.fieldname) && this.typeMap[field.fieldtype]) {
diff.added.push(field);
}
}
const validFieldNames = validFields.map(field => field.fieldname);
for (let column of tableColumns) {
if (!validFieldNames.includes(column)) {
diff.removed.push(column);
}
}
return diff;
}
async addColumns(doctype, added) {
for (let field of added) {
await this.runAddColumnQuery(doctype, field);
}
}
async removeColumns(doctype, removed) {
for (let column of removed) {
await this.runRemoveColumnQuery(doctype, column);
}
}
async getNewForeignKeys(doctype) {
let foreignKeys = await this.getForeignKeys(doctype);
let newForeignKeys = [];
let meta = frappe.getMeta(doctype);
for (let field of meta.getValidFields({ withChildren: false})) {
if (field.fieldtype==='Link' && !foreignKeys.includes(field.fieldname)) {
newForeignKeys.push(field);
}
}
return newForeignKeys;
}
async addForeignKeys(doctype, newForeignKeys) {
for (let field of newForeignKeys) {
this.addForeignKey(doctype, field);
}
}
async getForeignKey(doctype, field) {
}
async getTableColumns(doctype) {
return [];
}
async runAddColumnQuery(doctype, field) {
// alter table {doctype} add column ({column_def});
}
async get(doctype, name=null, fields = '*') {
let meta = frappe.getMeta(doctype);
let doc;
if (meta.isSingle) {
doc = await this.getSingle(doctype);
doc.name = doctype;
} else { } else {
if (!name) { await this.createTable(doctype);
throw new frappe.errors.ValueError('name is mandatory');
}
doc = await this.getOne(doctype, name, fields);
} }
await this.loadChildren(doc, meta); }
return doc; }
await this.commit();
}
async createTable(doctype, newName = null) {
let meta = frappe.getMeta(doctype);
let columns = [];
let indexes = [];
for (let field of meta.getValidFields({ withChildren: false })) {
if (this.typeMap[field.fieldtype]) {
this.updateColumnDefinition(field, columns, indexes);
}
} }
async loadChildren(doc, meta) { return await this.runCreateTableQuery(newName || doctype, columns, indexes);
// load children }
let tableFields = meta.getTableFields();
for (let field of tableFields) { async tableExists(table) {
doc[field.fieldname] = await this.getAll({ // return true if table exists
doctype: field.childtype, }
fields: ["*"],
filters: { parent: doc.name }, async runCreateTableQuery(doctype, columns, indexes) {
orderBy: 'idx', // override
order: 'asc' }
});
updateColumnDefinition(field, columns, indexes) {
// return `${df.fieldname} ${this.typeMap[df.fieldtype]} ${ ? "PRIMARY KEY" : ""} ${df.required && !df.default ? "NOT NULL" : ""} ${df.default ? `DEFAULT ${df.default}` : ""}`
}
async alterTable(doctype) {
// get columns
let diff = await this.getColumnDiff(doctype);
let newForeignKeys = await this.getNewForeignKeys(doctype);
if (diff.added.length) {
await this.addColumns(doctype, diff.added);
}
if (diff.removed.length) {
await this.removeColumns(doctype, diff.removed);
}
if (newForeignKeys.length) {
await this.addForeignKeys(doctype, newForeignKeys);
}
}
async getColumnDiff(doctype) {
const tableColumns = await this.getTableColumns(doctype);
const validFields = frappe.getMeta(doctype).getValidFields({ withChildren: false });
const diff = { added: [], removed: [] };
for (let field of validFields) {
if (!tableColumns.includes(field.fieldname) && this.typeMap[field.fieldtype]) {
diff.added.push(field);
}
}
const validFieldNames = validFields.map(field => field.fieldname);
for (let column of tableColumns) {
if (!validFieldNames.includes(column)) {
diff.removed.push(column);
}
}
return diff;
}
async addColumns(doctype, added) {
for (let field of added) {
await this.runAddColumnQuery(doctype, field);
}
}
async removeColumns(doctype, removed) {
for (let column of removed) {
await this.runRemoveColumnQuery(doctype, column);
}
}
async getNewForeignKeys(doctype) {
let foreignKeys = await this.getForeignKeys(doctype);
let newForeignKeys = [];
let meta = frappe.getMeta(doctype);
for (let field of meta.getValidFields({ withChildren: false })) {
if (field.fieldtype === 'Link' && !foreignKeys.includes(field.fieldname)) {
newForeignKeys.push(field);
}
}
return newForeignKeys;
}
async addForeignKeys(doctype, newForeignKeys) {
for (let field of newForeignKeys) {
this.addForeignKey(doctype, field);
}
}
async getForeignKey(doctype, field) {
}
async getTableColumns(doctype) {
return [];
}
async runAddColumnQuery(doctype, field) {
// alter table {doctype} add column ({column_def});
}
async get(doctype, name = null, fields = '*') {
let meta = frappe.getMeta(doctype);
let doc;
if (meta.isSingle) {
doc = await this.getSingle(doctype);
doc.name = doctype;
} else {
if (!name) {
throw new frappe.errors.ValueError('name is mandatory');
}
doc = await this.getOne(doctype, name, fields);
}
await this.loadChildren(doc, meta);
return doc;
}
async loadChildren(doc, meta) {
// load children
let tableFields = meta.getTableFields();
for (let field of tableFields) {
doc[field.fieldname] = await this.getAll({
doctype: field.childtype,
fields: ["*"],
filters: { parent: doc.name },
orderBy: 'idx',
order: 'asc'
});
}
}
async getSingle(doctype) {
let values = await this.getAll({
doctype: 'SingleValue',
fields: ['fieldname', 'value'],
filters: { parent: doctype },
orderBy: 'fieldname',
order: 'asc'
});
let doc = {};
for (let row of values) {
doc[row.fieldname] = row.value;
}
return doc;
}
async getOne(doctype, name, fields = '*') {
// select {fields} form {doctype} where name = ?
}
prepareFields(fields) {
if (fields instanceof Array) {
fields = fields.join(", ");
}
return fields;
}
triggerChange(doctype, name) {
this.trigger(`change:${doctype}`, { name: name }, 500);
this.trigger(`change`, { doctype: name, name: name }, 500);
}
async insert(doctype, doc) {
let meta = frappe.getMeta(doctype);
// insert parent
if (meta.isSingle) {
await this.updateSingle(meta, doc, doctype);
} else {
await this.insertOne(doctype, doc);
}
// insert children
await this.insertChildren(meta, doc, doctype);
this.triggerChange(doctype, doc.name);
return doc;
}
async insertChildren(meta, doc, doctype) {
let tableFields = meta.getTableFields();
for (let field of tableFields) {
let idx = 0;
for (let child of (doc[field.fieldname] || [])) {
this.prepareChild(doctype, doc.name, child, field, idx);
await this.insertOne(field.childtype, child);
idx++;
}
}
}
async insertOne(doctype, doc) {
// insert into {doctype} ({fields}) values ({values})
}
async update(doctype, doc) {
let meta = frappe.getMeta(doctype);
// update parent
if (meta.isSingle) {
await this.updateSingle(meta, doc, doctype);
} else {
await this.updateOne(doctype, doc);
}
// insert or update children
await this.updateChildren(meta, doc, doctype);
this.triggerChange(doctype, doc.name);
return doc;
}
async updateChildren(meta, doc, doctype) {
let tableFields = meta.getTableFields();
for (let field of tableFields) {
// first key is "parent" - for SQL params
let added = [doc.name];
for (let child of (doc[field.fieldname] || [])) {
this.prepareChild(doctype, doc.name, child, field, added.length - 1);
if (await this.exists(field.childtype, child.name)) {
await this.updateOne(field.childtype, child);
} }
} else {
await this.insertOne(field.childtype, child);
async getSingle(doctype) {
let values = await this.getAll({
doctype: 'SingleValue',
fields: ['fieldname', 'value'],
filters: { parent: doctype },
orderBy: 'fieldname',
order: 'asc'
});
let doc = {};
for (let row of values) {
doc[row.fieldname] = row.value;
} }
return doc; added.push(child.name);
}
await this.runDeleteOtherChildren(field, added);
}
}
async updateOne(doctype, doc) {
// update {doctype} set {field=value} where name=?
}
async runDeleteOtherChildren(field, added) {
// delete from doctype where parent = ? and name not in (?, ?, ?)
}
async updateSingle(meta, doc, doctype) {
await this.deleteSingleValues();
for (let field of meta.getValidFields({ withChildren: false })) {
let value = doc[field.fieldname];
if (value) {
let singleValue = frappe.newDoc({
doctype: 'SingleValue',
parent: doctype,
fieldname: field.fieldname,
value: value
})
await singleValue.insert();
}
}
}
async deleteSingleValues(name) {
// await frappe.db.run('delete from SingleValue where parent=?', name)
}
prepareChild(parenttype, parent, child, field, idx) {
if (!child.name) {
child.name = frappe.getRandomString();
}
child.parent = parent;
child.parenttype = parenttype;
child.parentfield = field.fieldname;
child.idx = idx;
}
getKeys(doctype) {
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
}
getFormattedValues(fields, doc) {
let values = fields.map(field => {
let value = doc[field.fieldname];
return this.getFormattedValue(field, value);
});
return values;
}
getFormattedValue(field, value) {
if (value instanceof Date) {
if (field.fieldtype === 'Date') {
// date
return value.toISOString().substr(0, 10);
} else {
// datetime
return value.toISOString();
}
} else if (field.fieldtype === 'Link' && !value) {
// empty value must be null to satisfy
// foreign key constraint
return null;
} else {
return value;
}
}
async deleteMany(doctype, names) {
for (const name of names) {
await this.delete(doctype, name);
}
}
async delete(doctype, name) {
await this.deleteOne(doctype, name);
// delete children
let tableFields = frappe.getMeta(doctype).getTableFields();
for (let field of tableFields) {
await this.deleteChildren(field.childtype, name);
} }
async getOne(doctype, name, fields = '*') { this.triggerChange(doctype, name);
// select {fields} form {doctype} where name = ? }
async deleteOne(doctype, name) {
// delete from {doctype} where name = ?
}
async deleteChildren(parenttype, parent) {
// delete from {parenttype} where parent = ?
}
async exists(doctype, name) {
return (await this.getValue(doctype, name)) ? true : false;
}
async getValue(doctype, filters, fieldname = 'name') {
if (typeof filters === 'string') {
filters = { name: filters };
} }
prepareFields(fields) { let row = await this.getAll({
if (fields instanceof Array) { doctype: doctype,
fields = fields.join(", "); fields: [fieldname],
} filters: filters,
return fields; start: 0,
} limit: 1,
orderBy: 'name',
order: 'asc'
});
return row.length ? row[0][fieldname] : null;
}
triggerChange(doctype, name) { async setValue(doctype, name, fieldname, value) {
this.trigger(`change:${doctype}`, {name:name}, 500); return await this.setValues(doctype, name, {
this.trigger(`change`, {doctype:name, name:name}, 500); [fieldname]: value
} });
}
async insert(doctype, doc) { async setValues(doctype, name, fieldValuePair) {
let meta = frappe.getMeta(doctype); //
}
// insert parent getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', order = 'desc' } = {}) {
if (meta.isSingle) { // select {fields} from {doctype} where {filters} order by {orderBy} {order} limit {start} {limit}
await this.updateSingle(meta, doc, doctype); }
} else {
await this.insertOne(doctype, doc); getFilterConditions(filters) {
// {"status": "Open"} => `status = "Open"`
// {"status": "Open", "name": ["like", "apple%"]}
// => `status="Open" and name like "apple%"
// {"date": [">=", "2017-09-09", "<=", "2017-11-01"]}
// => `date >= 2017-09-09 and date <= 2017-11-01`
let filtersArray = [];
for (let key in filters) {
let value = filters[key];
let field = key;
let operator = '=';
let comparisonValue = value;
if (Array.isArray(value)) {
operator = value[0];
comparisonValue = value[1];
operator = operator.toLowerCase();
if (operator === 'includes') {
operator = 'like';
} }
// insert children if (['like', 'includes'].includes(operator) && !comparisonValue.includes('%')) {
await this.insertChildren(meta, doc, doctype); comparisonValue = `%${comparisonValue}%`;
this.triggerChange(doctype, doc.name);
return doc;
}
async insertChildren(meta, doc, doctype) {
let tableFields = meta.getTableFields();
for (let field of tableFields) {
let idx = 0;
for (let child of (doc[field.fieldname] || [])) {
this.prepareChild(doctype, doc.name, child, field, idx);
await this.insertOne(field.childtype, child);
idx++;
}
} }
}
filtersArray.push([field, operator, comparisonValue]);
if (Array.isArray(value) && value.length > 2) {
// multiple conditions
let operator = value[2];
let comparisonValue = value[3];
filtersArray.push([field, operator, comparisonValue]);
}
} }
async insertOne(doctype, doc) { let conditions = filtersArray.map(filter => {
// insert into {doctype} ({fields}) values ({values}) const [field, operator, comparisonValue] = filter;
}
let placeholder = Array.isArray(comparisonValue) ?
async update(doctype, doc) { comparisonValue.map(v => '?').join(', ') :
let meta = frappe.getMeta(doctype); '?';
// update parent return `ifnull(${field}, '') ${operator} (${placeholder})`;
if (meta.isSingle) { });
await this.updateSingle(meta, doc, doctype);
} else { let values = filtersArray.reduce((acc, filter) => {
await this.updateOne(doctype, doc); const comparisonValue = filter[2];
} if (Array.isArray(comparisonValue)) {
acc = acc.concat(comparisonValue);
// insert or update children } else {
await this.updateChildren(meta, doc, doctype); acc.push(comparisonValue);
}
this.triggerChange(doctype, doc.name); return acc;
}, []);
return doc;
} return {
conditions: conditions.length ? conditions.join(" and ") : "",
async updateChildren(meta, doc, doctype) { values
let tableFields = meta.getTableFields(); };
for (let field of tableFields) { }
// first key is "parent" - for SQL params
let added = [doc.name]; async run(query, params) {
for (let child of (doc[field.fieldname] || [])) { // run query
this.prepareChild(doctype, doc.name, child, field, added.length - 1); }
if (await this.exists(field.childtype, child.name)) {
await this.updateOne(field.childtype, child); async sql(query, params) {
} // run sql
else { }
await this.insertOne(field.childtype, child);
} async commit() {
added.push(child.name); // commit
} }
await this.runDeleteOtherChildren(field, added);
} initTypeMap() {
} this.typeMap = {
'Autocomplete': 'text'
async updateOne(doctype, doc) { , 'Currency': 'real'
// update {doctype} set {field=value} where name=? , 'Int': 'integer'
} , 'Float': 'real'
, 'Percent': 'real'
async runDeleteOtherChildren(field, added) { , 'Check': 'integer'
// delete from doctype where parent = ? and name not in (?, ?, ?) , 'Small Text': 'text'
} , 'Long Text': 'text'
, 'Code': 'text'
async updateSingle(meta, doc, doctype) { , 'Text Editor': 'text'
await this.deleteSingleValues(); , 'Date': 'text'
for (let field of meta.getValidFields({withChildren: false})) { , 'Datetime': 'text'
let value = doc[field.fieldname]; , 'Time': 'text'
if (value) { , 'Text': 'text'
let singleValue = frappe.newDoc({ , 'Data': 'text'
doctype: 'SingleValue', , 'Link': 'text'
parent: doctype, , 'DynamicLink': 'text'
fieldname: field.fieldname, , 'Password': 'text'
value: value , 'Select': 'text'
}) , 'Read Only': 'text'
await singleValue.insert(); , 'File': 'text'
} , 'Attach': 'text'
} , 'Attach Image': 'text'
} , 'Signature': 'text'
, 'Color': 'text'
async deleteSingleValues(name) { , 'Barcode': 'text'
// await frappe.db.run('delete from SingleValue where parent=?', name) , 'Geolocation': 'text'
}
prepareChild(parenttype, parent, child, field, idx) {
if (!child.name) {
child.name = frappe.getRandomString();
}
child.parent = parent;
child.parenttype = parenttype;
child.parentfield = field.fieldname;
child.idx = idx;
}
getKeys(doctype) {
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
}
getFormattedValues(fields, doc) {
let values = fields.map(field => {
let value = doc[field.fieldname];
if (value instanceof Date) {
if (field.fieldtype==='Date') {
// date
return value.toISOString().substr(0, 10);
} else {
// datetime
return value.toISOString();
}
} else if (field.fieldtype === 'Link' && !value) {
// empty value must be null to satisfy
// foreign key constraint
return null;
} else {
return value;
}
});
return values;
}
async deleteMany(doctype, names) {
for (const name of names) {
await this.delete(doctype, name);
}
}
async delete(doctype, name) {
await this.deleteOne(doctype, name);
// delete children
let tableFields = frappe.getMeta(doctype).getTableFields();
for (let field of tableFields) {
await this.deleteChildren(field.childtype, name);
}
this.triggerChange(doctype, name);
}
async deleteOne(doctype, name) {
// delete from {doctype} where name = ?
}
async deleteChildren(parenttype, parent) {
// delete from {parenttype} where parent = ?
}
async exists(doctype, name) {
return (await this.getValue(doctype, name)) ? true : false;
}
async getValue(doctype, filters, fieldname = 'name') {
if (typeof filters === 'string') {
filters = { name: filters };
}
let row = await this.getAll({
doctype: doctype,
fields: [fieldname],
filters: filters,
start: 0,
limit: 1,
orderBy: 'name',
order: 'asc'
});
return row.length ? row[0][fieldname] : null;
}
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', order = 'desc' } = {}) {
// select {fields} from {doctype} where {filters} order by {orderBy} {order} limit {start} {limit}
}
getFilterConditions(filters) {
// {"status": "Open"} => `status = "Open"`
// {"status": "Open", "name": ["like", "apple%"]}
// => `status="Open" and name like "apple%"
// {"date": [">=", "2017-09-09", "<=", "2017-11-01"]}
// => `date >= 2017-09-09 and date <= 2017-11-01`
let filtersArray = [];
for (let key in filters) {
let value = filters[key];
let field = key;
let operator = '=';
let comparisonValue = value;
if (Array.isArray(value)) {
operator = value[0];
comparisonValue = value[1];
operator = operator.toLowerCase();
if (operator === 'includes') {
operator = 'like';
}
if (['like', 'includes'].includes(operator) && !comparisonValue.includes('%')) {
comparisonValue = `%${comparisonValue}%`;
}
}
filtersArray.push([field, operator, comparisonValue]);
if (Array.isArray(value) && value.length > 2) {
// multiple conditions
let operator = value[2];
let comparisonValue = value[3];
filtersArray.push([field, operator, comparisonValue]);
}
}
let conditions = filtersArray.map(filter => {
const [field, operator, comparisonValue] = filter;
let placeholder = Array.isArray(comparisonValue) ?
comparisonValue.map(v => '?').join(', ') :
'?';
return `ifnull(${field}, '') ${operator} (${placeholder})`;
});
let values = filtersArray.reduce((acc, filter) => {
const comparisonValue = filter[2];
if (Array.isArray(comparisonValue)) {
acc = acc.concat(comparisonValue);
} else {
acc.push(comparisonValue);
}
return acc;
}, []);
return {
conditions: conditions.length ? conditions.join(" and ") : "",
values
};
}
async run(query, params) {
// run query
}
async sql(query, params) {
// run sql
}
async commit() {
// commit
}
initTypeMap() {
this.typeMap = {
'Autocomplete': 'text'
, 'Currency': 'real'
, 'Int': 'integer'
, 'Float': 'real'
, 'Percent': 'real'
, 'Check': 'integer'
, 'Small Text': 'text'
, 'Long Text': 'text'
, 'Code': 'text'
, 'Text Editor': 'text'
, 'Date': 'text'
, 'Datetime': 'text'
, 'Time': 'text'
, 'Text': 'text'
, 'Data': 'text'
, 'Link': 'text'
, 'DynamicLink': 'text'
, 'Password': 'text'
, 'Select': 'text'
, 'Read Only': 'text'
, 'File': 'text'
, 'Attach': 'text'
, 'Attach Image': 'text'
, 'Signature': 'text'
, 'Color': 'text'
, 'Barcode': 'text'
, 'Geolocation': 'text'
}
} }
}
} }

View File

@ -2,157 +2,220 @@ const frappe = require('frappejs');
const Observable = require('frappejs/utils/observable'); const Observable = require('frappejs/utils/observable');
module.exports = class HTTPClient extends Observable { module.exports = class HTTPClient extends Observable {
constructor({ server, protocol = 'http' }) { constructor({ server, protocol = 'http' }) {
super(); super();
this.server = server; this.server = server;
this.protocol = protocol; this.protocol = protocol;
frappe.config.serverURL = this.getURL(); frappe.config.serverURL = this.getURL();
// if the backend is http, then always client! // if the backend is http, then always client!
frappe.isServer = false; frappe.isServer = false;
this.initTypeMap(); this.initTypeMap();
}
connect() {
}
async insert(doctype, doc) {
doc.doctype = doctype;
let filesToUpload = this.getFilesToUpload(doc);
let url = this.getURL('/api/resource', doctype);
const responseDoc = await this.fetch(url, {
method: 'POST',
body: JSON.stringify(doc)
});
await this.uploadFilesAndUpdateDoc(filesToUpload, doctype, responseDoc);
return responseDoc;
}
async get(doctype, name) {
name = encodeURIComponent(name);
let url = this.getURL('/api/resource', doctype, name);
return await this.fetch(url, {
method: 'GET',
headers: this.getHeaders()
})
}
async getAll({ doctype, fields, filters, start, limit, sortBy, order }) {
let url = this.getURL('/api/resource', doctype);
url = url + '?' + frappe.getQueryString({
fields: JSON.stringify(fields),
filters: JSON.stringify(filters),
start: start,
limit: limit,
sortBy: sortBy,
order: order
});
return await this.fetch(url, {
method: 'GET',
});
}
async update(doctype, doc) {
doc.doctype = doctype;
let filesToUpload = this.getFilesToUpload(doc);
let url = this.getURL('/api/resource', doctype, doc.name);
const responseDoc = await this.fetch(url, {
method: 'PUT',
body: JSON.stringify(doc)
});
await this.uploadFilesAndUpdateDoc(filesToUpload, doctype, responseDoc);
return responseDoc;
}
async delete(doctype, name) {
let url = this.getURL('/api/resource', doctype, name);
return await this.fetch(url, {
method: 'DELETE',
});
}
async deleteMany(doctype, names) {
let url = this.getURL('/api/resource', doctype);
return await this.fetch(url, {
method: 'DELETE',
body: JSON.stringify(names)
});
}
async exists(doctype, name) {
return (await this.getValue(doctype, name, 'name')) ? true : false;
}
async getValue(doctype, name, fieldname) {
let url = this.getURL('/api/resource', doctype, name, fieldname);
return (await this.fetch(url, {
method: 'GET',
})).value;
}
async fetch(url, args) {
args.headers = this.getHeaders();
let response = await frappe.fetch(url, args);
let data = await response.json();
if (response.status !== 200) {
throw Error(data.error);
} }
connect() { return data;
}
} getFilesToUpload(doc) {
const meta = frappe.getMeta(doc.doctype);
const fileFields = meta.getFieldsWith({ fieldtype: 'File' });
const filesToUpload = [];
async insert(doctype, doc) { if (fileFields.length > 0) {
doc.doctype = doctype; fileFields.forEach(df => {
let url = this.getURL('/api/resource', doctype); const files = doc[df.fieldname] || [];
return await this.fetch(url, { if (files.length) {
method: 'POST', filesToUpload.push({
body: JSON.stringify(doc) fieldname: df.fieldname,
}) files: files
} })
async get(doctype, name) {
let url = this.getURL('/api/resource', doctype, name);
return await this.fetch(url, {
method: 'GET',
headers: this.getHeaders()
})
}
async getAll({ doctype, fields, filters, start, limit, sort_by, order }) {
let url = this.getURL('/api/resource', doctype);
url = url + "?" + frappe.getQueryString({
fields: JSON.stringify(fields),
filters: JSON.stringify(filters),
start: start,
limit: limit,
sort_by: sort_by,
order: order
});
return await this.fetch(url, {
method: 'GET',
});
}
async update(doctype, doc) {
doc.doctype = doctype;
let url = this.getURL('/api/resource', doctype, doc.name);
return await this.fetch(url, {
method: 'PUT',
body: JSON.stringify(doc)
});
}
async delete(doctype, name) {
let url = this.getURL('/api/resource', doctype, name);
return await this.fetch(url, {
method: 'DELETE',
});
}
async deleteMany(doctype, names) {
let url = this.getURL('/api/resource', doctype);
return await this.fetch(url, {
method: 'DELETE',
body: JSON.stringify(names)
});
}
async exists(doctype, name) {
return (await this.getValue(doctype, name, 'name')) ? true : false;
}
async getValue(doctype, name, fieldname) {
let url = this.getURL('/api/resource', doctype, name, fieldname);
return (await this.fetch(url, {
method: 'GET',
})).value;
}
async fetch(url, args) {
args.headers = this.getHeaders();
let response = await frappe.fetch(url, args);
let data = await response.json();
if (response.status !== 200) {
throw Error(data.error);
} }
delete doc[df.fieldname];
return data; });
} }
getURL(...parts) { return filesToUpload;
return this.protocol + '://' + this.server + (parts || []).join('/'); }
async uploadFilesAndUpdateDoc(filesToUpload, doctype, doc) {
if (filesToUpload.length > 0) {
// upload files
for (const fileToUpload of filesToUpload) {
const files = await this.uploadFiles(fileToUpload.files, doctype, doc.name, fileToUpload.fieldname);
doc[fileToUpload.fieldname] = files[0].name;
}
}
}
async uploadFiles(fileList, doctype, name, fieldname) {
let url = this.getURL('/api/upload', doctype, name, fieldname);
let formData = new FormData();
for (const file of fileList) {
formData.append('files', file, file.name);
} }
getHeaders() { let response = await frappe.fetch(url, {
const headers = { method: 'POST',
'Accept': 'application/json', body: formData
'Content-Type': 'application/json' });
};
if (frappe.session && frappe.session.token) {
headers.token = frappe.session.token;
};
return headers;
}
initTypeMap() { const data = await response.json();
this.typeMap = { if (response.status !== 200) {
'Autocomplete': true throw Error(data.error);
, 'Currency': true
, 'Int': true
, 'Float': true
, 'Percent': true
, 'Check': true
, 'Small Text': true
, 'Long Text': true
, 'Code': true
, 'Text Editor': true
, 'Date': true
, 'Datetime': true
, 'Time': true
, 'Text': true
, 'Data': true
, 'Link': true
, 'DynamicLink': true
, 'Password': true
, 'Select': true
, 'Read Only': true
, 'File': true
, 'Attach': true
, 'Attach Image': true
, 'Signature': true
, 'Color': true
, 'Barcode': true
, 'Geolocation': true
}
} }
return data;
}
close() { getURL(...parts) {
return this.protocol + '://' + this.server + (parts || []).join('/');
}
getHeaders() {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (frappe.session && frappe.session.token) {
headers.token = frappe.session.token;
};
return headers;
}
initTypeMap() {
this.typeMap = {
'Autocomplete': true
, 'Currency': true
, 'Int': true
, 'Float': true
, 'Percent': true
, 'Check': true
, 'Small Text': true
, 'Long Text': true
, 'Code': true
, 'Text Editor': true
, 'Date': true
, 'Datetime': true
, 'Time': true
, 'Text': true
, 'Data': true
, 'Link': true
, 'DynamicLink': true
, 'Password': true
, 'Select': true
, 'Read Only': true
, 'File': true
, 'Attach': true
, 'Attach Image': true
, 'Signature': true
, 'Color': true
, 'Barcode': true
, 'Geolocation': true
} }
}
close() {
}
} }

View File

@ -4,244 +4,268 @@ const Database = require('./database');
const debug = false; const debug = false;
module.exports = class sqliteDatabase extends Database { module.exports = class sqliteDatabase extends Database {
constructor({ dbPath }) { constructor({ dbPath }) {
super(); super();
this.dbPath = dbPath; this.dbPath = dbPath;
} }
connect(dbPath) { connect(dbPath) {
if (dbPath) { if (dbPath) {
this.dbPath = dbPath; this.dbPath = dbPath;
}
return new Promise(resolve => {
this.conn = new sqlite3.Database(this.dbPath, () => {
if (debug) {
this.conn.on('trace', (trace) => console.log(trace));
} }
return new Promise(resolve => { this.run('PRAGMA foreign_keys=ON').then(resolve);
this.conn = new sqlite3.Database(this.dbPath, () => { });
if (debug) { });
this.conn.on('trace', (trace) => console.log(trace)); }
}
this.run('PRAGMA foreign_keys=ON').then(resolve);
});
});
}
async tableExists(table) { async tableExists(table) {
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`); const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
return (name && name.length) ? true : false; return (name && name.length) ? true : false;
} }
async addForeignKeys(doctype, newForeignKeys) { async addForeignKeys(doctype, newForeignKeys) {
await this.run('PRAGMA foreign_keys=OFF'); await this.run('PRAGMA foreign_keys=OFF');
await this.run('BEGIN TRANSACTION'); await this.run('BEGIN TRANSACTION');
const tempName = 'TEMP' + doctype const tempName = 'TEMP' + doctype
// create temp table // create temp table
await this.createTable(doctype, tempName); await this.createTable(doctype, tempName);
const columns = (await this.getTableColumns(tempName)).join(', '); const columns = (await this.getTableColumns(tempName)).join(', ');
// copy from old to new table // copy from old to new table
await this.run(`INSERT INTO ${tempName} (${columns}) SELECT ${columns} from ${doctype}`); await this.run(`INSERT INTO ${tempName} (${columns}) SELECT ${columns} from ${doctype}`);
// drop old table // drop old table
await this.run(`DROP TABLE ${doctype}`); await this.run(`DROP TABLE ${doctype}`);
// rename new table // rename new table
await this.run(`ALTER TABLE ${tempName} RENAME TO ${doctype}`); await this.run(`ALTER TABLE ${tempName} RENAME TO ${doctype}`);
await this.run('COMMIT'); await this.run('COMMIT');
await this.run('PRAGMA foreign_keys=ON'); await this.run('PRAGMA foreign_keys=ON');
} }
removeColumns() { removeColumns() {
// pass // pass
} }
async runCreateTableQuery(doctype, columns, indexes) { async runCreateTableQuery(doctype, columns, indexes) {
const query = `CREATE TABLE IF NOT EXISTS ${doctype} ( const query = `CREATE TABLE IF NOT EXISTS ${doctype} (
${columns.join(", ")} ${indexes.length ? (", " + indexes.join(", ")) : ''})`; ${columns.join(", ")} ${indexes.length ? (", " + indexes.join(", ")) : ''})`;
return await this.run(query); return await this.run(query);
}
updateColumnDefinition(field, columns, indexes) {
let def = this.getColumnDefinition(field);
columns.push(def);
if (field.fieldtype === 'Link' && field.target) {
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`);
} }
}
updateColumnDefinition(field, columns, indexes) { getColumnDefinition(field) {
let def = this.getColumnDefinition(field); let def = [
field.fieldname,
this.typeMap[field.fieldtype],
field.fieldname === 'name' ? 'PRIMARY KEY NOT NULL' : '',
field.required ? 'NOT NULL' : '',
field.default ? `DEFAULT ${field.default}` : ''
].join(' ');
columns.push(def); return def;
}
if (field.fieldtype==='Link' && field.target) { async getTableColumns(doctype) {
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`); return (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name);
} }
}
getColumnDefinition(field) { async getForeignKeys(doctype) {
let def = [ return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(d => d.from);
field.fieldname, }
this.typeMap[field.fieldtype],
field.fieldname === 'name' ? 'PRIMARY KEY NOT NULL' : '',
field.required ? 'NOT NULL' : '',
field.default ? `DEFAULT ${field.default}` : ''
].join(' ');
return def; async runAddColumnQuery(doctype, field, values) {
} await this.run(`ALTER TABLE ${doctype} ADD COLUMN ${this.getColumnDefinition(field)}`, values);
}
async getTableColumns(doctype) { getOne(doctype, name, fields = '*') {
return (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name); fields = this.prepareFields(fields);
} return new Promise((resolve, reject) => {
this.conn.get(`select ${fields} from ${doctype}
async getForeignKeys(doctype) {
return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(d => d.from);
}
async runAddColumnQuery(doctype, field, values) {
await this.run(`ALTER TABLE ${doctype} ADD COLUMN ${this.getColumnDefinition(field)}`, values);
}
getOne(doctype, name, fields = '*') {
fields = this.prepareFields(fields);
return new Promise((resolve, reject) => {
this.conn.get(`select ${fields} from ${doctype}
where name = ?`, name, where name = ?`, name,
(err, row) => { (err, row) => {
resolve(row || {}); resolve(row || {});
});
}); });
});
}
async insertOne(doctype, doc) {
let fields = this.getKeys(doctype);
let placeholders = fields.map(d => '?').join(', ');
if (!doc.name) {
doc.name = frappe.getRandomString();
} }
async insertOne(doctype, doc) { return await this.run(`insert into ${doctype}
let fields = this.getKeys(doctype);
let placeholders = fields.map(d => '?').join(', ');
if (!doc.name) {
doc.name = frappe.getRandomString();
}
return await this.run(`insert into ${doctype}
(${fields.map(field => field.fieldname).join(", ")}) (${fields.map(field => field.fieldname).join(", ")})
values (${placeholders})`, this.getFormattedValues(fields, doc)); values (${placeholders})`, this.getFormattedValues(fields, doc));
} }
async updateOne(doctype, doc) { async updateOne(doctype, doc) {
let fields = this.getKeys(doctype); let fields = this.getKeys(doctype);
let assigns = fields.map(field => `${field.fieldname} = ?`); let assigns = fields.map(field => `${field.fieldname} = ?`);
let values = this.getFormattedValues(fields, doc); let values = this.getFormattedValues(fields, doc);
// additional name for where clause // additional name for where clause
values.push(doc.name); values.push(doc.name);
return await this.run(`update ${doctype} return await this.run(`update ${doctype}
set ${assigns.join(", ")} where name=?`, values); set ${assigns.join(", ")} where name=?`, values);
} }
async runDeleteOtherChildren(field, added) { async runDeleteOtherChildren(field, added) {
// delete other children // delete other children
// `delete from doctype where parent = ? and name not in (?, ?, ?)}` // `delete from doctype where parent = ? and name not in (?, ?, ?)}`
await this.run(`delete from ${field.childtype} await this.run(`delete from ${field.childtype}
where where
parent = ? and parent = ? and
name not in (${added.slice(1).map(d => '?').join(', ')})`, added); name not in (${added.slice(1).map(d => '?').join(', ')})`, added);
}
async deleteOne(doctype, name) {
return await this.run(`delete from ${doctype} where name=?`, name);
}
async deleteChildren(parenttype, parent) {
await this.run(`delete from ${parenttype} where parent=?`, parent);
}
async deleteSingleValues(name) {
await frappe.db.run('delete from SingleValue where parent=?', name)
}
async setValues(doctype, name, fieldValuePair) {
const meta = frappe.getMeta(doctype);
const validFields = this.getKeys(doctype);
const validFieldnames = validFields.map(df => df.fieldname);
const fieldsToUpdate = Object.keys(fieldValuePair)
.filter(fieldname => validFieldnames.includes(fieldname))
// assignment part of query
const assigns = fieldsToUpdate.map(fieldname => `${fieldname} = ?`);
// values
const values = fieldsToUpdate.map(fieldname => {
const field = meta.getField(fieldname);
const value = fieldValuePair[fieldname];
return this.getFormattedValue(field, value);
});
// additional name for where clause
values.push(name);
return await this.run(`update ${doctype}
set ${assigns.join(', ')} where name=?`, values);
}
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) {
if (!fields) {
fields = frappe.getMeta(doctype).getKeywordFields();
}
if (typeof fields === 'string') {
fields = [fields];
} }
async deleteOne(doctype, name) { return new Promise((resolve, reject) => {
return await this.run(`delete from ${doctype} where name=?`, name); let conditions = this.getFilterConditions(filters);
} let query = `select ${fields.join(", ")}
async deleteChildren(parenttype, parent) {
await this.run(`delete from ${parenttype} where parent=?`, parent);
}
async deleteSingleValues(name) {
await frappe.db.run('delete from SingleValue where parent=?', name)
}
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) {
if (!fields) {
fields = frappe.getMeta(doctype).getKeywordFields();
}
if (typeof fields === 'string') {
fields = [fields];
}
return new Promise((resolve, reject) => {
let conditions = this.getFilterConditions(filters);
let query = `select ${fields.join(", ")}
from ${doctype} from ${doctype}
${conditions.conditions ? "where" : ""} ${conditions.conditions} ${conditions.conditions ? "where" : ""} ${conditions.conditions}
${groupBy ? ("group by " + groupBy.join(', ')) : ""} ${groupBy ? ("group by " + groupBy.join(', ')) : ""}
${orderBy ? ("order by " + orderBy) : ""} ${orderBy ? (order || "asc") : ""} ${orderBy ? ("order by " + orderBy) : ""} ${orderBy ? (order || "asc") : ""}
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`; ${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`;
this.conn.all(query, conditions.values, this.conn.all(query, conditions.values,
(err, rows) => { (err, rows) => {
if (err) { if (err) {
console.error(err); console.error(err);
reject(err); reject(err);
} else { } else {
resolve(rows); resolve(rows);
} }
});
}); });
} });
}
run(query, params) { run(query, params) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.conn.run(query, params, (err) => { this.conn.run(query, params, (err) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
resolve(); resolve();
}
});
});
}
sql(query, params) {
return new Promise((resolve) => {
this.conn.all(query, params, (err, rows) => {
resolve(rows);
});
});
}
async commit() {
try {
await this.run('commit');
} catch (e) {
if (e.errno !== 1) {
throw e;
}
} }
} });
});
}
initTypeMap() { sql(query, params) {
this.typeMap = { return new Promise((resolve) => {
'Autocomplete': 'text' this.conn.all(query, params, (err, rows) => {
, 'Currency': 'real' resolve(rows);
, 'Int': 'integer' });
, 'Float': 'real' });
, 'Percent': 'real' }
, 'Check': 'integer'
, 'Small Text': 'text' async commit() {
, 'Long Text': 'text' try {
, 'Code': 'text' await this.run('commit');
, 'Text Editor': 'text' } catch (e) {
, 'Date': 'text' if (e.errno !== 1) {
, 'Datetime': 'text' throw e;
, 'Time': 'text' }
, 'Text': 'text'
, 'Data': 'text'
, 'Link': 'text'
, 'DynamicLink': 'text'
, 'Password': 'text'
, 'Select': 'text'
, 'Read Only': 'text'
, 'File': 'text'
, 'Attach': 'text'
, 'Attach Image': 'text'
, 'Signature': 'text'
, 'Color': 'text'
, 'Barcode': 'text'
, 'Geolocation': 'text'
}
} }
}
initTypeMap() {
this.typeMap = {
'Autocomplete': 'text'
, 'Currency': 'real'
, 'Int': 'integer'
, 'Float': 'real'
, 'Percent': 'real'
, 'Check': 'integer'
, 'Small Text': 'text'
, 'Long Text': 'text'
, 'Code': 'text'
, 'Text Editor': 'text'
, 'Date': 'text'
, 'Datetime': 'text'
, 'Time': 'text'
, 'Text': 'text'
, 'Data': 'text'
, 'Link': 'text'
, 'DynamicLink': 'text'
, 'Password': 'text'
, 'Select': 'text'
, 'Read Only': 'text'
, 'File': 'text'
, 'Attach': 'text'
, 'Attach Image': 'text'
, 'Signature': 'text'
, 'Color': 'text'
, 'Barcode': 'text'
, 'Geolocation': 'text'
}
}
} }

View File

@ -29,6 +29,25 @@ module.exports = class BaseMeta extends BaseDocument {
return this._field_map[fieldname]; 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) { getLabel(fieldname) {
return this.getField(fieldname).label; return this.getField(fieldname).label;
} }

View File

@ -0,0 +1,67 @@
module.exports = {
name: 'File',
doctype: 'DocType',
isSingle: 0,
keywordFields: [
'name',
'filename'
],
fields: [
{
fieldname: 'name',
label: 'File Path',
fieldtype: 'Data',
required: 1,
},
{
fieldname: 'filename',
label: 'File Name',
fieldtype: 'Data',
required: 1,
},
{
fieldname: 'mimetype',
label: 'MIME Type',
fieldtype: 'Data',
},
{
fieldname: 'size',
label: 'File Size',
fieldtype: 'Int',
},
{
fieldname: 'referenceDoctype',
label: 'Reference DocType',
fieldtype: 'Data',
},
{
fieldname: 'referenceName',
label: 'Reference Name',
fieldtype: 'Data',
},
{
fieldname: 'referenceField',
label: 'Reference Field',
fieldtype: 'Data',
},
],
layout: [
{
columns: [
{ fields: ['filename'] },
]
},
{
columns: [
{ fields: ['mimetype'] },
{ fields: ['size'] },
]
},
{
columns: [
{ fields: ['referenceDoctype'] },
{ fields: ['referenceName'] },
]
},
]
}

View File

@ -1,66 +1,66 @@
const indicatorColor = require('frappejs/ui/constants/indicators'); const { BLUE, GREEN } = require('frappejs/ui/constants/indicators');
module.exports = { module.exports = {
name: "ToDo", name: 'ToDo',
label: "To Do", label: 'To Do',
naming: "autoincrement", naming: 'autoincrement',
pageSettings: { pageSettings: {
hideTitle: true hideTitle: true
},
isSingle: 0,
keywordFields: [
'subject',
'description'
],
titleField: 'subject',
indicators: {
key: 'status',
colors: {
Open: BLUE,
Closed: GREEN
}
},
fields: [
{
fieldname: 'subject',
label: 'Subject',
fieldtype: 'Data',
required: 1
}, },
"isSingle": 0, {
"keywordFields": [ fieldname: 'status',
"subject", label: 'Status',
"description" fieldtype: 'Select',
], options: [
titleField: 'subject', 'Open',
indicators: { 'Closed'
key: 'status', ],
colors: { default: 'Open',
Open: indicatorColor.BLUE, required: 1
Closed: indicatorColor.GREEN
}
}, },
"fields": [ {
{ fieldname: 'description',
"fieldname": "subject", label: 'Description',
"label": "Subject", fieldtype: 'Text'
"fieldtype": "Data", }
"required": 1 ],
},
{
"fieldname": "status",
"label": "Status",
"fieldtype": "Select",
"options": [
"Open",
"Closed"
],
"default": "Open",
"required": 1
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
}
],
links: [ links: [
{ {
label: 'Close', label: 'Close',
condition: (form) => form.doc.status !== 'Closed', condition: (form) => form.doc.status !== 'Closed',
action: async (form) => { action: async (form) => {
await form.doc.set('status', 'Closed'); await form.doc.set('status', 'Closed');
await form.doc.update(); await form.doc.update();
} }
}, },
{ {
label: 'Re-Open', label: 'Re-Open',
condition: (form) => form.doc.status !== 'Open', condition: (form) => form.doc.status !== 'Open',
action: async (form) => { action: async (form) => {
await form.doc.set('status', 'Open'); await form.doc.set('status', 'Open');
await form.doc.update(); await form.doc.update();
} }
} }
] ]
} }

View File

@ -11,6 +11,7 @@ module.exports = {
SystemSettings: require('./doctype/SystemSettings/SystemSettings.js'), SystemSettings: require('./doctype/SystemSettings/SystemSettings.js'),
ToDo: require('./doctype/ToDo/ToDo.js'), ToDo: require('./doctype/ToDo/ToDo.js'),
User: require('./doctype/User/User.js'), User: require('./doctype/User/User.js'),
UserRole: require('./doctype/UserRole/UserRole.js') UserRole: require('./doctype/UserRole/UserRole.js'),
File: require('./doctype/File/File.js'),
} }
} }

View File

@ -37,6 +37,7 @@
"luxon": "^1.0.0", "luxon": "^1.0.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"morgan": "^1.9.0", "morgan": "^1.9.0",
"multer": "^1.3.1",
"mysql": "^2.15.0", "mysql": "^2.15.0",
"node-fetch": "^1.7.3", "node-fetch": "^1.7.3",
"node-sass": "^4.7.2", "node-sass": "^4.7.2",

View File

@ -18,12 +18,15 @@ const auth = require('./../auth/auth')();
const morgan = require('morgan'); const morgan = require('morgan');
const { addWebpackMiddleware } = require('../webpack/serve'); const { addWebpackMiddleware } = require('../webpack/serve');
const { getAppConfig } = require('../webpack/utils'); const { getAppConfig } = require('../webpack/utils');
const appConfig = getAppConfig();
frappe.conf = getAppConfig();
require.extensions['.html'] = function (module, filename) { require.extensions['.html'] = function (module, filename) {
module.exports = fs.readFileSync(filename, 'utf8'); module.exports = fs.readFileSync(filename, 'utf8');
}; };
process.env.NODE_ENV = 'development';
module.exports = { module.exports = {
async start({backend, connectionParams, models, authConfig=null}) { async start({backend, connectionParams, models, authConfig=null}) {
await this.init(); await this.init();
@ -39,9 +42,8 @@ module.exports = {
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
for (let staticPath of [appConfig.distPath, appConfig.staticPath]) { app.use(express.static(frappe.conf.distPath));
app.use(express.static(staticPath)); app.use('/static', express.static(frappe.conf.staticPath))
}
app.use(morgan('tiny')); app.use(morgan('tiny'));
@ -65,7 +67,7 @@ module.exports = {
addWebpackMiddleware(app); addWebpackMiddleware(app);
} }
frappe.config.port = appConfig.dev.devServerPort frappe.config.port = frappe.conf.dev.devServerPort;
// listen // listen
server.listen(frappe.config.port, () => { server.listen(frappe.config.port, () => {

View File

@ -1,4 +1,6 @@
const frappe = require('frappejs'); const frappe = require('frappejs');
const path = require('path');
const multer = require('multer');
module.exports = { module.exports = {
setup(app) { setup(app) {
@ -43,6 +45,45 @@ module.exports = {
return response.json(doc.getValidDict()); return response.json(doc.getValidDict());
})); }));
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => {
cb(null, frappe.conf.staticPath)
},
filename: (req, file, cb) => {
const filename = file.originalname.split('.')[0];
const extension = path.extname(file.originalname);
const now = Date.now();
cb(null, filename + '-' + now + extension);
}
})
});
app.post('/api/upload/:doctype/:name/:fieldname', upload.array('files', 10), frappe.asyncHandler(async function(request, response) {
const files = request.files;
const { doctype, name, fieldname } = request.params;
let fileDocs = [];
for (let file of files) {
const doc = frappe.newDoc({
doctype: 'File',
name: path.join('/', file.path),
filename: file.originalname,
mimetype: file.mimetype,
size: file.size,
referenceDoctype: doctype,
referenceName: name,
referenceFieldname: fieldname
});
await doc.insert();
await frappe.db.setValue(doctype, name, fieldname, doc.name);
fileDocs.push(doc.getValidDict());
}
return response.json(fileDocs);
}));
// get document // get document
app.get('/api/resource/:doctype/:name', frappe.asyncHandler(async function(request, response) { app.get('/api/resource/:doctype/:name', frappe.asyncHandler(async function(request, response) {

View File

@ -1,6 +1,6 @@
<template> <template>
<form :class="['frappe-form-layout', { 'was-validated': invalid }]"> <form :class="['frappe-form-layout', { 'was-validated': invalid }]">
<div class="row" v-if="layoutConfig" <div class="form-row" v-if="layoutConfig"
v-for="(section, i) in layoutConfig.sections" :key="i" v-for="(section, i) in layoutConfig.sections" :key="i"
v-show="showSection(i)" v-show="showSection(i)"
> >

View File

@ -3,7 +3,7 @@
<list-actions <list-actions
:doctype="doctype" :doctype="doctype"
:showDelete="checkList.length" :showDelete="checkList.length"
@new="newDoc" @new="$emit('newDoc')"
@delete="deleteCheckedItems" @delete="deleteCheckedItems"
/> />
<ul class="list-group"> <ul class="list-group">
@ -60,18 +60,18 @@ export default {
this.updateList(); this.updateList();
}, },
methods: { methods: {
async newDoc() { async updateList(query = null) {
let doc = await frappe.getNewDoc(this.doctype); let filters = null;
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
},
async updateList(query=null) {
let filters = null
if (query) { if (query) {
filters = { filters = {
keywords : ['like', query] keywords: ['like', query]
} };
} }
const indicatorField = this.hasIndicator ? this.meta.indicators.key : null;
const indicatorField = this.hasIndicator
? this.meta.indicators.key
: null;
const fields = [ const fields = [
'name', 'name',
indicatorField, indicatorField,
@ -89,7 +89,7 @@ export default {
}, },
openForm(name) { openForm(name) {
this.activeItem = name; this.activeItem = name;
this.$router.push(`/edit/${this.doctype}/${name}`); this.$emit('openForm', name);
}, },
async deleteCheckedItems() { async deleteCheckedItems() {
await frappe.db.deleteMany(this.doctype, this.checkList); await frappe.db.deleteMany(this.doctype, this.checkList);
@ -112,7 +112,7 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/variables"; @import '../../styles/variables';
.list-group-item { .list-group-item {
border-left: none; border-left: none;

View File

@ -3,38 +3,55 @@ import Base from './Base';
export default { export default {
extends: Base, extends: Base,
computed: {
inputClass() {
return ['d-none'];
}
},
methods: { methods: {
getWrapperElement(h) { getWrapperElement(h) {
let fileName = this.docfield.placeholder || this._('Choose a file..'); let fileName = this.docfield.placeholder || this._('Choose a file..');
let filePath = null;
if (this.$refs.input && this.$refs.input.files.length) { if (this.value && typeof this.value === 'string') {
filePath = this.value;
}
else if (this.$refs.input && this.$refs.input.files.length) {
fileName = this.$refs.input.files[0].name; fileName = this.$refs.input.files[0].name;
} }
const fileButton = h('button', { const fileLink = h('a', {
class: ['btn btn-outline-secondary btn-block'],
domProps: {
textContent: fileName
},
attrs: { attrs: {
type: 'button' href: filePath,
target: '_blank'
}, },
on: { domProps: {
click: () => this.$refs.input.click() textContent: this._('View File')
} }
}); });
return h('div', { const helpText = h('small', {
class: 'form-text text-muted'
}, [fileLink]);
const fileNameLabel = h('label', {
class: ['custom-file-label'],
domProps: {
textContent: filePath || fileName
}
});
const fileInputWrapper = h('div', {
class: ['custom-file']
},
[this.getInputElement(h), fileNameLabel, filePath ? helpText : null]
);
return h(
'div',
{
class: ['form-group', ...this.wrapperClass], class: ['form-group', ...this.wrapperClass],
attrs: { attrs: {
'data-fieldname': this.docfield.fieldname 'data-fieldname': this.docfield.fieldname
} }
}, [this.getLabelElement(h), this.getInputElement(h), fileButton]); },
[this.getLabelElement(h), fileInputWrapper]
);
}, },
getInputAttrs() { getInputAttrs() {
return { return {
@ -48,6 +65,9 @@ export default {
accept: (this.docfield.filetypes || []).join(',') accept: (this.docfield.filetypes || []).join(',')
}; };
}, },
getInputClass() {
return 'custom-file-input';
},
getInputListeners() { getInputListeners() {
return { return {
change: e => { change: e => {

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="frappe-list-form row no-gutters"> <div class="frappe-list-form row no-gutters">
<div class="col-4 border-right"> <div class="col-4 border-right">
<frappe-list :doctype="doctype" :key="doctype" /> <frappe-list :doctype="doctype" :key="doctype" @newDoc="openNewDoc" @openForm="openForm" />
</div> </div>
<div class="col-8"> <div class="col-8">
<frappe-form v-if="name" :key="doctype + name" :doctype="doctype" :name="name" @save="onSave" /> <frappe-form v-if="name" :key="doctype + name" :doctype="doctype" :name="name" @save="onSave" />
@ -13,22 +13,30 @@ import List from '../components/List/List';
import Form from '../components/Form/Form'; import Form from '../components/Form/Form';
export default { export default {
props: ['doctype', 'name'], props: ['doctype', 'name'],
components: { components: {
FrappeList: List, FrappeList: List,
FrappeForm: Form FrappeForm: Form
},
methods: {
onSave(doc) {
if (doc.name !== this.$route.params.name) {
this.$router.push(`/edit/${doc.doctype}/${doc.name}`);
}
}, },
methods: { openForm(name) {
onSave(doc) { name = encodeURIComponent(name);
if (doc.name !== this.$route.params.name) { this.$router.push(`/edit/${this.doctype}/${name}`);
this.$router.push(`/edit/${doc.doctype}/${doc.name}`); },
} async openNewDoc() {
} let doc = await frappe.getNewDoc(this.doctype);
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
} }
} }
};
</script> </script>
<style> <style>
.frappe-list-form { .frappe-list-form {
min-height: calc(100vh - 4rem); min-height: calc(100vh - 4rem);
} }
</style> </style>