diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js new file mode 100644 index 0000000000..bb9aaf3d8c --- /dev/null +++ b/cypress/integration/kanban.js @@ -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'); + }); +}); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 636312376d..4847dbcf12 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -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 diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 2e27b8d6fe..bb1a8bc2d2 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -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 diff --git a/frappe/desk/doctype/kanban_board/kanban_board.json b/frappe/desk/doctype/kanban_board/kanban_board.json index f2e1a78d40..b1f120687c 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.json +++ b/frappe/desk/doctype/kanban_board/kanban_board.json @@ -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 } \ No newline at end of file diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index e864f68728..381f71438c 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -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 diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 069f353368..35e387b8d8 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -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({ diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js index 58d58b27fc..64e90f5326 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -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 ? `${__(field.label)}: ` : ''; + let value = frappe.format(card.doc[field_name], field); + fields.push(` +