@redocly/theme 0.40.6 → 0.41.0-rc.1
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 +103 -26
- 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 +3 -2
- package/lib/components/Search/SearchInput.js +11 -3
- package/lib/components/Search/SearchItem.d.ts +2 -2
- package/lib/components/Search/SearchItem.js +23 -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 +110 -98
- package/lib/components/Select/SelectInput.d.ts +22 -0
- package/lib/components/Select/SelectInput.js +118 -0
- package/lib/components/Select/variables.js +11 -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 +1 -1
- package/lib/core/hooks/__mocks__/index.js +1 -1
- package/lib/core/hooks/__mocks__/use-theme-hooks.d.ts +1 -1
- package/lib/core/hooks/__mocks__/use-theme-hooks.js +1 -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 +16 -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 -1
- package/lib/core/types/search.d.ts +43 -2
- package/lib/core/types/select.d.ts +29 -0
- package/lib/core/types/{select-option.js → select.js} +1 -1
- 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 +7 -1
- package/lib/index.js +7 -1
- package/package.json +2 -2
- 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 +134 -0
- package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +61 -0
- package/src/components/Search/Search.tsx +2 -2
- package/src/components/Search/SearchDialog.tsx +168 -42
- package/src/components/Search/SearchFilter.tsx +90 -0
- package/src/components/Search/SearchFilterField.tsx +84 -0
- package/src/components/Search/SearchGroups.tsx +80 -0
- package/src/components/Search/SearchHighlight.tsx +29 -2
- package/src/components/Search/SearchInput.tsx +17 -3
- package/src/components/Search/SearchItem.tsx +38 -24
- package/src/components/Search/variables.ts +48 -2
- package/src/components/Segmented/Segmented.tsx +2 -2
- package/src/components/Select/Select.tsx +170 -157
- package/src/components/Select/SelectInput.tsx +184 -0
- package/src/components/Select/variables.ts +11 -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 +1 -1
- package/src/core/hooks/__mocks__/use-theme-hooks.ts +1 -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 +24 -4
- package/src/core/types/index.ts +1 -1
- package/src/core/types/l10n.ts +7 -1
- package/src/core/types/search.ts +54 -2
- package/src/core/types/select.ts +31 -0
- package/src/icons/ResetIcon/ResetIcon.tsx +26 -0
- package/src/icons/SettingsIcon/SettingsIcon.tsx +30 -0
- package/src/index.ts +7 -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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { SearchFacet, SearchFacetCount } from '@redocly/theme/core/types';
|
|
5
|
+
|
|
6
|
+
import { Tag, type TagProps } from '@redocly/theme/components/Tag/Tag';
|
|
7
|
+
|
|
8
|
+
type SearchFilterFieldTagsProps = {
|
|
9
|
+
className?: string;
|
|
10
|
+
facet: SearchFacet;
|
|
11
|
+
selectedValues: string[];
|
|
12
|
+
onChange: (value: string | string[]) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function SearchFilterFieldTags({
|
|
16
|
+
className,
|
|
17
|
+
facet,
|
|
18
|
+
selectedValues,
|
|
19
|
+
onChange,
|
|
20
|
+
}: SearchFilterFieldTagsProps) {
|
|
21
|
+
return (
|
|
22
|
+
<FilterTagsWrapper
|
|
23
|
+
data-component-name="Search/FilterFields/SearchFilterFieldTags"
|
|
24
|
+
className={className}
|
|
25
|
+
>
|
|
26
|
+
{facet.values.map((facetCount, index) => {
|
|
27
|
+
const { value, count, isCounterVisible } = facetCount as SearchFacetCount;
|
|
28
|
+
const active = selectedValues.includes(value);
|
|
29
|
+
return (
|
|
30
|
+
<FilterTagWrapper
|
|
31
|
+
key={`${count}-${index}`}
|
|
32
|
+
color={value}
|
|
33
|
+
onClick={() => {
|
|
34
|
+
const values = active
|
|
35
|
+
? selectedValues.filter((item) => item !== value)
|
|
36
|
+
: [...selectedValues, value];
|
|
37
|
+
onChange(values);
|
|
38
|
+
}}
|
|
39
|
+
active={active}
|
|
40
|
+
borderless
|
|
41
|
+
>
|
|
42
|
+
{value} {isCounterVisible && <span>{count}</span>}
|
|
43
|
+
</FilterTagWrapper>
|
|
44
|
+
);
|
|
45
|
+
})}
|
|
46
|
+
</FilterTagsWrapper>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const FilterTagsWrapper = styled.div`
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-wrap: wrap;
|
|
53
|
+
gap: var(--search-filter-field-tags-gap);
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const FilterTagWrapper = styled(Tag)<{ color: TagProps['color'] }>`
|
|
57
|
+
text-transform: uppercase;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
${({ color }) => color && `background-color: var(--tag-operation-bg-color-${color});`}
|
|
60
|
+
margin: var(--search-filter-field-tags-tag-margin);
|
|
61
|
+
`;
|
|
@@ -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,12 @@ 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';
|
|
17
20
|
|
|
18
21
|
export type SearchDialogProps = {
|
|
19
22
|
onClose: () => void;
|
|
@@ -21,11 +24,20 @@ export type SearchDialogProps = {
|
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Element {
|
|
24
|
-
const { useTranslate, useCurrentProduct,
|
|
27
|
+
const { useTranslate, useCurrentProduct, useSearch, useProducts } = useThemeHooks();
|
|
25
28
|
const products = useProducts();
|
|
26
29
|
const currentProduct = useCurrentProduct();
|
|
27
30
|
const [product, setProduct] = useState(currentProduct);
|
|
28
|
-
const { query, setQuery, items,
|
|
31
|
+
const { query, setQuery, filter, setFilter, items, isSearchLoading, facets, setLoadMore } =
|
|
32
|
+
useSearch(product?.name);
|
|
33
|
+
const {
|
|
34
|
+
isFilterOpen,
|
|
35
|
+
onFilterToggle,
|
|
36
|
+
onFilterChange,
|
|
37
|
+
onFilterReset,
|
|
38
|
+
onFacetReset,
|
|
39
|
+
onTopFacetsReset,
|
|
40
|
+
} = useSearchFilter(filter, setFilter);
|
|
29
41
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
30
42
|
const { translate } = useTranslate();
|
|
31
43
|
|
|
@@ -39,19 +51,35 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
|
|
|
39
51
|
}
|
|
40
52
|
};
|
|
41
53
|
|
|
42
|
-
const mapItem = (item:
|
|
54
|
+
const mapItem = (item: SearchItemData) => {
|
|
43
55
|
let itemProduct;
|
|
44
|
-
if (!product && item.product) {
|
|
56
|
+
if (!product && item.document.product) {
|
|
45
57
|
const resolvedProduct = products.find((product) =>
|
|
46
|
-
product.slug.match(`/${item.product?.folder}/`),
|
|
58
|
+
product.slug.match(`/${item.document.product?.folder}/`),
|
|
47
59
|
);
|
|
48
60
|
itemProduct = resolvedProduct
|
|
49
61
|
? { name: resolvedProduct.name, icon: resolvedProduct.icon }
|
|
50
62
|
: undefined;
|
|
51
63
|
}
|
|
52
|
-
return <SearchItem key={item.id} item={item} product={itemProduct} />;
|
|
64
|
+
return <SearchItem key={item.document.id} item={item} product={itemProduct} />;
|
|
53
65
|
};
|
|
54
66
|
|
|
67
|
+
const showLoadMore = (groupKey: string, currentCount: number = 0) => {
|
|
68
|
+
const topFacet = facets.find((facet) => facet.isTop);
|
|
69
|
+
let needLoadMore = false;
|
|
70
|
+
if (topFacet) {
|
|
71
|
+
const groupValue = topFacet.values.find((value) => {
|
|
72
|
+
if (typeof value === 'object') {
|
|
73
|
+
return value.value === groupKey;
|
|
74
|
+
} else return false;
|
|
75
|
+
}) as SearchFacetCount;
|
|
76
|
+
needLoadMore = groupValue ? groupValue.count > currentCount : false;
|
|
77
|
+
}
|
|
78
|
+
return needLoadMore;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const showResults = !!((filter && filter.length) || query);
|
|
82
|
+
|
|
55
83
|
return (
|
|
56
84
|
<SearchOverlay
|
|
57
85
|
data-component-name="Search/SearchDialog"
|
|
@@ -61,34 +89,83 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
|
|
|
61
89
|
>
|
|
62
90
|
<SearchDialogWrapper className="scroll-lock" role="dialog">
|
|
63
91
|
<SearchDialogHeader>
|
|
92
|
+
{product && (
|
|
93
|
+
<>
|
|
94
|
+
<SearchProductTag color="product">
|
|
95
|
+
{product.name}
|
|
96
|
+
<CloseIcon onClick={() => setProduct(undefined)} color="--icon-color-additional" />
|
|
97
|
+
</SearchProductTag>
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
64
100
|
<SearchInput
|
|
65
101
|
value={query}
|
|
66
102
|
onChange={setQuery}
|
|
103
|
+
onFilterToggle={onFilterToggle}
|
|
67
104
|
placeholder={translate('theme.search.label', 'Search docs...')}
|
|
68
|
-
isLoading={
|
|
105
|
+
isLoading={isSearchLoading}
|
|
69
106
|
data-translation-key="theme.search.label"
|
|
70
107
|
/>
|
|
71
|
-
{product && (
|
|
72
|
-
<SearchProductTag color="product">
|
|
73
|
-
{product.name}
|
|
74
|
-
<CloseIcon onClick={() => setProduct(undefined)} color="--icon-color-additional" />
|
|
75
|
-
</SearchProductTag>
|
|
76
|
-
)}
|
|
77
108
|
</SearchDialogHeader>
|
|
78
109
|
<SearchDialogBody>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
<SearchDialogBodyMainView>
|
|
111
|
+
<SearchGroups
|
|
112
|
+
facets={facets}
|
|
113
|
+
searchFilter={filter}
|
|
114
|
+
onFilterChange={onFilterChange}
|
|
115
|
+
onTopFacetsReset={onTopFacetsReset}
|
|
116
|
+
/>
|
|
117
|
+
{showResults ? (
|
|
118
|
+
items && Object.keys(items).some((key) => items[key]?.length) ? (
|
|
119
|
+
Object.keys(items).map((key) =>
|
|
120
|
+
items[key]?.length ? (
|
|
121
|
+
<Fragment key={key}>
|
|
122
|
+
<SearchGroupTitle>{key}</SearchGroupTitle>
|
|
123
|
+
{items[key]?.map(mapItem)}
|
|
124
|
+
{showLoadMore(key, items[key]?.length || 0) && (
|
|
125
|
+
<SearchGroupFooter
|
|
126
|
+
data-translation-key="theme.search.showMore"
|
|
127
|
+
onClick={() =>
|
|
128
|
+
setLoadMore({ groupKey: key, offset: items[key]?.length || 0 })
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
{translate('theme.search.showMore', 'Show more')}
|
|
132
|
+
</SearchGroupFooter>
|
|
133
|
+
)}
|
|
134
|
+
</Fragment>
|
|
135
|
+
) : null,
|
|
136
|
+
)
|
|
137
|
+
) : isSearchLoading ? (
|
|
138
|
+
<SearchMessage>
|
|
139
|
+
<SpinnerLoader size="26px" color="var(--search-input-icon-color)" />
|
|
140
|
+
{translate('theme.search.loading', 'Loading...')}
|
|
141
|
+
</SearchMessage>
|
|
142
|
+
) : (
|
|
143
|
+
<SearchMessage data-translation-key="theme.search.noResults">
|
|
144
|
+
<b>{translate('theme.search.noResults.title', 'No results')}</b>
|
|
145
|
+
{translate(
|
|
146
|
+
'theme.search.noResults.description',
|
|
147
|
+
'Prease, try with a different query.',
|
|
148
|
+
)}
|
|
149
|
+
</SearchMessage>
|
|
150
|
+
)
|
|
82
151
|
) : (
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
152
|
+
<>
|
|
153
|
+
<SearchRecent onSelect={setQuery} />
|
|
154
|
+
<SearchSuggestedPages />
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</SearchDialogBodyMainView>
|
|
158
|
+
{isFilterOpen && (
|
|
159
|
+
<SearchDialogBodyFilterView>
|
|
160
|
+
<SearchFilter
|
|
161
|
+
facets={facets}
|
|
162
|
+
filter={filter}
|
|
163
|
+
query={query}
|
|
164
|
+
onFilterChange={onFilterChange}
|
|
165
|
+
onFilterReset={onFilterReset}
|
|
166
|
+
onFacetReset={onFacetReset}
|
|
167
|
+
/>
|
|
168
|
+
</SearchDialogBodyFilterView>
|
|
92
169
|
)}
|
|
93
170
|
</SearchDialogBody>
|
|
94
171
|
<SearchDialogFooter>
|
|
@@ -109,6 +186,12 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
|
|
|
109
186
|
text={translate('theme.search.keys.exit', 'to exit')}
|
|
110
187
|
/>
|
|
111
188
|
</SearchShortcuts>
|
|
189
|
+
{isSearchLoading && (
|
|
190
|
+
<SearchLoading>
|
|
191
|
+
<SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
|
|
192
|
+
{translate('theme.search.loading', 'Loading...')}
|
|
193
|
+
</SearchLoading>
|
|
194
|
+
)}
|
|
112
195
|
<SearchCancelButton
|
|
113
196
|
data-translation-key="theme.search.cancel"
|
|
114
197
|
variant="secondary"
|
|
@@ -139,7 +222,6 @@ const SearchOverlay = styled.div`
|
|
|
139
222
|
const SearchDialogWrapper = styled.div`
|
|
140
223
|
display: flex;
|
|
141
224
|
flex-direction: column;
|
|
142
|
-
justify-content: space-between;
|
|
143
225
|
overflow: auto;
|
|
144
226
|
width: 100vw;
|
|
145
227
|
height: 100vh;
|
|
@@ -155,39 +237,51 @@ const SearchDialogWrapper = styled.div`
|
|
|
155
237
|
max-height: 95vh;
|
|
156
238
|
height: auto;
|
|
157
239
|
resize: both;
|
|
158
|
-
min-width: 300px;
|
|
159
|
-
min-height: 200px;
|
|
160
240
|
}
|
|
161
241
|
`;
|
|
162
242
|
|
|
163
243
|
const SearchDialogHeader = styled.header`
|
|
164
244
|
display: flex;
|
|
165
245
|
align-items: center;
|
|
166
|
-
border-bottom:
|
|
167
|
-
background-color: var(--search-
|
|
168
|
-
padding: var(--
|
|
246
|
+
border-bottom: var(--search-modal-border);
|
|
247
|
+
background-color: var(--search-modal-header-bg-color);
|
|
248
|
+
padding: var(--search-modal-header-padding);
|
|
169
249
|
`;
|
|
170
250
|
|
|
171
251
|
const SearchDialogBody = styled.div`
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: row;
|
|
172
254
|
flex-grow: 1;
|
|
173
|
-
overflow-y: scroll;
|
|
174
|
-
overscroll-behavior: contain;
|
|
175
255
|
|
|
176
256
|
@media screen and (min-width: ${breakpoints.small}) {
|
|
177
257
|
height: var(--search-modal-min-height);
|
|
178
258
|
}
|
|
179
259
|
`;
|
|
180
260
|
|
|
261
|
+
const SearchDialogBodyMainView = styled.div`
|
|
262
|
+
flex: 2;
|
|
263
|
+
flex-grow: 2;
|
|
264
|
+
overflow-y: scroll;
|
|
265
|
+
overscroll-behavior: contain;
|
|
266
|
+
border-right: var(--search-modal-border);
|
|
267
|
+
`;
|
|
268
|
+
|
|
269
|
+
const SearchDialogBodyFilterView = styled.div`
|
|
270
|
+
overflow: scroll;
|
|
271
|
+
`;
|
|
272
|
+
|
|
181
273
|
const SearchDialogFooter = styled.footer`
|
|
182
|
-
|
|
183
|
-
|
|
274
|
+
display: flex;
|
|
275
|
+
gap: var(--search-modal-footer-gap);
|
|
276
|
+
padding: var(--search-modal-footer-padding);
|
|
277
|
+
border-top: var(--search-modal-border);
|
|
184
278
|
`;
|
|
185
279
|
|
|
186
280
|
const SearchShortcuts = styled.div`
|
|
187
281
|
display: none;
|
|
188
282
|
justify-content: flex-start;
|
|
189
283
|
align-items: center;
|
|
190
|
-
gap: var(--
|
|
284
|
+
gap: var(--search-shortcuts-gap);
|
|
191
285
|
|
|
192
286
|
@media screen and (min-width: ${breakpoints.small}) {
|
|
193
287
|
display: flex;
|
|
@@ -195,13 +289,21 @@ const SearchShortcuts = styled.div`
|
|
|
195
289
|
`;
|
|
196
290
|
|
|
197
291
|
const SearchMessage = styled.div`
|
|
198
|
-
|
|
199
|
-
|
|
292
|
+
display: flex;
|
|
293
|
+
height: 40%;
|
|
294
|
+
justify-content: center;
|
|
295
|
+
align-items: center;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
font-size: var(--search-message-font-size);
|
|
298
|
+
font-weight: var(--search-message-font-weight);
|
|
299
|
+
line-height: var(--search-message-line-height);
|
|
300
|
+
color: var(--search-message-text-color);
|
|
301
|
+
gap: var(--search-message-gap);
|
|
200
302
|
`;
|
|
201
303
|
|
|
202
304
|
const SearchProductTag = styled(Tag)`
|
|
203
305
|
--tag-border-radius: var(--border-radius);
|
|
204
|
-
|
|
306
|
+
border: none;
|
|
205
307
|
margin: var(--spacing-xs) var(--spacing-sm) !important;
|
|
206
308
|
`;
|
|
207
309
|
|
|
@@ -212,3 +314,27 @@ const SearchCancelButton = styled(Button)`
|
|
|
212
314
|
display: none;
|
|
213
315
|
}
|
|
214
316
|
`;
|
|
317
|
+
|
|
318
|
+
const SearchGroupTitle = styled.div`
|
|
319
|
+
border-bottom: var(--search-modal-border);
|
|
320
|
+
padding: var(--search-group-title-padding);
|
|
321
|
+
background-color: var(--search-group-title-bg-color);
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
const SearchGroupFooter = styled.div`
|
|
325
|
+
display: flex;
|
|
326
|
+
justify-content: center;
|
|
327
|
+
padding: var(--search-group-footer-padding);
|
|
328
|
+
color: var(--search-group-footer-text-color);
|
|
329
|
+
cursor: pointer;
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
const SearchLoading = styled.div`
|
|
333
|
+
display: none;
|
|
334
|
+
align-items: center;
|
|
335
|
+
gap: var(--spacing-xs);
|
|
336
|
+
|
|
337
|
+
@media screen and (min-width: ${breakpoints.small}) {
|
|
338
|
+
display: flex;
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
@@ -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,80 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import { type SearchFacet, type SearchFacetCount, type SearchFilterItem } from '@redocly/theme';
|
|
5
|
+
import { Tag } from '@redocly/theme/components/Tag/Tag';
|
|
6
|
+
|
|
7
|
+
type SearchGroupsProps = {
|
|
8
|
+
facets: SearchFacet[];
|
|
9
|
+
searchFilter: SearchFilterItem[];
|
|
10
|
+
onFilterChange: (field: string, value: string[], isTop?: boolean) => void;
|
|
11
|
+
onTopFacetsReset: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function SearchGroups({
|
|
15
|
+
facets,
|
|
16
|
+
searchFilter,
|
|
17
|
+
onFilterChange,
|
|
18
|
+
onTopFacetsReset,
|
|
19
|
+
}: SearchGroupsProps): JSX.Element {
|
|
20
|
+
const groupFacets = facets.filter((facet) => facet.isTop);
|
|
21
|
+
|
|
22
|
+
const handleGroupTagClick = (
|
|
23
|
+
value: string,
|
|
24
|
+
fieldId: string,
|
|
25
|
+
active: boolean,
|
|
26
|
+
currentValues: string[],
|
|
27
|
+
) => {
|
|
28
|
+
const values = active
|
|
29
|
+
? currentValues.filter((item) => item !== value)
|
|
30
|
+
: [...currentValues, value];
|
|
31
|
+
onFilterChange(fieldId, values, true);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<SearchGroupsWrapper>
|
|
36
|
+
<GroupTag
|
|
37
|
+
borderless
|
|
38
|
+
active={!searchFilter.some((item) => item.isTop)}
|
|
39
|
+
onClick={() => searchFilter.some((item) => item.isTop) && onTopFacetsReset()}
|
|
40
|
+
>
|
|
41
|
+
All
|
|
42
|
+
</GroupTag>
|
|
43
|
+
<Divider />
|
|
44
|
+
{groupFacets.flatMap((facet) =>
|
|
45
|
+
facet.values.map((facetCount, index) => {
|
|
46
|
+
const { value, count, isCounterVisible } = facetCount as SearchFacetCount;
|
|
47
|
+
const currentValues =
|
|
48
|
+
searchFilter.find((item) => item.field === facet.field)?.values || [];
|
|
49
|
+
const active = currentValues?.includes(value);
|
|
50
|
+
return (
|
|
51
|
+
<GroupTag
|
|
52
|
+
key={`${facet.field}-${index}`}
|
|
53
|
+
onClick={() => handleGroupTagClick(value, facet.field, active, currentValues)}
|
|
54
|
+
active={active}
|
|
55
|
+
borderless
|
|
56
|
+
>
|
|
57
|
+
{value} {isCounterVisible && <span>{count}</span>}
|
|
58
|
+
</GroupTag>
|
|
59
|
+
);
|
|
60
|
+
}),
|
|
61
|
+
)}
|
|
62
|
+
</SearchGroupsWrapper>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SearchGroupsWrapper = styled.div`
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: 4px;
|
|
69
|
+
padding: var(--spacing-md);
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const GroupTag = styled(Tag)`
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
gap: 4px;
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
const Divider = styled.div`
|
|
78
|
+
border-right: 1px solid var(--border-color-secondary);
|
|
79
|
+
margin: 5px 5px 5px 0;
|
|
80
|
+
`;
|