Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions 8Knot/assets/color.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
--button-text-light: #fff9f9;

/* MultiSelect pill colors */
--pill-default-bg: #555555;
--pill-default-color: #ffffff;
--pill-pending-bg: #B03A2E;
--pill-text-color: #ffffff;

Comment on lines 62 to 65
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR appears to contain styling changes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is from previous PRs/commits. Once it is rebased, these will be resolved.

/* Table colors */
--table-bg-dark: #222;
Expand All @@ -84,18 +84,18 @@
The .searching class is toggled by a callback in index_callbacks.py


/* Style for filter pills (grey by default when selected) */
/* Style for filter pills (red by default when selected but search not executed) */

.mantine-MultiSelect-pill,
.mantine-MultiSelect-value {
background-color: var(--gray-medium);
color: var(--color-white);
background-color: var(--pill-pending-bg);
color: var(--pill-text-color);
}

.searchbar-dropdown .mantine-MultiSelect-pill,
.searchbar-dropdown .mantine-MultiSelect-value {
background-color: var(--gray-medium);
color: var(--color-white);
background-color: var(--pill-pending-bg);
color: var(--pill-text-color);
}


Expand Down
22 changes: 7 additions & 15 deletions 8Knot/assets/main_layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
--placeholder-top-offset: 1px;
/* MultiSelect specific sizing */
--multiselect-min-height: 48px;
--multiselect-padding: 12px 16px 12px 44px;
--multiselect-padding: 12px 16px;
--multiselect-item-margin: 2px 4px;
}

Expand Down Expand Up @@ -332,20 +332,6 @@ a.sidebar-section-text {
white-space: nowrap;
}

.search-icon {
background-color: transparent;
border: none;
font-size: 16px;
width: 16px;
height: 16px;
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-100%);
font-weight: bold;
z-index: 2;
}


/* Search Controls */

Expand Down Expand Up @@ -473,6 +459,12 @@ button.icon-button.btn:active,
padding: 0 6px;
}

.search-button-wrapper {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-sm);
}

.search-controls-stack {
width: 100%;
justify-content: center;
Expand Down
32 changes: 32 additions & 0 deletions 8Knot/assets/search_keyboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Cascading Enter key handler for the search bar.
*
* When the Mantine MultiSelect dropdown is open (aria-expanded="true"),
* Enter selects the highlighted item (Mantine's default — we do nothing).
*
* When the dropdown is closed, Enter triggers the search by incrementing
* the search-trigger dcc.Store via dash_clientside.set_props.
*/
document.addEventListener("DOMContentLoaded", function () {
var observer = new MutationObserver(function () {
var input = document.querySelector("#projects input");
if (!input) return;

observer.disconnect();

input.addEventListener("keydown", function (e) {
if (e.key !== "Enter") return;

// If dropdown is open, let Mantine handle Enter to select the highlighted item
if (input.getAttribute("aria-expanded") === "true") return;

// Dropdown is closed — trigger search via the dcc.Store
e.preventDefault();
Comment on lines +20 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still fundamentally the same logic that I reviewed in the prior version. Ideally we shouldnt need to call preventDefault anywhere (with the possible exception of the event handler for the individual dropdown items)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well some of the things we've discussed it is not here anymore. It is not going in the opposite direction of the cascade of events anymore. It is a simpler logic as well, two situations, dropdown open, goes through normal events, dropdown closed, trigger the search.

However, as explained on the slack channel, I think we've achieved the objective, which was to allow for navigation and trigger the search all through the keyboard, and this is done with #1113

var store = document.querySelector("#search-trigger");
var current = store ? JSON.parse(store.textContent || "0") : 0;
dash_clientside.set_props("search-trigger", { data: current + 1 });
Comment on lines +25 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain how you ended up on this solution for triggering the search?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to replace that simulation of the click with .click, so instead we store the info through a dcc.store and use it to trigger the search as a function of the dropdown(open, close).
This is still allowing dash normal callbacks flow.

Idea is:
dcc.store("search-trigger") as an input to the callbacks that takes search-button.n_clicks. So the button and the store are equivalent entry points into the logic.
when dropdown is closed (on enter), a new value is stores using dash_clientside.set_props which triggers the whole flow: input -> callbacks -> repo query dispatch.
The counter adds 1, because of the dash callbacks, which only fire if there's changes.

The logic is simple as explained in the previous discussion: mantine's dropdown is open, do nothing, dropdown closed, trigger it.
The desired output in the end is the enter key and the search button goes through the same callback path, and this JS script pretty much just works by translating the keyboard event into storing info/variable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im really skeptical of introducing new dash callback inputs just to give us an alternative path to trigger a search.

is there a <form> that (i assume) wraps the search box? if so, can we call .submit() or the equivalent on that form to use the same codepath as a normal user would encounter without needing to simulate a click?

In other words: in the chain of all things that happen for a search (i.e. pressing the search button calling a function, a function calls some other function, that eventually becomes database calls to fetch data, that eventually updates the UI) can we call the same function that a button press would call, rather than simulating a click or introducing a new parallel path?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MoralCode FYI (quoting @EngCaioFonseca in another thread): "I don't think there's a need for the PR3 (enter key handler) at this point, with the modifications made (#1113), and with this search button, it is possible to navigate the dropdown, then exit and trigger the search using the button, all with the keyboard."

});
});

observer.observe(document.body, { childList: true, subtree: true });
});
23 changes: 13 additions & 10 deletions 8Knot/pages/index/index_callbacks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta

Check warning on line 1 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 W0611: Unused timedelta imported from datetime (unused-import) Raw Output: 8Knot/pages/index/index_callbacks.py:1:0: W0611: Unused timedelta imported from datetime (unused-import)

Check warning on line 1 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 W0611: Unused datetime imported from datetime (unused-import) Raw Output: 8Knot/pages/index/index_callbacks.py:1:0: W0611: Unused datetime imported from datetime (unused-import)
import re

Check warning on line 2 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 W0611: Unused import re (unused-import) Raw Output: 8Knot/pages/index/index_callbacks.py:2:0: W0611: Unused import re (unused-import)
import os
import time
Expand All @@ -8,7 +8,7 @@
import dash_bootstrap_components as dbc
import dash
from dash import callback, html
from dash.dependencies import Input, Output, State, MATCH

Check warning on line 11 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 W0611: Unused MATCH imported from dash.dependencies (unused-import) Raw Output: 8Knot/pages/index/index_callbacks.py:11:0: W0611: Unused MATCH imported from dash.dependencies (unused-import)
from app import augur
from flask_login import current_user
from cache_manager.cache_manager import CacheManager as cm
Expand All @@ -32,7 +32,7 @@
from queries.repo_info_query import repo_info_query as riq
from models import SearchItem
import redis
import flask

Check warning on line 35 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 W0611: Unused import flask (unused-import) Raw Output: 8Knot/pages/index/index_callbacks.py:35:0: W0611: Unused import flask (unused-import)
from .search_utils import fuzzy_search
from .search_utils import clean_repo_name

Expand Down Expand Up @@ -78,7 +78,7 @@
# TODO: check how old groups are. If they're pretty old (threshold tbd) then requery

# check if groups are not already cached, or if the refresh-button was pressed
if not users_cache.exists(f"{user_id}_groups") or (dash.ctx.triggered_id == "refresh-button"):

Check warning on line 81 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return) Raw Output: 8Knot/pages/index/index_callbacks.py:81:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
# kick off celery task to collect groups
# on query worker queue,
return [ugq.apply_async(args=[user_id], queue="data").id]
Expand Down Expand Up @@ -423,7 +423,7 @@
elif search_item == SearchItem.REPO:
formatted_v["label"] = f"repo: {v['label']}"
default_options.append(formatted_v)
except:

Check warning on line 426 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 W0702: No exception type(s) specified (bare-except) Raw Output: 8Knot/pages/index/index_callbacks.py:426:12: W0702: No exception type(s) specified (bare-except)
# If that fails, just return the raw selection values
default_options = [{"value": v, "label": f"ID: {v}"} for v in selections]

Expand All @@ -436,11 +436,14 @@
@callback(
[Output("results-output-container", "children"), Output("repo-choices", "data")],
[
Input("search", "n_clicks"),
Input("search-button", "n_clicks"),
Input("search-trigger", "data"),
State("projects", "value"),
],
)
def multiselect_values_to_repo_ids(n_clicks, user_vals):
def multiselect_values_to_repo_ids(search_button_clicks, search_trigger, user_vals):
if dash.ctx.triggered_id not in ("search-button", "search-trigger"):
raise dash.exceptions.PreventUpdate
if not user_vals:
logging.warning("NOTHING SELECTED IN SEARCH BAR")
raise dash.exceptions.PreventUpdate
Expand Down Expand Up @@ -789,7 +792,7 @@
ctx = dash.callback_context

# Check which input triggered the callback
if ctx.triggered_id == "sidebar-toggle" and n_clicks:

Check warning on line 795 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return) Raw Output: 8Knot/pages/index/index_callbacks.py:795:4: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
# Manual toggle - simply toggle the collapse state
return not is_open
elif ctx.triggered_id == "url":
Expand Down Expand Up @@ -852,7 +855,7 @@
)
def hide_loading_on_landing(pathname):
"""Hide loading components when on landing page."""
if pathname == "/" or pathname is None:

Check warning on line 858 in 8Knot/pages/index/index_callbacks.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return) Raw Output: 8Knot/pages/index/index_callbacks.py:858:4: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
# Hide loading components on landing page
return {"display": "none"}, {"display": "none"}
else:
Expand All @@ -867,35 +870,35 @@
# Callback to change pill color when search is clicked
#
# This callback implements dynamic pill coloring:
# - When user selects repos/orgs: pills are grey (pending)
# - When user selects repos/orgs: pills are red (pending)
# - When user clicks search icon: pills turn blue (active search)
# - Default selection (chaoss) starts blue since search is auto-triggered
#
# Works in conjunction with CSS in main_layout.css
# ============================================================================
@callback(
Output("projects", "className"),
[Input("search", "n_clicks"), Input("projects", "value")],
[Input("search-button", "n_clicks"), Input("search-trigger", "data"), Input("projects", "value")],
prevent_initial_call=True,
)
def update_pill_color_on_search(_, selected_repos_orgs):
def update_pill_color_on_search(search_button_clicks, search_trigger, selected_repos_orgs):
"""Update pill color based on search action.

When search icon is clicked, add 'searching' class to turn pills blue.
When values change (user is selecting), remove 'searching' class to keep pills grey.
When values change (user is selecting), remove 'searching' class to keep pills red.
"""
if not dash.ctx.triggered:
return dash.no_update

triggered_id = dash.ctx.triggered_id

if triggered_id == "search":
# Search button clicked - add 'searching' class to turn pills blue
if triggered_id in ("search-button", "search-trigger"):
# Search button clicked or Enter key pressed - turn pills blue
logging.info(f"PILL COLOR: Search clicked - turning pills BLUE")
return "searchbar-dropdown searching"
if triggered_id == "projects":
# Values changed (user selecting) - remove 'searching' class to keep pills grey
logging.info(f"PILL COLOR: Values changed - turning pills GREY. Selected: {selected_repos_orgs}")
# Values changed (user selecting) - remove 'searching' class to keep pills red
logging.info(f"PILL COLOR: Values changed - turning pills RED. Selected: {selected_repos_orgs}")
return "searchbar-dropdown"

return dash.no_update
41 changes: 17 additions & 24 deletions 8Knot/pages/index/search_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@
return options

# For very short queries (1-2 chars), use a simpler and faster matching approach
if len(query) <= 2:

Check warning on line 178 in 8Knot/pages/index/search_utils.py

View workflow job for this annotation

GitHub Actions / python-lint

[pylint] reported by reviewdog 🐶 R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return) Raw Output: 8Knot/pages/index/search_utils.py:178:4: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
return search_short_query(query, options)
else:
return search_with_fuzzy_matching(query, options, threshold, limit)
Expand Down Expand Up @@ -404,6 +404,7 @@
create_store("cached-options", storage_type="session"),
html.Div(id="cache-init-trigger", className="hidden"),
create_store("search-cache-init-hidden", storage_type="session"),
create_store("search-trigger", storage_type="memory", data=0),
]


Expand Down Expand Up @@ -443,14 +444,14 @@
"margin": "var(--multiselect-item-margin)",
"color": "var(--color-white)",
},
# Inline styles for grey pill default color (turns blue with "searching" class)
# Inline styles for pending pill color (red by default, turns blue with "searching" class)
"value": {
"backgroundColor": "var(--pill-default-bg)",
"color": "var(--pill-default-color)",
"backgroundColor": "var(--pill-pending-bg)",
"color": "var(--pill-text-color)",
},
"pill": {
"backgroundColor": "var(--pill-default-bg)",
"color": "var(--pill-default-color)",
"backgroundColor": "var(--pill-pending-bg)",
"color": "var(--pill-text-color)",
},
}

Expand Down Expand Up @@ -496,34 +497,26 @@
html.Div(
[
create_search_multiselect(initial_option),
create_button(
button_id="search",
content=html.I(className="fas fa-search"),
title="Search",
custom_style={
"backgroundColor": "transparent",
"border": "none",
"fontSize": "16px",
"width": "16px",
"height": "16px",
"position": "absolute",
"left": "10px",
"top": "50%",
"transform": "translateY(-100%)",
"fontWeight": "bold",
"zIndex": 2,
},
),
],
className="search-input-wrapper",
),
html.Div(
dbc.Button(
"Search",
id="search-button",
color="outline-secondary",
size="sm",
className="about-graph-button",
),
className="search-button-wrapper",
),
create_alert(
alert_id="help-alert",
children='Please ensure that your spelling is correct. \
If your selection definitely isn\'t present, please request that \
it be loaded using the help button "REPO/ORG Request" \
in the bottom right corner of the screen. \
The search is only confirmed when you click the search icon and the pill turns blue.',
The search is only confirmed when you click the Search button and the pill turns blue.',
color="info",
),
create_alert(
Expand Down
Loading