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

added indicators and throttling of events

This commit is contained in:
Rushabh Mehta 2018-02-22 16:51:42 +05:30
parent de9ecc9f96
commit 7c6ddcd3e5
13 changed files with 232 additions and 45 deletions

View File

@ -174,7 +174,7 @@ module.exports = class Database extends Observable {
} }
triggerChange(doctype, name) { triggerChange(doctype, name) {
this.trigger(`change:${doctype}`, {name:name}); this.trigger(`change:${doctype}`, {name:name}, 1000);
} }
async insert(doctype, doc) { async insert(doctype, doc) {

View File

@ -22,7 +22,13 @@ module.exports = class ListPage extends Page {
}); });
this.on('show', async () => { this.on('show', async () => {
await this.list.run(); await this.list.refresh();
});
frappe.docs.on('change', (params) => {
if (params.doc.doctype === doctype) {
this.list.refreshRow(params.doc);
}
}); });
} }
} }

View File

@ -3,6 +3,7 @@ const HTTPClient = require('frappejs/backends/http');
const frappe = require('frappejs'); const frappe = require('frappejs');
frappe.ui = require('./ui'); frappe.ui = require('./ui');
const Desk = require('./desk'); const Desk = require('./desk');
const Observable = require('frappejs/utils/observable');
module.exports = { module.exports = {
async start({server, columns = 2}) { async start({server, columns = 2}) {
@ -17,7 +18,7 @@ module.exports = {
this.socket = io.connect('http://localhost:8000'); // eslint-disable-line this.socket = io.connect('http://localhost:8000'); // eslint-disable-line
frappe.db.bindSocketClient(this.socket); frappe.db.bindSocketClient(this.socket);
frappe.flags.cacheDocs = true; frappe.docs = new Observable();
await frappe.getSingle('SystemSettings'); await frappe.getSingle('SystemSettings');

View File

@ -0,0 +1,68 @@
.indicator,
.indicator-right {
background: none;
vertical-align: middle;
}
.indicator::before,
.indicator-right::after {
content:'';
display: inline-block;
height: 8px;
width: 8px;
border-radius: 8px;
background: $gray-300;
}
.indicator::before {
margin:0 $spacer-2 0 0;
}
.indicator-right::after {
margin:0 0 0 $spacer-2;
}
.indicator.grey::before,
.indicator-right.grey::after {
background: $gray-300;
}
.indicator.blue::before,
.indicator-right.blue::after {
background: $blue;
}
.indicator.red::before,
.indicator-right.red::after {
background: $red;
}
.indicator.green::before,
.indicator-right.green::after {
background: $green;
}
.indicator.orange::before,
.indicator-right.orange::after {
background: $orange;
}
.indicator.purple::before,
.indicator-right.purple::after {
background: $purple;
}
.indicator.darkgrey::before,
.indicator-right.darkgrey::after {
background: $gray-600;
}
.indicator.black::before,
.indicator-right.black::after {
background: $gray-800;
}
.indicator.yellow::before,
.indicator-right.yellow::after {
background: $yellow;
}
.modal-header .indicator {
float: left;
margin-top: 7.5px;
margin-right: 3px;
}

View File

@ -3,7 +3,6 @@
@import "node_modules/flatpickr/dist/flatpickr"; @import "node_modules/flatpickr/dist/flatpickr";
@import "node_modules/flatpickr/dist/themes/airbnb"; @import "node_modules/flatpickr/dist/themes/airbnb";
@import "node_modules/codemirror/lib/codemirror"; @import "node_modules/codemirror/lib/codemirror";
// @import "node_modules/codemirror/theme/cobalt";
$spacer-1: 0.25rem; $spacer-1: 0.25rem;
$spacer-2: 0.5rem; $spacer-2: 0.5rem;
@ -11,6 +10,8 @@ $spacer-3: 1rem;
$spacer-4: 2rem; $spacer-4: 2rem;
$spacer-5: 3rem; $spacer-5: 3rem;
@import "./indicators.scss";
$page-width: 500px; $page-width: 500px;
html { html {

View File

@ -116,9 +116,9 @@ module.exports = class BaseForm extends Observable {
} }
async bindEvents(doc) { async bindEvents(doc) {
if (this.doc) { if (this.doc && this.docListener) {
// clear listeners of outgoing doc // stop listening to the old doc
this.doc.clearListeners(); this.doc.off(this.docListener);
} }
this.clearAlert(); this.clearAlert();
this.doc = doc; this.doc = doc;
@ -132,7 +132,7 @@ module.exports = class BaseForm extends Observable {
setupChangeListener() { setupChangeListener() {
// refresh value in control // refresh value in control
this.doc.on('change', (params) => { this.docListener = (params) => {
if (params.fieldname) { if (params.fieldname) {
// only single value changed // only single value changed
let control = this.controls[params.fieldname]; let control = this.controls[params.fieldname];
@ -144,7 +144,9 @@ module.exports = class BaseForm extends Observable {
this.refresh(); this.refresh();
} }
this.form.classList.remove('was-validated'); this.form.classList.remove('was-validated');
}); };
this.doc.on('change', this.docListener);
} }
checkValidity() { checkValidity() {
@ -174,7 +176,6 @@ module.exports = class BaseForm extends Observable {
} else { } else {
await this.doc.update(); await this.doc.update();
} }
await this.refresh();
this.showAlert('Saved', 'success'); this.showAlert('Saved', 'success');
} catch (e) { } catch (e) {
this.showAlert('Failed', 'danger'); this.showAlert('Failed', 'danger');

View File

@ -15,12 +15,8 @@ module.exports = class BaseList {
this.data = []; this.data = [];
frappe.db.on(`change:${this.doctype}`, (params) => { frappe.db.on(`change:${this.doctype}`, (params) => {
this.dirty = true; this.refresh();
}); });
setInterval(() => {
if (this.dirty) this.refresh();
}, 500);
} }
makeBody() { makeBody() {
@ -45,7 +41,8 @@ module.exports = class BaseList {
let data = await this.getData(); let data = await this.getData();
for (let i=0; i< Math.min(this.pageLength, data.length); i++) { for (let i=0; i< Math.min(this.pageLength, data.length); i++) {
this.renderRow(this.start + i, data[i]); let row = this.getRow(this.start + i);
this.renderRow(row, data[i]);
} }
if (this.start > 0) { if (this.start > 0) {
@ -87,15 +84,10 @@ module.exports = class BaseList {
return filters; return filters;
} }
renderRow(i, data) { renderRow(row, data) {
let row = this.getRow(i);
row.innerHTML = this.getRowBodyHTML(data); row.innerHTML = this.getRowBodyHTML(data);
row.docName = data.name; row.docName = data.name;
row.setAttribute('data-name', data.name); row.setAttribute('data-name', data.name);
row.style.display = 'flex';
// make element focusable
row.setAttribute('tabindex', -1);
} }
getRowBodyHTML(data) { getRowBodyHTML(data) {
@ -105,24 +97,42 @@ module.exports = class BaseList {
} }
getRowHTML(data) { getRowHTML(data) {
return `<div class="col-11">${data.name}</div>`; return `<div class="col-11">
${this.getNameHTML(data)}
</div>`;
}
getNameHTML(data) {
return `<span class="indicator ${this.meta.getIndicatorColor(data)}">${data[this.meta.titleField]}</span>`;
} }
getRow(i) { getRow(i) {
if (!this.rows[i]) { if (!this.rows[i]) {
this.rows[i] = frappe.ui.add('div', 'list-row row no-gutters', this.body); let row = frappe.ui.add('div', 'list-row row no-gutters', this.body);
// open on click // open on click
let me = this; let me = this;
this.rows[i].addEventListener('click', async function(e) { row.addEventListener('click', async function(e) {
if (!e.target.tagName !== 'input') { if (!e.target.tagName !== 'input') {
await me.showItem(this.docName); await me.showItem(this.docName);
} }
}); });
row.style.display = 'flex';
// make element focusable
row.setAttribute('tabindex', -1);
this.rows[i] = row;
} }
return this.rows[i]; return this.rows[i];
} }
refreshRow(doc) {
let row = this.getRowByName(doc.name);
if (row) {
this.renderRow(row, doc);
}
}
async showItem(name) { async showItem(name) {
if (this.meta.print) { if (this.meta.print) {
await frappe.router.setRoute('print', this.doctype, name); await frappe.router.setRoute('print', this.doctype, name);
@ -255,14 +265,18 @@ module.exports = class BaseList {
} }
if (name) { if (name) {
let myListRow = this.body.querySelector(`.list-row[data-name="${name}"]`); let row = this.getRowByName(name);
if (myListRow) { if (row) {
myListRow.classList.add('active'); row.classList.add('active');
myListRow.focus(); row.focus();
} }
} }
} }
getRowByName(name) {
return this.body.querySelector(`.list-row[data-name="${name}"]`);
}
getActiveListRow() { getActiveListRow() {
return this.body.querySelector('.list-row.active'); return this.body.querySelector('.list-row.active');
} }

View File

@ -19,10 +19,7 @@ module.exports = {
this.models = {}; this.models = {};
this.forms = {}; this.forms = {};
this.views = {}; this.views = {};
this.docs = {}; this.flags = {};
this.flags = {
cacheDocs: false
}
}, },
registerLibs(common) { registerLibs(common) {
@ -40,7 +37,7 @@ module.exports = {
}, },
addToCache(doc) { addToCache(doc) {
if (!this.flags.cacheDocs) return; if (!this.docs) return;
// add to `docs` cache // add to `docs` cache
if (doc.doctype && doc.name) { if (doc.doctype && doc.name) {
@ -53,11 +50,21 @@ module.exports = {
if (doc.doctype === doc.name) { if (doc.doctype === doc.name) {
this[doc.name] = doc; this[doc.name] = doc;
} }
// propogate change to `docs`
doc.on('change', params => {
this.docs.trigger('change', params);
});
} }
}, },
isDirty(doctype, name) {
return (this.docs && this.docs[doctype] && this.docs[doctype][name]
&& this.docs[doctype][name]._dirty) || false;
},
getDocFromCache(doctype, name) { getDocFromCache(doctype, name) {
if (this.docs[doctype] && this.docs[doctype][name]) { if (this.docs && this.docs[doctype] && this.docs[doctype][name]) {
return this.docs[doctype][name]; return this.docs[doctype][name];
} }
}, },

View File

@ -34,9 +34,12 @@ module.exports = class BaseDocument extends Observable {
// set value and trigger change // set value and trigger change
async set(fieldname, value) { async set(fieldname, value) {
if (this[fieldname] !== value) {
this._dirty = true;
this[fieldname] = await this.validateField(fieldname, value); this[fieldname] = await this.validateField(fieldname, value);
await this.applyChange(fieldname); await this.applyChange(fieldname);
} }
}
async applyChange(fieldname) { async applyChange(fieldname) {
if (await this.applyFormula()) { if (await this.applyFormula()) {
@ -152,6 +155,8 @@ module.exports = class BaseDocument extends Observable {
syncValues(data) { syncValues(data) {
this.clearValues(); this.clearValues();
Object.assign(this, data); Object.assign(this, data);
this._dirty = false;
this.trigger('change', {doc: this});
} }
clearValues() { clearValues() {

View File

@ -5,12 +5,13 @@ const model = require('./index')
module.exports = class BaseMeta extends BaseDocument { module.exports = class BaseMeta extends BaseDocument {
constructor(data) { constructor(data) {
super(data); super(data);
this.list_options = { this.setDefaultIndicators();
fields: ['name', 'modified']
};
if (this.setupMeta) { if (this.setupMeta) {
this.setupMeta(); this.setupMeta();
} }
if (!this.titleField) {
this.titleField = 'name';
}
} }
hasField(fieldname) { hasField(fieldname) {
@ -158,4 +159,30 @@ module.exports = class BaseMeta extends BaseDocument {
await super.trigger(event, params); await super.trigger(event, params);
} }
setDefaultIndicators() {
if (!this.indicators) {
this.indicators = {
key: 'docstatus',
colors: {
0: 'gray',
1: 'blue',
2: 'red'
}
}
}
}
getIndicatorColor(doc) {
if (frappe.isDirty(this.name, doc.name)) {
return 'orange';
} else {
let value = doc[this.indicators.key];
if (value) {
return this.indicators.colors[value] || 'gray';
} else {
return 'gray';
}
}
}
} }

View File

@ -7,6 +7,14 @@ module.exports = {
"subject", "subject",
"description" "description"
], ],
titleField: 'subject',
indicators: {
key: 'status',
colors: {
Open: 'gray',
Closed: 'green'
}
},
"fields": [ "fields": [
{ {
"fieldname": "subject", "fieldname": "subject",

View File

@ -4,8 +4,4 @@ module.exports = class ToDoList extends BaseList {
getFields() { getFields() {
return ['name', 'subject', 'status']; return ['name', 'subject', 'status'];
} }
getRowHTML(data) {
let symbol = data.status=="Closed" ? "✔" : "";
return `<a href="#edit/ToDo/${data.name}">${symbol} ${data.subject}</a>`;
}
} }

View File

@ -1,4 +1,9 @@
module.exports = class Observable { module.exports = class Observable {
constructor() {
this._isHot = {};
this._eventQueue = {};
}
on(event, listener) { on(event, listener) {
this._addListener('_listeners', event, listener); this._addListener('_listeners', event, listener);
if (this._socketClient) { if (this._socketClient) {
@ -6,6 +11,16 @@ module.exports = class Observable {
} }
} }
// remove listener
off(event, listener) {
for (let type of ['_listeners', '_onceListeners']) {
let index = this[type] && this[type][event] && this[type][event].indexOf(listener);
if (index) {
this[type][event].splice(index, 1);
}
}
}
once(event, listener) { once(event, listener) {
this._addListener('_onceListeners', event, listener); this._addListener('_onceListeners', event, listener);
} }
@ -20,7 +35,12 @@ module.exports = class Observable {
this._socketServer = socket; this._socketServer = socket;
} }
async trigger(event, params) { async trigger(event, params, throttle=false) {
if (this._throttled(event, params, throttle)) return;
// listify if throttled
if (throttle) params = [params];
await this._triggerEvent('_listeners', event, params); await this._triggerEvent('_listeners', event, params);
await this._triggerEvent('_onceListeners', event, params); await this._triggerEvent('_onceListeners', event, params);
@ -36,6 +56,39 @@ module.exports = class Observable {
} }
_throttled(event, params, throttle) {
if (throttle) {
if (this._isHot[event]) {
// hot, add to queue
if (this._eventQueue[event]) {
// queue exists, just add
this._eventQueue[event].push(params);
} else {
// create a new queue to be called after cool-off
this._eventQueue[event] = [params];
// call after cool-off
setTimeout(() => {
let _queuedParams = this._eventQueue[event];
// reset queues
this._isHot[event] = false;
this._eventQueue[event] = null;
this.trigger(event, _queuedParams, true);
}, throttle);
}
return true;
}
this._isHot[event] = true;
}
return false;
}
_addListener(name, event, listener) { _addListener(name, event, listener) {
if (!this[name]) { if (!this[name]) {
this[name] = {}; this[name] = {};