fix: speed up link field (#34689)

This commit is contained in:
Raffael Meyer 2025-12-01 19:31:27 +01:00 committed by GitHub
parent af1bacc691
commit a9c4bac950
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 234 additions and 167 deletions

View file

@ -93,6 +93,7 @@ context("Dynamic Link", () => {
it("Creating a dynamic link and verifying it in a dialog", () => {
get_dialog_with_dynamic_link().as("dialog");
cy.wait(500);
cy.get_field("doc_type").clear();
cy.fill_field("doc_type", "User", "Link");
cy.get_field("doc_id").click();
@ -106,7 +107,7 @@ context("Dynamic Link", () => {
cy.get(".btn-modal-close").click({ force: true, multiple: true });
});
it("Creating a dynamic link and verifying it", () => {
it("Shows dynamic link options in list filters", () => {
cy.visit("/desk/test-dynamic-link");
//Clicking on the Document ID field
@ -122,7 +123,9 @@ context("Dynamic Link", () => {
.find("div")
.its("length")
.should("be.gte", 0);
});
it("Shows dynamic link options in new form", () => {
//Opening a new form for dynamic link doctype
cy.new_form("Test Dynamic Link");
cy.get_field("doc_type").clear();
@ -138,14 +141,14 @@ context("Dynamic Link", () => {
.its("length")
.should("be.gte", 0);
cy.get_field("doc_type").clear();
});
it("Shows error when invalid DocType is passed", () => {
cy.new_form("Test Dynamic Link");
cy.get_field("doc_type").clear();
//Entering System Settings in the Doctype field
cy.intercept("/api/method/frappe.desk.search.search_link").as("search_query");
cy.fill_field("doc_type", "System Settings", "Link", { delay: 500 });
cy.wait("@search_query");
cy.get(`[data-fieldname="doc_type"] ul:visible div:first-child`).click({
scrollBehavior: false,
});
cy.get_field("doc_id").click();

View file

@ -62,12 +62,13 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("todo for link", { delay: 200 });
cy.wait(500);
cy.get("@input").type("todo for link", { delay: 100 });
cy.wait("@search_link");
cy.wait(500);
cy.get(".frappe-control[data-fieldname=link]").findByRole("listbox").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
cy.get(".frappe-control[data-fieldname=link] input").blur();
cy.get("@input").parent().findByRole("listbox").should("be.visible");
cy.get("@input").type("{enter}");
cy.get("@input").blur();
cy.get("@dialog").then((dialog) => {
cy.get("@todos").then((todos) => {
let value = dialog.get_value("link");
@ -80,24 +81,27 @@ context("Control Link", () => {
get_dialog_with_link().as("dialog");
cy.intercept("/api/method/frappe.client.validate_link*").as("validate_link");
cy.get(".frappe-control[data-fieldname=link] input")
.type("invalid value", { delay: 100 })
.blur();
cy.wait("@validate_link");
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.wait(500);
cy.get(".frappe-control[data-fieldname=link] input").should("have.value", "");
cy.get("@input").type("invalid value", { delay: 100 }).blur();
cy.wait("@validate_link");
cy.get("@input").should("have.value", "");
});
it("should be possible set empty value explicitly", () => {
get_dialog_with_link().as("dialog");
cy.intercept("/api/method/frappe.client.validate_link*").as("validate_link");
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.get(".frappe-control[data-fieldname=link] input").type(" ", { delay: 100 }).blur();
cy.wait("@validate_link");
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.wait(500);
cy.get(".frappe-control[data-fieldname=link] input").should("have.value", "");
cy.get("@input").type(" ", { delay: 100 }).blur();
cy.wait("@validate_link");
cy.get("@input").should("have.value", "");
cy.window()
.its("cur_dialog")
.then((dialog) => {
@ -111,10 +115,10 @@ context("Control Link", () => {
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.get("@todos").then((todos) => {
cy.get(".frappe-control[data-fieldname=link] input").as("input");
cy.get("@input").focus();
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type(todos[0]).blur();
cy.wait(500);
cy.get("@input").type(todos[0], { delay: 100 }).blur();
// not waiting for validate_link because it will not get called
cy.get("@input").trigger("mouseover");
cy.get(".frappe-control[data-fieldname=link] .btn-open")
@ -156,11 +160,13 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("todo for link", { delay: 200 });
cy.wait(500);
cy.get("@input").type("todo for link", { delay: 100 });
cy.wait("@search_link");
cy.wait(500);
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
cy.get(".frappe-control[data-fieldname=link] input").blur();
cy.get("@input").type("{enter}");
cy.get("@input").blur();
cy.get("@dialog").then((dialog) => {
cy.get("@todos").then((todos) => {
let field = dialog.get_field("link");
@ -179,13 +185,18 @@ context("Control Link", () => {
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.intercept("/api/method/frappe.client.validate_link*").as("validate_link");
cy.get(".frappe-control[data-fieldname=assigned_by] input").focus().as("input");
cy.get("@input").clear().type(cy.config("testUser"), { delay: 300 }).blur();
cy.wait("@validate_link");
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
"contain",
"Frappe"
);
cy.fill_field("assigned_by", cy.config("testUser"), "Link");
cy.call("frappe.client.get_value", {
doctype: "User",
filters: {
name: cy.config("testUser"),
},
fieldname: "full_name",
}).then((r) => {
cy.get(
".frappe-control[data-fieldname=assigned_by_full_name] .control-value"
).should("contain", r.message.full_name);
});
cy.window().its("cur_frm.doc.assigned_by").should("eq", cy.config("testUser"));
@ -234,10 +245,19 @@ context("Control Link", () => {
cy.new_form("ToDo");
cy.fill_field("description", "new", "Text Editor").blur().wait(200);
cy.save();
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
"contain",
"Frappe"
);
cy.call("frappe.client.get_value", {
doctype: "User",
filters: {
name: cy.config("testUser"),
},
fieldname: "full_name",
}).then((r) => {
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
"contain",
r.message.full_name
);
});
// if user clears default value explicitly, system should not reset default again
cy.get_field("assigned_by").clear().blur();
cy.save();
@ -264,11 +284,11 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("Sonstiges", { delay: 200 });
cy.get("@input").type("Sonstiges", { delay: 100 });
cy.wait("@search_link");
cy.wait(500);
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}");
cy.get(".frappe-control[data-fieldname=link] input").blur();
cy.get("@dialog").then((dialog) => {
let field = dialog.get_field("link");
@ -296,11 +316,11 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("Non-Conforming", { delay: 200 });
cy.get("@input").type("Non-Conforming", { delay: 100 });
cy.wait("@search_link");
cy.wait(500);
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}");
cy.get(".frappe-control[data-fieldname=link] input").blur();
cy.get("@dialog").then((dialog) => {
let field = dialog.get_field("link");

View file

@ -4,8 +4,7 @@ context("Customize Form", () => {
cy.visit("/desk/customize-form");
});
it("Changing to naming rule should update autoname", () => {
cy.fill_field("doc_type", "ToDo", "Link").blur();
cy.wait(2000);
cy.fill_field("doc_type", "ToDo", "Link");
cy.findByRole("tab", { name: "Details" }).click();
cy.click_form_section("Naming");
const naming_rule_default_autoname_map = {

View file

@ -8,13 +8,13 @@ context("Dashboard Chart", () => {
cy.new_form("Dashboard Chart");
cy.get('[data-fieldname="parent_document_type"]').should("have.css", "display", "none");
cy.get_field("document_type", "Link");
cy.fill_field("document_type", "Workspace Link", "Link").focus().blur();
cy.get_field("document_type", "Link").should("have.value", "Workspace Link");
cy.get_field("chart_name", "Data").should("be.visible");
cy.fill_field("chart_name", "Test Chart", "Data");
cy.fill_field("document_type", "Workspace Link", "Link");
cy.get('[data-fieldname="filters_json"]').click();
cy.get(".modal-dialog", { timeout: 500 }).should("be.visible");
cy.get('[data-fieldname="filters_json"]').click().wait(200);
cy.get(".modal-body .filter-action-buttons .add-filter").click();
cy.get(".modal-body .fieldname-select-area").click();
cy.get(".modal-actions .btn-modal-close").click();

View file

@ -49,23 +49,34 @@ context("Form Builder", () => {
cy.get(".modal-body .clear-filters").click();
cy.get(".modal-body .filter-action-buttons .add-filter").click();
cy.wait(100);
cy.get(".modal-body .filter-box .list_filter .filter-field .link-field input").type(
"Male"
);
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.get(".modal-body .filter-box .list_filter .filter-field .link-field input")
.focus()
.as("input");
cy.wait("@search_link");
cy.wait(500);
cy.get("@input").type("Male", { delay: 100 });
cy.wait("@search_link");
cy.wait(500);
cy.get("@input").type("{enter}", { delay: 100 });
cy.get("@input").blur();
cy.get(".btn-modal-primary").click();
cy.wait(500);
// Save the document
cy.click_doc_primary_button("Save");
cy.wait(1000);
// Open a new Form
cy.new_form(doctype_name);
// Click on the "salutation" field
cy.get_field("gender").clear().click();
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.wait("@search_link").then((data) => {
expect(data.response.body.message.length).to.eq(1);
expect(data.response.body.message[0].value).to.eq("Male");
cy.compare_document({
fields: [
{},
{
fieldname: "gender",
link_filters: '[["Gender","name","=","Male"]]',
},
],
});
});

View file

@ -9,7 +9,7 @@ context("Number Card", () => {
cy.get('[data-fieldname="parent_document_type"]').should("have.css", "display", "none");
cy.get_field("document_type", "Link");
cy.fill_field("document_type", "Workspace Link", "Link").focus().blur();
cy.fill_field("document_type", "Workspace Link", "Link");
cy.get_field("document_type", "Link").should("have.value", "Workspace Link");
cy.fill_field("label", "Test Number Card", "Data");

View file

@ -19,12 +19,18 @@ context("Web Form", () => {
cy.fill_field("doc_type", "Note", "Link");
cy.fill_field("module", "Website", "Link");
cy.click_custom_action_button("Get Fields");
// wait until Get Fields finishes populating the grid
cy.get('[data-fieldname="web_form_fields"] .grid-row').should(($rows) => {
expect($rows.length, "web form fields").to.be.greaterThan(0);
});
cy.click_custom_action_button("Publish");
cy.wait("@save_form");
cy.get_field("route").should("have.value", "note");
cy.get(".title-area .indicator-pill").contains("Published");
cy.get(".title-area .indicator-pill")
.should("contain.text", "Published")
.should("have.class", "green");
});
it("Open Web Form", () => {

View file

@ -72,7 +72,7 @@ context("Workspace Blocks", () => {
cy.get(".block-list-container .block-list-item").contains("Quick List").click();
cy.fill_field("label", "ToDo", "Data");
cy.fill_field("document_type", "ToDo", "Link").blur();
cy.fill_field("document_type", "ToDo", "Link");
cy.wait("@get_doctype");
cy.get_open_dialog().find(".filter-edit-area").should("contain", "No filters selected");
@ -143,6 +143,7 @@ context("Workspace Blocks", () => {
});
it("Number Card Block", () => {
cy.visit("/app/private/test-block-page");
cy.create_records([
{
doctype: "Number Card",
@ -155,12 +156,16 @@ context("Workspace Blocks", () => {
cy.get(".codex-editor__redactor .ce-block");
cy.get(".btn-edit-workspace").click();
cy.get(".ce-block").first().click({ force: true }).type("{enter}");
cy.get(".ce-block")
.first()
.realHover()
.find(".new-block-button")
.should("be.visible")
.click();
cy.get(".block-list-container .block-list-item").contains("Number Card").click();
// add number card
cy.fill_field("number_card_name", "Test Number Card", "Link");
cy.get('[data-fieldname="number_card_name"] ul div').contains("Test Number Card").click();
cy.click_modal_primary_button("Add");
cy.get(".ce-block .number-widget-box").first().as("number_card");
cy.get("@number_card").find(".widget-title").should("contain", "Test Number Card");

View file

@ -171,7 +171,24 @@ Cypress.Commands.add("fill_field", (fieldname, value, fieldtype = "Data") => {
cy.get("@input").clear().wait(200);
}
if (fieldtype === "Select") {
if (["Link", "Dynamic Link"].includes(fieldtype)) {
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.get("@input").clear().focus();
cy.wait("@search_link");
cy.get("@input").parent().findByRole("listbox").as("dropdown");
cy.get("@dropdown").should("be.visible");
cy.get("@input").type(value, { delay: 100 });
cy.wait("@search_link");
cy.get("@dropdown")
.should("be.visible")
.find("div[role='option']")
.first()
.should("include.text", value);
cy.get("@input").type("{enter}");
cy.get("@input").blur();
cy.get("@dropdown").should("not.exist");
cy.get("@input").should("have.value", value);
} else if (fieldtype === "Select") {
cy.get("@input").select(value);
} else {
cy.get("@input").type(value, {

View file

@ -26,7 +26,14 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
this.set_input_attributes();
this.$input.on("focus", function () {
if (!me.$input.val()) {
me.$input.val("").trigger("input");
me.$input.val("");
// Create a fake input event
const e = $.Event("input");
e.target = me.$input[0];
// Pass it to on_input directly, bypassing debounce, so the dropdown opens immediately
me.on_input(e);
}
me.show_link_and_clear_buttons();
@ -259,107 +266,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
this.custom_awesomplete_filter && this.custom_awesomplete_filter(this.awesomplete);
this.$input.on(
"input",
frappe.utils.debounce(function (e) {
var doctype = me.get_options();
if (!doctype) return;
if (!me.$input.cache[doctype]) {
me.$input.cache[doctype] = {};
}
var term = e.target.value;
if (me.$input.cache[doctype][term] != null) {
// immediately show from cache
me.awesomplete.list = me.$input.cache[doctype][term];
}
var args = {
txt: term,
doctype: doctype,
ignore_user_permissions: me.df.ignore_user_permissions,
reference_doctype: me.get_reference_doctype() || "",
page_length: cint(frappe.boot.sysdefaults?.link_field_results_limit) || 10,
form_doctype: me.doctype,
link_fieldname: me.df.fieldname,
};
me.set_custom_query(args);
frappe.call({
type: "POST",
method: "frappe.desk.search.search_link",
no_spinner: true,
args: args,
callback: function (r) {
if (!window.Cypress && !me.$input.is(":focus")) {
return;
}
r.message = me.merge_duplicates(r.message);
// show filter description in awesomplete
let filter_string = me.df.filter_description
? me.df.filter_description
: args.filters
? me.get_filter_description(args.filters)
: null;
if (filter_string) {
r.message.push({
html: `<span class="text-muted" style="line-height: 1.5">${filter_string}</span>`,
value: "",
action: () => {},
});
}
if (!me.df.only_select) {
if (frappe.model.can_create(doctype)) {
// new item
r.message.push({
html:
"<span class='link-option'>" +
"<i class='fa fa-plus' style='margin-right: 5px;'></i> " +
__("Create a new {0}", [__(me.get_options())]) +
"</span>",
label: __("Create a new {0}", [__(me.get_options())]),
value: "create_new__link_option",
action: me.new_doc,
});
}
//custom link actions
let custom__link_options =
frappe.ui.form.ControlLink.link_options &&
frappe.ui.form.ControlLink.link_options(me);
if (custom__link_options) {
r.message = r.message.concat(custom__link_options);
}
// advanced search
if (locals && locals["DocType"]) {
// not applicable in web forms
r.message.push({
html:
"<span class='link-option'>" +
"<i class='fa fa-search' style='margin-right: 5px;'></i> " +
__("Advanced Search") +
"</span>",
label: __("Advanced Search"),
value: "advanced_search__link_option",
action: me.open_advanced_search,
});
}
}
me.$input.cache[doctype][term] = r.message;
me.awesomplete.list = me.$input.cache[doctype][term];
me.toggle_href(doctype);
r.message.forEach((item) => {
frappe.utils.add_link_title(doctype, item.value, item.label);
});
},
});
}, 500)
);
this.$input.on("input", frappe.utils.debounce(this.on_input.bind(this), 500));
this.$input.on("blur", function () {
if (me.selected) {
@ -433,6 +340,105 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
});
}
on_input(e) {
var doctype = this.get_options();
if (!doctype) return;
if (!this.$input.cache[doctype]) {
this.$input.cache[doctype] = {};
}
var term = e.target.value;
if (this.$input.cache[doctype][term] != null) {
// immediately show from cache
this.awesomplete.list = this.$input.cache[doctype][term];
}
var args = {
txt: term,
doctype: doctype,
ignore_user_permissions: this.df.ignore_user_permissions,
reference_doctype: this.get_reference_doctype() || "",
page_length: cint(frappe.boot.sysdefaults?.link_field_results_limit) || 10,
form_doctype: this.doctype,
link_fieldname: this.df.fieldname,
};
this.set_custom_query(args);
frappe.call({
type: "POST",
method: "frappe.desk.search.search_link",
no_spinner: true,
args: args,
callback: (r) => {
if (!window.Cypress && !this.$input.is(":focus")) {
return;
}
r.message = this.merge_duplicates(r.message);
// show filter description in awesomplete
let filter_string = this.df.filter_description
? this.df.filter_description
: args.filters
? this.get_filter_description(args.filters)
: null;
if (filter_string) {
r.message.push({
html: `<span class="text-muted" style="line-height: 1.5">${filter_string}</span>`,
value: "",
action: () => {},
});
}
if (!this.df.only_select) {
if (frappe.model.can_create(doctype)) {
// new item
r.message.push({
html:
"<span class='link-option'>" +
"<i class='fa fa-plus' style='margin-right: 5px;'></i> " +
__("Create a new {0}", [__(this.get_options())]) +
"</span>",
label: __("Create a new {0}", [__(this.get_options())]),
value: "create_new__link_option",
action: this.new_doc,
});
}
//custom link actions
let custom__link_options =
frappe.ui.form.ControlLink.link_options &&
frappe.ui.form.ControlLink.link_options(this);
if (custom__link_options) {
r.message = r.message.concat(custom__link_options);
}
// advanced search
if (locals && locals["DocType"]) {
// not applicable in web forms
r.message.push({
html:
"<span class='link-option'>" +
"<i class='fa fa-search' style='margin-right: 5px;'></i> " +
__("Advanced Search") +
"</span>",
label: __("Advanced Search"),
value: "advanced_search__link_option",
action: this.open_advanced_search,
});
}
}
this.$input.cache[doctype][term] = r.message;
this.awesomplete.list = this.$input.cache[doctype][term];
this.toggle_href(doctype);
r.message.forEach((item) => {
frappe.utils.add_link_title(doctype, item.value, item.label);
});
},
});
}
show_untranslated() {
let value = this.get_input_value();
this.is_translatable() && this.set_input_value(value);