Merge pull request #31836 from iamejaaz/new-scrollable-list-view
feat: scrollable list view + new mobile list view
This commit is contained in:
commit
4e927eeaef
8 changed files with 233 additions and 24 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue