Merge pull request #31836 from iamejaaz/new-scrollable-list-view

feat: scrollable list view + new mobile list view
This commit is contained in:
Ejaaz Khan 2025-08-08 14:46:34 +05:30 committed by GitHub
commit 4e927eeaef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 233 additions and 24 deletions

View file

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

View file

@ -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");

View file

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

View file

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

View file

@ -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 `<div>` 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($(`<div class="result-container">`));
}
setup_result_area() {
this.$result = $(`<div class="result">`);
this.$frappe_list.append(this.$result);
this.$frappe_list.find(".result-container").append(this.$result);
}
setup_no_result_area() {

View file

@ -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 += `<div class="mobile-layout">${this.get_column_html(
col,
doc,
true
)}</div>`;
} 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 `
<div class="${css_class}">
${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 = "",

View file

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

View file

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