@@ -17,6 +17,10 @@ class KanbanBoard(Document): | |||
def on_update(self): | |||
frappe.clear_cache(doctype=self.reference_doctype) | |||
def before_insert(self): | |||
for column in self.columns: | |||
column.order = get_order_for_column(self, column.column_name) | |||
def validate_column_name(self): | |||
for column in self.columns: | |||
if not column.column_name: | |||
@@ -125,6 +129,53 @@ def update_order(board_name, order): | |||
board.save() | |||
return board, updated_cards | |||
@frappe.whitelist() | |||
def update_order_for_single_card(board_name, docname, from_colname, to_colname, old_index, new_index): | |||
'''Save the order of cards in columns''' | |||
board = frappe.get_doc('Kanban Board', board_name) | |||
doctype = board.reference_doctype | |||
fieldname = board.field_name | |||
old_index = frappe.parse_json(old_index) | |||
new_index = frappe.parse_json(new_index) | |||
# save current order and index of columns to be updated | |||
from_col_order, from_col_idx = get_kanban_column_order_and_index(board, from_colname) | |||
to_col_order, to_col_idx = get_kanban_column_order_and_index(board, to_colname) | |||
if from_colname == to_colname: | |||
from_col_order = to_col_order | |||
to_col_order.insert(new_index, from_col_order.pop((old_index))) | |||
# save updated order | |||
board.columns[from_col_idx].order = frappe.as_json(from_col_order) | |||
board.columns[to_col_idx].order = frappe.as_json(to_col_order) | |||
board.save() | |||
# update changed value in doc | |||
frappe.set_value(doctype, docname, fieldname, to_colname) | |||
return board | |||
def get_kanban_column_order_and_index(board, colname): | |||
for i, col in enumerate(board.columns): | |||
if col.column_name == colname: | |||
col_order = frappe.parse_json(col.order) | |||
col_idx = i | |||
return col_order, col_idx | |||
@frappe.whitelist() | |||
def add_card(board_name, docname, colname): | |||
board = frappe.get_doc('Kanban Board', board_name) | |||
col_order, col_idx = get_kanban_column_order_and_index(board, colname) | |||
col_order.insert(0, docname) | |||
board.columns[col_idx].order = frappe.as_json(col_order) | |||
board.save() | |||
return board | |||
@frappe.whitelist() | |||
def quick_kanban_board(doctype, board_name, field_name, project=None): | |||
@@ -133,6 +184,13 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): | |||
doc = frappe.new_doc('Kanban Board') | |||
meta = frappe.get_meta(doctype) | |||
doc.kanban_board_name = board_name | |||
doc.reference_doctype = doctype | |||
doc.field_name = field_name | |||
if project: | |||
doc.filters = '[["Task","project","=","{0}"]]'.format(project) | |||
options = '' | |||
for field in meta.fields: | |||
if field.fieldname == field_name: | |||
@@ -149,12 +207,6 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): | |||
column_name=column | |||
)) | |||
doc.kanban_board_name = board_name | |||
doc.reference_doctype = doctype | |||
doc.field_name = field_name | |||
if project: | |||
doc.filters = '[["Task","project","=","{0}"]]'.format(project) | |||
if doctype in ['Note', 'ToDo']: | |||
doc.private = 1 | |||
@@ -162,6 +214,12 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): | |||
doc.save() | |||
return doc | |||
def get_order_for_column(board, colname): | |||
filters = [[board.reference_doctype, board.field_name, '=', colname]] | |||
if board.filters: | |||
filters.append(frappe.parse_json(board.filters)[0]) | |||
return frappe.as_json(frappe.get_list(board.reference_doctype, filters=filters, pluck='name')) | |||
@frappe.whitelist() | |||
def update_column_order(board_name, order): | |||
@@ -164,7 +164,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const match_rules_list = frappe.perm.get_match_rules(this.doctype); | |||
if (match_rules_list.length) { | |||
this.restricted_list = $( | |||
`<button class="btn btn-default btn-xs restricted-button flex align-center"> | |||
`<button class="btn btn-xs restricted-button flex align-center"> | |||
${frappe.utils.icon('restriction', 'xs')} | |||
</button>` | |||
).click(() => this.show_restrictions(match_rules_list)).appendTo(this.page.page_form); | |||
@@ -676,7 +676,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if (col.type === "Tag") { | |||
const tags_display_class = !this.tags_shown ? 'hide' : ''; | |||
let tags_html = doc._user_tags ? this.get_tags_html(doc._user_tags) : '<div class="tags-empty">-</div>'; | |||
let tags_html = doc._user_tags ? this.get_tags_html(doc._user_tags, 2) : '<div class="tags-empty">-</div>'; | |||
return ` | |||
<div class="list-row-col tag-col ${tags_display_class} hidden-xs ellipsis"> | |||
${tags_html} | |||
@@ -790,13 +790,19 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
`; | |||
} | |||
get_tags_html(user_tags) { | |||
get_tags_html(user_tags, limit, colored=false) { | |||
let get_tag_html = tag => { | |||
let color = '', style = ''; | |||
if (tag) { | |||
return `<div class="tag-pill ellipsis" title="${tag}">${tag}</div>`; | |||
if (colored) { | |||
color = frappe.get_palette(tag); | |||
style = `background-color: var(${color[0]}); color: var(${color[1]})`; | |||
} | |||
return `<div class="tag-pill ellipsis" title="${tag}" style="${style}">${tag}</div>`; | |||
} | |||
}; | |||
return user_tags.split(',').slice(1, 3).map(get_tag_html).join(''); | |||
return user_tags.split(',').slice(1, limit + 1).map(get_tag_html).join(''); | |||
} | |||
get_meta_html(doc) { | |||
@@ -114,6 +114,7 @@ frappe.ui.Page = Class.extend({ | |||
this.get_main_icon(this.icon); | |||
this.body = this.main = this.wrapper.find(".layout-main-section"); | |||
this.container = this.wrapper.find(".page-body"); | |||
this.sidebar = this.wrapper.find(".layout-side-section"); | |||
this.footer = this.wrapper.find(".layout-footer"); | |||
this.indicator = this.wrapper.find(".indicator-pill"); | |||
@@ -124,7 +124,12 @@ frappe.provide("frappe.views"); | |||
const new_cards = state.cards.slice(); | |||
new_cards[index] = card; | |||
updater.set({ cards: new_cards }); | |||
fluxify.doAction('update_order'); | |||
const args = { | |||
new: 1, | |||
name: card.name, | |||
colname: updated_doc[state.board.field_name], | |||
}; | |||
fluxify.doAction('update_order_for_single_card', args); | |||
}); | |||
} else { | |||
frappe.new_doc(this.doctype, doc); | |||
@@ -155,6 +160,53 @@ frappe.provide("frappe.views"); | |||
fluxify.doAction('update_card', updated_card); | |||
}); | |||
}, | |||
update_order_for_single_card: function(updater, card) { | |||
// cache original order | |||
const _cards = this.cards.slice(); | |||
const _columns = this.columns.slice(); | |||
let args = {}; | |||
let method_name = ""; | |||
if (card.new) { | |||
method_name = "add_card"; | |||
args = { | |||
board_name: this.board.name, | |||
docname: card.name, | |||
colname: card.colname, | |||
}; | |||
} else { | |||
method_name = "update_order_for_single_card"; | |||
args = { | |||
board_name: this.board.name, | |||
docname: card.name, | |||
from_colname: card.from_colname, | |||
to_colname: card.to_colname, | |||
old_index: card.old_index, | |||
new_index: card.new_index, | |||
}; | |||
} | |||
frappe.call({ | |||
method: method_prefix + method_name, | |||
args: args, | |||
callback: (r) => { | |||
let board = r.message; | |||
let updated_cards = [{'name': card.name, 'column': card.to_colname || card.colname}]; | |||
let cards = update_cards_column(updated_cards); | |||
let columns = prepare_columns(board.columns); | |||
updater.set({ | |||
cards: cards, | |||
columns: columns | |||
}); | |||
} | |||
}).fail(function() { | |||
// revert original order | |||
updater.set({ | |||
cards: _cards, | |||
columns: _columns | |||
}); | |||
}); | |||
}, | |||
update_order: function(updater) { | |||
// cache original order | |||
const _cards = this.cards.slice(); | |||
@@ -446,16 +498,24 @@ frappe.provide("frappe.views"); | |||
group: "cards", | |||
animation: 150, | |||
dataIdAttr: 'data-name', | |||
forceFallback: true, | |||
onStart: function() { | |||
wrapper.find('.kanban-card.add-card').fadeOut(200, function() { | |||
wrapper.find('.kanban-cards').height('100vh'); | |||
}); | |||
}, | |||
onEnd: function() { | |||
onEnd: function(e) { | |||
wrapper.find('.kanban-card.add-card').fadeIn(100); | |||
wrapper.find('.kanban-cards').height('auto'); | |||
// update order | |||
fluxify.doAction('update_order'); | |||
const args = { | |||
name: $(e.item).attr('data-name'), | |||
from_colname: $(e.from).parents('.kanban-column').attr('data-column-value'), | |||
to_colname: $(e.to).parents('.kanban-column').attr('data-column-value'), | |||
old_index: e.oldIndex, | |||
new_index: e.newIndex, | |||
}; | |||
fluxify.doAction('update_order_for_single_card', args); | |||
}, | |||
onAdd: function() { | |||
}, | |||
@@ -546,14 +606,24 @@ frappe.provide("frappe.views"); | |||
var opts = { | |||
name: card.name, | |||
title: remove_img_tags(card.title), | |||
disable_click: card._disable_click ? 'disable-click' : '' | |||
disable_click: card._disable_click ? 'disable-click' : '', | |||
creation: card.creation, | |||
}; | |||
self.$card = $(frappe.render_template('kanban_card', opts)) | |||
.appendTo(wrapper); | |||
} | |||
function get_tags_html(card) { | |||
return card.tags | |||
? `<div class="kanban-tags"> | |||
${cur_list.get_tags_html(card.tags, 3, true)} | |||
</div>` | |||
: ''; | |||
} | |||
function render_card_meta() { | |||
var html = ""; | |||
let html = get_tags_html(card); | |||
if (card.comment_count > 0) | |||
html += | |||
`<span class="list-comment-count small text-muted "> | |||
@@ -563,7 +633,10 @@ frappe.provide("frappe.views"); | |||
const $assignees_group = get_assignees_group(); | |||
html += `<span class="kanban-assignments"></span>`; | |||
html += ` | |||
<span class="kanban-assignments"></span> | |||
${cur_list.get_like_html(card)} | |||
`; | |||
if (card.color && frappe.ui.color.validate_hex(card.color)) { | |||
const $div = $('<div>'); | |||
@@ -630,6 +703,8 @@ frappe.provide("frappe.views"); | |||
doctype: state.doctype, | |||
name: card.name, | |||
title: card[state.card_meta.title_field.fieldname], | |||
creation: moment(card.creation).format('MMM DD, YYYY'), | |||
tags: card._user_tags, | |||
column: card[state.board.field_name], | |||
assigned_list: card.assigned_list || assigned_list, | |||
comment_count: card.comment_count || comment_count, | |||
@@ -2,9 +2,12 @@ | |||
<a class="kanban-card-redirect" href="#"> | |||
<div class="kanban-card content"> | |||
<div class="kanban-title-area"> | |||
<div class="kanban-card-title ellipsis" title="{{title}}"> | |||
<div class="kanban-card-title" title="{{title}}"> | |||
{{ title }} | |||
</div> | |||
<div class="kanban-card-creation"> | |||
{{ creation }} | |||
</div> | |||
</div> | |||
<div class="kanban-card-meta"> | |||
</div> | |||
@@ -73,7 +73,11 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||
} | |||
setup_view() { | |||
if (this.board.columns.length > 5) { | |||
this.page.container.addClass('full-width'); | |||
} | |||
this.setup_realtime_updates(); | |||
this.setup_like(); | |||
} | |||
set_fields() { | |||
@@ -15,7 +15,7 @@ | |||
.kanban { | |||
display: flex; | |||
overflow: auto; | |||
overflow-y: hidden; | |||
-ms-overflow-style: none; /* IE and Edge */ | |||
scrollbar-width: none; /* Firefox */ | |||
@@ -32,19 +32,7 @@ | |||
border-radius: var(--border-radius); | |||
padding: var(--padding-md); | |||
min-height: calc(100vh - 250px); | |||
&.add-new-column { | |||
order: 1; | |||
} | |||
&:hover { | |||
.add-card { | |||
background-color: var(--kanban-new-card-hover-bg); | |||
box-shadow: var(--shadow-xs); | |||
} | |||
background-color: var(--kanban-new-card-bg); | |||
} | |||
max-height: calc(75vh - 10px); | |||
.add-card { | |||
@include flex(flex, center, center, null); | |||
@@ -70,6 +58,17 @@ | |||
} | |||
} | |||
.kanban-column:not(.add-new-column) { | |||
&:hover { | |||
.add-card { | |||
background-color: var(--kanban-new-card-hover-bg); | |||
box-shadow: var(--shadow-xs); | |||
} | |||
background-color: var(--kanban-new-card-bg); | |||
} | |||
} | |||
.kanban-column-header { | |||
@include flex(flex, space-between, null, null); | |||
margin-top: 0; | |||
@@ -138,7 +137,6 @@ | |||
} | |||
.kanban-cards { | |||
min-height: 100px; | |||
max-height: calc(100vh - 250px); | |||
margin: -5px; | |||
padding: 5px; | |||
@@ -191,10 +189,20 @@ | |||
} | |||
} | |||
.kanban-card-title { | |||
max-width: 90%; | |||
font-size: $font-size-base; | |||
font-weight: 500; | |||
.kanban-title-area { | |||
margin-bottom: 12px; | |||
.kanban-card-title { | |||
max-width: 90%; | |||
font-size: var(--text-md); | |||
font-weight: 500; | |||
} | |||
.kanban-card-creation { | |||
font-size: var(--text-md); | |||
color: var(--text-muted); | |||
margin-top: var(--margin-xs); | |||
} | |||
} | |||
.kanban-card-edit { | |||
@@ -232,28 +240,71 @@ | |||
} | |||
} | |||
.add-new-column { | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
min-height: 65px; | |||
.kanban-column.add-new-column { | |||
color: var(--text-muted); | |||
border: 1px dashed var(--gray-400); | |||
max-height: 80px; | |||
background-color: transparent; | |||
} | |||
order: 1; | |||
.add-new-column:hover { | |||
background-color: var(--kanban-column-bg); | |||
&:hover { | |||
background-color: none; | |||
} | |||
.kanban-column-title.compose-column { | |||
@include flex(flex, center, center, null); | |||
min-height: 65px; | |||
border-radius: var(--border-radius); | |||
border: 1px dashed var(--gray-400); | |||
font-size: var(--text-base); | |||
&:hover { | |||
background-color: var(--kanban-column-bg); | |||
} | |||
} | |||
} | |||
.kanban-card-meta { | |||
.list-comment-count { | |||
width: 30px; | |||
} | |||
.like-action:not(.liked) { | |||
.icon use { | |||
stroke: var(--text-muted); | |||
} | |||
} | |||
.kanban-tags { | |||
font-size: var(--text-sm); | |||
margin-bottom: 8px; | |||
.tag-pill { | |||
border-radius: 100px; | |||
height: 22px; | |||
width: auto; | |||
padding: 2px 8px; | |||
margin-right: var(--margin-xs); | |||
} | |||
} | |||
.kanban-assignments { | |||
display: flex; | |||
float: right; | |||
.avatar { | |||
cursor: default; | |||
width: 22px; | |||
height: 22px; | |||
} | |||
.avatar-action { | |||
width: 22px; | |||
height: 22px; | |||
.icon { | |||
width: 12px; | |||
height: 12px; | |||
} | |||
} | |||
} | |||
} | |||
@@ -44,6 +44,11 @@ | |||
.page-container { | |||
background-color: var(--bg-color); | |||
.page-body.full-width { | |||
width: 100%; | |||
max-width: 100%; | |||
} | |||
} | |||
.custom-actions { | |||