@redocly/theme 0.42.3 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Buttons/CopyButton.js +2 -2
- package/lib/components/Buttons/EditPageButton.js +1 -1
- package/lib/components/Catalog/CatalogActions.js +1 -1
- package/lib/components/Dropdown/DropdownMenu.d.ts +2 -0
- package/lib/components/Dropdown/DropdownMenu.js +3 -1
- package/lib/components/Feedback/Comment.js +6 -6
- package/lib/components/Feedback/Mood.js +7 -7
- package/lib/components/Feedback/Rating.js +4 -4
- package/lib/components/Feedback/Reasons.js +3 -3
- package/lib/components/Feedback/Scale.js +10 -10
- package/lib/components/Feedback/Sentiment.js +5 -5
- package/lib/components/Filter/FilterContent.js +2 -2
- package/lib/components/Filter/FilterInput.js +1 -1
- package/lib/components/Filter/FilterPopover.js +2 -2
- package/lib/components/Filter/FilterSelect.js +1 -1
- package/lib/components/Footer/FooterCopyright.js +2 -2
- package/lib/components/LastUpdated/LastUpdated.js +1 -1
- package/lib/components/Loaders/SpinnerLoader.d.ts +5 -0
- package/lib/components/Loaders/SpinnerLoader.js +32 -0
- package/lib/components/PageNavigation/NextButton.js +1 -1
- package/lib/components/PageNavigation/PreviousButton.js +1 -1
- package/lib/components/Product/ProductPicker.js +1 -1
- package/lib/components/Search/FilterFields/SearchFilterFieldSelect.d.ts +12 -0
- package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +113 -0
- package/lib/components/Search/FilterFields/SearchFilterFieldTags.d.ts +10 -0
- package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +37 -0
- package/lib/components/Search/Search.js +1 -1
- package/lib/components/Search/SearchDialog.js +113 -31
- package/lib/components/Search/SearchFilter.d.ts +11 -0
- package/lib/components/Search/SearchFilter.js +71 -0
- package/lib/components/Search/SearchFilterField.d.ts +11 -0
- package/lib/components/Search/SearchFilterField.js +43 -0
- package/lib/components/Search/SearchGroups.d.ts +9 -0
- package/lib/components/Search/SearchGroups.js +69 -0
- package/lib/components/Search/SearchHighlight.d.ts +1 -1
- package/lib/components/Search/SearchHighlight.js +28 -5
- package/lib/components/Search/SearchInput.d.ts +1 -1
- package/lib/components/Search/SearchInput.js +5 -2
- package/lib/components/Search/SearchItem.d.ts +2 -2
- package/lib/components/Search/SearchItem.js +24 -15
- package/lib/components/Search/SearchRecent.js +1 -1
- package/lib/components/Search/SearchSuggestedPages.js +1 -1
- package/lib/components/Search/SearchTrigger.js +2 -2
- package/lib/components/Search/variables.js +48 -2
- package/lib/components/Segmented/Segmented.d.ts +2 -5
- package/lib/components/Select/Select.d.ts +2 -36
- package/lib/components/Select/Select.js +136 -98
- package/lib/components/Select/SelectInput.d.ts +23 -0
- package/lib/components/Select/SelectInput.js +129 -0
- package/lib/components/Select/variables.js +12 -1
- package/lib/components/SidebarActions/ChangeViewButton.js +1 -1
- package/lib/components/SidebarActions/SidebarActions.js +2 -2
- package/lib/components/TableOfContent/TableOfContent.js +1 -1
- package/lib/components/Tag/Tag.d.ts +4 -2
- package/lib/components/Tag/Tag.js +40 -4
- package/lib/components/Tag/variables.dark.js +20 -5
- package/lib/components/Tag/variables.js +49 -17
- package/lib/components/UserMenu/LoginButton.js +1 -1
- package/lib/components/UserMenu/LogoutMenuItem.js +1 -1
- package/lib/components/UserMenu/UserMenu.js +1 -1
- package/lib/components/VersionPicker/VersionPicker.d.ts +2 -3
- package/lib/components/VersionPicker/VersionPicker.js +14 -31
- package/lib/core/hooks/__mocks__/index.d.ts +2 -1
- package/lib/core/hooks/__mocks__/index.js +2 -1
- package/lib/core/hooks/__mocks__/search/use-search-filter.d.ts +9 -0
- package/lib/core/hooks/__mocks__/search/use-search-filter.js +14 -0
- package/lib/core/hooks/__mocks__/use-theme-hooks.d.ts +6 -1
- package/lib/core/hooks/__mocks__/use-theme-hooks.js +6 -1
- package/lib/core/hooks/feedback/use-report-dialog.js +3 -3
- package/lib/core/hooks/index.d.ts +2 -1
- package/lib/core/hooks/index.js +2 -1
- package/lib/core/hooks/menu/use-mobile-menu-items.js +1 -1
- package/lib/core/hooks/menu/use-mobile-menu-levels.js +2 -2
- package/lib/core/hooks/search/use-recent-searches.js +2 -0
- package/lib/core/hooks/{use-search.d.ts → search/use-search-dialog.d.ts} +1 -1
- package/lib/core/hooks/{use-search.js → search/use-search-dialog.js} +5 -5
- package/lib/core/hooks/search/use-search-filter.d.ts +9 -0
- package/lib/core/hooks/search/use-search-filter.js +50 -0
- package/lib/core/types/hooks.d.ts +17 -4
- package/lib/core/types/index.d.ts +1 -1
- package/lib/core/types/index.js +1 -1
- package/lib/core/types/l10n.d.ts +1 -2
- package/lib/core/types/search.d.ts +42 -2
- package/lib/core/types/select.d.ts +31 -0
- package/lib/core/types/{select-option.js → select.js} +1 -1
- package/lib/core/utils/index.d.ts +1 -0
- package/lib/core/utils/index.js +1 -0
- package/lib/core/utils/menu.js +1 -1
- package/lib/core/utils/text-trimmer.d.ts +1 -0
- package/lib/core/utils/text-trimmer.js +16 -0
- package/lib/icons/ResetIcon/ResetIcon.d.ts +9 -0
- package/lib/icons/ResetIcon/ResetIcon.js +22 -0
- package/lib/icons/SettingsIcon/SettingsIcon.d.ts +9 -0
- package/lib/icons/SettingsIcon/SettingsIcon.js +23 -0
- package/lib/index.d.ts +8 -1
- package/lib/index.js +8 -1
- package/lib/layouts/Forbidden.js +2 -2
- package/lib/layouts/NotFound.js +3 -3
- package/lib/layouts/OIDCForbidden.js +1 -1
- package/lib/markdoc/tags/partial.js +1 -1
- package/package.json +9 -9
- package/src/components/Buttons/CopyButton.tsx +2 -2
- package/src/components/Buttons/EditPageButton.tsx +2 -2
- package/src/components/Catalog/CatalogActions.tsx +2 -2
- package/src/components/Dropdown/DropdownMenu.tsx +2 -1
- package/src/components/Feedback/Comment.tsx +8 -8
- package/src/components/Feedback/Mood.tsx +8 -8
- package/src/components/Feedback/Rating.tsx +5 -5
- package/src/components/Feedback/Reasons.tsx +4 -4
- package/src/components/Feedback/Scale.tsx +13 -13
- package/src/components/Feedback/Sentiment.tsx +6 -6
- package/src/components/Filter/FilterContent.tsx +3 -3
- package/src/components/Filter/FilterInput.tsx +1 -1
- package/src/components/Filter/FilterPopover.tsx +3 -3
- package/src/components/Filter/FilterSelect.tsx +5 -5
- package/src/components/Footer/FooterCopyright.tsx +3 -3
- package/src/components/LastUpdated/LastUpdated.tsx +1 -2
- package/src/components/Loaders/SpinnerLoader.tsx +31 -0
- package/src/components/PageNavigation/NextButton.tsx +1 -1
- package/src/components/PageNavigation/PreviousButton.tsx +1 -1
- package/src/components/Product/ProductPicker.tsx +2 -2
- package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +135 -0
- package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +61 -0
- package/src/components/Search/Search.tsx +2 -2
- package/src/components/Search/SearchDialog.tsx +190 -51
- package/src/components/Search/SearchFilter.tsx +90 -0
- package/src/components/Search/SearchFilterField.tsx +84 -0
- package/src/components/Search/SearchGroups.tsx +81 -0
- package/src/components/Search/SearchHighlight.tsx +29 -2
- package/src/components/Search/SearchInput.tsx +9 -3
- package/src/components/Search/SearchItem.tsx +39 -24
- package/src/components/Search/SearchRecent.tsx +2 -2
- package/src/components/Search/SearchSuggestedPages.tsx +2 -2
- package/src/components/Search/SearchTrigger.tsx +2 -2
- package/src/components/Search/variables.ts +48 -2
- package/src/components/Segmented/Segmented.tsx +2 -2
- package/src/components/Select/Select.tsx +208 -157
- package/src/components/Select/SelectInput.tsx +201 -0
- package/src/components/Select/variables.ts +12 -1
- package/src/components/SidebarActions/ChangeViewButton.tsx +1 -1
- package/src/components/SidebarActions/SidebarActions.tsx +2 -2
- package/src/components/TableOfContent/TableOfContent.tsx +2 -2
- package/src/components/Tag/Tag.tsx +57 -6
- package/src/components/Tag/variables.dark.ts +20 -5
- package/src/components/Tag/variables.ts +49 -17
- package/src/components/UserMenu/LoginButton.tsx +2 -2
- package/src/components/UserMenu/LogoutMenuItem.tsx +2 -2
- package/src/components/UserMenu/UserMenu.tsx +2 -2
- package/src/components/VersionPicker/VersionPicker.tsx +18 -42
- package/src/core/hooks/__mocks__/index.ts +2 -1
- package/src/core/hooks/__mocks__/search/use-search-filter.ts +10 -0
- package/src/core/hooks/__mocks__/use-theme-hooks.ts +6 -1
- package/src/core/hooks/feedback/use-report-dialog.ts +3 -3
- package/src/core/hooks/index.ts +2 -1
- package/src/core/hooks/menu/use-mobile-menu-items.ts +1 -1
- package/src/core/hooks/menu/use-mobile-menu-levels.ts +2 -2
- package/src/core/hooks/search/use-recent-searches.ts +3 -0
- package/src/core/hooks/{use-search.ts → search/use-search-dialog.ts} +1 -1
- package/src/core/hooks/search/use-search-filter.ts +57 -0
- package/src/core/types/hooks.ts +25 -4
- package/src/core/types/index.ts +1 -1
- package/src/core/types/l10n.ts +169 -97
- package/src/core/types/search.ts +53 -2
- package/src/core/types/select.ts +33 -0
- package/src/core/utils/index.ts +1 -0
- package/src/core/utils/menu.ts +1 -1
- package/src/core/utils/text-trimmer.ts +7 -0
- package/src/icons/ResetIcon/ResetIcon.tsx +26 -0
- package/src/icons/SettingsIcon/SettingsIcon.tsx +30 -0
- package/src/index.ts +8 -1
- package/src/layouts/Forbidden.tsx +4 -9
- package/src/layouts/NotFound.tsx +6 -6
- package/src/layouts/OIDCForbidden.tsx +2 -2
- package/src/markdoc/tags/partial.ts +1 -1
- package/lib/core/types/select-option.d.ts +0 -4
- package/src/core/types/select-option.ts +0 -4
- /package/lib/components/{Loading → Loaders}/Loading.d.ts +0 -0
- /package/lib/components/{Loading → Loaders}/Loading.js +0 -0
- /package/src/components/{Loading → Loaders}/Loading.tsx +0 -0
|
@@ -3,14 +3,14 @@ import styled from 'styled-components';
|
|
|
3
3
|
|
|
4
4
|
import { SearchTrigger } from '@redocly/theme/components/Search/SearchTrigger';
|
|
5
5
|
import { SearchDialog } from '@redocly/theme/components/Search/SearchDialog';
|
|
6
|
-
import {
|
|
6
|
+
import { useSearchDialog } from '@redocly/theme/core/hooks';
|
|
7
7
|
|
|
8
8
|
export type SearchProps = {
|
|
9
9
|
className?: string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export function Search({ className }: SearchProps): JSX.Element {
|
|
13
|
-
const { isOpen, onOpen, onClose } =
|
|
13
|
+
const { isOpen, onOpen, onClose } = useSearchDialog();
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<SearchWrapper data-component-name="Search/Search" className={className}>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React, { useRef, useState } from 'react';
|
|
1
|
+
import React, { Fragment, useRef, useState } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { MouseEvent } from 'react';
|
|
5
|
-
import type {
|
|
5
|
+
import type { SearchFacetCount, SearchItemData } from '@redocly/theme/core/types';
|
|
6
6
|
|
|
7
7
|
import { SearchInput } from '@redocly/theme/components/Search/SearchInput';
|
|
8
8
|
import { SearchShortcut } from '@redocly/theme/components/Search/SearchShortcut';
|
|
@@ -11,9 +11,13 @@ import { breakpoints, concatClassNames } from '@redocly/theme/core/utils';
|
|
|
11
11
|
import { SearchItem } from '@redocly/theme/components/Search/SearchItem';
|
|
12
12
|
import { SearchRecent } from '@redocly/theme/components/Search/SearchRecent';
|
|
13
13
|
import { SearchSuggestedPages } from '@redocly/theme/components/Search/SearchSuggestedPages';
|
|
14
|
-
import { useThemeHooks, useDialogHotKeys } from '@redocly/theme/core/hooks';
|
|
14
|
+
import { useThemeHooks, useDialogHotKeys, useSearchFilter } from '@redocly/theme/core/hooks';
|
|
15
15
|
import { Tag } from '@redocly/theme/components/Tag/Tag';
|
|
16
16
|
import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
|
|
17
|
+
import { SearchFilter } from '@redocly/theme/components/Search/SearchFilter';
|
|
18
|
+
import { SearchGroups } from '@redocly/theme/components/Search/SearchGroups';
|
|
19
|
+
import { SpinnerLoader } from '@redocly/theme/components/Loaders/SpinnerLoader';
|
|
20
|
+
import { SettingsIcon } from '@redocly/theme/icons/SettingsIcon/SettingsIcon';
|
|
17
21
|
|
|
18
22
|
export type SearchDialogProps = {
|
|
19
23
|
onClose: () => void;
|
|
@@ -21,11 +25,29 @@ export type SearchDialogProps = {
|
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Element {
|
|
24
|
-
const { useTranslate, useCurrentProduct,
|
|
28
|
+
const { useTranslate, useCurrentProduct, useSearch, useProducts } = useThemeHooks();
|
|
25
29
|
const products = useProducts();
|
|
26
30
|
const currentProduct = useCurrentProduct();
|
|
27
31
|
const [product, setProduct] = useState(currentProduct);
|
|
28
|
-
const {
|
|
32
|
+
const {
|
|
33
|
+
query,
|
|
34
|
+
setQuery,
|
|
35
|
+
filter,
|
|
36
|
+
setFilter,
|
|
37
|
+
items,
|
|
38
|
+
isSearchLoading,
|
|
39
|
+
facets,
|
|
40
|
+
setLoadMore,
|
|
41
|
+
advancedSearch,
|
|
42
|
+
} = useSearch(product?.name);
|
|
43
|
+
const {
|
|
44
|
+
isFilterOpen,
|
|
45
|
+
onFilterToggle,
|
|
46
|
+
onFilterChange,
|
|
47
|
+
onFilterReset,
|
|
48
|
+
onFacetReset,
|
|
49
|
+
onTopFacetsReset,
|
|
50
|
+
} = useSearchFilter(filter, setFilter);
|
|
29
51
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
30
52
|
const { translate } = useTranslate();
|
|
31
53
|
|
|
@@ -39,19 +61,35 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
|
|
|
39
61
|
}
|
|
40
62
|
};
|
|
41
63
|
|
|
42
|
-
const mapItem = (item:
|
|
64
|
+
const mapItem = (item: SearchItemData, index: number) => {
|
|
43
65
|
let itemProduct;
|
|
44
|
-
if (!product && item.product) {
|
|
66
|
+
if (!product && item.document.product) {
|
|
45
67
|
const resolvedProduct = products.find((product) =>
|
|
46
|
-
product.slug.match(`/${item.product?.folder}/`),
|
|
68
|
+
product.slug.match(`/${item.document.product?.folder}/`),
|
|
47
69
|
);
|
|
48
70
|
itemProduct = resolvedProduct
|
|
49
71
|
? { name: resolvedProduct.name, icon: resolvedProduct.icon }
|
|
50
72
|
: undefined;
|
|
51
73
|
}
|
|
52
|
-
return <SearchItem key={item.id} item={item} product={itemProduct} />;
|
|
74
|
+
return <SearchItem key={`${index}-${item.document.id}`} item={item} product={itemProduct} />;
|
|
53
75
|
};
|
|
54
76
|
|
|
77
|
+
const showLoadMore = (groupKey: string, currentCount: number = 0) => {
|
|
78
|
+
const topFacet = facets.find((facet) => facet.isTop);
|
|
79
|
+
let needLoadMore = false;
|
|
80
|
+
if (topFacet) {
|
|
81
|
+
const groupValue = topFacet.values.find((value) => {
|
|
82
|
+
if (typeof value === 'object') {
|
|
83
|
+
return value.value === groupKey;
|
|
84
|
+
} else return false;
|
|
85
|
+
}) as SearchFacetCount;
|
|
86
|
+
needLoadMore = groupValue ? groupValue.count > currentCount : false;
|
|
87
|
+
}
|
|
88
|
+
return needLoadMore;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const showResults = !!((filter && filter.length) || query);
|
|
92
|
+
|
|
55
93
|
return (
|
|
56
94
|
<SearchOverlay
|
|
57
95
|
data-component-name="Search/SearchDialog"
|
|
@@ -61,61 +99,115 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
|
|
|
61
99
|
>
|
|
62
100
|
<SearchDialogWrapper className="scroll-lock" role="dialog">
|
|
63
101
|
<SearchDialogHeader>
|
|
102
|
+
{product && (
|
|
103
|
+
<>
|
|
104
|
+
<SearchProductTag color="product">
|
|
105
|
+
{product.name}
|
|
106
|
+
<CloseIcon onClick={() => setProduct(undefined)} color="--icon-color-additional" />
|
|
107
|
+
</SearchProductTag>
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
64
110
|
<SearchInput
|
|
65
111
|
value={query}
|
|
66
112
|
onChange={setQuery}
|
|
67
|
-
placeholder={translate('
|
|
68
|
-
isLoading={
|
|
69
|
-
data-translation-key="
|
|
113
|
+
placeholder={translate('search.label', 'Search docs...')}
|
|
114
|
+
isLoading={isSearchLoading}
|
|
115
|
+
data-translation-key="search.label"
|
|
70
116
|
/>
|
|
71
|
-
{
|
|
72
|
-
<
|
|
73
|
-
{product.name}
|
|
74
|
-
<CloseIcon onClick={() => setProduct(undefined)} color="--icon-color-additional" />
|
|
75
|
-
</SearchProductTag>
|
|
117
|
+
{advancedSearch && (
|
|
118
|
+
<SearchFilterToggleButton icon={<SettingsIcon />} onClick={onFilterToggle} />
|
|
76
119
|
)}
|
|
77
120
|
</SearchDialogHeader>
|
|
78
121
|
<SearchDialogBody>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
122
|
+
<SearchDialogBodyMainView>
|
|
123
|
+
<SearchGroups
|
|
124
|
+
facets={facets}
|
|
125
|
+
searchFilter={filter}
|
|
126
|
+
onFilterChange={onFilterChange}
|
|
127
|
+
onTopFacetsReset={onTopFacetsReset}
|
|
128
|
+
/>
|
|
129
|
+
{showResults ? (
|
|
130
|
+
items && Object.keys(items).some((key) => items[key]?.length) ? (
|
|
131
|
+
Object.keys(items).map((key) =>
|
|
132
|
+
items[key]?.length ? (
|
|
133
|
+
<Fragment key={key}>
|
|
134
|
+
<SearchGroupTitle>{key}</SearchGroupTitle>
|
|
135
|
+
{items[key]?.map(mapItem)}
|
|
136
|
+
{showLoadMore(key, items[key]?.length || 0) && (
|
|
137
|
+
<SearchGroupFooter
|
|
138
|
+
data-translation-key="search.showMore"
|
|
139
|
+
onClick={() =>
|
|
140
|
+
setLoadMore({ groupKey: key, offset: items[key]?.length || 0 })
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
{translate('search.showMore', 'Show more')}
|
|
144
|
+
</SearchGroupFooter>
|
|
145
|
+
)}
|
|
146
|
+
</Fragment>
|
|
147
|
+
) : null,
|
|
148
|
+
)
|
|
149
|
+
) : isSearchLoading ? (
|
|
150
|
+
<SearchMessage>
|
|
151
|
+
<SpinnerLoader size="26px" color="var(--search-input-icon-color)" />
|
|
152
|
+
{translate('search.loading', 'Loading...')}
|
|
153
|
+
</SearchMessage>
|
|
154
|
+
) : (
|
|
155
|
+
<SearchMessage data-translation-key="search.noResults">
|
|
156
|
+
<b>{translate('search.noResults.title', 'No results')}</b>
|
|
157
|
+
{translate('search.noResults.description', 'Prease, try with a different query.')}
|
|
158
|
+
</SearchMessage>
|
|
159
|
+
)
|
|
82
160
|
) : (
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
161
|
+
<>
|
|
162
|
+
<SearchRecent onSelect={setQuery} />
|
|
163
|
+
<SearchSuggestedPages />
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
</SearchDialogBodyMainView>
|
|
167
|
+
{advancedSearch && isFilterOpen && (
|
|
168
|
+
<SearchDialogBodyFilterView>
|
|
169
|
+
<SearchFilter
|
|
170
|
+
facets={facets}
|
|
171
|
+
filter={filter}
|
|
172
|
+
query={query}
|
|
173
|
+
onFilterChange={onFilterChange}
|
|
174
|
+
onFilterReset={onFilterReset}
|
|
175
|
+
onFacetReset={onFacetReset}
|
|
176
|
+
/>
|
|
177
|
+
</SearchDialogBodyFilterView>
|
|
92
178
|
)}
|
|
93
179
|
</SearchDialogBody>
|
|
94
180
|
<SearchDialogFooter>
|
|
95
181
|
<SearchShortcuts>
|
|
96
182
|
<SearchShortcut
|
|
97
|
-
data-translation-key="
|
|
183
|
+
data-translation-key="search.keys.navigate"
|
|
98
184
|
combination="Tab"
|
|
99
|
-
text={translate('
|
|
185
|
+
text={translate('search.keys.navigate', 'to navigate')}
|
|
100
186
|
/>
|
|
101
187
|
<SearchShortcut
|
|
102
|
-
data-translation-key="
|
|
188
|
+
data-translation-key="search.keys.select"
|
|
103
189
|
combination="⏎"
|
|
104
|
-
text={translate('
|
|
190
|
+
text={translate('search.keys.select', 'to select')}
|
|
105
191
|
/>
|
|
106
192
|
<SearchShortcut
|
|
107
|
-
data-translation-key="
|
|
193
|
+
data-translation-key="search.keys.exit"
|
|
108
194
|
combination="Esc"
|
|
109
|
-
text={translate('
|
|
195
|
+
text={translate('search.keys.exit', 'to exit')}
|
|
110
196
|
/>
|
|
111
197
|
</SearchShortcuts>
|
|
198
|
+
{isSearchLoading && (
|
|
199
|
+
<SearchLoading>
|
|
200
|
+
<SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
|
|
201
|
+
{translate('search.loading', 'Loading...')}
|
|
202
|
+
</SearchLoading>
|
|
203
|
+
)}
|
|
112
204
|
<SearchCancelButton
|
|
113
|
-
data-translation-key="
|
|
205
|
+
data-translation-key="search.cancel"
|
|
114
206
|
variant="secondary"
|
|
115
207
|
size="small"
|
|
116
208
|
onClick={onClose}
|
|
117
209
|
>
|
|
118
|
-
{translate('
|
|
210
|
+
{translate('search.cancel', 'Cancel')}
|
|
119
211
|
</SearchCancelButton>
|
|
120
212
|
</SearchDialogFooter>
|
|
121
213
|
</SearchDialogWrapper>
|
|
@@ -139,7 +231,6 @@ const SearchOverlay = styled.div`
|
|
|
139
231
|
const SearchDialogWrapper = styled.div`
|
|
140
232
|
display: flex;
|
|
141
233
|
flex-direction: column;
|
|
142
|
-
justify-content: space-between;
|
|
143
234
|
overflow: auto;
|
|
144
235
|
width: 100vw;
|
|
145
236
|
height: 100vh;
|
|
@@ -155,39 +246,51 @@ const SearchDialogWrapper = styled.div`
|
|
|
155
246
|
max-height: 95vh;
|
|
156
247
|
height: auto;
|
|
157
248
|
resize: both;
|
|
158
|
-
min-width: 300px;
|
|
159
|
-
min-height: 200px;
|
|
160
249
|
}
|
|
161
250
|
`;
|
|
162
251
|
|
|
163
252
|
const SearchDialogHeader = styled.header`
|
|
164
253
|
display: flex;
|
|
165
254
|
align-items: center;
|
|
166
|
-
border-bottom:
|
|
167
|
-
background-color: var(--search-
|
|
168
|
-
padding: var(--
|
|
255
|
+
border-bottom: var(--search-modal-border);
|
|
256
|
+
background-color: var(--search-modal-header-bg-color);
|
|
257
|
+
padding: var(--search-modal-header-padding);
|
|
169
258
|
`;
|
|
170
259
|
|
|
171
260
|
const SearchDialogBody = styled.div`
|
|
261
|
+
display: flex;
|
|
262
|
+
flex-direction: row;
|
|
172
263
|
flex-grow: 1;
|
|
173
|
-
overflow-y: scroll;
|
|
174
|
-
overscroll-behavior: contain;
|
|
175
264
|
|
|
176
265
|
@media screen and (min-width: ${breakpoints.small}) {
|
|
177
266
|
height: var(--search-modal-min-height);
|
|
178
267
|
}
|
|
179
268
|
`;
|
|
180
269
|
|
|
270
|
+
const SearchDialogBodyMainView = styled.div`
|
|
271
|
+
flex: 2;
|
|
272
|
+
flex-grow: 2;
|
|
273
|
+
overflow-y: scroll;
|
|
274
|
+
overscroll-behavior: contain;
|
|
275
|
+
border-right: var(--search-modal-border);
|
|
276
|
+
`;
|
|
277
|
+
|
|
278
|
+
const SearchDialogBodyFilterView = styled.div`
|
|
279
|
+
overflow: scroll;
|
|
280
|
+
`;
|
|
281
|
+
|
|
181
282
|
const SearchDialogFooter = styled.footer`
|
|
182
|
-
|
|
183
|
-
|
|
283
|
+
display: flex;
|
|
284
|
+
gap: var(--search-modal-footer-gap);
|
|
285
|
+
padding: var(--search-modal-footer-padding);
|
|
286
|
+
border-top: var(--search-modal-border);
|
|
184
287
|
`;
|
|
185
288
|
|
|
186
289
|
const SearchShortcuts = styled.div`
|
|
187
290
|
display: none;
|
|
188
291
|
justify-content: flex-start;
|
|
189
292
|
align-items: center;
|
|
190
|
-
gap: var(--
|
|
293
|
+
gap: var(--search-shortcuts-gap);
|
|
191
294
|
|
|
192
295
|
@media screen and (min-width: ${breakpoints.small}) {
|
|
193
296
|
display: flex;
|
|
@@ -195,16 +298,28 @@ const SearchShortcuts = styled.div`
|
|
|
195
298
|
`;
|
|
196
299
|
|
|
197
300
|
const SearchMessage = styled.div`
|
|
198
|
-
|
|
199
|
-
|
|
301
|
+
display: flex;
|
|
302
|
+
height: 40%;
|
|
303
|
+
justify-content: center;
|
|
304
|
+
align-items: center;
|
|
305
|
+
flex-direction: column;
|
|
306
|
+
font-size: var(--search-message-font-size);
|
|
307
|
+
font-weight: var(--search-message-font-weight);
|
|
308
|
+
line-height: var(--search-message-line-height);
|
|
309
|
+
color: var(--search-message-text-color);
|
|
310
|
+
gap: var(--search-message-gap);
|
|
200
311
|
`;
|
|
201
312
|
|
|
202
313
|
const SearchProductTag = styled(Tag)`
|
|
203
314
|
--tag-border-radius: var(--border-radius);
|
|
204
|
-
|
|
315
|
+
border: none;
|
|
205
316
|
margin: var(--spacing-xs) var(--spacing-sm) !important;
|
|
206
317
|
`;
|
|
207
318
|
|
|
319
|
+
const SearchFilterToggleButton = styled(Button)`
|
|
320
|
+
margin-left: 0;
|
|
321
|
+
`;
|
|
322
|
+
|
|
208
323
|
const SearchCancelButton = styled(Button)`
|
|
209
324
|
width: 100%;
|
|
210
325
|
|
|
@@ -212,3 +327,27 @@ const SearchCancelButton = styled(Button)`
|
|
|
212
327
|
display: none;
|
|
213
328
|
}
|
|
214
329
|
`;
|
|
330
|
+
|
|
331
|
+
const SearchGroupTitle = styled.div`
|
|
332
|
+
border-bottom: var(--search-modal-border);
|
|
333
|
+
padding: var(--search-group-title-padding);
|
|
334
|
+
background-color: var(--search-group-title-bg-color);
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
const SearchGroupFooter = styled.div`
|
|
338
|
+
display: flex;
|
|
339
|
+
justify-content: center;
|
|
340
|
+
padding: var(--search-group-footer-padding);
|
|
341
|
+
color: var(--search-group-footer-text-color);
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
const SearchLoading = styled.div`
|
|
346
|
+
display: none;
|
|
347
|
+
align-items: center;
|
|
348
|
+
gap: var(--spacing-xs);
|
|
349
|
+
|
|
350
|
+
@media screen and (min-width: ${breakpoints.small}) {
|
|
351
|
+
display: flex;
|
|
352
|
+
}
|
|
353
|
+
`;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { SearchFilterItem, SearchFacet } from '@redocly/theme/core/types';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@redocly/theme/components/Button/Button';
|
|
7
|
+
import { CleanIcon } from '@redocly/theme/icons/CleanIcon/CleanIcon';
|
|
8
|
+
import { SearchFilterField } from '@redocly/theme/components/Search/SearchFilterField';
|
|
9
|
+
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
|
10
|
+
|
|
11
|
+
export type SearchFilterProps = {
|
|
12
|
+
className?: string;
|
|
13
|
+
facets: SearchFacet[];
|
|
14
|
+
filter: SearchFilterItem[];
|
|
15
|
+
query: string;
|
|
16
|
+
onFilterChange: (field: string, value: string | string[], isTop?: boolean) => void;
|
|
17
|
+
onFilterReset: () => void;
|
|
18
|
+
onFacetReset: (field: string) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function SearchFilter({
|
|
22
|
+
className,
|
|
23
|
+
facets,
|
|
24
|
+
filter,
|
|
25
|
+
query,
|
|
26
|
+
onFilterChange,
|
|
27
|
+
onFilterReset,
|
|
28
|
+
onFacetReset,
|
|
29
|
+
}: SearchFilterProps): JSX.Element {
|
|
30
|
+
const { useTranslate } = useThemeHooks();
|
|
31
|
+
const { translate } = useTranslate();
|
|
32
|
+
return (
|
|
33
|
+
<SearchFilterWrapper data-component-name="Search/SearchFilter" className={className}>
|
|
34
|
+
<SearchFilterHeader>
|
|
35
|
+
<span data-translation-key="search.filter.title">
|
|
36
|
+
{translate('search.filter.title', 'Advanced filter')}
|
|
37
|
+
</span>
|
|
38
|
+
<Button
|
|
39
|
+
data-translation-key="search.filter.reset"
|
|
40
|
+
onClick={onFilterReset}
|
|
41
|
+
variant="ghost"
|
|
42
|
+
icon={<CleanIcon />}
|
|
43
|
+
>
|
|
44
|
+
{translate('search.filter.reset', 'Reset filters')}
|
|
45
|
+
</Button>
|
|
46
|
+
</SearchFilterHeader>
|
|
47
|
+
|
|
48
|
+
<SearchFilterFields>
|
|
49
|
+
{facets.map((facet, index) => (
|
|
50
|
+
<SearchFilterField
|
|
51
|
+
key={`${facet.field}-${index}`}
|
|
52
|
+
facet={facet}
|
|
53
|
+
onFilterChange={onFilterChange}
|
|
54
|
+
onFacetReset={onFacetReset}
|
|
55
|
+
filter={filter}
|
|
56
|
+
query={query}
|
|
57
|
+
/>
|
|
58
|
+
))}
|
|
59
|
+
</SearchFilterFields>
|
|
60
|
+
</SearchFilterWrapper>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SearchFilterWrapper = styled.div`
|
|
65
|
+
width: var(--search-filter-width);
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
padding: var(--search-filter-padding);
|
|
69
|
+
font-size: var(--search-filter-font-size);
|
|
70
|
+
font-weight: var(--search-filter-font-weight);
|
|
71
|
+
line-height: var(--search-filter-line-height);
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const SearchFilterHeader = styled.div`
|
|
75
|
+
position: sticky;
|
|
76
|
+
top: 0px;
|
|
77
|
+
display: flex;
|
|
78
|
+
justify-content: space-between;
|
|
79
|
+
align-items: center;
|
|
80
|
+
padding: var(--search-filter-header-padding);
|
|
81
|
+
color: var(--search-filter-header-text-color);
|
|
82
|
+
background-color: var(--search-filter-bg-color);
|
|
83
|
+
z-index: var(--search-filter-header-z-index);
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const SearchFilterFields = styled.div`
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
gap: var(--search-filter-fields-gap);
|
|
90
|
+
`;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { SearchFacet, SearchFilterItem } from '@redocly/theme/core/types';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@redocly/theme/components/Button/Button';
|
|
7
|
+
import { ResetIcon } from '@redocly/theme/icons/ResetIcon/ResetIcon';
|
|
8
|
+
import { useThemeHooks } from '@redocly/theme/core';
|
|
9
|
+
import { SearchFilterFieldSelect } from '@redocly/theme/components/Search/FilterFields/SearchFilterFieldSelect';
|
|
10
|
+
import { SearchFilterFieldTags } from '@redocly/theme/components/Search/FilterFields/SearchFilterFieldTags';
|
|
11
|
+
|
|
12
|
+
type SearchFilterFieldProps = {
|
|
13
|
+
className?: string;
|
|
14
|
+
facet: SearchFacet;
|
|
15
|
+
filter: SearchFilterItem[];
|
|
16
|
+
query: string;
|
|
17
|
+
onFilterChange: (field: string, value: string | string[], isTop?: boolean) => void;
|
|
18
|
+
onFacetReset: (filed: string) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function SearchFilterField({
|
|
22
|
+
className,
|
|
23
|
+
facet,
|
|
24
|
+
filter,
|
|
25
|
+
query,
|
|
26
|
+
onFilterChange,
|
|
27
|
+
onFacetReset,
|
|
28
|
+
}: SearchFilterFieldProps): JSX.Element {
|
|
29
|
+
const { useTranslate } = useThemeHooks();
|
|
30
|
+
const { translate } = useTranslate();
|
|
31
|
+
const selectedValues = filter.find((item) => item.field === facet.field)?.values || [];
|
|
32
|
+
|
|
33
|
+
const onChange = (value: string | string[]) => {
|
|
34
|
+
onFilterChange(facet.field, value, facet.isTop);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const onReset = () => {
|
|
38
|
+
onFacetReset(facet.field);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<FilterFieldWrapper data-component-name="Search/SearchFilterField" className={className}>
|
|
43
|
+
<FilterFieldLabel>
|
|
44
|
+
{facet.name}
|
|
45
|
+
{selectedValues.length > 0 && (
|
|
46
|
+
<Button
|
|
47
|
+
data-translation-key="search.filter.field.reset"
|
|
48
|
+
icon={<ResetIcon />}
|
|
49
|
+
variant="ghost"
|
|
50
|
+
size="small"
|
|
51
|
+
onClick={onReset}
|
|
52
|
+
>
|
|
53
|
+
{translate('search.filter.field.reset', 'Reset')}
|
|
54
|
+
</Button>
|
|
55
|
+
)}
|
|
56
|
+
</FilterFieldLabel>
|
|
57
|
+
{['select', 'multi-select'].includes(facet.type) && (
|
|
58
|
+
<SearchFilterFieldSelect
|
|
59
|
+
facet={facet}
|
|
60
|
+
filter={filter}
|
|
61
|
+
query={query}
|
|
62
|
+
selectedValues={selectedValues}
|
|
63
|
+
onChange={onChange}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
{/* Special default HTTP Methods filed */}
|
|
67
|
+
{facet.type === 'tags' && (
|
|
68
|
+
<SearchFilterFieldTags facet={facet} selectedValues={selectedValues} onChange={onChange} />
|
|
69
|
+
)}
|
|
70
|
+
</FilterFieldWrapper>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FilterFieldWrapper = styled.div`
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 4px;
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const FilterFieldLabel = styled.div`
|
|
81
|
+
display: flex;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
align-items: center;
|
|
84
|
+
`;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { SearchFacet, SearchFacetCount, SearchFilterItem } from '@redocly/theme';
|
|
5
|
+
|
|
6
|
+
import { Tag } from '@redocly/theme/components/Tag/Tag';
|
|
7
|
+
|
|
8
|
+
type SearchGroupsProps = {
|
|
9
|
+
facets: SearchFacet[];
|
|
10
|
+
searchFilter: SearchFilterItem[];
|
|
11
|
+
onFilterChange: (field: string, value: string[], isTop?: boolean) => void;
|
|
12
|
+
onTopFacetsReset: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function SearchGroups({
|
|
16
|
+
facets,
|
|
17
|
+
searchFilter,
|
|
18
|
+
onFilterChange,
|
|
19
|
+
onTopFacetsReset,
|
|
20
|
+
}: SearchGroupsProps): JSX.Element {
|
|
21
|
+
const groupFacets = facets.filter((facet) => facet.isTop);
|
|
22
|
+
|
|
23
|
+
const handleGroupTagClick = (
|
|
24
|
+
value: string,
|
|
25
|
+
fieldId: string,
|
|
26
|
+
active: boolean,
|
|
27
|
+
currentValues: string[],
|
|
28
|
+
) => {
|
|
29
|
+
const values = active
|
|
30
|
+
? currentValues.filter((item) => item !== value)
|
|
31
|
+
: [...currentValues, value];
|
|
32
|
+
onFilterChange(fieldId, values, true);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<SearchGroupsWrapper>
|
|
37
|
+
<GroupTag
|
|
38
|
+
borderless
|
|
39
|
+
active={!searchFilter.some((item) => item.isTop)}
|
|
40
|
+
onClick={() => searchFilter.some((item) => item.isTop) && onTopFacetsReset()}
|
|
41
|
+
>
|
|
42
|
+
All
|
|
43
|
+
</GroupTag>
|
|
44
|
+
<Divider />
|
|
45
|
+
{groupFacets.flatMap((facet) =>
|
|
46
|
+
facet.values.map((facetCount, index) => {
|
|
47
|
+
const { value, count, isCounterVisible } = facetCount as SearchFacetCount;
|
|
48
|
+
const currentValues =
|
|
49
|
+
searchFilter.find((item) => item.field === facet.field)?.values || [];
|
|
50
|
+
const active = currentValues?.includes(value);
|
|
51
|
+
return (
|
|
52
|
+
<GroupTag
|
|
53
|
+
key={`${facet.field}-${index}`}
|
|
54
|
+
onClick={() => handleGroupTagClick(value, facet.field, active, currentValues)}
|
|
55
|
+
active={active}
|
|
56
|
+
borderless
|
|
57
|
+
>
|
|
58
|
+
{value} {isCounterVisible && <span>{count}</span>}
|
|
59
|
+
</GroupTag>
|
|
60
|
+
);
|
|
61
|
+
}),
|
|
62
|
+
)}
|
|
63
|
+
</SearchGroupsWrapper>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SearchGroupsWrapper = styled.div`
|
|
68
|
+
display: flex;
|
|
69
|
+
gap: 4px;
|
|
70
|
+
padding: var(--spacing-md);
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
const GroupTag = styled(Tag)`
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
gap: 4px;
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const Divider = styled.div`
|
|
79
|
+
border-right: 1px solid var(--border-color-secondary);
|
|
80
|
+
margin: 5px 5px 5px 0;
|
|
81
|
+
`;
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
function slicer(str: string, arr: string[]): any[] {
|
|
5
|
+
const markOpenIndex = str.indexOf('<mark>');
|
|
6
|
+
const markCloseIndex = str.indexOf('</mark>');
|
|
7
|
+
|
|
8
|
+
if (markOpenIndex !== -1 && markCloseIndex !== -1) {
|
|
9
|
+
const pre = str.slice(0, markOpenIndex);
|
|
10
|
+
const entry = str.slice(markOpenIndex + 6, markCloseIndex);
|
|
11
|
+
const suf = str.slice(markCloseIndex + 7, str.length);
|
|
12
|
+
return (arr = [...arr, pre, { entry }, ...slicer(suf, arr)]);
|
|
13
|
+
} else return [...arr, str];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function searchHighlight(text: string | string[]): JSX.Element | string {
|
|
17
|
+
if (!Array.isArray(text)) {
|
|
18
|
+
const arr = slicer(text, []);
|
|
19
|
+
if (arr.length === 1) return text;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<span>
|
|
23
|
+
{arr.map((item, index) => {
|
|
24
|
+
if (typeof item === 'object') {
|
|
25
|
+
return <Highlight key={index}>{item.entry}</Highlight>;
|
|
26
|
+
} else {
|
|
27
|
+
return item;
|
|
28
|
+
}
|
|
29
|
+
})}
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
6
33
|
|
|
7
34
|
const [pre, entry, suf] = text;
|
|
8
35
|
return (
|