We only maintain branches 6.4, 6.8, 7.2 (and main). babi
has a 7.0 branch which was created by @yangoon but as recently babi started depending on voyager and this one has no 7.0 branch it will not work.
Starting with 7.4 version, we intend to maintain all versions (7.4, 7.6, etc) for all our modules, but we will obsolete older branches faster and will not maintain a LTS version as core does. In general we’ll keep them for one year (or less if we can upgrade all customers fast enough).
Also we do not maintain setup and the rest of installation-related files. If anything, we’ll remove them in the future, but they don’t cause any harm so it’s not a priority to remove them.
We just clone the repositories in trytond/trytond/modules and that’s it.
For babi
I suggest you pick the 7.2 branch as it has improved user management and the possibility to export the content of a table as XLSX.
If you manage to make everything work, you’ll need some custom .js code to make the charts, dashboards and code (Monaco) editor work (to create SQL queries). I paste the content you need in custom.js below.
PS: It would be great if tryton made adding custom javascript more extensible and that the code could be provided by the modules.
Here’s the code:
/* Dashboard */
render_plot = function(chart_id, data, layout, config) {
loadPlotly();
if (typeof Plotly == 'undefined') {
return;
}
Plotly.newPlot(chart_id, data, layout, config);
}
render_widget = function(json, el, chart_id) {
el.empty()
var chart = JSON.parse(json);
var data = chart.data;
var layout = chart.layout;
var config = chart.config;
if (data && data.length > 0) {
type = data[0]['type'];
if (type == 'value') {
el.append(jQuery('<h1><center>' + data[0]['value'] + '</center></h1>'));
} else if (type == 'error') {
el.append(jQuery('<p>' + data[0]['message'] + '</p>'));
} else {
// https://plotly.com/javascript/reference/layout/
layout.autosize = true;
layout.separators = ',.';
layout.margin = {
t: 40,
l: 40,
r: 40,
b: 40,
autoexpand: false,
width: 500
};
config.responsive = true;
render_plot(chart_id, data, layout, config);
}
}
}
DashboardView = Sao.class_(Object, {
init: function(attributes, context) {
var Widget, view_prm;
var attributes, attribute, node, actions_prms;
this.attributes = attributes;
this.context = context;
this.widgets = {};
this.el = jQuery('<div/>', {
'class': 'content-box'
});
},
setup: function() {
var widgets;
this.el.empty();
widgets = JSON.parse(this.attributes['view']);
this.create_widgets(widgets, this.el);
},
create_widgets: function(items, parent) {
var item, widget_prm;
Widget = new Sao.Model('babi.widget');
table = jQuery('<table/>', {
'style': 'width: 100%',
'table-layout': 'fixed'
}).appendTo(parent);
var count = 0;
var row;
for (i = 0, len = items.length; i < len; i++) {
var item = items[i];
var widget_id = item['widget'];
var colspan = item['colspan'];
var height = item['height'];
if ((count % 4) == 0) {
row = jQuery('<tr/>').appendTo(table);
}
count += colspan;
if (count >= 4) {
count = 0;
}
widget_prm = Widget.execute('read', [
[widget_id],
['chart']
], this.context)
w = jQuery('<td/>', {
// TODO: It will not work if the same widget is shown twice
'id': 'chart-' + widget_id,
'width': 70,
'word-wrap': 'break-word',
'colspan': colspan,
'style': 'height: ' + height + 'px; vertical-align: top;'
}).appendTo(row);
this.widgets[widget_id] = w;
widget_prm.done(response => {
var record;
record = response[0];
render_widget(record['chart'], this.widgets[record['id']],
'chart-' + record['id']);
})
}
},
reload: function() {
this.setup();
},
});
DashboardTab = Sao.class_(Sao.Tab, {
class_: 'tab-board',
init: function(attributes) {
var Dashboard, view_prm;
Sao.Tab.Board._super.init.call(this, attributes);
this.dashboard_id = attributes.dashboard;
this.context = attributes.context;
this.name = attributes.name;
this.dialogs = [];
this.board = null;
Dashboard = new Sao.Model('babi.dashboard');
this.view_prm = Dashboard.execute('read', [
[this.dashboard_id],
['name', 'view']
], this.context);
this.view_prm.done(response => {
var board, record;
record = response[0];
this.set_name(record.name);
this.board = new DashboardView(record, this.context);
this.board.setup();
this.content.append(this.board.el);
});
this.create_tabcontent();
this.set_name(this.name);
this.title.text(this.name_el.text());
},
compare: function(attributes) {
if (this.attributes.dashboard != attributes.dashboard) {
return false;
}
if (this.attributes.context != attributes.context) {
return false
}
return true;
},
reload: function() {
this.board.setup();
},
record_message: function() {
},
refresh_resources: function() {
},
update_resources: function() {
},
});
ChartFormWidget = Sao.class_(Sao.View.Form.Widget, {
class_: 'form-chart',
expand: true,
init: function(view, attributes) {
ChartFormWidget._super.init.call(this, view, attributes);
this.chart_id = 'abcd';
this.el = jQuery('<div/>', {
'id': this.chart_id,
'class': this.class_
});
},
display: function() {
ChartFormWidget._super.display.call(this);
if (!this.record) {
return;
}
this.el.empty();
var value = this.record.field_get_client(this.field_name);
if (!value) {
return;
}
render_widget(value, this.el, this.chart_id);
},
focus: function() {
},
get modified() {
return false;
},
get_value: function() {
return null;
},
set_value: function() {
},
set_readonly: function(readonly) {
ChartFormWidget._super.set_readonly.call(this, readonly);
}
});
/* Register chart factory */
Sao.View.FormXMLViewParser.WIDGETS['chart'] = ChartFormWidget;
/* Override core existing Sao.Action.exec_action */
Sao.Action.exec_action = function (core) {
return function (action, data, context) {
core(action, data, context);
if (action.type == 'babi.action.dashboard') {
// Check if tab already exists
for (const other of Sao.Tab.tabs) {
if (other.compare(action)) {
var tablist = jQuery('#tablist');
tablist.find('a[href="#' + other.id + '"]')
.tab('show')[0].scrollIntoView();
return;
}
}
tab = new DashboardTab(action);
Sao.Tab.add(tab);
}
};
}(Sao.Action.exec_action);
/* WIDGETS */
function loadStylesheetSync(url) {
const css = jQuery('<link/>', {
'rel': 'stylesheet',
'href': url,
})[0];
// We override the 'onload' and 'onerror' function.
css.onload = function() {};
css.onerror = function(error) {
console.error('Error while loading CSS in sync mode!', error);
};
document.head.appendChild(css);
}
loadedScripts = {}
function loadScriptSync(url) {
if (loadedScripts[url]) {
return;
}
loadedScripts[url] = true;
const loader = new XMLHttpRequest();
loader.open('GET', url, false); // <- 'false' is what makes the 'loader' sync
loader.onreadystatechange = function() {
if (loader.readyState === 4 && (loader.status === 200 || loader.status === 0)) {
eval(loader.responseText);
}
};
loader.send(null);
const script = jQuery('<script/>', {
'type': 'text/javascript',
'text': loader.responseText
})[0];
document.head.appendChild(script);
}
function loadPlotly() {
if (typeof Plotly == 'undefined') {
loadScriptSync('https://cdn.plot.ly/plotly-2.35.2.min.js');
}
}
function loadMonaco() {
// Plotly must be loaded before Monaco due to the issue described here:
// https://stackoverflow.com/questions/55057425/can-only-have-one-anonymous-define-call-per-script-file
// So whenever we need monaco we must ensure that plotly is loaded first.
// In some cases we will not need plotly but if we need it afterwards and
// trying to load it will fail.
loadPlotly();
loadEditorJS();
if (typeof monaco == 'undefined') {
loadStylesheetSync('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.0/min/vs/editor/editor.main.min.css');
loadScriptSync('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.0/min/vs/loader.min.js');
}
}
let autoIncrement = 0;
/* Code Editor (Monaco) */
CodeFormWidget = Sao.class_(Sao.View.Form.Widget, {
class_: 'code-block',
expand: true,
init: function(view, attributes) {
CodeFormWidget._super.init.call(this, view, attributes);
this.code_id = "code-" + (autoIncrement++);
// The root tag of the widget
this.el = jQuery('<div/>', {
'class': this.class_,
'id': this.code_id,
});
if (this.el[0]) {
// TODO: Find a better way to allow Ctrl+S to be used within the widget
this.el[0].addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault()
e.stopPropagation()
document.querySelector('#save').click()
}
})
};
this.code = null;
loadMonaco();
// We load the monaco link for proper creation instance
require.config({
paths: {
'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.0/min/vs'
}
});
// We create the MONACO instance
require(["vs/editor/editor.main"], () => {
this.code = monaco.editor.create(this.el[0], {
value: '',
language: 'html',
theme: 'vs-dark',
automaticLayout: true,
});
send_modified = this.send_modified.bind(this);
this.code.getModel().onDidChangeContent(function(event) {
send_modified();
});
this.display();
});
},
display: function() {
CodeFormWidget._super.display.call(this);
if (!this.code) {
return;
}
let value = null;
if (this.record) {
value = this.record.field_get_client(this.field_name);
}
if (!value) {
value = '';
}
this.code.setValue(value);
},
focus: function() {},
get modified() {
if (!this.code) {
return false;
}
if (this.record) {
return this.record.field_get_client(this.field_name) != this.code.getValue();
}
return false;
},
set_value: function() {
if (!this.code) {
return;
}
this.field.set_client(this.record, this.code.getValue());
},
set_readonly: function(readonly) {
CodeFormWidget._super.set_readonly.call(this, readonly);
if (!this.code) {
return;
}
this.code.updateOptions({
readOnly: readonly
});
},
get_value: function() {
if (!this.code) {
return;
}
return this.code.getValue();
},
});
Sao.View.FormXMLViewParser.WIDGETS['code'] = CodeFormWidget;