Optimize global search result view (#2107)

Enhance search result view by sorting the categories after their translation (repositories will still be sticky on top). Further disable categories with no search results and be more explicit with the text displayed if no search results were found.
This commit is contained in:
Matthias Thieroff
2022-08-18 09:04:45 +02:00
committed by GitHub
parent 56ace2811b
commit 3e236fe5ac
6 changed files with 69 additions and 41 deletions

View File

@@ -0,0 +1,2 @@
- type: changed
description: Enhance search result view by sorting translated categories and disable categories with no search results ([#2107](https://github.com/scm-manager/scm-manager/pull/2107))

View File

@@ -203,7 +203,7 @@
"subtitle": "{{type}} Ergebnisse für \"{{query}}\"",
"subtitleWithContext": "{{type}} Ergebnisse für \"{{query}}\" in \"{{context}}\"",
"types": "Ergebnisse",
"noHits": "Die Suche ergab keine Treffer",
"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>",
"quickSearch": {
"resultHeading": "Hilfe beim Suchen?",

View File

@@ -204,7 +204,7 @@
"subtitle": "{{type}} results for \"{{query}}\"",
"subtitleWithContext": "{{type}} results for \"{{query}}\" in \"{{context}}\"",
"types": "Results",
"noHits": "No results found",
"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>",
"quickSearch": {
"resultHeading": "Need help?",

View File

@@ -95,7 +95,7 @@ const AvatarSection: FC<{ repository: Repository }> = ({ repository }) => {
);
};
const HitsList: FC<Omit<HitsProps, "showHelp" | "hits">> = ({ entries }) => {
const HitList: FC<Omit<HitsProps, "showHelp" | "hits">> = ({ entries }) => {
return (
<ul id="omni-search-results" aria-expanded="true" role="listbox">
{entries}
@@ -155,7 +155,7 @@ const Hits: FC<HitsProps> = ({ entries, hits, showHelp, ...rest }) => {
<>
<div className="dropdown-content p-0">
<ScreenReaderHitSummary hits={hits} />
<HitsList entries={entries} {...rest} />
<HitList entries={entries} {...rest} />
<ResultHeading
className={classNames(
"dropdown-item",
@@ -352,7 +352,7 @@ const OmniSearch: FC = () => {
namespaceContext: context.namespace || "",
repositoryNameContext: context.name || "",
});
searchTypes.sort(orderTypes);
searchTypes.sort(orderTypes(t));
const id = useCallback(namespaceAndName, []);
@@ -362,6 +362,7 @@ const OmniSearch: FC = () => {
if (context.namespace && context.name && searchTypes.length > 0) {
newEntries.push(
<HitEntry
key="search.quickSearch.searchRepo"
selected={newEntries.length === index}
clear={clearQuery}
label={t("search.quickSearch.searchRepo")}
@@ -372,6 +373,7 @@ const OmniSearch: FC = () => {
if (context.namespace) {
newEntries.push(
<HitEntry
key="search.quickSearch.searchNamespace"
selected={newEntries.length === index}
clear={clearQuery}
label={t("search.quickSearch.searchNamespace")}
@@ -381,6 +383,7 @@ const OmniSearch: FC = () => {
}
newEntries.push(
<HitEntry
key="search.quickSearch.searchEverywhere"
selected={newEntries.length === index}
clear={clearQuery}
label={t("search.quickSearch.searchEverywhere")}
@@ -391,7 +394,7 @@ const OmniSearch: FC = () => {
hits?.forEach((hit, idx) => {
newEntries.push(
<HitEntry
key={idx}
key={`search.quickSearch.hit${idx}`}
selected={length + idx === index}
clear={clearQuery}
label={id(hit)}

View File

@@ -49,12 +49,12 @@ const Results: FC<Props> = ({ result, type, page, query }) => {
}
return (
<div className="panel">
<Hits type={type} hits={hits} />
<div className="panel-footer">
<LinkPaginator collection={result} page={page} filter={query} />
<>
<div className="panel">
<Hits type={type} hits={hits} />
</div>
</div>
<LinkPaginator collection={result} page={page} filter={query} />
</>
);
};

View File

@@ -34,11 +34,18 @@ import {
urls,
} from "@scm-manager/ui-components";
import { Link, useLocation, useParams } from "react-router-dom";
import { useSearch, useSearchCounts, useSearchTypes, useNamespaceAndNameContext } from "@scm-manager/ui-api";
import { 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";
const DisabledNavLink = styled.div`
opacity: 0.4;
cursor: not-allowed;
`;
type PathParams = {
type: string;
@@ -80,17 +87,16 @@ const usePageParams = () => {
};
};
export const orderTypes = (left: string, right: string) => {
if (left === "repository" && right !== "repository") {
export const orderTypes = (t: TFunction) => (a: string, b: string) => {
if (!a || !b) {
return 0;
}
if (a === "repository" && b !== "repository") {
return -1;
} else if (left !== "repository" && right === "repository") {
return 1;
} else if (left < right) {
return -1;
} else if (left > right) {
} else if (a !== "repository" && b === "repository") {
return 1;
}
return 0;
return t(`plugins:search.types.${a}.navItem`, a)?.localeCompare(t(`plugins:search.types.${b}.navItem`, b)) ?? 0;
};
type Props = {
@@ -144,7 +150,7 @@ const Search: FC = () => {
};
const { data, isLoading, error } = useSearch(query, searchOptions);
const types = useSearchTypes(searchOptions);
types.sort(orderTypes);
types.sort(orderTypes(t));
const searchCounts = useSearchCounts(
types.filter((type) => type !== selectedType),
@@ -174,27 +180,44 @@ const Search: FC = () => {
<Results result={data} query={query} page={page} type={selectedType} />
</PrimaryContentColumn>
<SecondaryNavigation label={t("search.types")} collapsible={false}>
{types.map((type) => (
<NavLink
key={type}
to={`/search/${type}/?q=${query}${namespace ? "&namespace=" + namespace : ""}${
name ? "&name=" + name : ""
}`}
label={type}
activeOnlyWhenExact={false}
>
<Level
left={t(`plugins:search.types.${type}.navItem`, type)}
right={
<Count
isLoading={counts[type].isLoading}
isSelected={type === selectedType}
count={counts[type].data}
{types.map((type) =>
type !== selectedType && (counts[type].isLoading || counts[type].data === 0) ? (
<li>
<DisabledNavLink className="p-4 is-unselectable">
<Level
left={t(`plugins:search.types.${type}.navItem`, type)}
right={
<Count
isLoading={counts[type].isLoading}
isSelected={type === selectedType}
count={counts[type].data}
/>
}
/>
}
/>
</NavLink>
))}
</DisabledNavLink>
</li>
) : (
<NavLink
key={type}
to={`/search/${type}/?q=${query}${namespace ? "&namespace=" + namespace : ""}${
name ? "&name=" + name : ""
}`}
label={type}
activeOnlyWhenExact={false}
>
<Level
left={t(`plugins:search.types.${type}.navItem`, type)}
right={
<Count
isLoading={counts[type].isLoading}
isSelected={type === selectedType}
count={counts[type].data}
/>
}
/>
</NavLink>
)
)}
</SecondaryNavigation>
</CustomQueryFlexWrappedColumns>
) : null}