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:
Himanshu 2022-04-18 11:15:19 +01:00 committed by GitHub
parent 8b9ae0da23
commit 803f1fb061
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 614 additions and 349 deletions

View 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');
});
});

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -1580,13 +1580,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
if (frappe.user.has_role("System Manager")) {
items.push({
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,
});
}
return items;
};
}
show_list_settings() {

View file

@ -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
};
}

View file

@ -1,5 +1,4 @@
<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">
@ -8,16 +7,18 @@
{% } %}
<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 }}
</a>
<br>
<div class="kanban-card-doc text-muted">
{{ doc_content }}
</div>
</div>
<div class="kanban-card-meta">
</div>
</div>
</div>
</a>
</div>

View 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)
};
});
}
}

View file

@ -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',

View file

@ -139,6 +139,7 @@
}
.kanban-cards {
height: 100%;
max-height: calc(100vh - 250px);
margin: -5px;
padding: 5px;
@ -149,8 +150,14 @@
&::-webkit-scrollbar {
display: none;
}
}
.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);
@ -159,59 +166,6 @@
$padding: 0,
$background-color: var(--kanban-card-bg)
);
.kanban-card-body {
padding: var(--padding-sm);
}
}
}
.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 {
box-shadow: var(--shadow-base);
}
.kanban-card-wrapper:hover {
cursor: pointer;
text-decoration: none;
.kanban-card-edit {
opacity: 1;
}
}
.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;
@ -234,6 +188,96 @@
)
}
.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:hover,
.new-card-area,
.edit-card-area {
box-shadow: var(--shadow-base);
}
.kanban-card-wrapper:hover {
text-decoration: none;
.kanban-card-edit {
opacity: 1;
}
}
.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;