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(` +
+ ${label} + ${value} +
+ `); + } + + return fields.join(""); + } + function get_tags_html(card) { return card.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 }; } diff --git a/frappe/public/js/frappe/views/kanban/kanban_card.html b/frappe/public/js/frappe/views/kanban/kanban_card.html index 88cf366ccc..ed8e4cc6ff 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_card.html +++ b/frappe/public/js/frappe/views/kanban/kanban_card.html @@ -1,23 +1,24 @@
- -
- {% if(image_url) { %} -
- {{title}} -
- {% } %} -
diff --git a/frappe/public/js/frappe/views/kanban/kanban_settings.js b/frappe/public/js/frappe/views/kanban/kanban_settings.js new file mode 100644 index 0000000000..b41922e544 --- /dev/null +++ b/frappe/public/js/frappe/views/kanban/kanban_settings.js @@ -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 += ` +
+ +
+
+ ${frappe.utils.icon("drag", "xs", "", "", "sortable-handle")} +
+
+ ${__(field.label)} +
+ +
+
`; + } + + fields_html.html(` +
+
+ +
+
+ ${fields} +
+

+ + + Add / Remove Fields + +

+
+ `); + + 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) + }; + }); + } +} diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js index 3bf3a16189..b546365fd9 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_view.js +++ b/frappe/public/js/frappe/views/kanban/kanban_view.js @@ -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', diff --git a/frappe/public/scss/desk/kanban.scss b/frappe/public/scss/desk/kanban.scss index 41bd0b2859..8f286b7b35 100644 --- a/frappe/public/scss/desk/kanban.scss +++ b/frappe/public/scss/desk/kanban.scss @@ -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;