Additional Search in the Search Page

Another Search Bar is added in the Search Page directly. This one persists the current query even after triggering another search.

Committed-by: Tarik Gürsoy <tarik.guersoy@cloudogu.com>
Authored-by: tzerr <thomas.zerr@cloudogu.com>
This commit is contained in:
Thomas Zerr
2023-09-05 12:40:04 +02:00
parent 3a99fe7a82
commit 14ded177c1
6 changed files with 87 additions and 29 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: The search page now contains another search bar within, that persists the current query

View File

@@ -260,8 +260,8 @@
"ariaLabel": "Globale Suche",
"placeholder": "Suche...",
"title": "Suche",
"subtitle": "{{type}} Ergebnisse für \"{{query}}\"",
"subtitleWithContext": "{{type}} Ergebnisse für \"{{query}}\" in \"{{context}}\"",
"subtitle": "{{type}} Ergebnisse",
"subtitleWithContext": "{{type}} Ergebnisse für in \"{{context}}\"",
"types": "Ergebnisse",
"noHits": "Keine Ergebnisse gefunden. Auf der rechten Seite finden Sie weitere Kategorien, in denen Ergebnisse vorhanden sein könnten",
"syntaxHelp": "Finden Sie bessere Ergebnisse durch die Nutzung der vollen <0>Such-Syntax</0>",

View File

@@ -261,8 +261,8 @@
"ariaLabel": "Global search",
"placeholder": "Search...",
"title": "Search",
"subtitle": "{{type}} results for \"{{query}}\"",
"subtitleWithContext": "{{type}} results for \"{{query}}\" in \"{{context}}\"",
"subtitle": "{{type}} results",
"subtitleWithContext": "{{type}} results in \"{{context}}\"",
"types": "Results",
"noHits": "No results found. On the right side you will find other categories where results might be available",
"syntaxHelp": "Find better results by using the full <0>search syntax</0>",

View File

@@ -139,7 +139,7 @@ const NavigationBar: FC<Props> = ({ links }) => {
</div>
<div className="is-active navbar-header-actions">
<Alerts className="navbar-item" />
<OmniSearch links={links} />
<OmniSearch links={links} shouldClear={true} ariaId="navbar" />
<Notifications className="navbar-item" />
</div>
<div className="navbar-end">

View File

@@ -53,6 +53,11 @@ const Input = styled.input`
`;
type Props = {
shouldClear: boolean;
ariaId: string;
};
type GuardProps = Props & {
links: Links;
};
@@ -67,6 +72,7 @@ type HitsProps = {
entries: ReactElement<ExtractProps<typeof HitEntry>>[];
hits: Hit[];
showHelp: () => void;
ariaId: string;
};
const QuickSearchNotification: FC = ({ children }) => <div className="dropdown-content p-4">{children}</div>;
@@ -97,21 +103,22 @@ const AvatarSection: FC<{ repository: Repository }> = ({ repository }) => {
);
};
const HitList: FC<Omit<HitsProps, "showHelp" | "hits">> = ({ entries }) => {
const HitList: FC<Omit<HitsProps, "showHelp" | "hits">> = ({ entries, ariaId }) => {
return (
<ul id="omni-search-results" aria-expanded="true" role="listbox">
<ul id={`omni-search-results-${ariaId}`} aria-expanded="true" role="listbox">
{entries}
</ul>
);
};
const HitEntry: FC<{ selected: boolean; link: string; label: string; clear: () => void; repository?: Repository }> = ({
selected,
link,
label,
clear,
repository,
}) => {
const HitEntry: FC<{
selected: boolean;
link: string;
label: string;
clear: () => void;
repository?: Repository;
ariaId: string;
}> = ({ selected, link, label, clear, repository, ariaId }) => {
return (
<li
key={label}
@@ -119,7 +126,7 @@ const HitEntry: FC<{ selected: boolean; link: string; label: string; clear: () =
onClick={clear}
role="option"
aria-selected={selected}
id={selected ? "omni-search-selected-option" : undefined}
id={selected ? `omni-search-selected-option-${ariaId}` : undefined}
>
<Link
className={classNames("is-flex", "dropdown-item", "has-text-weight-medium", "is-ellipsis-overflow", {
@@ -327,7 +334,7 @@ const useSearchParams = () => {
};
};
const OmniSearch: FC = () => {
const OmniSearch: FC<Props> = ({ shouldClear, ariaId }) => {
const [t] = useTranslation("commons");
const { searchType, initialQuery } = useSearchParams();
const [query, setQuery] = useState(initialQuery);
@@ -345,9 +352,17 @@ const OmniSearch: FC = () => {
setIndex(-1);
}, []);
useEffect(() => {
setQuery(shouldClear ? "" : initialQuery);
}, [shouldClear, initialQuery]);
const openHelp = () => setShowHelp(true);
const closeHelp = () => setShowHelp(false);
const clearQuery = useCallback(() => setQuery(""), []);
const clearQuery = useCallback(() => {
if (shouldClear) {
setQuery("");
}
}, [shouldClear]);
const hits = data?._embedded?.hits || [];
const searchTypes = useSearchTypes({
@@ -375,6 +390,7 @@ const OmniSearch: FC = () => {
link={`/search/${searchTypes[0]}/?q=${encodeURIComponent(query)}&namespace=${context.namespace}&name=${
context.name
}`}
ariaId={ariaId}
/>
);
}
@@ -386,6 +402,7 @@ const OmniSearch: FC = () => {
clear={clearQuery}
label={t("search.quickSearch.searchNamespace")}
link={`/search/repository/?q=${encodeURIComponent(query)}&namespace=${context.namespace}`}
ariaId={ariaId}
/>
);
}
@@ -396,6 +413,7 @@ const OmniSearch: FC = () => {
clear={clearQuery}
label={t("search.quickSearch.searchEverywhere")}
link={`/search/repository/?q=${encodeURIComponent(query)}`}
ariaId={ariaId}
/>
);
const length = newEntries.length;
@@ -408,11 +426,12 @@ const OmniSearch: FC = () => {
label={id(hit)}
link={`/repo/${id(hit)}`}
repository={hit._embedded?.repository}
ariaId={ariaId}
/>
);
});
return newEntries;
}, [clearQuery, context.name, context.namespace, hits, id, index, query, searchTypes, t]);
}, [ariaId, clearQuery, context.name, context.namespace, hits, id, index, query, searchTypes, t]);
const defaultLink = `/search/${searchType}/?q=${encodeURIComponent(query)}`;
const { onKeyDown } = useKeyBoardNavigation(entries, clearQuery, hideResults, index, setIndex, defaultLink);
@@ -428,7 +447,7 @@ const OmniSearch: FC = () => {
<div className={classNames("dropdown", { "is-active": (!!data || error) && showResults })}>
<div className="dropdown-trigger">
<SearchInput
className="input is-small"
className="input is-small omni-search-bar"
type="text"
placeholder={t("search.placeholder")}
onChange={(e) => setQuery(e.target.value)}
@@ -439,8 +458,8 @@ const OmniSearch: FC = () => {
data-omnisearch="true"
aria-expanded={query.length > 2}
aria-label={t("search.ariaLabel")}
aria-owns="omni-search-results"
aria-activedescendant={index >= 0 ? "omni-search-selected-option" : undefined}
aria-owns={`omni-search-results-${ariaId}`}
aria-activedescendant={index >= 0 ? `omni-search-selected-option-${ariaId}` : undefined}
ref={searchInputRef}
{...handlers}
/>
@@ -456,7 +475,7 @@ const OmniSearch: FC = () => {
<SearchErrorNotification error={error} showHelp={openHelp} />
</QuickSearchNotification>
) : null}
{!error && data ? <Hits showHelp={openHelp} hits={hits} entries={entries} /> : null}
{!error && data ? <Hits showHelp={openHelp} hits={hits} entries={entries} ariaId={ariaId} /> : null}
</DropdownMenu>
</div>
</div>
@@ -464,11 +483,11 @@ const OmniSearch: FC = () => {
);
};
const OmniSearchGuard: FC<Props> = ({ links }) => {
const OmniSearchGuard: FC<GuardProps> = ({ links, shouldClear, ariaId }) => {
if (!links.search) {
return null;
}
return <OmniSearch />;
return <OmniSearch shouldClear={shouldClear} ariaId={ariaId} />;
};
export default OmniSearchGuard;

View File

@@ -35,19 +35,53 @@ import {
urls,
} from "@scm-manager/ui-components";
import { Link, useLocation, useParams } from "react-router-dom";
import { useNamespaceAndNameContext, useSearch, useSearchCounts, useSearchTypes } from "@scm-manager/ui-api";
import { useIndex, useNamespaceAndNameContext, useSearch, useSearchCounts, useSearchTypes } from "@scm-manager/ui-api";
import Results from "./Results";
import { Trans, useTranslation } from "react-i18next";
import SearchErrorNotification from "./SearchErrorNotification";
import SyntaxModal from "./SyntaxModal";
import type { TFunction } from "i18next";
import styled from "styled-components";
import OmniSearch from "../containers/OmniSearch";
import { Links } from "@scm-manager/ui-types";
const DisabledNavLink = styled.div`
opacity: 0.4;
cursor: not-allowed;
`;
const OmniSearchWrapper = styled.div`
.omni-search-bar {
width: 100% !important;
font-size: 1rem !important;
}
.icon {
font-size: 1rem !important;
}
.navbar-item {
padding: 0 !important;
}
.dropdown {
width: 100% !important;
}
.dropdown-trigger {
width: 100% !important;
}
.control {
width: 100% !important;
}
.dropdown-menu {
width: 100% !important;
max-width: none !important;
}
`;
type PathParams = {
type: string;
page: string;
@@ -103,27 +137,29 @@ export const orderTypes = (t: TFunction) => (a: string, b: string) => {
type Props = {
selectedType: string;
query: string;
links: Links;
};
const SyntaxHelpLink: FC = ({ children }) => <Link to="/help/search-syntax">{children}</Link>;
const SearchSubTitle: FC<Props> = ({ selectedType, query }) => {
const SearchSubTitle: FC<Props> = ({ selectedType, query, links }) => {
const [t] = useTranslation("commons");
const context = useNamespaceAndNameContext();
return (
<>
{context.namespace
? t("search.subtitleWithContext", {
query,
type: t(`plugins:search.types.${selectedType}.subtitle`, selectedType),
context: `${context.namespace}${context.name ? `/${context.name}` : ""}`,
})
: t("search.subtitle", {
query,
type: t(`plugins:search.types.${selectedType}.subtitle`, selectedType),
})}
<br />
<Trans i18nKey="search.syntaxHelp" components={[<SyntaxHelpLink />]} />
<OmniSearchWrapper className={"mt-4 mb-2"}>
<OmniSearch links={links} shouldClear={false} ariaId={"searchPage"} />
</OmniSearchWrapper>
</>
);
};
@@ -134,6 +170,7 @@ const InvalidSearch: FC = () => {
};
const Search: FC = () => {
const { data: index } = useIndex();
const [t] = useTranslation(["commons", "plugins"]);
const [showHelp, setShowHelp] = useState(false);
const { query, selectedType, page, namespace, name } = usePageParams();
@@ -179,7 +216,7 @@ const Search: FC = () => {
return (
<Page
title={t("search.title")}
subtitle={<SearchSubTitle query={query} selectedType={selectedType} />}
subtitle={<SearchSubTitle query={query} selectedType={selectedType} links={index?._links || {}} />}
loading={isLoading}
>
{showHelp ? <SyntaxModal close={() => setShowHelp(false)} /> : null}