-
Notifications
You must be signed in to change notification settings - Fork 8
Tutorial: Adding buttons to the Results page
On the off-chance that it helps someone, here is a hack of the Results page that adds three buttons to the left of the main blue buttons, like so:
Whoops, nice CSS, Kevin.
The buttons manipulate server-side state by hitting the endpoint /api/state/results_mode/
; I don't provide the server-side Python code here.
The UI portion is implemented in a js extension of the standard Cilantro ResultsWorkflow, and an accompanying HTML template:
- static/scripts/javascript/src/main.js (see below)
- static/scripts/javascript/src/ui/workflows/results.js (see below)
- static/scripts/javascript/src/templates/workflows/results.html (see below)
The state is held in a simple Backbone model:
- static/scripts/javascript/src/models.js (see below)
That will work in development. For r.js optimization, I had to add:
- static/scripts/javascript/app.build.js (see below)
- static/scripts/javascript/tpl.js (stock)
This is a lightly modified copy of Cilantro's own main.js. I don't like duplicating Cilantro's code and suspect that there is a better way.
require({
shim: {
'bootstrap': ['jquery'],
'marionette': {
deps: ['backbone'],
exports: 'Marionette'
},
'highcharts': {
deps: ['jquery'],
exports: 'Highcharts'
}
}
}, ['jquery',
'cilantro',
'project/ui/workflows/results',
'project/csrf'
], function ($, c, results) {
// Default session options
var options = {
url: c.config.get('url'),
credentials: c.config.get('credentials')
};
// Open the default session when Cilantro is ready
c.ready(function() {
// Open the default session defined in the pre-defined configuration.
// Initialize routes once data is confirmed to be available
c.sessions.open(options).then(function() {
// Panels are defined in their own namespace since they shared
// across workflows
c.panels = {
concept: new c.ui.ConceptPanel({
collection: this.data.concepts.queryable
}),
context: new c.ui.ContextPanel({
model: this.data.contexts.session
})
};
c.dialogs = {
exporter: new c.ui.ExporterDialog({
// TODO rename data.exporter on session
exporters: this.data.exporter
}),
columns: new c.ui.ConceptColumnsDialog({
view: this.data.views.session,
concepts: this.data.concepts.viewable
}),
query: new c.ui.EditQueryDialog({
view: this.data.views.session,
context: this.data.contexts.session,
collection: this.data.queries
}),
deleteQuery: new c.ui.DeleteQueryDialog()
};
var elements = [];
// Render and append panels in the designated main element
// prior to starting the session and loading the initial workflow
// Render and append element for insertion
$.each(c.panels, function(key, view) {
view.render();
elements.push(view.el);
});
$.each(c.dialogs, function(key, view) {
view.render();
elements.push(view.el);
});
// Set the initial HTML with all the global views
var main = $(c.config.get('main'));
main.append.apply(main, elements);
c.workflows = {
query: new c.ui.QueryWorkflow({
context: this.data.contexts.session,
concepts: this.data.concepts.queryable
}),
results: new results.ResultsWorkflow({
view: this.data.views.session,
results: this.data.preview
})
};
// Define routes
var routes = [{
id: 'query',
route: 'query/',
view: c.workflows.query
}, {
id: 'results',
route: 'results/',
view: c.workflows.results
}];
// Workspace supported as of 2.1.0
if (c.isSupported('2.1.0')) {
c.workflows.workspace = new c.ui.WorkspaceWorkflow({
queries: this.data.queries,
context: this.data.contexts.session,
view: this.data.views.session,
public_queries: this.data.public_queries, // jshint ignore:line
stats: this.data.stats
});
routes.push({
id: 'workspace',
route: 'workspace/',
view: c.workflows.workspace
});
}
// Query URLs supported as of 2.2.0
if (c.isSupported('2.2.0')) {
c.workflows.queryload = new c.ui.QueryLoader({
queries: this.data.queries,
context: this.data.contexts.session,
view: this.data.views.session
});
routes.push({
id: 'query-load',
route: 'results/:query_id/',
view: c.workflows.queryload
});
}
// Register routes and start the session
this.start(routes);
});
});
});
This extends the standard Cilantro ResultsWorkflow, i.e. the Results page.
/* global define */
define([
'jquery',
'underscore',
'cilantro',
'../../models',
'tpl!project/templates/workflows/results.html'
], function($, _, c, models, results_tpl) {
var ResultsWorkflow = c.ui.ResultsWorkflow.extend({
template: results_tpl,
ui: function() {
var relativesBtn = '[data-action=add-relatives]';
var triosBtn = '[data-action=just-trios]';
var normalBtn = '[data-action=normal-results]';
return _.extend({
relativesBtn: relativesBtn,
triosBtn: triosBtn,
normalBtn: normalBtn
}, c.ui.ResultsWorkflow.prototype.ui)
},
events: function () {
return _.extend({
'click @ui.triosBtn': 'toggleJustTrios',
'click @ui.relativesBtn': 'toggleAddRelatives',
'click @ui.normalBtn': 'toggleNormalResults',
}, c.ui.ResultsWorkflow.prototype.events);
},
initialize: function() {
c.ui.ResultsWorkflow.prototype.initialize.call(this);
this.resultsMode = new models.ResultsModeModel();
this.resultsMode.url = c.session.get('url') + 'state/results_mode/';
_.bindAll(this, 'onResultsModeFetched');
},
onRender: function() {
c.ui.ResultsWorkflow.prototype.onRender.call(this);
this.resultsMode.fetch({success: this.onResultsModeFetched});
this.pullUpLoadingOverlay();
},
deactivateButton: function(btn) {
btn.removeClass('btn-info');
},
activateButton: function(btn) {
var btnString = btn.attr('data-action');
console.log('activating button: ' + btnString);
btn.addClass('btn-info');
// Implement mutual exclusion of buttons by hand; bootstrap not working, maybe stomped by google theme?
if (btnString == 'add-relatives') {
this.deactivateButton(this.ui.triosBtn);
this.deactivateButton(this.ui.normalBtn);
} else if (btnString == 'just-trios') {
this.deactivateButton(this.ui.relativesBtn);
this.deactivateButton(this.ui.normalBtn);
} else if (btnString == 'normal-results') {
this.deactivateButton(this.ui.triosBtn);
this.deactivateButton(this.ui.relativesBtn);
}
},
pullUpLoadingOverlay: function() {
// Make sure loading overlay is on top of my buttons
$('.loading-overlay').css('z-index', 9999);
},
onResultsModeFetched: function(model, response, options) {
var state = model.get('results_mode');
console.log("onResultsModeFetched: state = " + state);
if (state == 'normal-results') {
this.activateButton(this.ui.normalBtn);
} else if (state == 'just-trios') {
this.activateButton(this.ui.triosBtn);
this.ui.triosBtn.addClass('active');
} else if (state == 'add-relatives') {
this.activateButton(this.ui.relativesBtn);
}
},
saveResultsMode: function() {
this.resultsMode.save(null, {
success: function(model, response) {
c.data.preview.markAsDirty();
}
});
},
toggleJustTrios: function() {
console.log("Toggle just-trios");
if (this.resultsMode.get('results_mode') != 'just-trios') {
this.activateButton(this.ui.triosBtn);
this.resultsMode.set('results_mode', 'just-trios');
this.saveResultsMode();
}
},
toggleAddRelatives: function() {
console.log("Toggle add-relatives");
if (this.resultsMode.get('results_mode') != 'add-relatives') {
this.activateButton(this.ui.relativesBtn);
this.resultsMode.set('results_mode', 'add-relatives');
this.saveResultsMode();
}
},
toggleNormalResults: function() {
console.log("Toggle normal-results");
if (this.resultsMode.get('results_mode') != 'normal-results') {
this.activateButton(this.ui.normalBtn);
this.resultsMode.set('results_mode', 'normal-results');
this.saveResultsMode();
}
}
});
return {
ResultsWorkflow: ResultsWorkflow
};
});
<div class='navbar navbar-masthead results-workflow-navbar'>
<div class='navbar-inner'>
<div class='navbar-text pull-right'>
<span class='paginator-region'></span>
<div class="btn-group" role="group" data-toggle="buttons-radio">
<button data-action="normal-results" type="button" class="btn btn-mini">Original</button>
<button data-action="add-relatives" type="button" class="btn btn-mini">Add All Relatives</button>
<button data-action="just-trios" type="button" class="btn btn-mini">Trio View</button>
</div>
<button data-toggle=columns-dialog class='btn btn-primary btn-mini' title='Change Columns'>
<i class=icon-columns></i> <span class=large-display-button-text>Change Columns...</span>
</button>
<button data-toggle=exporter-dialog class='btn btn-primary btn-mini' title='Export Data'>
<i class=icon-file></i> <span class=large-display-button-text>Export...</span>
</button>
<button data-toggle=query-dialog class='btn btn-primary btn-mini' title='Save/Share Query'>
<i class=icon-save></i> <span class=large-display-button-text>Save/Share Query...</span>
</button>
<button data-toggle=context-panel class='btn btn-primary btn-mini expand-collapse' title='Hide Filter Panel'>
<i class=icon-expand-alt></i> <span class=large-display-button-text>Hide Filters</span>
</button>
</div>
<span class='navbar-text count-region'></span>
</div>
</div>
<div class='loading-overlay hide'>
<h4><i class='icon-spinner icon-spin'></i> Loading Results</h4>
</div>
<div class=table-region></div>
/* global define */
define([
'backbone'
], function(Backbone) {
var ResultsModeModel = Backbone.Model.extend({
idAttribute: 'results_mode'
});
return {ResultsModeModel: ResultsModeModel};
});
// RequireJS optimization configuration
// Full example: https://github.com/jrburke/r.js/blob/master/build/example.build.js
({
// Optimize relative to this url (i.e. the current directory)
baseUrl: '.',
// The source directory of the modules
appDir: 'src',
// The target directory of the optimized modules
dir: 'min',
optimize: 'uglify',
optimizeCss: 'none',
paths: {
'project': '.',
'cilantro': 'empty:',
'jquery': 'empty:',
'underscore': 'empty:',
'backbone': 'empty:',
'tpl': '../tpl'
},
name: 'main'
})