mirror of
https://github.com/frappe/books.git
synced 2024-11-14 01:14:03 +00:00
added indicators and throttling of events
This commit is contained in:
parent
de9ecc9f96
commit
7c6ddcd3e5
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
68
client/style/indicators.scss
Normal file
68
client/style/indicators.scss
Normal 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;
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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');
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
19
index.js
19
index.js
@ -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];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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() {
|
||||||
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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",
|
||||||
|
@ -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>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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] = {};
|
||||||
|
Loading…
Reference in New Issue
Block a user