* 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 methodversion-14
@@ -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) |
@@ -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", | |||
@@ -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 |
@@ -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", | |||
@@ -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 = [] | |||
@@ -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; | |||
@@ -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%; | |||
} |
@@ -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; | |||
@@ -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); | |||
} | |||
}); | |||
} | |||
} |
@@ -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, | |||
@@ -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() { | |||
@@ -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; | |||
@@ -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> |
@@ -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"> | |||
@@ -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); | |||
}); | |||
} | |||
})(); |
@@ -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; | |||
@@ -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%; | |||
} | |||
} |