--- a/static/js/framework/models.js Sun Jul 30 22:54:22 2017 +0800
+++ b/static/js/framework/models.js Sun Jul 30 23:00:46 2017 +0800
var url = Backbone.Model.prototype.url.call(this);
return url[url.length - 1] === '/' ? url : url + '/';
contentType: 'application/json',
data: JSON.stringify(data)
return (this.sync || Backbone.sync).call(this, 'update', model, options).then(function(serverAttrs) {
var Project = Model.extend({
switchColor: function() {
var colorChain = [null, 'info', 'success', 'warning', 'danger'];
var currentColorIndex = colorChain.indexOf(this.get('color') || null);
var nextColor = colorChain[(currentColorIndex + 1) % colorChain.length];
return this.adhoc({color: nextColor});
--- a/static/js/framework/router.js Sun Jul 30 22:54:22 2017 +0800
+++ b/static/js/framework/router.js Sun Jul 30 23:00:46 2017 +0800
':workspace': 'setWorkspace'
makeRandomString: function(length) {
return _.map(_.range(length), function() {
return (Math.random()*16|0).toString(16);
var randomString = this.makeRandomString(8);
this.navigate(randomString, {trigger: true, replace: true});
setWorkspace: function(workspace) {
fruitbar.trigger('workspace', workspace);
--- a/static/js/framework/views/base.js Sun Jul 30 22:54:22 2017 +0800
+++ b/static/js/framework/views/base.js Sun Jul 30 23:00:46 2017 +0800
bindCollection: function() {
.on('add', this.appendItem, this)
.on('change', this.updateItem, this)
compileTemplates: function(templates) {
_.each(templates, function(template, name) {
this.templates[name] = _.template(template);
--- a/static/js/framework/views/inline.js Sun Jul 30 22:54:22 2017 +0800
+++ b/static/js/framework/views/inline.js Sun Jul 30 23:00:46 2017 +0800
initialize: function(options) {
renderInput: function(options) {
this.$input = $('<input type="text" class="form-control input-sm">').val(options.target.text());
this.$input.keyup(function(e) {
this.$group.append(this.$saveButton, this.$cancelButton);
this.$el.append(this.$input, this.$group);
options.target.after(this.$el);
renderTextarea: function(options) {
this.$input = $('<textarea class="form-control input-sm" rows="3">').text(options.target.text());
this.$input.keyup(function(e) {
this.$group.append(this.$saveButton, this.$cancelButton);
this.$el.append(this.$input, this.$group);
options.target.after(this.$el);
render: function(options) {
this.$saveButton = $('<button class="btn btn-sm btn-success">').html('<i class="glyphicon glyphicon-ok"></i>');
this.$cancelButton = $('<button class="btn btn-sm btn-default">').html('<i class="glyphicon glyphicon-remove"></i>');
this.$el = $('<div class="inline-editor input-group">');
this.$group = $('<span class="input-group-btn">');
this.$cancelButton.click(function() {
this.$saveButton.click(function() {
view.trigger('save', view.$input.val());
return this.renderInput(options);
bindInlineEditable: function(model, selector) {
model.$item.delegate(selector, 'click', function(e) {
var attribute = $this.attr('data-model-attribute');
var inlineEditor = new InlineEditorView({
type: $this.attr('data-input-type') || 'input'
inlineEditor.on('save', function(value) {
model.adhoc(data).then(function() {
--- a/static/js/framework/views/projects.js Sun Jul 30 22:54:22 2017 +0800
+++ b/static/js/framework/views/projects.js Sun Jul 30 23:00:46 2017 +0800
initialize: function(options) {
this.tasks = options.tasks;
this.rawTemplates = options.templates;
this.compileTemplates(options.templates);
options.controls.create.click(function() {
options.collection.create({
renderItem: function(project) {
var $item = $(this.templates.project(project.toJSON()));
$item.tasksView = new TasksView({
el: $item.find('.tasks'),
collection: project.tasks,
create: $item.find('.btn-new-task')
$progress.removeClass('active');
.find('.btn-delete-project')
project.destroy({wait: true});
updateProgress: function(project) {
.attr('aria-valuenow', (100 * project.tasks.progress()))
.css('width', (100 * project.tasks.progress()) + '%');
appendItem: function(project) {
project.tasks = new Backbone.Shard({
filter: function(task) { return task.get('project_id') === project.id; }
project.tasks.progress = function() {
return this.filter(function(task) { return task.get('done') === true; }).length / this.size();
project.tasks.create = function(attributes, options) {
attributes = attributes || {};
attributes.project_id = project.id;
return tasks.create(attributes, options);
project.$item = this.renderItem(project);
this.bindInlineEditable(project, '.inline-editable[data-model="project"]');
this.updateProgress(project);
project.on('change:name', function() {
project.$item.find('[data-model="project"][data-model-attribute="name"]').text(project.get('name'));
project.on('change:color', function() {
this.updateProgress(project);
project.tasks.on('add remove change:done reset', function() {
this.updateProgress(project);
this.$el.append(project.$item);
removeItem: function(project) {
--- a/static/js/framework/views/tasks.js Sun Jul 30 22:54:22 2017 +0800
+++ b/static/js/framework/views/tasks.js Sun Jul 30 23:00:46 2017 +0800
initialize: function(options) {
this.compileTemplates(options.templates);
options.controls.create.click(function() {
options.collection.create({
renderItem: function(task) {
var $item = $(this.templates.task(task.toJSON()));
task.adhoc({'done': this.checked});
.find('.btn-delete-task')
task.destroy({wait: true});
updateNote: function(task) {
var $note = task.$item.find('[data-model="task"][data-model-attribute="note"]');
$note.text(task.get('note'));
task.$item.find('.task-note').removeClass('muted').addClass('present');
task.$item.find('.task-note').addClass('muted').removeClass('present');
appendItem: function(task) {
task.$item = this.renderItem(task);
this.bindInlineEditable(task, '.inline-editable[data-model="task"]');
task.on('change:done', function() {
task.$item.find('input:checkbox').prop('checked', task.get('done'));
task.on('change:name', function() {
task.$item.find('[data-model="task"][data-model-attribute="name"]').text(task.get('name'));
task.on('change:note', function() {
this.$el.append(task.$item);
removeItem: function(task) {
--- a/static/js/ui.js Sun Jul 30 22:54:22 2017 +0800
+++ b/static/js/ui.js Sun Jul 30 23:00:46 2017 +0800
fruitbar.tasks = new Tasks();
fruitbar.projects = new Projects();
fruitbar.router = new Router();
fruitbar.xhrs.done = function() {
return _(this).all(function(xhr) {
_(this).each(function(xhr) {
fruitbar.fetchAll = function(url) {
fruitbar.tasks.set(data.tasks);
fruitbar.workspaceTitleDisplayer = new Displayer({
el: $('.workspace-title')
fruitbar.workspaceTabs = new WorkspaceTabsView({
el: $('.workspace-tabs'),
collection: fruitbar.workspaces,
tab: $.trim($('#workspace-tab-template').html())
fruitbar.projectCounter = new CollectionCounterView({
el: $('.project-counter'),
collection: fruitbar.projects,
plurals: ['project', 'projects']
fruitbar.projectsView = new ProjectsView({
collection: fruitbar.projects,
create: $('.btn-new-project')
fruitbar.on('workspace', function(workspace) {
$('body').stop().animate({opacity: 0});
this.workspace = workspace;
this.workspaces.create({name: workspace});
this.tasks.url = '/' + encodeURIComponent(workspace) + '/tasks/';
this.projects.url = '/' + encodeURIComponent(workspace) + '/projects/';
this.xhrs.push(this.fetchAll('/' + encodeURIComponent(workspace) + '/all/'));
_(this.xhrs).each(function(fetch, index, xhrs) {
fruitbar.workspaceTitleDisplayer.render(workspace);
fruitbar.workspaceTabs.activate(workspace);
$('body').stop().animate({opacity: 1});
fruitbar.once('workspace', function() {
window.setInterval(function() {
fruitbar.xhrs.push(fruitbar.fetchAll('/' + encodeURIComponent(fruitbar.workspace) + '/all/'));