@redocly/theme 0.42.2 → 0.43.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/Dropdown/DropdownMenu.d.ts +2 -0
- package/lib/components/Dropdown/DropdownMenu.js +3 -1
- package/lib/components/Loaders/SpinnerLoader.d.ts +5 -0
- package/lib/components/Loaders/SpinnerLoader.js +32 -0
- 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 +109 -27
- 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/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/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/VersionPicker/VersionPicker.d.ts +2 -3
- package/lib/components/VersionPicker/VersionPicker.js +13 -30
- 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/index.d.ts +2 -1
- package/lib/core/hooks/index.js +2 -1
- 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/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/markdoc/components/Cards/Card.js +6 -6
- package/package.json +3 -3
- package/src/components/Dropdown/DropdownMenu.tsx +2 -1
- package/src/components/Filter/FilterSelect.tsx +3 -3
- package/src/components/Loaders/SpinnerLoader.tsx +31 -0
- 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 +183 -41
- 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/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/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/VersionPicker/VersionPicker.tsx +15 -39
- 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/index.ts +2 -1
- 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 +110 -38
- 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/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/markdoc/components/Cards/Card.tsx +15 -15
- 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
|
@@ -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,34 +99,85 @@ 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
113
|
placeholder={translate('theme.search.label', 'Search docs...')}
|
|
68
|
-
isLoading={
|
|
114
|
+
isLoading={isSearchLoading}
|
|
69
115
|
data-translation-key="theme.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="theme.search.showMore"
|
|
139
|
+
onClick={() =>
|
|
140
|
+
setLoadMore({ groupKey: key, offset: items[key]?.length || 0 })
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
{translate('theme.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('theme.search.loading', 'Loading...')}
|
|
153
|
+
</SearchMessage>
|
|
154
|
+
) : (
|
|
155
|
+
<SearchMessage data-translation-key="theme.search.noResults">
|
|
156
|
+
<b>{translate('theme.search.noResults.title', 'No results')}</b>
|
|
157
|
+
{translate(
|
|
158
|
+
'theme.search.noResults.description',
|
|
159
|
+
'Prease, try with a different query.',
|
|
160
|
+
)}
|
|
161
|
+
</SearchMessage>
|
|
162
|
+
)
|
|
82
163
|
) : (
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
164
|
+
<>
|
|
165
|
+
<SearchRecent onSelect={setQuery} />
|
|
166
|
+
<SearchSuggestedPages />
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
169
|
+
</SearchDialogBodyMainView>
|
|
170
|
+
{advancedSearch && isFilterOpen && (
|
|
171
|
+
<SearchDialogBodyFilterView>
|
|
172
|
+
<SearchFilter
|
|
173
|
+
facets={facets}
|
|
174
|
+
filter={filter}
|
|
175
|
+
query={query}
|
|
176
|
+
onFilterChange={onFilterChange}
|
|
177
|
+
onFilterReset={onFilterReset}
|
|
178
|
+
onFacetReset={onFacetReset}
|
|
179
|
+
/>
|
|
180
|
+
</SearchDialogBodyFilterView>
|
|
92
181
|
)}
|
|
93
182
|
</SearchDialogBody>
|
|
94
183
|
<SearchDialogFooter>
|
|
@@ -109,6 +198,12 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
|
|
|
109
198
|
text={translate('theme.search.keys.exit', 'to exit')}
|
|
110
199
|
/>
|
|
111
200
|
</SearchShortcuts>
|
|
201
|
+
{isSearchLoading && (
|
|
202
|
+
<SearchLoading>
|
|
203
|
+
<SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
|
|
204
|
+
{translate('theme.search.loading', 'Loading...')}
|
|
205
|
+
</SearchLoading>
|
|
206
|
+
)}
|
|
112
207
|
<SearchCancelButton
|
|
113
208
|
data-translation-key="theme.search.cancel"
|
|
114
209
|
variant="secondary"
|
|
@@ -139,7 +234,6 @@ const SearchOverlay = styled.div`
|
|
|
139
234
|
const SearchDialogWrapper = styled.div`
|
|
140
235
|
display: flex;
|
|
141
236
|
flex-direction: column;
|
|
142
|
-
justify-content: space-between;
|
|
143
237
|
overflow: auto;
|
|
144
238
|
width: 100vw;
|
|
145
239
|
height: 100vh;
|
|
@@ -155,39 +249,51 @@ const SearchDialogWrapper = styled.div`
|
|
|
155
249
|
max-height: 95vh;
|
|
156
250
|
height: auto;
|
|
157
251
|
resize: both;
|
|
158
|
-
min-width: 300px;
|
|
159
|
-
min-height: 200px;
|
|
160
252
|
}
|
|
161
253
|
`;
|
|
162
254
|
|
|
163
255
|
const SearchDialogHeader = styled.header`
|
|
164
256
|
display: flex;
|
|
165
257
|
align-items: center;
|
|
166
|
-
border-bottom:
|
|
167
|
-
background-color: var(--search-
|
|
168
|
-
padding: var(--
|
|
258
|
+
border-bottom: var(--search-modal-border);
|
|
259
|
+
background-color: var(--search-modal-header-bg-color);
|
|
260
|
+
padding: var(--search-modal-header-padding);
|
|
169
261
|
`;
|
|
170
262
|
|
|
171
263
|
const SearchDialogBody = styled.div`
|
|
264
|
+
display: flex;
|
|
265
|
+
flex-direction: row;
|
|
172
266
|
flex-grow: 1;
|
|
173
|
-
overflow-y: scroll;
|
|
174
|
-
overscroll-behavior: contain;
|
|
175
267
|
|
|
176
268
|
@media screen and (min-width: ${breakpoints.small}) {
|
|
177
269
|
height: var(--search-modal-min-height);
|
|
178
270
|
}
|
|
179
271
|
`;
|
|
180
272
|
|
|
273
|
+
const SearchDialogBodyMainView = styled.div`
|
|
274
|
+
flex: 2;
|
|
275
|
+
flex-grow: 2;
|
|
276
|
+
overflow-y: scroll;
|
|
277
|
+
overscroll-behavior: contain;
|
|
278
|
+
border-right: var(--search-modal-border);
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
const SearchDialogBodyFilterView = styled.div`
|
|
282
|
+
overflow: scroll;
|
|
283
|
+
`;
|
|
284
|
+
|
|
181
285
|
const SearchDialogFooter = styled.footer`
|
|
182
|
-
|
|
183
|
-
|
|
286
|
+
display: flex;
|
|
287
|
+
gap: var(--search-modal-footer-gap);
|
|
288
|
+
padding: var(--search-modal-footer-padding);
|
|
289
|
+
border-top: var(--search-modal-border);
|
|
184
290
|
`;
|
|
185
291
|
|
|
186
292
|
const SearchShortcuts = styled.div`
|
|
187
293
|
display: none;
|
|
188
294
|
justify-content: flex-start;
|
|
189
295
|
align-items: center;
|
|
190
|
-
gap: var(--
|
|
296
|
+
gap: var(--search-shortcuts-gap);
|
|
191
297
|
|
|
192
298
|
@media screen and (min-width: ${breakpoints.small}) {
|
|
193
299
|
display: flex;
|
|
@@ -195,16 +301,28 @@ const SearchShortcuts = styled.div`
|
|
|
195
301
|
`;
|
|
196
302
|
|
|
197
303
|
const SearchMessage = styled.div`
|
|
198
|
-
|
|
199
|
-
|
|
304
|
+
display: flex;
|
|
305
|
+
height: 40%;
|
|
306
|
+
justify-content: center;
|
|
307
|
+
align-items: center;
|
|
308
|
+
flex-direction: column;
|
|
309
|
+
font-size: var(--search-message-font-size);
|
|
310
|
+
font-weight: var(--search-message-font-weight);
|
|
311
|
+
line-height: var(--search-message-line-height);
|
|
312
|
+
color: var(--search-message-text-color);
|
|
313
|
+
gap: var(--search-message-gap);
|
|
200
314
|
`;
|
|
201
315
|
|
|
202
316
|
const SearchProductTag = styled(Tag)`
|
|
203
317
|
--tag-border-radius: var(--border-radius);
|
|
204
|
-
|
|
318
|
+
border: none;
|
|
205
319
|
margin: var(--spacing-xs) var(--spacing-sm) !important;
|
|
206
320
|
`;
|
|
207
321
|
|
|
322
|
+
const SearchFilterToggleButton = styled(Button)`
|
|
323
|
+
margin-left: 0;
|
|
324
|
+
`;
|
|
325
|
+
|
|
208
326
|
const SearchCancelButton = styled(Button)`
|
|
209
327
|
width: 100%;
|
|
210
328
|
|
|
@@ -212,3 +330,27 @@ const SearchCancelButton = styled(Button)`
|
|
|
212
330
|
display: none;
|
|
213
331
|
}
|
|
214
332
|
`;
|
|
333
|
+
|
|
334
|
+
const SearchGroupTitle = styled.div`
|
|
335
|
+
border-bottom: var(--search-modal-border);
|
|
336
|
+
padding: var(--search-group-title-padding);
|
|
337
|
+
background-color: var(--search-group-title-bg-color);
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
const SearchGroupFooter = styled.div`
|
|
341
|
+
display: flex;
|
|
342
|
+
justify-content: center;
|
|
343
|
+
padding: var(--search-group-footer-padding);
|
|
344
|
+
color: var(--search-group-footer-text-color);
|
|
345
|
+
cursor: pointer;
|
|
346
|
+
`;
|
|
347
|
+
|
|
348
|
+
const SearchLoading = styled.div`
|
|
349
|
+
display: none;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: var(--spacing-xs);
|
|
352
|
+
|
|
353
|
+
@media screen and (min-width: ${breakpoints.small}) {
|
|
354
|
+
display: flex;
|
|
355
|
+
}
|
|
356
|
+
`;
|
|
@@ -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="theme.search.filter.title">
|
|
36
|
+
{translate('theme.search.filter.title', 'Advanced filter')}
|
|
37
|
+
</span>
|
|
38
|
+
<Button
|
|
39
|
+
data-translation-key="theme.search.filter.reset"
|
|
40
|
+
onClick={onFilterReset}
|
|
41
|
+
variant="ghost"
|
|
42
|
+
icon={<CleanIcon />}
|
|
43
|
+
>
|
|
44
|
+
{translate('theme.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="theme.search.filter.field.reset"
|
|
48
|
+
icon={<ResetIcon />}
|
|
49
|
+
variant="ghost"
|
|
50
|
+
size="small"
|
|
51
|
+
onClick={onReset}
|
|
52
|
+
>
|
|
53
|
+
{translate('theme.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 (
|
|
@@ -5,14 +5,14 @@ import type { ChangeEvent, SyntheticEvent } from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { SearchIcon } from '@redocly/theme/icons/SearchIcon/SearchIcon';
|
|
7
7
|
import { Spinner } from '@redocly/theme/icons/Spinner/Spinner';
|
|
8
|
-
import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
|
|
9
8
|
import { Button } from '@redocly/theme/components/Button/Button';
|
|
10
9
|
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
|
10
|
+
import { CloseFilledIcon } from '@redocly/theme/icons/CloseFilledIcon/CloseFilledIcon';
|
|
11
11
|
|
|
12
12
|
export type SearchInputProps = {
|
|
13
13
|
placeholder?: string;
|
|
14
14
|
value: string;
|
|
15
|
-
onChange(value: string)
|
|
15
|
+
onChange: (value: string) => void;
|
|
16
16
|
inputRef?: React.RefObject<HTMLInputElement>;
|
|
17
17
|
isLoading: boolean;
|
|
18
18
|
className?: string;
|
|
@@ -52,7 +52,9 @@ export function SearchInput({
|
|
|
52
52
|
onChange={handleOnChange}
|
|
53
53
|
onClick={stopPropagation}
|
|
54
54
|
/>
|
|
55
|
-
{!!value &&
|
|
55
|
+
{!!value && (
|
|
56
|
+
<ResetButton variant="ghost" onClick={handleOnReset} icon={<CloseFilledIcon />} />
|
|
57
|
+
)}
|
|
56
58
|
</SearchInputWrapper>
|
|
57
59
|
);
|
|
58
60
|
}
|
|
@@ -82,3 +84,7 @@ const SearchInputField = styled.input`
|
|
|
82
84
|
color: var(--search-input-placeholder-color);
|
|
83
85
|
}
|
|
84
86
|
`;
|
|
87
|
+
|
|
88
|
+
const ResetButton = styled(Button)`
|
|
89
|
+
color: var(--color-warm-grey-5);
|
|
90
|
+
`;
|