2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 03:19:01 +00:00

added fetch

This commit is contained in:
Rushabh Mehta 2018-02-09 18:25:55 +05:30
parent bc095b5bc3
commit 3af4fb8671
23 changed files with 218 additions and 118 deletions

View File

@ -51,7 +51,7 @@ module.exports = class Database {
}
getColumnDefinition(df) {
return `${df.fieldname} ${this.type_map[df.fieldtype]} ${df.reqd && !df.default ? "not null" : ""} ${df.default ? `default ${df.default}` : ""}`
return `${df.fieldname} ${this.type_map[df.fieldtype]} ${df.required && !df.default ? "not null" : ""} ${df.default ? `default ${df.default}` : ""}`
}
async alterTable(doctype) {

View File

@ -89,7 +89,13 @@ module.exports = class RESTClient {
async fetch(url, args) {
args.headers = this.getHeaders();
let response = await frappe.fetch(url, args);
return await response.json();
let data = await response.json();
if (response.status !== 200) {
throw Error(data.error);
}
return data;
}
getURL(...parts) {

View File

@ -36,7 +36,7 @@ module.exports = class sqliteDatabase extends Database {
}
getColumnDefinition(df) {
return `${df.fieldname} ${this.type_map[df.fieldtype]} ${df.reqd && !df.default ? "not null" : ""} ${df.default ? `default ${df.default}` : ""}`
return `${df.fieldname} ${this.type_map[df.fieldtype]} ${df.required && !df.default ? "not null" : ""} ${df.default ? `default ${df.default}` : ""}`
}
async getTableColumns(doctype) {

View File

@ -15,36 +15,38 @@ module.exports = class Desk {
this.navbar = new Navbar();
this.container = frappe.ui.add('div', 'container-fluid', body);
this.container_row = frappe.ui.add('div', 'row', this.container)
this.sidebar = frappe.ui.add('div', 'col-md-2 p-3 sidebar d-none d-md-block', this.container_row);
this.body = frappe.ui.add('div', 'col-md-10 p-3 main', this.container_row);
this.containerRow = frappe.ui.add('div', 'row', this.container)
this.sidebar = frappe.ui.add('div', 'col-md-2 p-3 sidebar d-none d-md-block', this.containerRow);
this.sidebarList = frappe.ui.add('div', 'list-group list-group-flush', this.sidebar);
this.body = frappe.ui.add('div', 'col-md-10 p-4 main', this.containerRow);
this.sidebar_items = [];
this.pages = {
lists: {},
forms: {}
};
this.init_routes();
this.routeItems = {};
this.initRoutes();
// this.search = new Search(this.nav);
}
init_routes() {
initRoutes() {
frappe.router.add('not-found', async (params) => {
if (!this.not_found_page) {
this.not_found_page = new Page('Not Found');
if (!this.notFoundPage) {
this.notFoundPage = new Page('Not Found');
}
await this.not_found_page.show();
this.not_found_page.renderError('Not Found', params ? params.route : '');
await this.notFoundPage.show();
this.notFoundPage.renderError('Not Found', params ? params.route : '');
})
frappe.router.add('list/:doctype', async (params) => {
let page = this.getList_page(params.doctype);
let page = this.getListPage(params.doctype);
await page.show(params);
});
frappe.router.add('edit/:doctype/:name', async (params) => {
let page = this.get_form_page(params.doctype);
let page = this.getFormPage(params.doctype);
await page.show(params);
})
@ -55,30 +57,47 @@ module.exports = class Desk {
await doc.set('name', '');
});
frappe.router.on('change', () => {
if (this.routeItems[window.location.hash]) {
this.setActive(this.routeItems[window.location.hash]);
}
})
}
getList_page(doctype) {
getListPage(doctype) {
if (!this.pages.lists[doctype]) {
this.pages.lists[doctype] = new ListPage(doctype);
}
return this.pages.lists[doctype];
}
get_form_page(doctype) {
getFormPage(doctype) {
if (!this.pages.forms[doctype]) {
this.pages.forms[doctype] = new FormPage(doctype);
}
return this.pages.forms[doctype];
}
add_sidebar_item(label, action) {
let item = frappe.ui.add('a', '', frappe.ui.add('p', null, frappe.desk.sidebar));
setActive(item) {
let className = 'list-group-item-secondary';
let activeItem = this.sidebarList.querySelector('.' + className);
if (activeItem) {
activeItem.classList.remove(className);
}
item.classList.add(className);
}
addSidebarItem(label, action) {
let item = frappe.ui.add('a', 'list-group-item list-group-item-action', this.sidebarList);
item.textContent = label;
if (typeof action === 'string') {
item.href = action;
this.routeItems[action] = item;
} else {
item.addEventHandler('click', () => {
action();
this.setActive(item);
});
}
}

View File

@ -4,7 +4,7 @@ module.exports = class Navbar {
constructor({brand_label = 'Home'} = {}) {
Object.assign(this, arguments[0]);
this.items = {};
this.navbar = frappe.ui.add('div', 'navbar navbar-expand-md border-bottom', document.querySelector('body'));
this.navbar = frappe.ui.add('div', 'navbar navbar-expand-md border-bottom navbar-dark bg-dark', document.querySelector('body'));
this.brand = frappe.ui.add('a', 'navbar-brand', this.navbar);
this.brand.href = '#';

View File

@ -7,13 +7,23 @@ $spacer-3: 1rem;
$spacer-4: 2rem;
html {
font-size: 16px;
font-size: 14px;
}
.main {
border-left: 1px solid $gray-300;
}
.sidebar {
.list-group-item {
padding: $spacer-2 $spacer-3;
}
.list-group-flush {
margin: -$spacer-3;
}
}
.hide {
display: none !important;
}
@ -90,8 +100,8 @@ mark {
}
.table-wrapper {
margin-top: $spacer-3;
margin-bottom: $spacer-3;
margin-top: $spacer-4;
margin-bottom: $spacer-4;
}
.table-toolbar {

View File

@ -2,7 +2,7 @@ const $ = require('jquery');
const bootstrap = require('bootstrap');
module.exports = class Modal {
constructor({ title, body, primary_label, primary_action, secondary_label, secondary_action }) {
constructor({ title, body, primary, secondary }) {
Object.assign(this, arguments[0]);
this.$modal = $(`<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
@ -22,23 +22,23 @@ module.exports = class Modal {
</div>
</div>`).appendTo(document.body);
if (this.primary_label) {
this.add_primary(this.primary_label, this.primary_action);
if (this.primary) {
this.addPrimary(this.primary.label, this.primary.action);
}
if (this.secondary_label) {
this.add_secondary(this.secondary_label, this.secondary_action);
if (this.secondary) {
this.addSecondary(this.secondary.label, this.secondary.action);
}
this.show();
}
add_primary(label, action) {
addPrimary(label, action) {
this.$primary = $(`<button type="button" class="btn btn-primary">
${label}</button>`)
.appendTo(this.$modal.find('.modal-footer'))
.on('click', () => action(this));
}
add_secondary(label, action) {
addSecondary(label, action) {
this.$primary = $(`<button type="button" class="btn btn-secondary">
${label}</button>`)
.appendTo(this.$modal.find('.modal-footer'))
@ -53,7 +53,7 @@ module.exports = class Modal {
this.$modal.modal('hide');
}
get_body() {
getBody() {
return this.$modal.find('.modal-body').get(0);
}
}

View File

@ -2,9 +2,12 @@ const frappe = require('frappejs');
class BaseControl {
constructor({field, parent, form}) {
BaseControl.count++;
Object.assign(this, field);
this.parent = parent;
this.form = form;
this.id = 'control-' + BaseControl.count;
if (!this.fieldname) {
this.fieldname = frappe.slug(this.label);
@ -41,10 +44,12 @@ class BaseControl {
}
this.makeInput();
this.setInputName();
this.setRequiredAttribute();
this.setDisabled();
if (!this.onlyInput) {
this.makeDescription();
}
this.bindChangeEvent();
this.addChangeHandler();
}
}
@ -53,13 +58,21 @@ class BaseControl {
}
makeLabel() {
this.label_element = frappe.ui.add('label', null, this.formGroup);
this.label_element.textContent = this.label;
this.labelElement = frappe.ui.add('label', null, this.formGroup);
this.labelElement.textContent = this.label;
this.labelElement.setAttribute('for', this.id);
}
makeInput() {
this.input = frappe.ui.add('input', 'form-control', this.get_input_parent());
this.input.setAttribute('autocomplete', 'off');
this.input.autocomplete = "off";
this.input.id = this.id;
}
setDisabled() {
if (this.readonly || this.disabled) {
this.input.disabled = true;
}
}
get_input_parent() {
@ -70,6 +83,12 @@ class BaseControl {
this.input.setAttribute('name', this.fieldname);
}
setRequiredAttribute() {
if (this.required) {
this.input.required = true;
}
}
makeDescription() {
if (this.description) {
this.description_element = frappe.ui.add('small', 'form-text text-muted', this.formGroup);
@ -104,11 +123,14 @@ class BaseControl {
return value;
}
bindChangeEvent() {
this.input.addEventListener('change', (e) => this.handleChange());
addChangeHandler() {
this.input.addEventListener('change', () => {
if (this.skipChangeEvent) return;
this.handleChange();
});
}
async handleChange() {
async handleChange(event) {
let value = await this.parse(this.getInputValue());
value = await this.validate(value);
if (this.doc[this.fieldname] !== value) {
@ -135,4 +157,6 @@ class BaseControl {
}
}
BaseControl.count = 0;
module.exports = BaseControl;

View File

@ -11,15 +11,16 @@ class TableControl extends BaseControl {
this.wrapper.innerHTML =
`<div class="datatable-wrapper"></div>
<div class="table-toolbar">
<button class="btn btn-sm btn-outline-secondary btn-add">${frappe._("Add")}</button>
<button class="btn btn-sm btn-outline-danger btn-remove">${frappe._("Remove")}</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-add">
${frappe._("Add")}</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-remove">
${frappe._("Remove")}</button>
</div>`;
this.datatable = new DataTable(this.wrapper.querySelector('.datatable-wrapper'), {
columns: this.getColumns(),
data: this.getTableData(),
takeAvailableSpace: true,
enableClusterize: true,
addCheckboxColumn: true,
editing: this.getTableInput.bind(this),
});
@ -56,16 +57,19 @@ class TableControl extends BaseControl {
let field = this.datatable.getColumn(colIndex).field;
if (field.fieldtype==='Text') {
return this.getControlInModal(field);
} else {
return this.getControl(field, parent);
// text in modal
parent = this.getControlModal(field).getBody();
}
return this.getControl(field, parent);
}
getControl(field, parent) {
field.onlyInput = true;
const control = controls.makeControl({field: field, parent: parent});
// change will be triggered by datatable
control.skipChangeEvent = true;
return {
initValue: (value, rowIndex, column) => {
control.parent_control = this;
@ -74,7 +78,7 @@ class TableControl extends BaseControl {
return control.setInputValue(value);
},
setValue: async (value, rowIndex, column) => {
// triggers change event
control.handleChange();
},
getValue: () => {
return control.getInputValue();
@ -83,21 +87,24 @@ class TableControl extends BaseControl {
}
getControlInModal(field, parent) {
getControlModal(field) {
this.modal = new Modal({
title: frappe._('Edit {0}', field.label),
body: '',
primary_label: frappe._('Submit'),
primary_action: (modal) => {
this.datatable.cellmanager.submitEditing();
modal.hide();
primary: {
label: frappe._('Submit'),
action: (modal) => {
this.datatable.cellmanager.submitEditing();
modal.hide();
}
}
});
this.modal.$modal.on('hidden.bs.modal', () => {
this.datatable.cellmanager.deactivateEditing();
})
this.datatable.cellmanager.$focusedCell.focus();
});
return this.getControl(field, this.modal.get_body());
return this.modal;
}
getColumns() {
@ -106,8 +113,11 @@ class TableControl extends BaseControl {
id: field.fieldname,
field: field,
content: field.label,
editable: true,
width: 120,
editable: field.disabled ? false : true,
sortable: false,
resizable: true,
dropdown: false,
align: ['Int', 'Float', 'Currency'].includes(field.fieldtype) ? 'right' : 'left',
format: (value) => frappe.format(value, field)
}
@ -115,7 +125,7 @@ class TableControl extends BaseControl {
}
getChildFields() {
return frappe.getMeta(this.childtype).fields;
return frappe.getMeta(this.childtype).fields.filter(f => f.hidden ? false : true);
}
getDefaultData() {

View File

@ -1,4 +1,5 @@
const BaseControl = require('./base');
const frappe = require('frappejs');
class TextControl extends BaseControl {
makeInput() {

View File

@ -24,9 +24,15 @@ module.exports = class BaseForm extends Observable {
this.body = frappe.ui.add('div', 'form-body', this.parent);
this.makeToolbar();
this.form = frappe.ui.add('div', 'form-container', this.body);
this.form = frappe.ui.add('form', 'form-container', this.body);
this.form.onValidate = true;
this.makeControls();
}
makeControls() {
for(let field of this.meta.fields) {
if (controls.getControlClass(field.fieldtype)) {
if (!field.hidden && controls.getControlClass(field.fieldtype)) {
let control = controls.makeControl({field: field, form: this});
this.controlList.push(control);
this.controls[field.fieldname] = control;
@ -35,29 +41,33 @@ module.exports = class BaseForm extends Observable {
}
makeToolbar() {
this.btnSubmit = this.page.addButton(frappe._("Save"), 'btn-primary', async () => {
this.btnSubmit = this.page.addButton(frappe._("Save"), 'btn-primary', async (event) => {
await this.submit();
})
this.btnDelete = this.page.addButton(frappe._("Delete"), 'btn-outline-secondary', async () => {
this.btnDelete = this.page.addButton(frappe._("Delete"), 'btn-outline-secondary', async (e) => {
await this.doc.delete();
this.showAlert('Deleted', 'success');
this.trigger('delete');
});
}
async use(doc, is_new = false) {
async use(doc) {
if (this.doc) {
// clear handlers of outgoing doc
this.doc.clearHandlers();
}
this.clearAlert();
this.doc = doc;
this.is_new = is_new;
for (let control of this.controlList) {
control.bind(this.doc);
}
this.setupChangeHandler();
this.trigger('use', {doc:doc});
}
setupChangeHandler() {
// refresh value in control
this.doc.addHandler('change', (params) => {
if (params.fieldname) {
@ -70,14 +80,17 @@ module.exports = class BaseForm extends Observable {
// multiple values changed
this.refresh();
}
this.form.classList.remove('was-validated');
});
this.trigger('use', {doc:doc});
}
async submit() {
if (!this.form.checkValidity()) {
this.form.classList.add('was-validated');
return;
}
try {
if (this.is_new || this.doc.__not_inserted) {
if (this.doc._notInserted) {
await this.doc.insert();
} else {
await this.doc.update();
@ -86,6 +99,7 @@ module.exports = class BaseForm extends Observable {
this.showAlert('Saved', 'success');
} catch (e) {
this.showAlert('Failed', 'danger');
return;
}
await this.trigger('submit');
}

View File

@ -23,7 +23,7 @@ module.exports = class Page extends Observable {
addButton(label, cssClass, action) {
this.head.classList.remove('hide');
this.button = frappe.ui.add('button', 'btn btn-sm ' + cssClass, this.head);
this.button = frappe.ui.add('button', 'btn ' + cssClass, this.head);
this.button.innerHTML = label;
this.button.addEventListener('click', action);
return this.button;

View File

@ -1,7 +1,8 @@
const frappe = require('frappejs');
const Observable = require('frappejs/utils/observable');
module.exports = class Router {
module.exports = class Router extends Observable {
constructor() {
super();
this.last_route = null;
this.current_page = null;
this.static_routes = [];
@ -103,6 +104,7 @@ module.exports = class Router {
} else {
await this.match('not-found').handler({route: route});
}
await this.trigger('change');
}
match(route) {

View File

@ -84,7 +84,7 @@ module.exports = {
async getNewDoc(doctype) {
let doc = this.newDoc({doctype: doctype});
doc.setName();
doc.__not_inserted = true;
doc._notInserted = true;
this.addToCache(doc);
return doc;
},

View File

@ -17,7 +17,7 @@ module.exports = {
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"reqd": 1
"required": 1
}
]
}`);

View File

@ -3,6 +3,7 @@ const frappe = require('frappejs');
module.exports = class BaseDocument {
constructor(data) {
this.handlers = {};
this.fetchValues = {};
this.setup();
Object.assign(this, data);
}
@ -29,7 +30,7 @@ module.exports = class BaseDocument {
// set value and trigger change
async set(fieldname, value) {
this[fieldname] = await this.validateField(fieldname, value);
if (this.applyFormulae()) {
if (await this.applyFormulae()) {
// multiple changes
await this.trigger('change', { doc: this });
} else {
@ -138,12 +139,12 @@ module.exports = class BaseDocument {
}
}
applyFormulae() {
if (!this.hasFormulae()) {
async applyFormulae() {
if (!this.meta.hasFormulae()) {
return false;
}
let doc;
let doc = this;
// children
for (let tablefield of this.meta.getTableFields()) {
@ -151,16 +152,15 @@ module.exports = class BaseDocument {
if (formulaFields.length) {
// for each row
for (doc of this[tablefield.fieldname]) {
for (let row of this[tablefield.fieldname]) {
for (let field of formulaFields) {
doc[field.fieldname] = eval(field.formula);
row[field.fieldname] = await eval(field.formula);
}
}
}
}
// parent
doc = this;
for (let field of this.meta.getFormulaFields()) {
doc[field.fieldname] = eval(field.formula);
}
@ -168,30 +168,13 @@ module.exports = class BaseDocument {
return true;
}
hasFormulae() {
if (this._hasFormulae===undefined) {
this._hasFormulae = false;
if (this.meta.getFormulaFields().length) {
this._hasFormulae = true;
} else {
for (let tablefield of this.meta.getTableFields()) {
if (frappe.getMeta(tablefield.childtype).getFormulaFields().length) {
this._hasFormulae = true;
break;
}
}
}
}
return this._hasFormulae;
}
async commit() {
// re-run triggers
this.setName();
this.setStandardValues();
this.setKeywords();
this.setChildIdx();
this.applyFormulae();
await this.applyFormulae();
await this.trigger('validate');
await this.trigger('commit');
}
@ -233,4 +216,18 @@ module.exports = class BaseDocument {
}
}
}
// helper functions
getSum(tablefield, childfield) {
return this[tablefield].map(d => (d[childfield] || 0)).reduce((a, b) => a + b, 0);
}
async getFrom(doctype, name, fieldname) {
if (!name) return '';
let key = `${doctype}:${name}:${fieldname}`;
if (!this.fetchValues[key]) {
this.fetchValues[key] = await frappe.db.getValue(doctype, name, fieldname);
}
return this.fetchValues[key];
}
};

View File

@ -17,41 +17,41 @@ module.exports = {
},
common_fields: [
{
fieldname: 'name', fieldtype: 'Data', reqd: 1
fieldname: 'name', fieldtype: 'Data', required: 1
}
],
parent_fields: [
{
fieldname: 'owner', fieldtype: 'Link', reqd: 1, options: 'User'
fieldname: 'owner', fieldtype: 'Link', required: 1, options: 'User'
},
{
fieldname: 'modified_by', fieldtype: 'Link', reqd: 1, options: 'User'
fieldname: 'modified_by', fieldtype: 'Link', required: 1, options: 'User'
},
{
fieldname: 'creation', fieldtype: 'Datetime', reqd: 1
fieldname: 'creation', fieldtype: 'Datetime', required: 1
},
{
fieldname: 'modified', fieldtype: 'Datetime', reqd: 1
fieldname: 'modified', fieldtype: 'Datetime', required: 1
},
{
fieldname: 'keywords', fieldtype: 'Text'
},
{
fieldname: 'docstatus', fieldtype: 'Int', reqd: 1, default: 0
fieldname: 'docstatus', fieldtype: 'Int', required: 1, default: 0
}
],
child_fields: [
{
fieldname: 'idx', fieldtype: 'Int', reqd: 1
fieldname: 'idx', fieldtype: 'Int', required: 1
},
{
fieldname: 'parent', fieldtype: 'Data', reqd: 1
fieldname: 'parent', fieldtype: 'Data', required: 1
},
{
fieldname: 'parenttype', fieldtype: 'Link', reqd: 1, options: 'DocType'
fieldname: 'parenttype', fieldtype: 'Link', required: 1, options: 'DocType'
},
{
fieldname: 'parentfield', fieldtype: 'Data', reqd: 1
fieldname: 'parentfield', fieldtype: 'Data', required: 1
}
]
};

View File

@ -37,6 +37,23 @@ module.exports = class BaseMeta extends BaseDocument {
return this._formulaFields;
}
hasFormulae() {
if (this._hasFormulae===undefined) {
this._hasFormulae = false;
if (this.getFormulaFields().length) {
this._hasFormulae = true;
} else {
for (let tablefield of this.getTableFields()) {
if (frappe.getMeta(tablefield.childtype).getFormulaFields().length) {
this._hasFormulae = true;
break;
}
}
}
}
return this._hasFormulae;
}
on(key, fn) {
if (!this.event_handlers[key]) {
this.event_handlers[key] = [];
@ -112,7 +129,7 @@ module.exports = class BaseMeta extends BaseDocument {
}
getKeywordFields() {
return this.keyword_fields || this.meta.fields.filter(field => field.reqd).map(field => field.fieldname);
return this.keyword_fields || this.meta.fields.filter(field => field.required).map(field => field.fieldname);
}
validate_select(field, value) {

View File

@ -9,13 +9,13 @@
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"reqd": 1
"required": 1
},
{
"fieldname": "current",
"label": "Current",
"fieldtype": "Int",
"reqd": 1
"required": 1
}
]
}

View File

@ -9,7 +9,7 @@
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"reqd": 1
"required": 1
}
]
}

View File

@ -9,13 +9,13 @@
"fieldname": "username",
"label": "Username",
"fieldtype": "Data",
"reqd": 1
"required": 1
},
{
"fieldname": "password",
"label": "Password",
"fieldtype": "Password",
"reqd": 1
"required": 1
}
]
}

View File

@ -12,12 +12,7 @@
"fieldname": "subject",
"label": "Subject",
"fieldtype": "Data",
"reqd": 1
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
"required": 1
},
{
"fieldname": "status",
@ -28,7 +23,12 @@
"Closed"
],
"default": "Open",
"reqd": 1
"required": 1
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
}
]
}

View File

@ -12,13 +12,13 @@
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"reqd": 1
"required": 1
},
{
"fieldname": "full_name",
"label": "Full Name",
"fieldtype": "Data",
"reqd": 1
"required": 1
},
{
"fieldname": "roles",