mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-05-07 05:05:50 +02:00
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:
2
gradle/changelog/searchbar_in_search_page.yaml
Normal file
2
gradle/changelog/searchbar_in_search_page.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: The search page now contains another search bar within, that persists the current query
|
||||
@@ -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>",
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user