Kanban Board enhancement and fixes frappe/erpnext#8005version-14
@@ -1,5 +1,6 @@ | |||||
{ | { | ||||
"allow_copy": 0, | "allow_copy": 0, | ||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | "allow_import": 0, | ||||
"allow_rename": 1, | "allow_rename": 1, | ||||
"autoname": "field:kanban_board_name", | "autoname": "field:kanban_board_name", | ||||
@@ -22,6 +23,8 @@ | |||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Kanban Board Name", | "label": "Kanban Board Name", | ||||
@@ -49,6 +52,8 @@ | |||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Reference DocType", | "label": "Reference DocType", | ||||
@@ -77,6 +82,8 @@ | |||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Field Name", | "label": "Field Name", | ||||
@@ -104,6 +111,8 @@ | |||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"length": 0, | "length": 0, | ||||
@@ -130,6 +139,8 @@ | |||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Columns", | "label": "Columns", | ||||
@@ -155,9 +166,11 @@ | |||||
"columns": 0, | "columns": 0, | ||||
"fieldname": "filters", | "fieldname": "filters", | ||||
"fieldtype": "Text", | "fieldtype": "Text", | ||||
"hidden": 0, | |||||
"hidden": 1, | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Filters", | "label": "Filters", | ||||
@@ -174,20 +187,49 @@ | |||||
"search_index": 0, | "search_index": 0, | ||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"unique": 0 | "unique": 0 | ||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "private", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Private", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 1, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
} | } | ||||
], | ], | ||||
"has_web_view": 0, | |||||
"hide_heading": 0, | "hide_heading": 0, | ||||
"hide_toolbar": 0, | "hide_toolbar": 0, | ||||
"idx": 0, | "idx": 0, | ||||
"image_view": 0, | "image_view": 0, | ||||
"in_create": 0, | "in_create": 0, | ||||
"in_dialog": 0, | |||||
"is_submittable": 0, | "is_submittable": 0, | ||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-02-09 12:23:30.059564", | |||||
"modified_by": "Administrator", | |||||
"modified": "2017-03-14 23:02:13.267243", | |||||
"modified_by": "faris@erpnext.com", | |||||
"module": "Desk", | "module": "Desk", | ||||
"name": "Kanban Board", | "name": "Kanban Board", | ||||
"name_case": "", | "name_case": "", | ||||
@@ -217,6 +259,7 @@ | |||||
"quick_entry": 0, | "quick_entry": 0, | ||||
"read_only": 0, | "read_only": 0, | ||||
"read_only_onload": 0, | "read_only_onload": 0, | ||||
"show_name_in_global_search": 0, | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"track_changes": 0, | "track_changes": 0, | ||||
@@ -11,10 +11,29 @@ from frappe.model.document import Document | |||||
class KanbanBoard(Document): | class KanbanBoard(Document): | ||||
def validate(self): | def validate(self): | ||||
self.validate_column_name() | |||||
def validate_column_name(self): | |||||
for column in self.columns: | for column in self.columns: | ||||
if not column.column_name: | if not column.column_name: | ||||
frappe.msgprint(frappe._("Column Name cannot be empty"), raise_exception=True) | frappe.msgprint(frappe._("Column Name cannot be empty"), raise_exception=True) | ||||
def get_permission_query_conditions(user): | |||||
if not user: user = frappe.session.user | |||||
if user == "Administrator": | |||||
return "" | |||||
return """(`tabKanban Board`.private=0 or `tabKanban Board`.owner="{user}")""".format(user=user) | |||||
def has_permission(doc, ptype, user): | |||||
if doc.private == 0 or user == "Administrator": | |||||
return True | |||||
if user == doc.owner: | |||||
return True | |||||
return False | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def add_column(board_name, column_title): | def add_column(board_name, column_title): | ||||
@@ -118,6 +137,18 @@ def quick_kanban_board(doctype, board_name, field_name): | |||||
doc.kanban_board_name = board_name | doc.kanban_board_name = board_name | ||||
doc.reference_doctype = doctype | doc.reference_doctype = doctype | ||||
doc.field_name = field_name | doc.field_name = field_name | ||||
if doctype == 'Task': | |||||
project = frappe.new_doc('Project') | |||||
project.project_name = board_name | |||||
project.status = 'Open' | |||||
project.save() | |||||
doc.filters = '[["Task","project","=","{0}"]]'.format(board_name) | |||||
if doctype == 'Note': | |||||
doc.private = 1 | |||||
doc.save() | doc.save() | ||||
return doc | return doc | ||||
@@ -176,12 +176,12 @@ class FormMeta(Meta): | |||||
self.load_kanban_column_fields() | self.load_kanban_column_fields() | ||||
def load_kanban_boards(self): | def load_kanban_boards(self): | ||||
kanban_boards = frappe.get_all( | |||||
'Kanban Board', fields=['name', 'filters', 'reference_doctype'], filters={'reference_doctype': self.name}) | |||||
kanban_boards = frappe.get_list( | |||||
'Kanban Board', fields=['name', 'filters', 'reference_doctype', 'private'], filters={'reference_doctype': self.name}) | |||||
self.set("__kanban_boards", kanban_boards, as_value=True) | self.set("__kanban_boards", kanban_boards, as_value=True) | ||||
def load_kanban_column_fields(self): | def load_kanban_column_fields(self): | ||||
values = frappe.get_all( | |||||
values = frappe.get_list( | |||||
'Kanban Board', fields=['field_name'], | 'Kanban Board', fields=['field_name'], | ||||
filters={'reference_doctype': self.name}) | filters={'reference_doctype': self.name}) | ||||
@@ -78,6 +78,7 @@ permission_query_conditions = { | |||||
"ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions", | "ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions", | ||||
"User": "frappe.core.doctype.user.user.get_permission_query_conditions", | "User": "frappe.core.doctype.user.user.get_permission_query_conditions", | ||||
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions", | "Note": "frappe.desk.doctype.note.note.get_permission_query_conditions", | ||||
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions", | |||||
"Contact": "frappe.geo.address_and_contact.get_permission_query_conditions_for_contact", | "Contact": "frappe.geo.address_and_contact.get_permission_query_conditions_for_contact", | ||||
"Address": "frappe.geo.address_and_contact.get_permission_query_conditions_for_address" | "Address": "frappe.geo.address_and_contact.get_permission_query_conditions_for_address" | ||||
} | } | ||||
@@ -87,6 +88,7 @@ has_permission = { | |||||
"ToDo": "frappe.desk.doctype.todo.todo.has_permission", | "ToDo": "frappe.desk.doctype.todo.todo.has_permission", | ||||
"User": "frappe.core.doctype.user.user.has_permission", | "User": "frappe.core.doctype.user.user.has_permission", | ||||
"Note": "frappe.desk.doctype.note.note.has_permission", | "Note": "frappe.desk.doctype.note.note.has_permission", | ||||
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission", | |||||
"Contact": "frappe.geo.address_and_contact.has_permission", | "Contact": "frappe.geo.address_and_contact.has_permission", | ||||
"Address": "frappe.geo.address_and_contact.has_permission", | "Address": "frappe.geo.address_and_contact.has_permission", | ||||
"Communication": "frappe.core.doctype.communication.communication.has_permission", | "Communication": "frappe.core.doctype.communication.communication.has_permission", | ||||
@@ -2,7 +2,7 @@ | |||||
min-height: calc(100vh - 252px); | min-height: calc(100vh - 252px); | ||||
background-color: #fafbfc; | background-color: #fafbfc; | ||||
display: flex; | display: flex; | ||||
overflow: scroll; | |||||
overflow: auto; | |||||
} | } | ||||
.kanban .kanban-column { | .kanban .kanban-column { | ||||
flex: 0 0 230px; | flex: 0 0 230px; | ||||
@@ -59,7 +59,7 @@ frappe.form.formatters = { | |||||
if(value) { | if(value) { | ||||
return '<i class="octicon octicon-check" style="margin-right: 3px;"></i>'; | return '<i class="octicon octicon-check" style="margin-right: 3px;"></i>'; | ||||
} else { | } else { | ||||
return '<i class="fa fa-circle-o text-extra-muted" style="margin-right: 3px; margin-bottom: -2px;"></i>'; | |||||
return '<i class="fa fa-square-o text-extra-muted" style="margin-right: 3px; margin-bottom: -2px; font-size: 14px;"></i>'; | |||||
} | } | ||||
}, | }, | ||||
Link: function(value, docfield, options, doc) { | Link: function(value, docfield, options, doc) { | ||||
@@ -130,16 +130,18 @@ frappe.views.ListSidebar = Class.extend({ | |||||
$('<li role="separator" class="divider"></li>').appendTo($dropdown); | $('<li role="separator" class="divider"></li>').appendTo($dropdown); | ||||
divider = true; | divider = true; | ||||
} | } | ||||
$(`<li><a href="#${route}">${__(board.name)}</a></li>`).appendTo($dropdown); | |||||
$(`<li><a href="#${route}"> | |||||
<span>${__(board.name)}</span> | |||||
${board.private ? '<i class="fa fa-lock fa-fw text-warning"></i>' : ''} | |||||
</a></li>`).appendTo($dropdown); | |||||
}); | }); | ||||
$dropdown.find('.new-kanban-board').click(function() { | $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) | var select_fields = frappe.get_meta(me.doctype) | ||||
.fields.filter(function(df) { | .fields.filter(function(df) { | ||||
return df.fieldtype === 'Select'; | |||||
}).map(function(df) { | |||||
return df.fieldname; | |||||
return df.fieldtype === 'Select' && | |||||
df.fieldname !== 'kanban_column'; | |||||
}); | }); | ||||
var fields = [ | var fields = [ | ||||
@@ -149,45 +151,61 @@ frappe.views.ListSidebar = Class.extend({ | |||||
label: __('Kanban Board Name'), | label: __('Kanban Board Name'), | ||||
reqd: 1 | reqd: 1 | ||||
} | } | ||||
] | |||||
]; | |||||
if(select_fields.length > 0) { | if(select_fields.length > 0) { | ||||
fields = fields.concat([{ | fields = fields.concat([{ | ||||
fieldtype: 'Select', | fieldtype: 'Select', | ||||
fieldname: 'field_name', | fieldname: 'field_name', | ||||
label: __('Columns based on'), | label: __('Columns based on'), | ||||
options: select_fields.join('\n'), | |||||
options: select_fields.map(df => df.label).join('\n'), | |||||
default: select_fields[0] | default: select_fields[0] | ||||
}, | }, | ||||
{ | { | ||||
fieldtype: 'Check', | fieldtype: 'Check', | ||||
fieldname: 'custom_column', | fieldname: 'custom_column', | ||||
label: __('Add Custom Column Field'), | |||||
label: __('Custom Column'), | |||||
default: 0, | default: 0, | ||||
onchange: function(e) { | onchange: function(e) { | ||||
var checked = d.get_value('custom_column'); | var checked = d.get_value('custom_column'); | ||||
if(checked) { | if(checked) { | ||||
d.get_input('field_name').prop('disabled', true); | |||||
$(d.body).find('.frappe-control[data-fieldname="field_name"]').hide(); | |||||
} else { | } else { | ||||
d.get_input('field_name').prop('disabled', null); | |||||
$(d.body).find('.frappe-control[data-fieldname="field_name"]').show(); | |||||
} | } | ||||
} | } | ||||
}]); | }]); | ||||
} | } | ||||
if(me.doctype === 'Task') { | |||||
fields[0].description = __('A new Project with this name will be created'); | |||||
} | |||||
if(me.doctype === 'Note') { | |||||
fields[0].description = __('This Kanban Board will be private'); | |||||
} | |||||
var d = new frappe.ui.Dialog({ | var d = new frappe.ui.Dialog({ | ||||
title: __('New Kanban Board'), | title: __('New Kanban Board'), | ||||
fields: fields, | fields: fields, | ||||
primary_action: function() { | |||||
var values = d.get_values(); | |||||
primary_action_label: __('Save'), | |||||
primary_action: function(values) { | |||||
var custom_column = values.custom_column !== undefined ? | var custom_column = values.custom_column !== undefined ? | ||||
values.custom_column : 1; | values.custom_column : 1; | ||||
if(custom_column) { | |||||
var field_name = 'kanban_column'; | |||||
} else { | |||||
var field_name = | |||||
select_fields | |||||
.find(df => df.label === values.field_name) | |||||
.fieldname; | |||||
} | |||||
me.add_custom_column_field(custom_column) | me.add_custom_column_field(custom_column) | ||||
.then(function(custom_column) { | .then(function(custom_column) { | ||||
var f = custom_column ? | |||||
'kanban_column' : values.field_name; | |||||
return me.make_kanban_board(values.board_name, f) | |||||
return me.make_kanban_board(values.board_name, field_name) | |||||
}) | }) | ||||
.then(function() { | .then(function() { | ||||
d.hide(); | d.hide(); | ||||
@@ -231,11 +249,16 @@ frappe.views.ListSidebar = Class.extend({ | |||||
field_name: field_name | field_name: field_name | ||||
}, | }, | ||||
callback: function(r) { | callback: function(r) { | ||||
var kb = r.message; | |||||
if(kb.filters) { | |||||
frappe.provide('frappe.kanban_filters'); | |||||
frappe.kanban_filters[kb.kanban_board_name] = kb.filters; | |||||
} | |||||
frappe.set_route( | frappe.set_route( | ||||
'List', | 'List', | ||||
me.doctype, | me.doctype, | ||||
'Kanban', | 'Kanban', | ||||
r.message.kanban_board_name | |||||
kb.kanban_board_name | |||||
); | ); | ||||
} | } | ||||
}); | }); | ||||
@@ -79,7 +79,12 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({ | |||||
.html(label) | .html(label) | ||||
.click(function() { | .click(function() { | ||||
me.primary_action_fulfilled = true; | me.primary_action_fulfilled = true; | ||||
click.apply(me); | |||||
// get values and send it | |||||
// as first parameter to click callback | |||||
// if no values then return | |||||
var values = me.get_values(); | |||||
if(!values) return; | |||||
click.apply(me, [values]); | |||||
}); | }); | ||||
}, | }, | ||||
make_head: function() { | make_head: function() { | ||||
@@ -913,7 +913,8 @@ frappe.provide("frappe.views"); | |||||
if (df.fieldname === board.field_name && df.fieldtype === "Select") { | if (df.fieldname === board.field_name && df.fieldtype === "Select") { | ||||
if (action === "add") { | if (action === "add") { | ||||
//add column_name to Select field's option field | //add column_name to Select field's option field | ||||
df.options += "\n" + title; | |||||
if(!df.options.includes(title)) | |||||
df.options += "\n" + title; | |||||
} else if (action === "delete") { | } else if (action === "delete") { | ||||
var options = df.options.split("\n"); | var options = df.options.split("\n"); | ||||
var index = options.indexOf(title); | var index = options.indexOf(title); | ||||
@@ -41,7 +41,13 @@ frappe.views.KanbanView = frappe.views.ListRenderer.extend({ | |||||
var kb = this.meta.__kanban_boards.find( | var kb = this.meta.__kanban_boards.find( | ||||
board => board.name === board_name | board => board.name === board_name | ||||
); | ); | ||||
frappe.kanban_filters[board_name] = JSON.parse(kb && kb.filters || "[]"); | |||||
frappe.kanban_filters[board_name] = JSON.parse(kb && kb.filters || '[]'); | |||||
} | |||||
if(typeof frappe.kanban_filters[board_name] === 'string') { | |||||
frappe.kanban_filters[board_name] = | |||||
JSON.parse( | |||||
frappe.kanban_filters[board_name] || '[]' | |||||
) | |||||
} | } | ||||
var filters = frappe.kanban_filters[board_name]; | var filters = frappe.kanban_filters[board_name]; | ||||
return filters; | return filters; | ||||
@@ -49,14 +55,11 @@ frappe.views.KanbanView = frappe.views.ListRenderer.extend({ | |||||
set_defaults: function() { | set_defaults: function() { | ||||
this._super(); | this._super(); | ||||
this.no_realtime = true; | this.no_realtime = true; | ||||
this.show_no_result = false; | |||||
this.page_title = __(this.get_board_name()); | this.page_title = __(this.get_board_name()); | ||||
}, | }, | ||||
get_board_name: function() { | get_board_name: function() { | ||||
var route = frappe.get_route(); | var route = frappe.get_route(); | ||||
if(!route[3] || !this.meta.__kanban_boards.find(b => b.name === route[3])) { | |||||
frappe.throw(__(`Kanban Board <b>${route[3] || ''}</b> not found`)); | |||||
return; | |||||
} | |||||
return route[3]; | return route[3]; | ||||
}, | }, | ||||
get_header_html: function() { | get_header_html: function() { | ||||
@@ -4,7 +4,7 @@ | |||||
min-height: ~"calc(100vh - 252px)"; | min-height: ~"calc(100vh - 252px)"; | ||||
background-color: @light-bg; | background-color: @light-bg; | ||||
display: flex; | display: flex; | ||||
overflow: scroll; | |||||
overflow: auto; | |||||
.kanban-column { | .kanban-column { | ||||
flex: 0 0 230px; | flex: 0 0 230px; | ||||