diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index d6cc16fef1..397c91215b 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -65,10 +65,9 @@ context("List View", () => { cy.go_to_list("ToDo"); // Check if the 'Open' button is present in the ToDo list view - cy.get(".btn-default[data-name='" + todo_name + "']") - .should((el) => { - expect(el).to.exist; - }) + cy.get(`.btn-default[data-name="${todo_name}"]`) + .scrollIntoView({ inline: "center", block: "nearest" }) + .should("be.visible") .click(); cy.window() diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js index 9c66edb5d5..5dafb7a04a 100644 --- a/cypress/integration/list_view_settings.js +++ b/cypress/integration/list_view_settings.js @@ -15,7 +15,7 @@ context("List View Settings", () => { cy.clear_filters(); cy.wait(300); cy.get(".list-count").should("contain", "20 of"); - cy.get("[href='#es-line-chat-alt']").should("be.visible"); + cy.get(".frappe-list svg.es-icon.es-line").should("be.visible"); cy.get(".menu-btn-group button").click(); cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click(); cy.get(".modal-dialog").should("contain", "DocType Settings"); diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.json b/frappe/desk/doctype/list_view_settings/list_view_settings.json index ddadffb2f7..fb8b2c6998 100644 --- a/frappe/desk/doctype/list_view_settings/list_view_settings.json +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.json @@ -39,7 +39,7 @@ "fieldname": "total_fields", "fieldtype": "Select", "label": "Maximum Number of Fields", - "options": "\n4\n5\n6\n7\n8\n9\n10" + "options": "\n4\n5\n6\n7\n8\n9\n10\n15\n20\n25\n30" }, { "fieldname": "fields_html", @@ -75,7 +75,7 @@ ], "grid_page_length": 50, "links": [], - "modified": "2025-03-12 16:28:46.073808", + "modified": "2025-03-24 14:17:39.888956", "modified_by": "Administrator", "module": "Desk", "name": "List View Settings", diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py index 5d1d4e153d..975d2807cd 100644 --- a/frappe/desk/doctype/list_view_settings/list_view_settings.py +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py @@ -21,7 +21,7 @@ class ListViewSettings(Document): disable_count: DF.Check disable_sidebar_stats: DF.Check fields: DF.Code | None - total_fields: DF.Literal["", "4", "5", "6", "7", "8", "9", "10"] + total_fields: DF.Literal["", "4", "5", "6", "7", "8", "9", "10", "15", "20", "25", "30"] # end: auto-generated types pass diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index c8360163e6..63eba2f33c 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -305,6 +305,7 @@ frappe.views.BaseList = class BaseList { this.show_or_hide_sidebar, this.setup_filter_area, this.setup_sort_selector, + this.setup_result_container_area, this.setup_result_area, this.setup_no_result_area, this.setup_freeze_area, @@ -345,9 +346,17 @@ frappe.views.BaseList = class BaseList { this.refresh(); } + /** + * Sets up a result container area by appending a new `
` element with the class `result-container` + * to the `frappe_list` container. This container is used to create a scrollable area for the result content. + */ + setup_result_container_area() { + this.$frappe_list.append($(`
`)); + } + setup_result_area() { this.$result = $(`
`); - this.$frappe_list.append(this.$result); + this.$frappe_list.find(".result-container").append(this.$result); } setup_no_result_area() { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 9a02eb753c..64cc2a6c39 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -33,6 +33,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ); this.count_upper_bound = 1001; this._element_factory = new ElementFactory(this.doctype); + this.column_max_widths = {}; } has_permissions() { @@ -633,14 +634,27 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.$result.find(".list-row-container").remove(); this.render_header(); + let has_assignto = false; + if (this.data.length > 0) { // append rows let idx = 0; for (let doc of this.data) { doc._idx = idx++; this.$result.append(this.get_list_row_html(doc)); + if (!has_assignto && doc._assign) { + has_assignto = true; + } } } + this.apply_column_widths(); + + // add class to result to indetify that it has assignto + if (has_assignto) { + this.$result.addClass("has-assign-to"); + } else { + this.$result.addClass("no-assign-to"); + } } render_count() { @@ -703,6 +717,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { col.type == "Subject" ? "list-subject level" : "hidden-xs", col.type == "Tag" ? "tag-col hide" : "", frappe.model.is_numeric_field(col.df) ? "text-right" : "", + col.df?.fieldname, ].join(" "); let html = ""; @@ -767,7 +782,22 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } get_left_html(doc) { - let left_html = this.columns.map((col) => this.get_column_html(col, doc)).join(""); + // let left_html = this.columns.map((col) => this.get_column_html(col, doc)).join(""); + + let left_html = ""; + for (let i = 0; i < this.columns.length; i++) { + let col = this.columns[i]; + + if (frappe.is_mobile() && col.type == "Field" && [3, 4].includes(i)) { + left_html += `
${this.get_column_html( + col, + doc, + true + )}
`; + } else { + left_html += this.get_column_html(col, doc, false); + } + } left_html += this.generate_button_html(doc); left_html += this.generate_dropdown_html(doc); @@ -798,7 +828,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { `; } - get_column_html(col, doc) { + get_column_html(col, doc, show_in_mobile) { if (col.type === "Status" || col.df?.options == "Workflow State") { let show_workflow_state = col.df?.options == "Workflow State"; return ` @@ -906,12 +936,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const class_map = { Subject: "list-subject level", - Field: "hidden-xs", + Field: !show_in_mobile ? "hidden-xs" : "", }; - const css_class = [ + let css_class = [ "list-row-col ellipsis", class_map[col.type], frappe.model.is_numeric_field(df) ? "text-right" : "", + fieldname, ].join(" "); let column_html; @@ -928,6 +959,29 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }[col.type]; } + if (frappe.is_mobile() && col.type == "Subject") { + css_class += " bold"; + } + + /** + * Calculates the width of a text element based on its length. + * If the length of the text is not available, it defaults to a length of 22.5. + */ + let textLength = $(column_html).text()?.trim()?.length || 22.5; + let calculatedWidth = (textLength * 10) / 1.3; + + /** + * Updates the `column_max_widths` object by setting the maximum width for a specific column (fieldname). + * If no width is set for the column, or the newly calculated width exceeds the current width, the width is updated. + */ + if ( + (!this.column_max_widths[fieldname] || + calculatedWidth > this.column_max_widths[fieldname]) && + !frappe.is_mobile() + ) { + this.column_max_widths[fieldname] = calculatedWidth; + } + return `
${column_html} @@ -935,6 +989,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { `; } + /** + * Applies dynamically calculated widths to elements based on their respective class names. + * Iterates through `column_max_widths` and sets the `width` and `flex` styles for each column. + * The width for each column is applied as both a fixed `width` and a flexible `flex` property. + */ + apply_column_widths() { + Object.entries(this.column_max_widths).forEach(([fieldname, width]) => { + $(`.${fieldname}`).css({ + width: width, + flex: `1 0 ${width}px`, + }); + }); + } + get_tags_html(user_tags, limit, colored = false) { let get_tag_html = (tag) => { let color = "", diff --git a/frappe/public/scss/desk/list.scss b/frappe/public/scss/desk/list.scss index 79e2fd9c45..77c04cec33 100644 --- a/frappe/public/scss/desk/list.scss +++ b/frappe/public/scss/desk/list.scss @@ -60,7 +60,6 @@ .list-row-container { display: flex; flex-direction: column; - padding: var(--padding-xs) var(--padding-md); border-bottom: 1px solid $border-color; &:focus { @@ -73,15 +72,26 @@ padding-top: 0; border-bottom: none; } +} - &:last-child { - border-bottom: none; +.frappe-list { + margin: var(--margin-xs) var(--margin-md); + .result.has-assign-to { + .list-row .level-right { + flex: 0 0 180px; + width: 180px; + } + } + + .result.no-assign-to { + .list-row .level-right { + flex: 0 0 130px; + width: 130px; + } } } .list-row { - padding-right: 15px; - height: var(--list-row-height); border-radius: var(--border-radius); cursor: pointer; transition: color 0.2s; @@ -90,6 +100,14 @@ &:hover:not(.list-row-head) { background-color: var(--highlight-color); + border-radius: unset; + .level-right { + box-shadow: -5px 0px 5px var(--highlight-color); + } + } + + &:hover .level-right { + background-color: var(--highlight-color); } &:last-child { @@ -99,11 +117,14 @@ .level-left { flex: 4; min-width: 80%; + padding: var(--padding-xs) 0; } .level-right { - flex: 1; - overflow: visible; - align-items: center; + position: sticky; + right: 0; + background-color: var(--bg-color); + box-shadow: -5px 0px 5px rgb(255, 255, 255); + padding: 6px 10px; } .tag-col { @@ -180,6 +201,18 @@ .checkbox-actions { display: none; } + .level-right { + background-color: var(--subtle-fg); + border-radius: var(--border-radius); + height: var(--list-row-height); + box-shadow: none; + &:hover { + background-color: var(--subtle-fg); + } + } + &:hover .level-right { + background-color: var(--subtle-fg); + } } .list-row-col { @@ -308,7 +341,6 @@ input.list-header-checkbox { align-items: center; cursor: pointer; - height: var(--list-row-height); padding-left: 15px; @include get_textstyle("base", "regular"); @@ -499,3 +531,70 @@ input.list-header-checkbox { height: calc(100vh - 284px); z-index: 0; } + +@media (max-width: map-get($grid-breakpoints, "lg")) { + .layout-main-section-wrapper { + width: 100%; + } + .frappe-list { + .list-row { + .level-right { + flex: 0 0 auto; + width: auto; + } + } + } +} + +@media (max-width: map-get($grid-breakpoints, "sm")) { + .layout-main-section .frappe-list .result-container { + .result { + overflow: hidden; + input.list-row-checkbox, + input.list-header-checkbox { + width: 15px !important; + height: 15px; + } + } + .list-row-container:not(:has(.list-row-head)) { + .list-row { + &.level { + align-items: flex-start; + } + .level-left { + min-width: auto; + display: block; + .mobile-layout { + display: inline-block; + z-index: 1; + position: relative; + padding-left: 10px; + .list-row-col { + margin-right: 0px; + } + } + .mobile-layout:not(.mobile-layout ~ .mobile-layout) { + padding-left: 27px; + margin-right: 6px; + + &::after { + content: "\2022"; + position: absolute; + right: 0; + top: 50%; + transform: translate(50%, -50%); + padding-left: 15px; + } + } + } + .level-right { + flex: 0 0 auto; + width: auto; + .level-item.visible-xs { + margin-top: 5px; + } + } + } + } + } +} diff --git a/frappe/public/scss/desk/page.scss b/frappe/public/scss/desk/page.scss index 044148534f..bd86a517e8 100644 --- a/frappe/public/scss/desk/page.scss +++ b/frappe/public/scss/desk/page.scss @@ -93,7 +93,8 @@ } .layout-main-section-wrapper { - width: 100%; + flex: 1 0 80%; + width: 80%; } .layout-main-section.frappe-card { @@ -179,12 +180,39 @@ .layout-main-section { scroll-margin-top: var(--navbar-height); + .frappe-list { + .result-container { + overflow-x: auto; + .result { + min-width: 100%; + width: auto; + .list-row-container { + width: fit-content; + min-width: 100%; + } + .list-row-container:first-child { + position: sticky; + top: 0; + z-index: 2; + } + .list-row-container { + .level-left { + .list-row-col { + min-width: 150px; + max-width: 400px; + } + } + } + } + } + } + .frappe-list, .report-wrapper { .result, .no-result, .freeze { - min-height: "200px"; + min-height: 200px; } .result { @@ -221,3 +249,9 @@ margin-top: var(--margin-xs); text-align: center; } + +.frappe-control { + .form-control.fields_order { + padding-top: 1.5px; + } +}