feat: add/remove fields from kanban board (#16257)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Co-authored-by: Shariq Ansari <30859809+shariquerik@users.noreply.github.com>
This commit is contained in:
parent
8b9ae0da23
commit
803f1fb061
11 changed files with 614 additions and 349 deletions
84
cypress/integration/kanban.js
Normal file
84
cypress/integration/kanban.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
context('Kanban Board', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
});
|
||||
|
||||
it('Create ToDo Kanban', () => {
|
||||
cy.visit('/app/todo');
|
||||
|
||||
cy.get('.page-actions .custom-btn-group button').click();
|
||||
cy.get('.page-actions .custom-btn-group ul.dropdown-menu li').contains('Kanban').click();
|
||||
|
||||
cy.fill_field('board_name', 'ToDo Kanban', 'Data');
|
||||
cy.fill_field('field_name', 'Status', 'Select');
|
||||
cy.click_modal_primary_button('Save');
|
||||
|
||||
cy.get('.title-text').should('contain', 'ToDo Kanban');
|
||||
});
|
||||
|
||||
it('Create ToDo from kanban', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.client.save'
|
||||
}).as('save-todo');
|
||||
|
||||
cy.click_listview_primary_button('Add ToDo');
|
||||
|
||||
cy.fill_field('description', 'Test Kanban ToDo', 'Text Editor');
|
||||
cy.get('.modal-footer .btn-primary').last().click();
|
||||
|
||||
cy.wait('@save-todo');
|
||||
});
|
||||
|
||||
it('Add and Remove fields', () => {
|
||||
cy.visit('/app/todo/view/kanban/ToDo Kanban');
|
||||
|
||||
cy.intercept('POST', '/api/method/frappe.desk.doctype.kanban_board.kanban_board.save_settings').as('save-kanban');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order').as('update-order');
|
||||
|
||||
cy.get('.page-actions .menu-btn-group > .btn').click();
|
||||
cy.get('.page-actions .menu-btn-group .dropdown-menu li').contains('Kanban Settings').click();
|
||||
cy.get('.add-new-fields').click();
|
||||
|
||||
cy.get('.checkbox-options .checkbox').contains('ID').click();
|
||||
cy.get('.checkbox-options .checkbox').contains('Status').first().click();
|
||||
cy.get('.checkbox-options .checkbox').contains('Priority').click();
|
||||
|
||||
cy.get('.modal-footer .btn-primary').last().click();
|
||||
|
||||
cy.get('.frappe-control .label-area').contains('Show Labels').click();
|
||||
cy.click_modal_primary_button('Save');
|
||||
|
||||
cy.wait('@save-kanban');
|
||||
|
||||
cy.get('.kanban-column[data-column-value="Open"] .kanban-cards').as('open-cards');
|
||||
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'ID:');
|
||||
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'Status:');
|
||||
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'Priority:');
|
||||
|
||||
cy.get('.page-actions .menu-btn-group > .btn').click();
|
||||
cy.get('.page-actions .menu-btn-group .dropdown-menu li').contains('Kanban Settings').click();
|
||||
cy.get_open_dialog().find('.frappe-control[data-fieldname="fields_html"] div[data-label="ID"] .remove-field').click();
|
||||
|
||||
cy.wait('@update-order');
|
||||
cy.get_open_dialog().find('.frappe-control .label-area').contains('Show Labels').click();
|
||||
cy.get('.modal-footer .btn-primary').last().click();
|
||||
|
||||
cy.wait('@save-kanban');
|
||||
|
||||
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('not.contain', 'ID:');
|
||||
|
||||
});
|
||||
|
||||
it('Drag todo', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card'
|
||||
}).as('drag-completed');
|
||||
|
||||
cy.get('.kanban-card-body:first').drag('[data-column-value="Closed"] .kanban-cards', {force: true});
|
||||
|
||||
cy.wait('@drag-completed');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'cypress-file-upload';
|
||||
import '@testing-library/cypress/add-commands';
|
||||
import '@4tw/cypress-drag-drop';
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
|
|
|
|||
|
|
@ -870,7 +870,7 @@ def run_ui_tests(
|
|||
# install cypress
|
||||
click.secho("Installing Cypress...", fg="yellow")
|
||||
frappe.commands.popen(
|
||||
"yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile"
|
||||
"yarn add cypress@^6 cypress-file-upload@^5 @4tw/cypress-drag-drop@^2 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile"
|
||||
)
|
||||
|
||||
# run for headless mode
|
||||
|
|
|
|||
|
|
@ -1,267 +1,124 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:kanban_board_name",
|
||||
"beta": 0,
|
||||
"creation": "2016-10-19 12:26:04.809812",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"kanban_board_name",
|
||||
"reference_doctype",
|
||||
"field_name",
|
||||
"column_break_4",
|
||||
"private",
|
||||
"show_labels",
|
||||
"section_break_3",
|
||||
"columns",
|
||||
"filters",
|
||||
"fields"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "kanban_board_name",
|
||||
"fieldtype": "Data",
|
||||
"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": "Kanban Board Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Reference Document Type",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "field_name",
|
||||
"fieldtype": "Select",
|
||||
"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": "Field Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break",
|
||||
"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,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Table",
|
||||
"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": "Columns",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Kanban Board Column",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"options": "Kanban Board Column"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "filters",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"fieldtype": "Code",
|
||||
"label": "Filters",
|
||||
"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
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "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
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fields",
|
||||
"fieldtype": "Code",
|
||||
"label": "Fields",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_labels",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Labels",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-09-05 14:22:27.664645",
|
||||
"links": [],
|
||||
"modified": "2022-04-13 12:10:20.284367",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Kanban Board",
|
||||
"name_case": "",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"read": 1,
|
||||
"role": "All"
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"if_owner": 1,
|
||||
"read": 1,
|
||||
"role": "All",
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"set_user_permissions": 0,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ class KanbanBoard(Document):
|
|||
def validate_column_name(self):
|
||||
for column in self.columns:
|
||||
if not column.column_name:
|
||||
frappe.msgprint(frappe._("Column Name cannot be empty"), raise_exception=True)
|
||||
frappe.msgprint(_("Column Name cannot be empty"), raise_exception=True)
|
||||
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
|
|
@ -92,7 +92,6 @@ def update_order(board_name, order):
|
|||
|
||||
updated_cards = []
|
||||
for col_name, cards in order_dict.items():
|
||||
order_list = []
|
||||
for card in cards:
|
||||
column = frappe.get_value(doctype, {"name": card}, fieldname)
|
||||
if column != col_name:
|
||||
|
|
@ -246,3 +245,22 @@ def set_indicator(board_name, column_name, indicator):
|
|||
|
||||
board.save()
|
||||
return board
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_settings(board_name: str, settings: str) -> Document:
|
||||
settings = json.loads(settings)
|
||||
doc = frappe.get_doc("Kanban Board", board_name)
|
||||
|
||||
fields = settings["fields"]
|
||||
if not isinstance(fields, str):
|
||||
fields = json.dumps(fields)
|
||||
|
||||
doc.fields = fields
|
||||
doc.show_labels = settings["show_labels"]
|
||||
doc.save()
|
||||
|
||||
resp = doc.as_dict()
|
||||
resp["fields"] = frappe.parse_json(resp["fields"])
|
||||
|
||||
return resp
|
||||
|
|
|
|||
|
|
@ -1580,15 +1580,22 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
if (frappe.user.has_role("System Manager")) {
|
||||
items.push({
|
||||
label: __("List Settings", null, "Button in list view menu"),
|
||||
action: () => this.show_list_settings(),
|
||||
standard: true,
|
||||
});
|
||||
if (this.get_view_settings) {
|
||||
items.push(this.get_view_settings());
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
get_view_settings() {
|
||||
return {
|
||||
label: __("List Settings", null, "Button in list view menu"),
|
||||
action: () => this.show_list_settings(),
|
||||
standard: true,
|
||||
};
|
||||
}
|
||||
|
||||
show_list_settings() {
|
||||
frappe.model.with_doctype(this.doctype, () => {
|
||||
new ListSettings({
|
||||
|
|
|
|||
|
|
@ -630,8 +630,6 @@ frappe.provide("frappe.views");
|
|||
if(!card) return;
|
||||
make_dom();
|
||||
render_card_meta();
|
||||
add_task_link();
|
||||
// edit_card_title();
|
||||
}
|
||||
|
||||
function make_dom() {
|
||||
|
|
@ -640,12 +638,35 @@ frappe.provide("frappe.views");
|
|||
title: frappe.utils.html2text(card.title),
|
||||
disable_click: card._disable_click ? 'disable-click' : '',
|
||||
creation: card.creation,
|
||||
doc_content: get_doc_content(card),
|
||||
image_url: cur_list.get_image_url(card),
|
||||
form_link: frappe.utils.get_form_link(card.doctype, card.name)
|
||||
};
|
||||
|
||||
self.$card = $(frappe.render_template('kanban_card', opts))
|
||||
.appendTo(wrapper);
|
||||
}
|
||||
|
||||
function get_doc_content(card) {
|
||||
let fields = [];
|
||||
for (let field_name of cur_list.board.fields) {
|
||||
let field = (
|
||||
frappe.meta.get_docfield(card.doctype, field_name, card.name)
|
||||
|| frappe.model.get_std_field(field_name)
|
||||
);
|
||||
let label = cur_list.board.show_labels ? `<span>${__(field.label)}: </span>` : '';
|
||||
let value = frappe.format(card.doc[field_name], field);
|
||||
fields.push(`
|
||||
<div class="text-muted text-truncate">
|
||||
${label}
|
||||
<span>${value}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return fields.join("");
|
||||
}
|
||||
|
||||
function get_tags_html(card) {
|
||||
return card.tags
|
||||
? `<div class="kanban-tags">
|
||||
|
|
@ -688,12 +709,6 @@ frappe.provide("frappe.views");
|
|||
.find('.kanban-assignments').append($assignees_group);
|
||||
}
|
||||
|
||||
function add_task_link() {
|
||||
let task_link = frappe.utils.get_form_link(card.doctype, card.name);
|
||||
self.$card.find('.kanban-card-redirect')
|
||||
.attr('href', task_link);
|
||||
}
|
||||
|
||||
function get_assignees_group() {
|
||||
return frappe.avatar_group(card.assigned_list, 3, {
|
||||
css_class: 'avatar avatar-small',
|
||||
|
|
@ -744,7 +759,7 @@ frappe.provide("frappe.views");
|
|||
assigned_list: card.assigned_list || assigned_list,
|
||||
comment_count: card.comment_count || comment_count,
|
||||
color: card.color || null,
|
||||
doc: doc
|
||||
doc: doc || card
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{encodeURIComponent(name)}}">
|
||||
<a class="kanban-card-redirect" href="#">
|
||||
<div class="kanban-card content">
|
||||
{% if(image_url) { %}
|
||||
<div class="kanban-image">
|
||||
<img src="{{image_url}}" alt="{{title}}">
|
||||
</div>
|
||||
{% } %}
|
||||
<div class="kanban-card-body">
|
||||
<div class="kanban-title-area">
|
||||
<div class="kanban-card content">
|
||||
{% if(image_url) { %}
|
||||
<div class="kanban-image">
|
||||
<img src="{{image_url}}" alt="{{title}}">
|
||||
</div>
|
||||
{% } %}
|
||||
<div class="kanban-card-body">
|
||||
<div class="kanban-title-area">
|
||||
<a href="{{ form_link }}">
|
||||
<div class="kanban-card-title ellipsis" title="{{title}}">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="kanban-card-creation">
|
||||
{{ creation }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban-card-meta">
|
||||
</a>
|
||||
<br>
|
||||
<div class="kanban-card-doc text-muted">
|
||||
{{ doc_content }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban-card-meta">
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
264
frappe/public/js/frappe/views/kanban/kanban_settings.js
Normal file
264
frappe/public/js/frappe/views/kanban/kanban_settings.js
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
export default class KanbanSettings {
|
||||
constructor({ kanbanview, doctype, meta, settings }) {
|
||||
if (!doctype) {
|
||||
frappe.throw(__("DocType required"));
|
||||
}
|
||||
|
||||
this.kanbanview = kanbanview;
|
||||
this.doctype = doctype;
|
||||
this.meta = meta;
|
||||
this.settings = settings;
|
||||
this.dialog = null;
|
||||
this.fields = this.settings && this.settings.fields;
|
||||
|
||||
frappe.model.with_doctype("List View Settings", () => {
|
||||
this.make();
|
||||
this.get_fields();
|
||||
this.setup_fields();
|
||||
this.setup_remove_fields();
|
||||
this.add_new_fields();
|
||||
this.show_dialog();
|
||||
});
|
||||
}
|
||||
|
||||
make() {
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __("{0} Settings", [__(this.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "show_labels",
|
||||
label: __("Show Labels"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "fields_html",
|
||||
fieldtype: "HTML"
|
||||
},
|
||||
{
|
||||
fieldname: "fields",
|
||||
fieldtype: "Code",
|
||||
hidden: 1
|
||||
}
|
||||
]
|
||||
});
|
||||
this.dialog.set_values(this.settings);
|
||||
this.dialog.set_primary_action(__("Save"), () => {
|
||||
frappe.show_alert({
|
||||
message: __("Saving"),
|
||||
indicator: "green"
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
method:
|
||||
"frappe.desk.doctype.kanban_board.kanban_board.save_settings",
|
||||
args: {
|
||||
board_name: this.settings.name,
|
||||
settings: this.dialog.get_values()
|
||||
},
|
||||
callback: r => {
|
||||
this.kanbanview.board = r.message;
|
||||
this.kanbanview.render();
|
||||
this.dialog.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.setup_fields();
|
||||
this.add_new_fields();
|
||||
this.setup_remove_fields();
|
||||
}
|
||||
|
||||
show_dialog() {
|
||||
if (!this.settings.fields) {
|
||||
this.update_fields();
|
||||
}
|
||||
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
setup_fields() {
|
||||
const fields_html = this.dialog.get_field("fields_html");
|
||||
const wrapper = fields_html.$wrapper[0];
|
||||
let fields = "";
|
||||
|
||||
for (let fieldname of this.fields) {
|
||||
let field = this.get_docfield(fieldname);
|
||||
|
||||
fields += `
|
||||
<div class="control-input flex align-center form-control fields_order sortable"
|
||||
style="display: block; margin-bottom: 5px;"
|
||||
data-fieldname="${field.fieldname}"
|
||||
data-label="${field.label}"
|
||||
data-type="${field.type}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-1">
|
||||
${frappe.utils.icon("drag", "xs", "", "", "sortable-handle")}
|
||||
</div>
|
||||
<div class="col-md-10" style="padding-left:0px;">
|
||||
${__(field.label)}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<a class="text-muted remove-field" data-fieldname="${field.fieldname}">
|
||||
${frappe.utils.icon("delete", "xs")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
fields_html.html(`
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label" style="padding-right: 0px;">Fields</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
${fields}
|
||||
</div>
|
||||
<p class="help-box small text-muted">
|
||||
<a class="add-new-fields text-muted">
|
||||
+ Add / Remove Fields
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
new Sortable(
|
||||
wrapper.getElementsByClassName("control-input-wrapper")[0],
|
||||
{
|
||||
handle: ".sortable-handle",
|
||||
draggable: ".sortable",
|
||||
onUpdate: params => {
|
||||
this.fields.splice(
|
||||
params.newIndex,
|
||||
0,
|
||||
this.fields.splice(params.oldIndex, 1)[0]
|
||||
);
|
||||
this.dialog.set_value(
|
||||
"fields",
|
||||
JSON.stringify(this.fields)
|
||||
);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
add_new_fields() {
|
||||
let add_new_fields = this.get_dialog_fields_wrapper().getElementsByClassName(
|
||||
"add-new-fields"
|
||||
)[0];
|
||||
add_new_fields.onclick = () => this.show_column_selector();
|
||||
}
|
||||
|
||||
setup_remove_fields() {
|
||||
let remove_fields = this.get_dialog_fields_wrapper().getElementsByClassName(
|
||||
"remove-field"
|
||||
);
|
||||
|
||||
for (let idx = 0; idx < remove_fields.length; idx++) {
|
||||
remove_fields.item(idx).onclick = () =>
|
||||
this.remove_fields(
|
||||
remove_fields.item(idx).getAttribute("data-fieldname")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get_dialog_fields_wrapper() {
|
||||
return this.dialog.get_field("fields_html").$wrapper[0];
|
||||
}
|
||||
|
||||
remove_fields(fieldname) {
|
||||
this.fields = this.fields.filter(field => field !== fieldname);
|
||||
this.dialog.set_value("fields", JSON.stringify(this.fields));
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
update_fields() {
|
||||
const wrapper = this.dialog.get_field("fields_html").$wrapper[0];
|
||||
let fields_order = wrapper.getElementsByClassName("fields_order");
|
||||
this.fields = [];
|
||||
|
||||
for (let idx = 0; idx < fields_order.length; idx++) {
|
||||
this.fields.push(
|
||||
fields_order.item(idx).getAttribute("data-fieldname")
|
||||
);
|
||||
}
|
||||
|
||||
this.dialog.set_value("fields", JSON.stringify(this.fields));
|
||||
}
|
||||
|
||||
show_column_selector() {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("{0} Fields", [__(this.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
label: __("Select Fields"),
|
||||
fieldtype: "MultiCheck",
|
||||
fieldname: "fields",
|
||||
options: this.get_multiselect_fields(),
|
||||
columns: 2
|
||||
}
|
||||
]
|
||||
});
|
||||
dialog.set_primary_action(__("Save"), () => {
|
||||
this.fields = dialog.get_values().fields || [];
|
||||
this.dialog.set_value("fields", JSON.stringify(this.fields));
|
||||
this.refresh();
|
||||
dialog.hide();
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
get_fields() {
|
||||
this.fields = this.settings.fields;
|
||||
this.fields.uniqBy(f => f.fieldname);
|
||||
}
|
||||
|
||||
get_docfield(field_name) {
|
||||
return (
|
||||
frappe.meta.get_docfield(this.doctype, field_name) ||
|
||||
frappe.model.get_std_field(field_name)
|
||||
);
|
||||
}
|
||||
|
||||
get_multiselect_fields() {
|
||||
const ignore_fields = [
|
||||
"idx",
|
||||
"lft",
|
||||
"rgt",
|
||||
"old_parent",
|
||||
"_user_tags",
|
||||
"_liked_by",
|
||||
"_comments",
|
||||
"_assign",
|
||||
this.meta.title_field || "name"
|
||||
];
|
||||
|
||||
const ignore_fieldtypes = [
|
||||
"Attach Image",
|
||||
"Text Editor",
|
||||
"HTML Editor",
|
||||
"Code",
|
||||
"Color",
|
||||
...frappe.model.no_value_type
|
||||
];
|
||||
|
||||
return frappe.model.std_fields
|
||||
.concat(this.kanbanview.get_fields_in_list_view())
|
||||
.filter(
|
||||
field =>
|
||||
!ignore_fields.includes(field.fieldname) &&
|
||||
!ignore_fieldtypes.includes(field.fieldtype)
|
||||
)
|
||||
.map(field => {
|
||||
return {
|
||||
label: __(field.label),
|
||||
value: field.fieldname,
|
||||
checked: this.fields.includes(field.fieldname)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import KanbanSettings from "./kanban_settings";
|
||||
|
||||
frappe.provide('frappe.views');
|
||||
|
||||
frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
||||
|
|
@ -57,6 +59,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
.then(board => {
|
||||
this.board = board;
|
||||
this.board.filters_array = JSON.parse(this.board.filters || '[]');
|
||||
this.board.fields = JSON.parse(this.board.fields || '[]');
|
||||
this.filters = this.board.filters_array;
|
||||
});
|
||||
}
|
||||
|
|
@ -187,6 +190,25 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
};
|
||||
}
|
||||
|
||||
get_view_settings() {
|
||||
return {
|
||||
label: __("Kanban Settings", null, "Button in kanban view menu"),
|
||||
action: () => this.show_kanban_settings(),
|
||||
standard: true,
|
||||
};
|
||||
}
|
||||
|
||||
show_kanban_settings() {
|
||||
frappe.model.with_doctype(this.doctype, () => {
|
||||
new KanbanSettings({
|
||||
kanbanview: this,
|
||||
doctype: this.doctype,
|
||||
settings: this.board,
|
||||
meta: frappe.get_meta(this.doctype)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get required_libs() {
|
||||
return [
|
||||
'assets/frappe/js/lib/fluxify.min.js',
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@
|
|||
}
|
||||
|
||||
.kanban-cards {
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 250px);
|
||||
margin: -5px;
|
||||
padding: 5px;
|
||||
|
|
@ -149,39 +150,120 @@
|
|||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card {
|
||||
@include flex(flex, space-between, null, column);
|
||||
margin-top: var(--margin-sm);
|
||||
min-height: 100px;
|
||||
@include card(
|
||||
$padding: 0,
|
||||
$background-color: var(--kanban-card-bg)
|
||||
);
|
||||
.kanban-card-body {
|
||||
padding: var(--padding-sm);
|
||||
.kanban-card-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
&:last-child .kanban-card {
|
||||
margin-bottom: var(--margin-xl);
|
||||
}
|
||||
.kanban-card {
|
||||
@include flex(flex, space-between, null, column);
|
||||
margin-top: var(--margin-sm);
|
||||
min-height: 100px;
|
||||
@include card(
|
||||
$padding: 0,
|
||||
$background-color: var(--kanban-card-bg)
|
||||
);
|
||||
|
||||
.kanban-image {
|
||||
height: 125px;
|
||||
|
||||
img {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
object-position: top;
|
||||
object-fit: cover;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 125px,
|
||||
$top: -4px,
|
||||
)
|
||||
}
|
||||
|
||||
.kanban-card-body {
|
||||
cursor: grab;
|
||||
padding: var(--padding-sm);
|
||||
|
||||
.kanban-title-area {
|
||||
margin-bottom: 12px;
|
||||
max-width: 90%;
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
|
||||
.kanban-card-doc {
|
||||
.text-muted div {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card-creation {
|
||||
font-size: var(--text-md);
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--margin-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.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-bottom: var(--margin-xs);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card-wrapper {
|
||||
position: relative;
|
||||
|
||||
.kanban-card-redirect {
|
||||
display: block;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child .kanban-card {
|
||||
margin-bottom: var(--margin-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card:hover,
|
||||
.new-card-area,
|
||||
.edit-card-area {
|
||||
|
|
@ -189,7 +271,6 @@
|
|||
}
|
||||
|
||||
.kanban-card-wrapper:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
.kanban-card-edit {
|
||||
|
|
@ -197,43 +278,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.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-image {
|
||||
height: 125px;
|
||||
|
||||
img {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
object-position: top;
|
||||
object-fit: cover;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 125px,
|
||||
$top: -4px,
|
||||
)
|
||||
}
|
||||
|
||||
.kanban-card-edit {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
|
@ -291,54 +335,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.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-bottom: var(--margin-xs);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-empty-state {
|
||||
width: 100%;
|
||||
line-height: 400px;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue