Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise search page design #1025

Merged
merged 11 commits into from
Dec 21, 2023
2 changes: 1 addition & 1 deletion backend/src/api/model/known_roles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,5 @@ pub(crate) async fn search_known_users(
items.extend(results.hits.into_iter().map(|h| h.result));
}

Ok(KnownUsersSearchOutcome::Results(SearchResults { items }))
Ok(KnownUsersSearchOutcome::Results(SearchResults { items, total_hits: None }))
}
4 changes: 4 additions & 0 deletions backend/src/api/model/search/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ impl search::Event {
&self.title
}

fn series_id(&self) -> Option<Id> {
self.series_id.map(|id| Id::search_series(id.0))
}

fn series_title(&self) -> Option<&str> {
self.series_title.as_deref()
}
Expand Down
22 changes: 17 additions & 5 deletions backend/src/api/model/search/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ pub(crate) enum SearchOutcome {

pub(crate) struct SearchResults<T> {
pub(crate) items: Vec<T>,
pub(crate) total_hits: Option<usize>,
}

#[juniper::graphql_object(Context = Context)]
impl SearchResults<NodeValue> {
fn items(&self) -> &[NodeValue] {
&self.items
}
fn total_hits(&self) -> Option<i32> {
self.total_hits.map(|usize| usize as i32)
}
}

#[juniper::graphql_object(Context = Context, name = "EventSearchResults")]
Expand Down Expand Up @@ -122,13 +126,14 @@ pub(crate) async fn perform(
let query = format!("select {selection} from search_events \
where id = (select id from events where opencast_id = $1) \
and (read_roles || 'ROLE_ADMIN'::text) && $2");
let items = context.db
let items: Vec<NodeValue> = context.db
.query_opt(&query, &[&uuid_query, &context.auth.roles_vec()])
.await?
.map(|row| search::Event::from_row_start(&row).into())
.into_iter()
.collect();
return Ok(SearchOutcome::Results(SearchResults { items }));
let total_hits = items.len();
return Ok(SearchOutcome::Results(SearchResults { items, total_hits: Some(total_hits) }));
}


Expand Down Expand Up @@ -179,8 +184,13 @@ pub(crate) async fn perform(
let mut merged = realms.chain(events).collect::<Vec<_>>();
merged.sort_unstable_by(|(_, score0), (_, score1)| score1.unwrap().total_cmp(&score0.unwrap()));

let total_hits: usize = [event_results.estimated_total_hits, realm_results.estimated_total_hits]
.iter()
.filter_map(|&x| x)
.sum();

let items = merged.into_iter().map(|(node, _)| node).collect();
Ok(SearchOutcome::Results(SearchResults { items }))
Ok(SearchOutcome::Results(SearchResults { items, total_hits: Some(total_hits) }))
}

fn looks_like_opencast_uuid(query: &str) -> bool {
Expand Down Expand Up @@ -240,8 +250,9 @@ pub(crate) async fn all_events(
.await;
let results = handle_search_result!(res, EventSearchOutcome);
let items = results.hits.into_iter().map(|h| h.result).collect();
let total_hits = results.estimated_total_hits.unwrap_or(0);

Ok(EventSearchOutcome::Results(SearchResults { items }))
Ok(EventSearchOutcome::Results(SearchResults { items, total_hits: Some(total_hits) }))
}

// See `EventSearchOutcome` for additional information.
Expand Down Expand Up @@ -292,8 +303,9 @@ pub(crate) async fn all_series(
.await;
let results = handle_search_result!(res, SeriesSearchOutcome);
let items = results.hits.into_iter().map(|h| h.result).collect();
let total_hits = results.estimated_total_hits.unwrap_or(0);

Ok(SeriesSearchOutcome::Results(SearchResults { items }))
Ok(SeriesSearchOutcome::Results(SearchResults { items, total_hits: Some(total_hits) }))
}

fn acl_filter(action: &str, context: &Context) -> Option<Filter> {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ realm:

search:
input-label: Suche
title: Suchergebnisse für „{{query}}“
title: Suchergebnisse für „{{query}}“ ({{hits}} Treffer)
no-query: Suchergebnisse
no-results: Keine Ergebnisse
too-few-characters: Tippen Sie weitere Zeichen, um die Suche zu starten.
unavailable: Die Suchfunktion ist zurzeit nicht verfügbar. Versuchen Sie es später erneut.
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ realm:

search:
input-label: Search
title: Search results for “{{query}}”
title: Search results for “{{query}}” ({{hits}} hits)
no-query: Search results
no-results: No results
too-few-characters: Please type more characters to start the search.
unavailable: The search is currently unavailable. Try again later.
Expand Down
30 changes: 27 additions & 3 deletions frontend/src/layout/header/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React, { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { HiOutlineSearch } from "react-icons/hi";
import { LuX } from "react-icons/lu";
import { useRouter } from "../../router";
import { isSearchActive } from "../../routes/Search";
import { handleNavigation, isSearchActive } from "../../routes/Search";
import { focusStyle } from "../../ui";
import { Spinner } from "../../ui/Spinner";
import { currentRef } from "../../util";

import { BREAKPOINT as NAV_BREAKPOINT } from "../Navigation";
import { COLORS } from "../../color";
import { screenWidthAtMost } from "@opencast/appkit";
import { ProtoButton, screenWidthAtMost } from "@opencast/appkit";


type SearchFieldProps = {
variant: "desktop" | "mobile";
Expand All @@ -23,6 +25,12 @@ export const SearchField: React.FC<SearchFieldProps> = ({ variant }) => {
// Register global shortcut to focus search bar
useEffect(() => {
const handleShortcut = (ev: KeyboardEvent) => {
// Clear input field. See comment inside `handleNavigation` function
// in `routes/Search.tsx` for explanation. I'd rather do this here than
// needing to pass the input ref.
if (ev.key === "Escape" && ref.current) {
ref.current.value = "";
}
// If any element is focussed that could receive character input, we
// don't do anything.
if (/^input|textarea|select|button$/i.test(document.activeElement?.tagName ?? "")) {
Expand All @@ -47,6 +55,8 @@ export const SearchField: React.FC<SearchFieldProps> = ({ variant }) => {
const height = 42;
const spinnerSize = 24;
const paddingSpinner = (height - spinnerSize) / 2;
const iconStyle = { position: "absolute", right: paddingSpinner, top: paddingSpinner } as const;


const lastTimeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => () => clearTimeout(lastTimeout.current));
Expand Down Expand Up @@ -128,8 +138,22 @@ export const SearchField: React.FC<SearchFieldProps> = ({ variant }) => {
</form>
{router.isTransitioning && isSearchActive() && <Spinner
size={spinnerSize}
css={{ position: "absolute", right: paddingSpinner, top: paddingSpinner }}
css={iconStyle}
/>}
{!router.isTransitioning && isSearchActive() && <ProtoButton
onClick={() => handleNavigation(router, ref)}
css={{
":hover, :focus": {
color: COLORS.neutral90,
borderColor: COLORS.neutral25,
outline: `2.5px solid ${COLORS.neutral25}`,
},
...focusStyle({}),
borderRadius: 4,
color: COLORS.neutral60,
...iconStyle,
}}
><LuX size={spinnerSize} css={{ display: "block" }} /></ProtoButton>}
</div>
);
};
6 changes: 6 additions & 0 deletions frontend/src/rauta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ export interface RouterControl {
* to show a loading indicator.
*/
isTransitioning: boolean;

/**
* Indicates whether a user navigated to the current route from outside Tobira.
*/
internalOrigin: boolean;
}

export const makeRouter = <C extends Config, >(config: C): RouterLib => {
Expand Down Expand Up @@ -261,6 +266,7 @@ export const makeRouter = <C extends Config, >(config: C): RouterLib => {
debugLog(`Setting active route for '${href}' (index ${currentIndex}) `
+ "to: ", newRoute);
},
internalOrigin: window.history.state.index > 0,
};
};

Expand Down
Loading