Merge pull request #34899 from iamejaaz/cool-awesomebar

feat: cool awesomebar
This commit is contained in:
Ejaaz Khan 2025-11-27 10:27:25 +05:30 committed by GitHub
commit 8208ff2d17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 292 additions and 123 deletions

View file

@ -10,11 +10,15 @@ context("Awesome Bar", () => {
});
beforeEach(() => {
cy.get("body").click(0, 0); // Click on some blank space to avoid any modals.
let txt = `Search or type a command (${
window.navigator.platform === "MacIntel" ? "⌘" : "Ctrl"
} + K)`;
cy.findByPlaceholderText(txt).as("awesome_bar");
cy.get("@awesome_bar").type("{selectall}");
cy.contains(txt).as("awesome_bar_search");
cy.get("@awesome_bar_search").click();
cy.get("#navbar-search").as("awesome_bar");
cy.get("#navbar-search").type("{selectall}");
cy.wait(400);
});
after(() => {
@ -22,6 +26,10 @@ context("Awesome Bar", () => {
cy.clear_filters();
});
it("opens awesome bar on click", () => {
cy.get("@awesome_bar").should("be.visible");
});
it("navigates to doctype list", () => {
cy.get("@awesome_bar").type("todo");
cy.wait(100); // Wait a bit before hitting enter.
@ -45,6 +53,8 @@ context("Awesome Bar", () => {
cy.wait(200); // Wait a bit longer before checking the filter.
cy.get('[data-original-title="ID"]:visible > input').as("filter");
cy.get("@filter").should("have.value", "%test%");
cy.get("@awesome_bar_search").click();
cy.wait(400);
cy.get("@awesome_bar").type("anothertest in todo");
cy.wait(200); // Wait a bit longer before hitting enter.
cy.get("@awesome_bar").type("{enter}");

View file

@ -36,13 +36,13 @@
position: relative;
}
#navbar-search{
#navbar-modal-search{
padding-left: 32px;
}
.desktop-search-icon{
position: absolute;
left: 10px;
top: 2px;
top: 4px;
}
.desktop-search-icon > .icon {

View file

@ -9,13 +9,9 @@
>
</div>
<div class="desktop-search-wrapper input-group search-bar text-muted ">
<input
id="navbar-search"
type="text"
class="form-control"
aria-haspopup="true"
placeholder="Search for type a command"
>
<div id="navbar-modal-search">
Search or type a command
</div>
<span class="desktop-search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>

View file

@ -199,21 +199,12 @@ class DesktopPage {
setup_awesomebar() {
if (frappe.boot.desk_settings.search_bar) {
let awesome_bar = new frappe.search.AwesomeBar();
awesome_bar.setup(".desktop-search-wrapper #navbar-search");
awesome_bar.setup(".desktop-search-wrapper #navbar-modal-search");
}
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+g",
action: function (e) {
$(".desktop-search-wrapper #navbar-search").focus();
e.preventDefault();
return false;
},
description: __("Open Awesomebar"),
});
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+k",
action: function (e) {
$(".desktop-search-wrapper #navbar-search").focus();
$(".desktop-search-wrapper #navbar-modal-search").click();
e.preventDefault();
return false;
},

View file

@ -200,17 +200,7 @@ frappe.ui.keys.add_shortcut({
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+k",
action: function (e) {
$("#navbar-search").focus();
e.preventDefault();
return false;
},
description: __("Open Awesomebar"),
});
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+k",
action: function (e) {
$("#navbar-search").focus();
$("#navbar-modal-search").click();
e.preventDefault();
return false;
},
@ -220,7 +210,7 @@ frappe.ui.keys.add_shortcut({
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+g",
action: function (e) {
$("#navbar-search").focus();
$("#navbar-modal-search").click();
e.preventDefault();
return false;
},

View file

@ -5,15 +5,80 @@ frappe.provide("frappe.tags");
frappe.search.AwesomeBar = class AwesomeBar {
setup(element) {
var me = this;
$(".search-bar").removeClass("hidden");
var $input = $(element);
var input = $input.get(0);
this.options = [];
this.global_results = [];
this.setup_search_modal(element);
frappe.search.utils.setup_recent();
}
setup_search_modal(element) {
let is_event_listeners_added = false;
let $search_element = $(element);
let search_modal = new frappe.get_modal("Search", "");
search_modal.on("shown.bs.modal", () => {
search_modal.find("#navbar-search").get(0).focus();
});
let search_modal_body = `<div class="align-baseline flex py-2 px-1 relative navbar-modal-wrapper">
<div class="modal-search-icon absolute pr-2 pl-3">${frappe.utils.icon("search")}</div>
<input
id="navbar-search"
type="text"
class="form-control bg-transparent shadow-none" aria-haspopup="true"
placeholder="${__("Search or type a command")}" autocomplete="off"
/>
<div class="modal-divider"></div>
</div>`;
let search_modal_footer = `<div class="awesomebar-modal-footer flex justify-between w-100">
<div class="help-navigation">
<span class="help-item-navigate">
<span class="help-item">${frappe.utils.icon("arrow-up")}</span>
<span class="help-item">${frappe.utils.icon("arrow-down")}</span>
<span>${__("to navigate")}</span>
</span>
<span class="help-item-navigate">
<span class="help-item">${frappe.utils.icon("corner-down-left")}</span>
<span>${__("to select")}</span>
</span>
<span class="help-item">${__("esc")}</span>
<span>${__("to close")}</span>
</div>
<div class="pointer">${frappe.utils.icon("circle-question-mark")}</div>
</div>`;
search_modal.find(".modal-body").css("padding", "0").html(search_modal_body);
search_modal.find(".modal-header").css("display", "none");
search_modal
.find(".modal-footer")
.removeClass("hide")
.addClass("cool-awesomebar-modal-footer")
.html(search_modal_footer);
search_modal.find(".pointer").on("click", () => {
this.show_help();
});
$search_element.on("click", () => {
search_modal.modal("show");
if (is_event_listeners_added) return;
is_event_listeners_added = true;
this.setup_event_listeners(search_modal);
});
}
setup_event_listeners(search_modal) {
var me = this;
let $input = search_modal.find("#navbar-search");
let input = $input.get(0);
var awesomplete = new Awesomplete(input, {
minChars: 0,
maxItems: 99,
@ -85,10 +150,9 @@ frappe.search.AwesomeBar = class AwesomeBar {
);
me.options = me.options.concat(frappe.search.utils.get_frequent_links());
}
me.add_help();
awesomplete.list = me.deduplicate(me.options);
}, 100)
}, 50)
);
var open_recent = function () {
@ -96,6 +160,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
$(this).trigger("input");
}
};
$input.on("focus", open_recent);
$input.on("awesomplete-open", function (e) {
@ -130,6 +195,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
}
$input.val("");
$input.trigger("blur");
search_modal.modal("hide");
});
$input.on("awesomplete-selectcomplete", function (e) {
@ -141,51 +207,43 @@ frappe.search.AwesomeBar = class AwesomeBar {
$input.trigger("blur");
}
});
frappe.search.utils.setup_recent();
}
add_help() {
this.options.push({
value: __("Help on Search"),
index: -10,
default: "Help",
onclick: function () {
var txt =
'<table class="table table-bordered">\
<tr><td style="width: 50%">' +
__("Create a new record") +
"</td><td>" +
__("new type of document") +
"</td></tr>\
<tr><td>" +
__("List a document type") +
"</td><td>" +
__("document type..., e.g. customer") +
"</td></tr>\
<tr><td>" +
__("Search in a document type") +
"</td><td>" +
__("text in document type") +
"</td></tr>\
<tr><td>" +
__("Tags") +
"</td><td>" +
__("tag name..., e.g. #tag") +
"</td></tr>\
<tr><td>" +
__("Open a module or tool") +
"</td><td>" +
__("module name...") +
"</td></tr>\
<tr><td>" +
__("Calculate") +
"</td><td>" +
__("e.g. (55 + 434) / 4 or =Math.sin(Math.PI/2)...") +
"</td></tr>\
</table>";
frappe.msgprint(txt, __("Search Help"));
},
});
show_help() {
const txt =
'<table class="table table-bordered">\
<tr><td style="width: 50%">' +
__("Create a new record") +
"</td><td>" +
__("new type of document") +
"</td></tr>\
<tr><td>" +
__("List a document type") +
"</td><td>" +
__("document type..., e.g. customer") +
"</td></tr>\
<tr><td>" +
__("Search in a document type") +
"</td><td>" +
__("text in document type") +
"</td></tr>\
<tr><td>" +
__("Tags") +
"</td><td>" +
__("tag name..., e.g. #tag") +
"</td></tr>\
<tr><td>" +
__("Open a module or tool") +
"</td><td>" +
__("module name...") +
"</td></tr>\
<tr><td>" +
__("Calculate") +
"</td><td>" +
__("e.g. (55 + 434) / 4 or =Math.sin(Math.PI/2)...") +
"</td></tr>\
</table>";
frappe.msgprint(txt, __("Search Help"));
}
set_specifics(txt, end_txt) {
@ -299,7 +357,10 @@ frappe.search.AwesomeBar = class AwesomeBar {
this.options.push({
label: `
<span class="flex justify-between text-medium">
<span class="ellipsis">${__("Search for {0}", [frappe.utils.xss_sanitise(txt).bold()])}</span>
<span class="ellipsis">${
frappe.search.utils.make_icon("search") +
__("Search for {0}", [frappe.utils.xss_sanitise(txt).bold()])
}</span>
<kbd></kbd>
</span>
`,
@ -324,10 +385,12 @@ frappe.search.AwesomeBar = class AwesomeBar {
var options = {};
options[search_field] = ["like", "%" + txt + "%"];
this.options.push({
label: __("Find {0} in {1}", [
frappe.utils.xss_sanitise(txt).bold(),
__(route[1]).bold(),
]),
label:
frappe.search.utils.make_icon("search") +
__("Find {0} in {1}", [
frappe.utils.xss_sanitise(txt).bold(),
__(route[1]).bold(),
]),
value: __("Find {0} in {1}", [frappe.utils.xss_sanitise(txt), __(route[1])]),
route_options: options,
onclick: function () {
@ -394,7 +457,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
make_random(txt) {
if (txt.toLowerCase().includes("random")) {
this.options.push({
label: __("Generate Random Password"),
label: frappe.search.utils.make_icon("key") + __("Generate Random Password"),
value: frappe.utils.get_random(16),
onclick: function () {
frappe.msgprint(frappe.utils.get_random(16), __("Result"));

View file

@ -22,13 +22,13 @@
</span>
{% } %}
<div class="input-group search-bar text-muted hidden">
<input
id="navbar-search"
type="text"
class="form-control"
placeholder="{%= __('Search or type a command ({0})', [frappe.utils.is_mac() ? '⌘ + K' : 'Ctrl + K']) %}"
aria-haspopup="true"
<div
id="navbar-modal-search"
class=""
placeholder="Search for type a command"
>
{%= __('Search or type a command ({0})', [frappe.utils.is_mac() ? '⌘ + K' : 'Ctrl + K']) %}
</div>
<span class="search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>

View file

@ -5,6 +5,11 @@ frappe.search.utils = {
setup_recent: function () {
this.recent = JSON.parse(frappe.boot.user.recent || "[]") || [];
},
make_icon(name) {
return `<span style="padding: 0 3px; margin-right: 5px;">${frappe.utils.icon(
name
)}</span>`;
},
results_to_hide: [],
get_recent_pages: function (keywords) {
if (keywords === null) keywords = "";
@ -66,10 +71,10 @@ frappe.search.utils = {
const doctype = route[1];
if (route.length > 2 && doctype !== route[2]) {
const docname = route[2];
out.label = __(doctype) + " " + docname.bold();
out.label = me.make_icon("sticky-note") + __(doctype) + " " + docname.bold();
out.value = __(doctype) + " " + docname;
} else {
out.label = __(doctype).bold();
out.label = me.make_icon("sticky-note") + __(doctype).bold();
out.value = __(doctype);
}
} else if (
@ -78,24 +83,30 @@ frappe.search.utils = {
) {
const view_type = route[0];
const view_name = route[1];
let icon, labelSuffix;
switch (view_type) {
case "List":
out.label = __("{0} List", [__(view_name).bold()]);
out.value = __("{0} List", [__(view_name)]);
icon = me.make_icon("list");
labelSuffix = __("List");
break;
case "Tree":
out.label = __("{0} Tree", [__(view_name).bold()]);
out.value = __("{0} Tree", [__(view_name)]);
icon = me.make_icon("list-tree");
labelSuffix = __("Tree");
break;
case "Workspaces":
out.label = __("{0} Workspace", [__(view_name).bold()]);
out.value = __("{0} Workspace", [__(view_name)]);
icon = me.make_icon("wallpaper");
labelSuffix = __("Workspace");
break;
case "query-report":
out.label = __("{0} Report", [__(view_name).bold()]);
out.value = __("{0} Report", [__(view_name)]);
icon = me.make_icon("table");
labelSuffix = __("Report");
break;
}
out.label = icon + __(view_name.bold()) + " " + labelSuffix;
out.value = __(view_name) + " " + labelSuffix;
} else if (match[0]) {
out.label = frappe.utils.escape_html(match[0]).bold();
out.value = match[0];
@ -173,7 +184,9 @@ frappe.search.utils = {
if (level) {
out.push({
type: "New",
label: __("New {0}", [search_result.marked_string || __(item)]),
label:
me.make_icon("circle-plus") +
__("New {0}", [search_result.marked_string || __(item)]),
value: __("New {0}", [__(item)]),
index: 1 + level,
match: item,
@ -201,6 +214,13 @@ frappe.search.utils = {
} else {
label = __(`{0} ${skip_list ? "" : type}`, [marked_string || __(target)]);
}
if (type === "List") {
label = me.make_icon("list") + label;
} else if (type === "Report") {
label = me.make_icon("table") + label;
} else {
label = me.make_icon("search") + label;
}
return {
type: type,
label: label,
@ -223,7 +243,9 @@ frappe.search.utils = {
var match = item;
out.push({
type: "New",
label: __("New {0}", [search_result.marked_string || __(item)]),
label:
me.make_icon("circle-plus") +
__("New {0}", [search_result.marked_string || __(item)]),
value: __("New {0}", [__(item)]),
index: score + 0.015,
match: item,
@ -257,7 +279,9 @@ frappe.search.utils = {
else route = ["query-report", item];
out.push({
type: "Report",
label: __("Report {0}", [search_result.marked_string || __(item)]),
label:
me.make_icon("grid-3x3") +
__("Report {0}", [search_result.marked_string || __(item)]),
value: __("Report {0}", [__(item)]),
index: level,
route: route,
@ -283,7 +307,9 @@ frappe.search.utils = {
var page = me.pages[item];
out.push({
type: "Page",
label: __("Open {0}", [search_result.marked_string || __(item)]),
label:
me.make_icon("file-text") +
__("Open {0}", [search_result.marked_string || __(item)]),
value: __("Open {0}", [__(item)]),
match: item,
index: level,
@ -295,6 +321,7 @@ frappe.search.utils = {
if (__("calendar").indexOf(keywords.toLowerCase()) === 0) {
out.push({
type: "Calendar",
label: me.make_icon("calendar") + __("Open {0}", [__(target)]),
value: __("Open {0}", [__(target)]),
index: me.fuzzy_search(keywords, "Calendar"),
match: target,
@ -305,6 +332,7 @@ frappe.search.utils = {
if (__("hub").indexOf(keywords.toLowerCase()) === 0) {
out.push({
type: "Hub",
label: me.make_icon("earth-lock") + __("Open {0}", [__(target)]),
value: __("Open {0}", [__(target)]),
index: me.fuzzy_search(keywords, "Hub"),
match: target,
@ -314,6 +342,7 @@ frappe.search.utils = {
if (__("email inbox").indexOf(keywords.toLowerCase()) === 0) {
out.push({
type: "Inbox",
label: me.make_icon("inbox") + __("Open {0}", [__("Email Inbox")]),
value: __("Open {0}", [__("Email Inbox")]),
index: me.fuzzy_search(keywords, "email inbox"),
match: target,
@ -332,7 +361,9 @@ frappe.search.utils = {
if (level > 0) {
var ret = {
type: "Workspace",
label: __("Open {0}", [search_result.marked_string || __(item.name)]),
label:
me.make_icon("wallpaper") +
__("Open {0}", [search_result.marked_string || __(item.name)]),
value: __("Open {0}", [__(item.name)]),
index: level,
route: [frappe.router.slug(item.name)],
@ -353,7 +384,9 @@ frappe.search.utils = {
if (level > 0) {
var ret = {
type: "Dashboard",
label: __("{0} Dashboard", [search_result.marked_string || __(item.name)]),
label:
me.make_icon("layout-dashboard") +
__("{0} Dashboard", [search_result.marked_string || __(item.name)]),
value: __("{0} Dashboard", [__(item.name)]),
index: level,
route: ["dashboard-view", item.name],
@ -675,7 +708,9 @@ frappe.search.utils = {
const search_result = me.fuzzy_search(keywords, item.title, true);
if (search_result.score > 0) {
var ret = {
label: __("Install {0} from Marketplace", [search_result.marked_string]),
label:
me.make_icon("arrow-down-from-line") +
__("Install {0} from Marketplace", [search_result.marked_string]),
value: __("Install {0} from Marketplace", [__(item.title)]),
index: search_result.score * 0.8,
route: [

View file

@ -184,7 +184,7 @@ frappe.ui.toolbar.Toolbar = class {
setup_awesomebar() {
if (frappe.boot.desk_settings.search_bar) {
let awesome_bar = new frappe.search.AwesomeBar();
awesome_bar.setup("#navbar-search");
awesome_bar.setup("#navbar-modal-search");
frappe.search.utils.make_function_searchable(
frappe.utils.generate_tracking_url,

View file

@ -922,21 +922,44 @@ Object.assign(frappe.utils, {
let route = route_str.split("/");
if (route[2] === "Report" || route[0] === "query-report") {
return (__(route[3]) || __(route[1])).bold() + " " + __("Report");
return (
frappe.search.utils.make_icon("table") +
(__(route[3]) || __(route[1])).bold() +
" " +
__("Report")
);
}
if (route[0] === "List") {
return __(route[1]).bold() + " " + __("List");
return frappe.search.utils.make_icon("list") + __(route[1]).bold() + " " + __("List");
}
if (route[0] === "modules") {
return __(route[1]).bold() + " " + __("Module");
return (
frappe.search.utils.make_icon("component") +
__(route[1]).bold() +
" " +
__("Module")
);
}
if (route[0] === "Workspaces") {
return __(route[1]).bold() + " " + __("Workspace");
return (
frappe.search.utils.make_icon("wallpaper") +
__(route[1]).bold() +
" " +
__("Workspace")
);
}
if (route[0] === "dashboard") {
return __(route[1]).bold() + " " + __("Dashboard");
return (
frappe.search.utils.make_icon("dashboard") +
__(route[1]).bold() +
" " +
__("Dashboard")
);
}
return __(frappe.utils.to_title_case(__(route[0]), true));
return (
frappe.search.utils.make_icon("file-text") +
__(frappe.utils.to_title_case(__(route[0]), true))
);
},
report_column_total: function (values, column, type) {
if (column.column.disable_total) {

View file

@ -799,7 +799,7 @@ frappe.views.Workspace = class Workspace {
"abcdefghijklmnopqrstuvwxyz".split("").forEach((letter) => {
const default_shortcut = {
action: (e) => {
$("#navbar-search").focus();
$("#navbar-modal-search").click();
return false; // don't prevent default = type the letter in awesomebar
},
page: this.page,

View file

@ -44,3 +44,11 @@
color: var(--ink-gray-9) !important;
}
}
#navbar-modal-search {
border-radius: var(--border-radius-sm);
padding: var(--input-padding);
background-color: var(--control-bg);
width: 100%;
padding-left: 40px;
}

View file

@ -90,6 +90,59 @@
}
}
.navbar-modal-wrapper {
.modal-search-icon {
position: absolute;
}
#navbar-search {
padding-left: 42px;
}
.awesomplete {
width: 100%;
ul {
position: relative;
background-color: transparent;
border: none;
border-radius: unset;
box-shadow: none;
margin-top: 12px;
}
ul[hidden] {
display: block !important;
}
}
.modal-divider {
width: 100%;
height: 1px;
background-color: var(--border-color);
position: absolute;
top: 40px;
border-radius: 50%;
}
}
.cool-awesomebar-modal-footer {
padding: 10px 18px !important;
}
.awesomebar-modal-footer {
font-size: 12px;
.help-navigation {
.help-item-navigate {
margin-right: 1rem;
}
.help-item {
background-color: var(--border-color);
padding: 2px;
margin-right: 0.25rem;
border-radius: 4px;
}
}
}
.navbar.bg-dark {
.dropdown-menu {
font-size: 0.75rem;

View file

@ -23,7 +23,7 @@
.navbar {
padding: 0 1rem;
.navbar-search {
.navbar-modal-search {
width: 75vw;
}
}
@ -59,7 +59,7 @@
color: white;
}
.navbar-search {
.navbar-modal-search {
background-color: var(--blue-400);
width: 300px;
margin-right: 10px;

View file

@ -1,7 +1,7 @@
{% if navbar_search %}
<li>
<form action='/search'>
<input name='q' class='form-control navbar-search' type='text'
<input name='q' class='form-control navbar-modal-search' type='text'
value='{{ frappe.form_dict.q|e if frappe.form_dict.q else ''}}'
{% if not frappe.form_dict.q %}placeholder="{{ _("Search...") }}"{% endif %}>
</form>