Browse Source

Kanban enhancements (#2609)

* quick kb, some styling

* card ordering fixed

* AssignTo code cleanup

* card ordering fixed

* Empty column_name validation

* filter autosave

* column ordering

* column indicator color

* KB based on custom field

* added add_custom_field method
version-14
Faris Ansari 8 years ago
committed by Rushabh Mehta
parent
commit
9ddecfb14b
17 changed files with 585 additions and 192 deletions
  1. +8
    -1
      frappe/custom/doctype/custom_field/custom_field.py
  2. +2
    -2
      frappe/desk/doctype/kanban_board/kanban_board.json
  3. +103
    -10
      frappe/desk/doctype/kanban_board/kanban_board.py
  4. +9
    -8
      frappe/desk/doctype/kanban_board_column/kanban_board_column.json
  5. +15
    -2
      frappe/desk/form/meta.py
  6. +4
    -0
      frappe/public/css/indicator.css
  7. +32
    -8
      frappe/public/css/kanban.css
  8. +4
    -0
      frappe/public/css/website.css
  9. +15
    -1
      frappe/public/js/frappe/db.js
  10. +5
    -7
      frappe/public/js/frappe/form/footer/assign_to.js
  11. +106
    -1
      frappe/public/js/frappe/list/list_sidebar.js
  12. +1
    -0
      frappe/public/js/frappe/list/listview.js
  13. +0
    -6
      frappe/public/js/frappe/views/kanban/kanban_card.html
  14. +3
    -3
      frappe/public/js/frappe/views/kanban/kanban_column.html
  15. +228
    -137
      frappe/public/js/frappe/views/kanban/kanban_view.js
  16. +5
    -0
      frappe/public/less/indicator.less
  17. +45
    -6
      frappe/public/less/kanban.less

+ 8
- 1
frappe/custom/doctype/custom_field/custom_field.py View File

@@ -3,6 +3,7 @@

from __future__ import unicode_literals
import frappe
import json
from frappe.utils import cstr
from frappe import _
from frappe.model.document import Document
@@ -95,5 +96,11 @@ def create_custom_field(doctype, df):
"fieldtype": df.fieldtype,
"options": df.options,
"insert_after": df.insert_after,
"print_hide": df.print_hide
"print_hide": df.print_hide,
"hidden": df.hidden or 0
}).insert()

@frappe.whitelist()
def add_custom_field(doctype, df):
df = json.loads(df)
return create_custom_field(doctype, df)

+ 2
- 2
frappe/desk/doctype/kanban_board/kanban_board.json View File

@@ -148,7 +148,7 @@
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
@@ -192,7 +192,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-01-10 04:51:19.413720",
"modified": "2017-01-18 13:53:44.283037",
"modified_by": "Administrator",
"module": "Desk",
"name": "Kanban Board",


+ 103
- 10
frappe/desk/doctype/kanban_board/kanban_board.py View File

@@ -8,8 +8,13 @@ import json
from frappe import _
from frappe.model.document import Document


class KanbanBoard(Document):
pass
def validate(self):
for column in self.columns:
if not column.column_name:
frappe.msgprint(frappe._("Column Name cannot be empty"), raise_exception=True)


@frappe.whitelist()
def add_column(board_name, column_title):
@@ -20,12 +25,12 @@ def add_column(board_name, column_title):
frappe.throw(_("Column <b>{0}</b> already exist.").format(column_title))

doc.append("columns", dict(
column_name=column_title,
color=""
column_name=column_title
))
doc.save()
return doc.columns


@frappe.whitelist()
def archive_restore_column(board_name, column_title, status):
'''Set column's status to status'''
@@ -37,6 +42,7 @@ def archive_restore_column(board_name, column_title, status):
doc.save()
return doc.columns


@frappe.whitelist()
def update_doc(doc):
'''Updates the doc when card is edited'''
@@ -56,13 +62,100 @@ def update_doc(doc):
}
return doc


@frappe.whitelist()
def update_order(board_name, column_title, order):
'''Save the order of cards in a column'''
doc = frappe.get_doc('Kanban Board', board_name)
def update_order(board_name, order):
'''Save the order of cards in columns'''
board = frappe.get_doc('Kanban Board', board_name)
doctype = board.reference_doctype
fieldname = board.field_name
order_dict = json.loads(order)

for col in doc.columns:
if column_title == col.column_name:
col.order = order
updated_cards = []
for col_name, cards in order_dict.iteritems():
order_list = []
for card in cards:
column = frappe.get_value(
doctype,
{'name': card},
fieldname
)
if column != col_name:
frappe.set_value(doctype, card, fieldname, col_name)
updated_cards.append(dict(
name=card,
column=col_name
))

for column in board.columns:
if column.column_name == col_name:
column.order = json.dumps(cards)

board.save()
return board, updated_cards


@frappe.whitelist()
def quick_kanban_board(doctype, board_name, field_name):
'''Create new KanbanBoard quickly with default options'''
doc = frappe.new_doc('Kanban Board')
options = frappe.get_value('DocField', dict(
parent=doctype,
fieldname=field_name
), 'options')

columns = []
if options:
columns = options.split('\n')

for column in columns:
doc.append("columns", dict(
column_name=column
))

doc.kanban_board_name = board_name
doc.reference_doctype = doctype
doc.field_name = field_name
doc.save()
return doc
return doc


@frappe.whitelist()
def update_column_order(board_name, order):
'''Set the order of columns in Kanban Board'''
board = frappe.get_doc('Kanban Board', board_name)
order = json.loads(order)
old_columns = board.columns
new_columns = []

for col in order:
for column in old_columns:
if col == column.column_name:
new_columns.append(column)
old_columns.remove(column)

new_columns.extend(old_columns)

board.columns = []
for col in new_columns:
board.append("columns", dict(
column_name=col.column_name,
status=col.status,
order=col.order,
indicator=col.indicator,
))

board.save()
return board

@frappe.whitelist()
def set_indicator(board_name, column_name, indicator):
'''Set the indicator color of column'''
board = frappe.get_doc('Kanban Board', board_name)

for column in board.columns:
if column.column_name == column_name:
column.indicator = indicator
board.save()
return board

+ 9
- 8
frappe/desk/doctype/kanban_board_column/kanban_board_column.json View File

@@ -44,7 +44,8 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "color",
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
@@ -52,10 +53,10 @@
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Color",
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nRed\nBlue\nGreen",
"options": "Active\nArchived",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -73,8 +74,8 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Active",
"fieldname": "status",
"default": "darkgrey",
"fieldname": "indicator",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
@@ -82,10 +83,10 @@
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Status",
"label": "Indicator",
"length": 0,
"no_copy": 0,
"options": "Active\nArchived",
"options": "blue\norange\nred\ngreen\ndarkgrey\npurple\nyellow\nlightblue",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -137,7 +138,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-01-02 12:11:48.389715",
"modified": "2017-01-17 15:23:43.520379",
"modified_by": "Administrator",
"module": "Desk",
"name": "Kanban Board Column",


+ 15
- 2
frappe/desk/form/meta.py View File

@@ -41,14 +41,14 @@ class FormMeta(Meta):
self.load_workflows()
self.load_templates()
self.load_dashboard()
self.load_kanban_boards()
self.load_kanban_meta()

def as_dict(self, no_nulls=False):
d = super(FormMeta, self).as_dict(no_nulls=no_nulls)
for k in ("__js", "__css", "__list_js", "__calendar_js", "__map_js",
"__linked_with", "__messages", "__print_formats", "__workflow_docs",
"__form_grid_templates", "__listview_template", "__tree_js",
"__dashboard", "__kanban_boards"):
"__dashboard", "__kanban_boards", "__kanban_column_fields"):
d[k] = self.get(k)

for i, df in enumerate(d.get("fields")):
@@ -170,11 +170,24 @@ class FormMeta(Meta):

def load_dashboard(self):
self.set('__dashboard', self.get_dashboard_data())
def load_kanban_meta(self):
self.load_kanban_boards()
self.load_kanban_column_fields()

def load_kanban_boards(self):
kanban_boards = frappe.get_all(
'Kanban Board', filters={'reference_doctype': self.name})
self.set("__kanban_boards", kanban_boards, as_value=True)
def load_kanban_column_fields(self):
values = frappe.get_all(
'Kanban Board', fields=['field_name'],
filters={'reference_doctype': self.name})

fields = [x['field_name'] for x in values]
fields = list(set(fields))
self.set("__kanban_column_fields", fields, as_value=True)

def get_code_files_via_hooks(hook, name):
code_files = []


+ 4
- 0
frappe/public/css/indicator.css View File

@@ -60,6 +60,10 @@
.indicator-right.light-blue::after {
background: #7CD6FD;
}
.indicator.lightblue::before,
.indicator-right.lightblue::after {
background: #7CD6FD;
}
.modal-header .indicator {
float: left;
margin-top: 7.5px;


+ 32
- 8
frappe/public/css/kanban.css View File

@@ -22,15 +22,24 @@
font-weight: bold;
font-size: 12px;
}
.kanban .kanban-column-title .column-options {
position: absolute;
right: 0px;
.kanban .kanban-column-title .column-options .button-group {
display: flex;
padding: 12px 14px;
}
.kanban .kanban-column-title .column-options .button-group .btn.indicator {
flex: 1;
}
.kanban .sortable-ghost > .kanban-card {
.kanban .kanban-column-title .column-options .indicator::before {
margin: 0;
}
.kanban .kanban-column-title:hover {
cursor: pointer;
}
.kanban .sortable-ghost > .kanban-card:not(.add-card) {
background: #ccc !important;
color: transparent;
}
.kanban .sortable-ghost > .kanban-card * {
.kanban .sortable-ghost > .kanban-card:not(.add-card) * {
background: transparent !important;
color: transparent !important;
}
@@ -47,8 +56,7 @@
color: #8D99A6;
}
.kanban .kanban-card.add-card:hover {
background-color: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
box-shadow: none;
color: #36414C;
cursor: pointer;
}
@@ -107,7 +115,7 @@
outline: none;
}
.kanban .add-new-column a:hover {
color: #36414C;
color: #36414C !important;
}
.kanban .kanban-card-meta {
margin-top: 8px;
@@ -117,3 +125,19 @@
width: 16px;
height: 16px;
}
body[data-route*="Kanban"] .modal .add-assignment:hover i {
color: #36414C !important;
}
.edit-card-title .h4 {
margin-top: 5px;
margin-bottom: 5px;
}
.edit-card-title span:hover {
background-color: #fffce7;
cursor: pointer;
}
.edit-card-title input {
border: none;
outline: none;
width: 100%;
}

+ 4
- 0
frappe/public/css/website.css View File

@@ -381,6 +381,10 @@ a.no-decoration:active {
.indicator-right.light-blue::after {
background: #7CD6FD;
}
.indicator.lightblue::before,
.indicator-right.lightblue::after {
background: #7CD6FD;
}
.modal-header .indicator {
float: left;
margin-top: 7.5px;


+ 15
- 1
frappe/public/js/frappe/db.js View File

@@ -3,7 +3,7 @@

frappe.db = {
get_value: function(doctype, filters, fieldname, callback) {
frappe.call({
return frappe.call({
method: "frappe.client.get_value",
args: {
doctype: doctype,
@@ -14,5 +14,19 @@ frappe.db = {
callback(r.message);
}
});
},
set_value: function(doctype, docname, fieldname, value, callback) {
return frappe.call({
method: "frappe.client.set_value",
args: {
doctype: doctype,
name: docname,
fieldname: fieldname,
value: value
},
callback: function(r) {
callback(r.message);
}
});
}
}

+ 5
- 7
frappe/public/js/frappe/form/footer/assign_to.js View File

@@ -132,7 +132,7 @@ frappe.ui.form.AssignTo = Class.extend({
frappe.ui.form.AssignToDialog = Class.extend({
init: function(opts){
var me = this
$.extend(me,new frappe.ui.Dialog({
$.extend(me, new frappe.ui.Dialog({
title: __('Add to To Do'),
fields: [
{fieldtype: 'Link', fieldname:'assign_to', options:'User',
@@ -152,11 +152,7 @@ frappe.ui.form.AssignToDialog = Class.extend({
{value:'High', label:__('High')}],
'default':'Medium'},
],
primary_action: function() {
var assign_to = opts.obj.dialog.fields_dict.assign_to.get_value();
var args = opts.obj.dialog.get_values();
frappe.ui.add_assignment(assign_to, args, opts, opts.obj.dialog);
},
primary_action: function() { frappe.ui.add_assignment(opts, me) },
primary_action_label: __("Add")
}));

@@ -184,7 +180,9 @@ frappe.ui.form.AssignToDialog = Class.extend({

});

frappe.ui.add_assignment = function(assign_to, args, opts, dialog) {
frappe.ui.add_assignment = function(opts, dialog) {
var assign_to = opts.obj.dialog.fields_dict.assign_to.get_value();
var args = opts.obj.dialog.get_values();
if(args && assign_to) {
return frappe.call({
method: opts.method,


+ 106
- 1
frappe/public/js/frappe/list/list_sidebar.js View File

@@ -129,7 +129,112 @@ frappe.views.ListSidebar = Class.extend({
});

$dropdown.find('.new-kanban-board').click(function() {
frappe.new_doc('Kanban Board', {reference_doctype: me.doctype});
// frappe.new_doc('Kanban Board', {reference_doctype: me.doctype});
var select_fields = frappe.get_meta(me.doctype)
.fields.filter(function(df) {
return df.fieldtype === 'Select';
}).map(function(df) {
return df.fieldname;
});

var fields = [
{
fieldtype: 'Data',
fieldname: 'board_name',
label: __('Kanban Board Name'),
reqd: 1
}
]

if(select_fields.length > 0) {
fields = fields.concat([{
fieldtype: 'Select',
fieldname: 'field_name',
label: __('Columns based on'),
options: select_fields.join('\n'),
default: select_fields[0]
},
{
fieldtype: 'Check',
fieldname: 'custom_column',
label: __('Add Custom Column Field'),
default: 0,
onchange: function(e) {
var checked = d.get_value('custom_column');
if(checked) {
d.get_input('field_name').prop('disabled', true);
} else {
d.get_input('field_name').prop('disabled', null);
}
}
}]);
}

var d = new frappe.ui.Dialog({
title: __('New Kanban Board'),
fields: fields,
primary_action: function() {
var values = d.get_values();
var custom_column = values.custom_column !== undefined ?
values.custom_column : 1;

me.add_custom_column_field(custom_column)
.then(function(custom_column) {
console.log(custom_column)
var f = custom_column ?
'kanban_column' : values.field_name;
console.log(f)
return me.make_kanban_board(values.board_name, f)
})
.then(function() {
d.hide();
}, function(err) {
msgprint(err);
});
}
});
d.show();
});
},
add_custom_column_field: function(flag) {
var me = this;
return new Promise(function(resolve, reject) {
if(!flag) resolve(false);
frappe.call({
method: 'frappe.custom.doctype.custom_field.custom_field.add_custom_field',
args: {
doctype: me.doctype,
df: {
label: 'Kanban Column',
fieldname: 'kanban_column',
fieldtype: 'Select',
hidden: 1
}
}
}).success(function() {
resolve(true);
}).error(function(err) {
reject(err);
});
});
},
make_kanban_board: function(board_name, field_name) {
var me = this;
return frappe.call({
method: 'frappe.desk.doctype.kanban_board.kanban_board.quick_kanban_board',
args: {
doctype: me.doctype,
board_name: board_name,
field_name: field_name
},
callback: function(r) {
frappe.set_route(
'List',
me.doctype,
'Kanban',
r.message.kanban_board_name
);
}
});
},
setup_assigned_to_me: function() {


+ 1
- 0
frappe/public/js/frappe/list/listview.js View File

@@ -100,6 +100,7 @@ frappe.views.ListView = Class.extend({
me.fields.push(d);
});
}
me.fields = me.fields.concat(me.meta.__kanban_column_fields);
},
set_columns: function() {
var me = this;


+ 0
- 6
frappe/public/js/frappe/views/kanban/kanban_card.html View File

@@ -1,15 +1,9 @@
<div class="kanban-card-wrapper" data-name="{{name}}">
<div class="kanban-card content">
<button class="btn btn-default btn-xs kanban-card-edit">
<span class="octicon octicon-pencil text-muted"></span>
</button>
<div class="kanban-card-title">
{{ title }}
</div>
<div class="kanban-card-meta">
</div>
</div>
<div class="kanban-card edit-card-area">
<textarea></textarea>
</div>
</div>

+ 3
- 3
frappe/public/js/frappe/views/kanban/kanban_column.html View File

@@ -1,7 +1,7 @@
<div class="kanban-column" data-column-value="{{title}}">
<div class="kanban-column-title">
<div class="kanban-column-title indicator {{indicator}}">
<span>{{ __(title) }}</span>
<div class="btn-group column-options">
<div class="btn-group column-options dropdown pull-right">
<a class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button class="btn btn-default btn-xs"><span class="caret"></span></button>
</a>
@@ -16,7 +16,7 @@

<div class="kanban-card add-card">
<div class="kanban-card-title">
<i class="octicon octicon-plus"></i> {{ __("Add a card") }}
<i class="octicon octicon-plus"></i> {{ __("Add " + doctype) }}
</div>
</div>
<div class="kanban-card new-card-area">


+ 228
- 137
frappe/public/js/frappe/views/kanban/kanban_view.js View File

@@ -2,6 +2,8 @@ frappe.provide("frappe.views");

(function () {

var method_prefix = 'frappe.desk.doctype.kanban_board.kanban_board.';

var store = fluxify.createStore({
id: 'store',
initialState: {
@@ -70,35 +72,16 @@ frappe.provide("frappe.views");
},
save_filters: function (updater) {
var filters = JSON.stringify(this.cur_list.filter_list.get_filters());
frappe.call({
method: "frappe.client.set_value",
args: {
doctype: 'Kanban Board',
name: this.board.name,
fieldname: 'filters',
value: filters
},
callback: function () {
frappe.db.set_value(
'Kanban Board', this.board.name,
'filters', filters,
function() {
updater.set({ filters_modified: false });
show_alert({ message: __("Filters saved"), indicator: 'green' }, 1);
}
});
},
change_card_column: function (updater, card, column_title) {
var state = this;
frappe.call({
method: "frappe.client.set_value",
args: {
doctype: this.doctype,
name: card.name,
fieldname: this.board.field_name,
value: column_title
}
}).then(function (r) {
// var doc = r.message;
// var new_card = prepare_card(card, state, doc);
// fluxify.doAction('update_card', new_card);
});
show_alert({
message: __("Filters saved"),
indicator: 'green'
}, 1);
});
},
add_card: function (updater, card_title, column_title) {
var doc = frappe.model.get_new_doc(this.doctype);
@@ -149,7 +132,7 @@ frappe.provide("frappe.views");
update_doc: function (updater, doc, card) {
var state = this;
return frappe.call({
method: "frappe.desk.doctype.kanban_board.kanban_board.update_doc",
method: method_prefix + "update_doc",
args: { doc: doc },
freeze: true
}).then(function (r) {
@@ -158,31 +141,55 @@ frappe.provide("frappe.views");
fluxify.doAction('update_card', updated_card);
});
},
assign_to: function (updater, user, desc, card) {
var opts = {
method: "frappe.desk.form.assign_to.add",
doctype: this.doctype,
docname: card.name,
}
var args = {
description: desc
}
return frappe.ui.add_assignment(user, args, opts)
.then(function () {
card.assigned_list.push(user);
fluxify.doAction('update_card', card);
update_order: function(updater, order) {
return frappe.call({
method: method_prefix + "update_order",
args: {
board_name: this.board.name,
order: order
}
}).then(function(r) {
var state = this;
var board = r.message[0];
var updated_cards = r.message[1];
var cards = update_cards_column(updated_cards);
var columns = prepare_columns(board.columns);
updater.set({
cards: cards,
columns: columns
});
});
},
update_order: function(updater, column_title, order) {
var board = store.getState().board.name;
update_column_order: function(updater, order) {
return frappe.call({
method: "frappe.desk.doctype.kanban_board.kanban_board.update_order",
method: method_prefix + "update_column_order",
args: {
board_name: board,
column_title: column_title,
board_name: this.board.name,
order: order
}
}).then(function(r) {
var board = r.message;
var columns = prepare_columns(board.columns);
updater.set({
columns: columns
});
});
},
set_indicator: function(updater, column, color) {
return frappe.call({
method: method_prefix + "set_indicator",
args: {
board_name: this.board.name,
column_name: column.title,
indicator: color
}
}).then(function(r) {
var board = r.message;
var columns = prepare_columns(board.columns);
updater.set({
columns: columns
});
})
}
}
});
@@ -206,6 +213,7 @@ frappe.provide("frappe.views");
self.$kanban_board.appendTo(self.wrapper);
self.$filter_area = self.cur_list.$page.find('.set-filters');
bind_events();
setup_sortable();
}

function make_columns() {
@@ -219,7 +227,22 @@ frappe.provide("frappe.views");

function bind_events() {
bind_add_column();
bind_save_filter_button();
bind_save_filter();
}

function setup_sortable() {
var sortable = new Sortable(self.$kanban_board.get(0), {
group: 'columns',
animation: 150,
dataIdAttr: 'data-column-value',
filter: '.add-new-column',
handle: '.kanban-column-title',
onEnd: function(evt) {
var order = sortable.toArray();
order = order.slice(1);
fluxify.doAction('update_column_order', order);
}
});
}

function bind_add_column() {
@@ -261,21 +284,9 @@ frappe.provide("frappe.views");
});
}

function bind_save_filter_button() {
self.$save_filter_btn = self.$filter_area.find('.save-filters');
if (self.$save_filter_btn.length) return;

//make save filter button
self.$save_filter_btn = $('<button>', {
class: 'btn btn-xs btn-default text-muted save-filters',
text: __('Save filters')
}).on('click', function () {
fluxify.doAction('save_filters')
}).appendTo(self.$filter_area).hide();

store.on('change:filters_modified', function (val) {
val ? self.$save_filter_btn.show() :
self.$save_filter_btn.hide();
function bind_save_filter() {
store.on('change:filters_modified', function (modified) {
if(modified) fluxify.doAction('save_filters');
});

self.cur_list.wrapper.on('render-complete', function () {
@@ -337,8 +348,12 @@ frappe.provide("frappe.views");
}

function make_dom() {
self.$kanban_column = $(frappe.render_template('kanban_column',
{ title: column.title })).appendTo(wrapper);
self.$kanban_column = $(frappe.render_template(
'kanban_column', {
title: column.title,
doctype: store.getState().doctype,
indicator: column.indicator
})).appendTo(wrapper);
self.$kanban_cards = self.$kanban_column.find('.kanban-cards');
}

@@ -350,7 +365,7 @@ frappe.provide("frappe.views");

var order = column.order;
if(order) {
order = order.split('|');
order = JSON.parse(order);
order.forEach(function(name) {
frappe.views.KanbanBoardCard(get_card(name), self.$kanban_cards);
});
@@ -380,37 +395,24 @@ frappe.provide("frappe.views");
onEnd: function (evt) {
wrapper.find('.kanban-card.add-card').fadeIn(100);
wrapper.find('.kanban-cards').height('auto');
var card_name = $(evt.item).data().name;
var card = get_card(card_name);
var board = store.getState().board.name;
// update order
var order = sortable.toArray();
fluxify.doAction('update_order', column.title, order.join('|'));
var order = {}
wrapper.find('.kanban-column[data-column-value]')
.each(function() {
var col_name = $(this).data().columnValue;
order[col_name] = [];
$(this).find('.kanban-card-wrapper').each(function() {
var card_name = $(this).data().name;
order[col_name].push(card_name);
});
});
fluxify.doAction('update_order', order);
},
onAdd: function (evt) {
var card_name = $(evt.item).data().name;
var card = get_card(card_name);
fluxify.doAction('change_card_column', card, column.title);
// update order
var order = sortable.toArray();
fluxify.doAction('update_order', column.title, order.join('|'));
},
});
}

function get_card_by_order(order) {
var board = store.getState().board.name;
filtered_cards.find(function(c) {
return c.kanban_column_order[board] === order;
});
}

function get_card(name) {
return store.getState().cards.find(function (c) {
return c.name === name;
});
}

function bind_add_card() {
var $wrapper = self.$kanban_column;
var $btn_add = $wrapper.find('.add-card');
@@ -451,14 +453,28 @@ frappe.provide("frappe.views");

function bind_options() {
self.$kanban_column.find(".column-options .dropdown-menu")
.on("click", "a", function (e) {
.on("click", "[data-action]", function (e) {
var $btn = $(this);
var action = $btn.data().action;

if (action === "archive") {
fluxify.doAction('archive_column', column);
} else if (action === "indicator") {
var color = $btn.data().indicator;
fluxify.doAction('set_indicator', column, color);
}
});
get_column_indicators(function(indicators) {
var html = '<li class="button-group">'
html += indicators.reduce(function(prev, curr) {
return prev + '<div \
data-action="indicator" data-indicator="'+curr+'"\
class="btn btn-default btn-xs indicator ' + curr + '"></div>'
}, "");
html += '</li>';
self.$kanban_column.find(".column-options .dropdown-menu")
.append(html);
});
}

init();
@@ -472,7 +488,7 @@ frappe.provide("frappe.views");
make_dom();
render_card_meta();
bind_edit_card();
edit_card_title();
// edit_card_title();
}

function make_dom() {
@@ -501,9 +517,9 @@ frappe.provide("frappe.views");
}

function setup_edit_card() {
if (self.dialog) {
if (self.edit_dialog) {
refresh_dialog();
self.dialog.show();
self.edit_dialog.show();
return;
}

@@ -533,6 +549,7 @@ frappe.provide("frappe.views");

refresh_dialog();
make_timeline();
edit_card_title();

d.set_primary_action(__('Save'), function () {
if (d.working) return;
@@ -542,7 +559,7 @@ frappe.provide("frappe.views");
fluxify.doAction('update_doc', doc, card)
.then(function (r) {
d.working = false;
// fluxify.doAction('update_card', card)
d.hide();
});
});
d.show();
@@ -555,10 +572,10 @@ frappe.provide("frappe.views");
}

function set_dialog_fields() {
self.dialog.fields.forEach(function (df) {
self.edit_dialog.fields.forEach(function (df) {
var value = card.doc[df.fieldname];
if (value) {
self.dialog.set_value(df.fieldname, value);
self.edit_dialog.set_value(df.fieldname, value);
}
});
}
@@ -586,17 +603,17 @@ frappe.provide("frappe.views");
}

function make_edit_dialog(title, fields) {
self.dialog = new frappe.ui.Dialog({
self.edit_dialog = new frappe.ui.Dialog({
title: title,
fields: fields
});
return self.dialog;
return self.edit_dialog;
}

function make_assignees() {
var d = self.dialog;
var html = get_assignees_html() + '<a class="strong add-assignment">\
Assign <i class="octicon octicon-plus" style="margin-left: 2px;"></i></a>';
var d = self.edit_dialog;
var html = get_assignees_html() + '<a class="add-assignment avatar avatar-small avatar-empty">\
<i class="octicon octicon-plus text-muted" style="margin: 3px 0 0 5px;"></i></a>';

d.$wrapper.find("[data-fieldname='assignees'] .control-input-wrapper").empty().append(html);
d.$wrapper.find(".add-assignment").on("click", function () {
@@ -615,27 +632,24 @@ frappe.provide("frappe.views");
}

function show_assign_to_dialog() {
var ad = new frappe.ui.Dialog({
title: __("Assign to"),
fields: [
{ fieldtype: "Link", fieldname: "user", label: __("User"), options: "User" },
{ fieldtype: "Small Text", fieldname: "description", label: __("Description") }
]
})
ad.set_primary_action(__("Save"), function () {
var values = ad.get_values();
fluxify.doAction('assign_to', values.user, values.description, card)
.then(function () {
refresh_dialog();
ad.hide();
});
self.dialog = new frappe.ui.form.AssignToDialog({
obj: self,
method: 'frappe.desk.form.assign_to.add',
doctype: card.doctype,
docname: card.name,
callback: function(r) {
var user = self.assign_to_dialog.get_values().assign_to;
card.assigned_list.push(user);
fluxify.doAction('update_card', card);
refresh_dialog();
}
});
ad.show();
self.assign_to_dialog = ad;
self.assign_to_dialog = self.dialog;
self.assign_to_dialog.show();
}

function make_timeline() {
var d = self.dialog;
var d = self.edit_dialog;
// timeline wrapper
d.$wrapper.find('.modal-body').append('<div class="form-comments" style="padding:7px">');

@@ -680,9 +694,53 @@ frappe.provide("frappe.views");
}

function edit_card_title() {
var $edit_card_area = self.$card.find('.edit-card-area').hide();
var $kanban_card_area = self.$card.find('.kanban-card.content');
var $textarea = $edit_card_area.find('textarea').val(card.title);
var $card_title = self.edit_dialog.header.find('.modal-title');
var $title_wrapper = $card_title.parent();

$title_wrapper.addClass('edit-card-title').empty();

var template = repl('<div class="h4">\
<span>%(card_title)s</span>\
<input type="text">\
</div>', { card_title: card.title });

$title_wrapper.html(template);

var $input = $title_wrapper.find('input').hide();
var $span = $title_wrapper.find('span');

$span.on('click', function() {
$input.show();
$span.hide();
$input.val(card.title);
$input.focus();
});

$input.on('blur', function() {
$input.hide();
$span.show();
});

$input.keydown(function(e) {
if (e.which === 13) {
e.preventDefault();
var new_title = $input.val();
if (card.title === new_title) {
return;
}
get_doc().then(function () {
var tf = store.getState().card_meta.title_field.fieldname;
var doc = card.doc;
doc[tf] = new_title;
fluxify.doAction('update_doc', doc, card);
$span.html(new_title);
$input.trigger('blur');
})
}
})
}

function edit_card_title_old() {

self.$card.find('.kanban-card-edit').on('click', function (e) {
e.stopPropagation();
@@ -763,8 +821,8 @@ frappe.provide("frappe.views");
if (df.fieldtype === "Text Editor" && !description_field) {
description_field = df;
}
if (df.fieldtype === "Date" && df.fieldname.indexOf("end") !== -1 && !due_date_field) {
due_date_field = df;
if (!due_date_field) {
due_date_field = get_date_field(meta.fields);
}
});
return {
@@ -775,19 +833,22 @@ frappe.provide("frappe.views");
}
}

function get_date_field(fields) {
var filtered = fields.filter(function(df) {
return df.fieldtype === 'Date' &&
df.fieldname.indexOf('date') !== -1;
});
var field = filtered.find(function(df) {
return df.fieldname.indexOf('end') !== -1;
});
return field || filtered[0];
}

function prepare_card(card, state, doc) {
var assigned_list = card._assign ?
JSON.parse(card._assign) : [];
var comment_count = card._comment_count || 0;

if (card.kanban_column_order === null || card.kanban_column_order === '') {
var kanban_column_order = {};
} else if (typeof card.kanban_column_order === 'string') {
kanban_column_order = JSON.parse(card.kanban_column_order);
} else if (typeof card.kanban_column_order === 'object') {
kanban_column_order = card.kanban_column_order;
}

if (doc) {
card = Object.assign({}, card, doc);
}
@@ -799,7 +860,6 @@ frappe.provide("frappe.views");
column: card[state.board.field_name],
assigned_list: card.assigned_list || assigned_list,
comment_count: card.comment_count || comment_count,
kanban_column_order: kanban_column_order,
doc: doc
};
}
@@ -809,9 +869,10 @@ frappe.provide("frappe.views");
return {
title: col.column_name,
status: col.status,
order: col.order
order: col.order,
indicator: col.indicator || 'darkgrey'
};
})
});
}

function modify_column_field_in_c11n(doc, board, title, action) {
@@ -882,7 +943,7 @@ frappe.provide("frappe.views");
args.status = action === 'archive' ? 'Archived' : 'Active';
}
return frappe.call({
method: 'frappe.desk.doctype.kanban_board.kanban_board.' + method,
method: method_prefix + method,
args: args
});
}
@@ -902,4 +963,34 @@ frappe.provide("frappe.views");
});
}

})();
function get_card(name) {
return store.getState().cards.find(function (c) {
return c.name === name;
});
}

function update_cards_column(updated_cards) {
var cards = store.getState().cards;
cards.forEach(function(c) {
updated_cards.forEach(function(uc) {
if(uc.name === c.name) {
c.column = uc.column;
}
});
});
return cards;
}

function get_column_indicators(callback) {
frappe.model.with_doctype('Kanban Board Column', function() {
var meta = frappe.get_meta('Kanban Board Column');
var indicators;
meta.fields.forEach(function(df) {
if(df.fieldname==='indicator') {
indicators = df.options.split("\n");
}
});
callback(indicators);
});
}
})();

+ 5
- 0
frappe/public/less/indicator.less View File

@@ -70,6 +70,11 @@
background: @indicator-light-blue;
}

.indicator.lightblue::before,
.indicator-right.lightblue::after {
background: @indicator-light-blue;
}

.modal-header .indicator {
float: left;
margin-top: 7.5px;


+ 45
- 6
frappe/public/less/kanban.less View File

@@ -27,12 +27,27 @@
font-size: 12px;

.column-options {
position: absolute;
right: 0px;

.button-group {
display: flex;
padding: 12px 14px;

.btn.indicator {
flex: 1;
}
}

.indicator::before {
margin: 0;
}
}

&:hover {
cursor: pointer;
}
}

.sortable-ghost > .kanban-card {
.sortable-ghost > .kanban-card:not(.add-card) {
background: #ccc !important;
color: transparent;

@@ -56,8 +71,7 @@
color: @text-muted;

&:hover {
background-color: #fff;
box-shadow: 0 1px 2px rgba(0,0,0,0.30);
box-shadow: none;
color: @text-color;
cursor: pointer;
}
@@ -127,7 +141,7 @@
}

.add-new-column a:hover {
color: @text-color;
color: @text-color !important;
}

.kanban-card-meta {
@@ -139,4 +153,29 @@
height: 16px;
}
}
}

body[data-route*="Kanban"] {
.modal .add-assignment:hover {
// border-color: @text-color;
i {
color: @text-color !important;
}
}
}

.edit-card-title {
.h4 {
margin-top: 5px;
margin-bottom: 5px;
}
span:hover {
background-color: @light-yellow;
cursor: pointer;
}
input {
border: none;
outline: none;
width: 100%;
}
}

Loading…
Cancel
Save