2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 11:29:03 +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');
module.exports = class Database extends Observable {
constructor() {
super();
this.initTypeMap();
}
constructor() {
super();
this.initTypeMap();
}
async connect() {
// this.conn
}
async connect() {
// this.conn
}
close() {
this.conn.close();
}
close() {
this.conn.close();
}
async migrate() {
for (let doctype in frappe.models) {
// check if controller module
let meta = frappe.getMeta(doctype);
if (!meta.isSingle) {
if (await this.tableExists(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;
async migrate() {
for (let doctype in frappe.models) {
// check if controller module
let meta = frappe.getMeta(doctype);
if (!meta.isSingle) {
if (await this.tableExists(doctype)) {
await this.alterTable(doctype);
} else {
if (!name) {
throw new frappe.errors.ValueError('name is mandatory');
}
doc = await this.getOne(doctype, name, fields);
await this.createTable(doctype);
}
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) {
// 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'
});
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 {
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);
}
}
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;
else {
await this.insertOne(field.childtype, child);
}
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 = '*') {
// select {fields} form {doctype} where 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 };
}
prepareFields(fields) {
if (fields instanceof Array) {
fields = fields.join(", ");
}
return fields;
}
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;
}
triggerChange(doctype, name) {
this.trigger(`change:${doctype}`, {name:name}, 500);
this.trigger(`change`, {doctype:name, name:name}, 500);
}
async setValue(doctype, name, fieldname, value) {
return await this.setValues(doctype, name, {
[fieldname]: value
});
}
async insert(doctype, doc) {
let meta = frappe.getMeta(doctype);
async setValues(doctype, name, fieldValuePair) {
//
}
// insert parent
if (meta.isSingle) {
await this.updateSingle(meta, doc, doctype);
} else {
await this.insertOne(doctype, doc);
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';
}
// 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++;
}
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]);
}
}
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);
}
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];
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'
}
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');
module.exports = class HTTPClient extends Observable {
constructor({ server, protocol = 'http' }) {
super();
constructor({ server, protocol = 'http' }) {
super();
this.server = server;
this.protocol = protocol;
frappe.config.serverURL = this.getURL();
this.server = server;
this.protocol = protocol;
frappe.config.serverURL = this.getURL();
// if the backend is http, then always client!
frappe.isServer = false;
// if the backend is http, then always client!
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) {
doc.doctype = doctype;
let url = this.getURL('/api/resource', doctype);
return await this.fetch(url, {
method: 'POST',
body: JSON.stringify(doc)
})
}
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);
if (fileFields.length > 0) {
fileFields.forEach(df => {
const files = doc[df.fieldname] || [];
if (files.length) {
filesToUpload.push({
fieldname: df.fieldname,
files: files
})
}
return data;
delete doc[df.fieldname];
});
}
getURL(...parts) {
return this.protocol + '://' + this.server + (parts || []).join('/');
return filesToUpload;
}
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() {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (frappe.session && frappe.session.token) {
headers.token = frappe.session.token;
};
return headers;
}
let response = await frappe.fetch(url, {
method: 'POST',
body: formData
});
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
}
const data = await response.json();
if (response.status !== 200) {
throw Error(data.error);
}
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;
module.exports = class sqliteDatabase extends Database {
constructor({ dbPath }) {
super();
this.dbPath = dbPath;
}
constructor({ dbPath }) {
super();
this.dbPath = dbPath;
}
connect(dbPath) {
if (dbPath) {
this.dbPath = dbPath;
connect(dbPath) {
if (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.conn = new sqlite3.Database(this.dbPath, () => {
if (debug) {
this.conn.on('trace', (trace) => console.log(trace));
}
this.run('PRAGMA foreign_keys=ON').then(resolve);
});
});
}
this.run('PRAGMA foreign_keys=ON').then(resolve);
});
});
}
async tableExists(table) {
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
return (name && name.length) ? true : false;
}
async tableExists(table) {
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
return (name && name.length) ? true : false;
}
async addForeignKeys(doctype, newForeignKeys) {
await this.run('PRAGMA foreign_keys=OFF');
await this.run('BEGIN TRANSACTION');
async addForeignKeys(doctype, newForeignKeys) {
await this.run('PRAGMA foreign_keys=OFF');
await this.run('BEGIN TRANSACTION');
const tempName = 'TEMP' + doctype
const tempName = 'TEMP' + doctype
// create temp table
await this.createTable(doctype, tempName);
// create temp table
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
await this.run(`INSERT INTO ${tempName} (${columns}) SELECT ${columns} from ${doctype}`);
// copy from old to new table
await this.run(`INSERT INTO ${tempName} (${columns}) SELECT ${columns} from ${doctype}`);
// drop old table
await this.run(`DROP TABLE ${doctype}`);
// drop old table
await this.run(`DROP TABLE ${doctype}`);
// rename new table
await this.run(`ALTER TABLE ${tempName} RENAME TO ${doctype}`);
// rename new table
await this.run(`ALTER TABLE ${tempName} RENAME TO ${doctype}`);
await this.run('COMMIT');
await this.run('PRAGMA foreign_keys=ON');
}
await this.run('COMMIT');
await this.run('PRAGMA foreign_keys=ON');
}
removeColumns() {
// pass
}
removeColumns() {
// pass
}
async runCreateTableQuery(doctype, columns, indexes) {
const query = `CREATE TABLE IF NOT EXISTS ${doctype} (
async runCreateTableQuery(doctype, columns, indexes) {
const query = `CREATE TABLE IF NOT EXISTS ${doctype} (
${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) {
let def = this.getColumnDefinition(field);
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) {
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`);
}
}
async getTableColumns(doctype) {
return (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name);
}
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(' ');
async getForeignKeys(doctype) {
return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(d => d.from);
}
return def;
}
async runAddColumnQuery(doctype, field, values) {
await this.run(`ALTER TABLE ${doctype} ADD COLUMN ${this.getColumnDefinition(field)}`, values);
}
async getTableColumns(doctype) {
return (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name);
}
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}
getOne(doctype, name, fields = '*') {
fields = this.prepareFields(fields);
return new Promise((resolve, reject) => {
this.conn.get(`select ${fields} from ${doctype}
where name = ?`, name,
(err, row) => {
resolve(row || {});
});
(err, 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) {
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}
return await this.run(`insert into ${doctype}
(${fields.map(field => field.fieldname).join(", ")})
values (${placeholders})`, this.getFormattedValues(fields, doc));
}
}
async updateOne(doctype, doc) {
let fields = this.getKeys(doctype);
let assigns = fields.map(field => `${field.fieldname} = ?`);
let values = this.getFormattedValues(fields, doc);
async updateOne(doctype, doc) {
let fields = this.getKeys(doctype);
let assigns = fields.map(field => `${field.fieldname} = ?`);
let values = this.getFormattedValues(fields, doc);
// additional name for where clause
values.push(doc.name);
// additional name for where clause
values.push(doc.name);
return await this.run(`update ${doctype}
return await this.run(`update ${doctype}
set ${assigns.join(", ")} where name=?`, values);
}
}
async runDeleteOtherChildren(field, added) {
// delete other children
// `delete from doctype where parent = ? and name not in (?, ?, ?)}`
await this.run(`delete from ${field.childtype}
async runDeleteOtherChildren(field, added) {
// delete other children
// `delete from doctype where parent = ? and name not in (?, ?, ?)}`
await this.run(`delete from ${field.childtype}
where
parent = ? and
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 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)
}
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(", ")}
return new Promise((resolve, reject) => {
let conditions = this.getFilterConditions(filters);
let query = `select ${fields.join(", ")}
from ${doctype}
${conditions.conditions ? "where" : ""} ${conditions.conditions}
${groupBy ? ("group by " + groupBy.join(', ')) : ""}
${orderBy ? ("order by " + orderBy) : ""} ${orderBy ? (order || "asc") : ""}
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`;
this.conn.all(query, conditions.values,
(err, rows) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(rows);
}
});
this.conn.all(query, conditions.values,
(err, rows) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(rows);
}
});
}
});
}
run(query, params) {
return new Promise((resolve, reject) => {
this.conn.run(query, params, (err) => {
if (err) {
reject(err);
} else {
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;
}
run(query, params) {
return new Promise((resolve, reject) => {
this.conn.run(query, params, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
}
});
});
}
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'
}
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() {
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];
}
/**
* 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) {
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 = {
name: "ToDo",
label: "To Do",
naming: "autoincrement",
pageSettings: {
hideTitle: true
name: 'ToDo',
label: 'To Do',
naming: 'autoincrement',
pageSettings: {
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": [
"subject",
"description"
],
titleField: 'subject',
indicators: {
key: 'status',
colors: {
Open: indicatorColor.BLUE,
Closed: indicatorColor.GREEN
}
{
fieldname: 'status',
label: 'Status',
fieldtype: 'Select',
options: [
'Open',
'Closed'
],
default: 'Open',
required: 1
},
"fields": [
{
"fieldname": "subject",
"label": "Subject",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "status",
"label": "Status",
"fieldtype": "Select",
"options": [
"Open",
"Closed"
],
"default": "Open",
"required": 1
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
}
],
{
fieldname: 'description',
label: 'Description',
fieldtype: 'Text'
}
],
links: [
{
label: 'Close',
condition: (form) => form.doc.status !== 'Closed',
action: async (form) => {
await form.doc.set('status', 'Closed');
await form.doc.update();
}
},
{
label: 'Re-Open',
condition: (form) => form.doc.status !== 'Open',
action: async (form) => {
await form.doc.set('status', 'Open');
await form.doc.update();
}
}
]
links: [
{
label: 'Close',
condition: (form) => form.doc.status !== 'Closed',
action: async (form) => {
await form.doc.set('status', 'Closed');
await form.doc.update();
}
},
{
label: 'Re-Open',
condition: (form) => form.doc.status !== 'Open',
action: async (form) => {
await form.doc.set('status', 'Open');
await form.doc.update();
}
}
]
}

View File

@ -11,6 +11,7 @@ module.exports = {
SystemSettings: require('./doctype/SystemSettings/SystemSettings.js'),
ToDo: require('./doctype/ToDo/ToDo.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",
"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",

View File

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

View File

@ -1,4 +1,6 @@
const frappe = require('frappejs');
const path = require('path');
const multer = require('multer');
module.exports = {
setup(app) {
@ -43,6 +45,45 @@ module.exports = {
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
app.get('/api/resource/:doctype/:name', frappe.asyncHandler(async function(request, response) {

View File

@ -1,6 +1,6 @@
<template>
<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-show="showSection(i)"
>

View File

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

View File

@ -3,38 +3,55 @@ import Base from './Base';
export default {
extends: Base,
computed: {
inputClass() {
return ['d-none'];
}
},
methods: {
getWrapperElement(h) {
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;
}
const fileButton = h('button', {
class: ['btn btn-outline-secondary btn-block'],
domProps: {
textContent: fileName
},
const fileLink = h('a', {
attrs: {
type: 'button'
href: filePath,
target: '_blank'
},
on: {
click: () => this.$refs.input.click()
domProps: {
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],
attrs: {
'data-fieldname': this.docfield.fieldname
}
}, [this.getLabelElement(h), this.getInputElement(h), fileButton]);
},
[this.getLabelElement(h), fileInputWrapper]
);
},
getInputAttrs() {
return {
@ -48,6 +65,9 @@ export default {
accept: (this.docfield.filetypes || []).join(',')
};
},
getInputClass() {
return 'custom-file-input';
},
getInputListeners() {
return {
change: e => {

View File

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