Skip to content

Commit f371daa

Browse files
authored
feat(Search): added All results page for search + hide pages without search results (#399)
* feat: added all results search page * fix: added 8px margin to the "All results" page + fixed the Gear icon * fix: fixed e2e screenshots * fix: fixed "Go to" links when searching
1 parent f8c4533 commit f371daa

14 files changed

+177
-35
lines changed

assets/icons/gear.svg

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/components/Settings/Settings.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import {isSectionSelected} from './Selection/utils';
1616
import {SettingsMenu, SettingsMenuInstance} from './SettingsMenu/SettingsMenu';
1717
import {SettingsMenuMobile} from './SettingsMenuMobile/SettingsMenuMobile';
18+
import {useAllResultsPage} from './SettingsSearch/AllResultsPage';
1819
import {SettingsSearch} from './SettingsSearch/SettingsSearch';
1920
import type {
2021
SettingsItem,
@@ -184,19 +185,26 @@ function SettingsContent({
184185
}, []);
185186

186187
let activePage = selectedPage;
187-
if (!activePage || pages[activePage]?.hidden) {
188+
189+
if (!activePage || !pages[activePage] || pages[activePage].hidden) {
188190
activePage = Object.values(pages).find(({hidden}) => !hidden)?.id;
189191
}
190192

191193
const handlePageChange = (newPage: string | undefined) => {
192194
setCurrentPage((prevPage) => {
193-
if (prevPage !== newPage) {
195+
if (prevPage !== newPage && !isFakePage(newPage)) {
194196
onPageChange?.(newPage);
195197
}
196198
return newPage;
197199
});
198200
};
199201

202+
const {isAllSearchPage} = useAllResultsPage({pages, handlePageChange});
203+
204+
function isFakePage(page: string | undefined) {
205+
return isAllSearchPage(page);
206+
}
207+
200208
React.useEffect(() => {
201209
if (activePage !== selectedPage) {
202210
handlePageChange(activePage);
@@ -301,6 +309,7 @@ function SettingsContent({
301309
inputRef={searchInputRef}
302310
className={b('search')}
303311
initialValue={initialSearch}
312+
selection={selection}
304313
onChange={setSearch}
305314
autoFocus={false}
306315
inputSize={'xl'}
@@ -333,6 +342,7 @@ function SettingsContent({
333342
inputRef={searchInputRef}
334343
className={b('search')}
335344
initialValue={initialSearch}
345+
selection={selection}
336346
onChange={setSearch}
337347
placeholder={filterPlaceholder}
338348
autoFocus

src/components/Settings/SettingsMenu/SettingsMenu.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ $block: '.#{variables.$ns}settings-menu';
1919
}
2020
}
2121

22+
&__item + &__group {
23+
margin-top: 8px;
24+
}
25+
2226
&__item {
2327
$item: &;
2428
display: flex;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React from 'react';
2+
3+
import {ListUl} from '@gravity-ui/icons';
4+
import last from 'lodash/last';
5+
6+
import type {
7+
SettingsDescription,
8+
SettingsMenu,
9+
SettingsMenuItem,
10+
SettingsPage,
11+
SettingsPageSection,
12+
} from '../collect-settings';
13+
import i18n from '../i18n';
14+
15+
const allSearchResultsId = 'allSearchResults';
16+
const allSearchResultsLabel = i18n('label_all-results');
17+
18+
export function useAllResultsPage({
19+
pages,
20+
handlePageChange,
21+
}: {
22+
pages: SettingsDescription['pages'];
23+
handlePageChange: (page: string | undefined) => void;
24+
}) {
25+
const allSearchResultsPage = pages[allSearchResultsId];
26+
const hasAllSearchResultsPage = Boolean(allSearchResultsPage);
27+
28+
React.useEffect(() => {
29+
if (hasAllSearchResultsPage) {
30+
handlePageChange(allSearchResultsId);
31+
}
32+
// eslint-disable-next-line react-hooks/exhaustive-deps
33+
}, [hasAllSearchResultsPage]);
34+
35+
return {
36+
isAllSearchPage: (page: string | undefined) => page === allSearchResultsId,
37+
};
38+
}
39+
40+
export function getSettingsDescriptionWithAllResultsPage(
41+
settingsDescription: SettingsDescription,
42+
): SettingsDescription {
43+
const {menu, pages} = settingsDescription;
44+
const menuWithoutAllResults = getMenuWithoutAllResults(menu);
45+
const pagesList = Object.values(pages);
46+
47+
return {
48+
...settingsDescription,
49+
menu: [createAllResultsMenuItem(), ...menuWithoutAllResults],
50+
pages: {
51+
...pages,
52+
[allSearchResultsId]: createAllResultsPage(pagesList, menuWithoutAllResults),
53+
},
54+
};
55+
}
56+
57+
function getMenuWithoutAllResults(menu: SettingsMenu) {
58+
return menu.filter((item) => !('id' in item) || item.id !== allSearchResultsId);
59+
}
60+
61+
function createAllResultsPage(pages: SettingsPage[], menu: SettingsMenu): SettingsPage {
62+
const breadcrumbsMap: Record<string, string[] | undefined> = {};
63+
64+
for (const menuItem of menu) {
65+
if ('items' in menuItem) {
66+
for (const pageItem of menuItem.items) {
67+
breadcrumbsMap[pageItem.id] = [menuItem.groupTitle, pageItem.title];
68+
}
69+
} else {
70+
breadcrumbsMap[menuItem.id] = [menuItem.title];
71+
}
72+
}
73+
74+
return {
75+
id: allSearchResultsId,
76+
sections: Object.values(pages)
77+
.map((page) => {
78+
return page.sections.map((section): SettingsPageSection => {
79+
const breadcrumbs = breadcrumbsMap[page.id] ?? [];
80+
const lastBreadcrumb = last(breadcrumbs);
81+
const breadcrumbsTitlePart = breadcrumbs.join(' / ');
82+
83+
return {
84+
...section,
85+
title:
86+
section.title === lastBreadcrumb
87+
? breadcrumbsTitlePart
88+
: `${breadcrumbsTitlePart} / ${section.title}`,
89+
};
90+
});
91+
})
92+
.flat(),
93+
};
94+
}
95+
96+
function createAllResultsMenuItem(): SettingsMenuItem {
97+
return {
98+
id: allSearchResultsId,
99+
title: allSearchResultsLabel,
100+
icon: {data: ListUl},
101+
};
102+
}

src/components/Settings/SettingsSearch/SettingsSearch.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React from 'react';
1+
import React, {useEffect} from 'react';
22

33
import {TextInput, TextInputSize} from '@gravity-ui/uikit';
44
import debounceFn from 'lodash/debounce';
55

66
import {block} from '../../utils/cn';
7+
import {SettingsSelection} from '../Selection/types';
78
import {useStableCallback} from '../helpers';
89
import i18n from '../i18n';
910

@@ -18,6 +19,7 @@ interface SettingsSearchProps {
1819
inputSize?: TextInputSize;
1920
placeholder?: string;
2021
autoFocus?: boolean;
22+
selection?: SettingsSelection;
2123
}
2224

2325
export function SettingsSearch({
@@ -29,15 +31,27 @@ export function SettingsSearch({
2931
inputSize,
3032
placeholder,
3133
autoFocus = true,
34+
selection,
3235
}: SettingsSearchProps) {
3336
const [value, setValue] = React.useState(initialValue ?? '');
3437

35-
const onChangeDebounced = useStableCallback(debounceFn(onChange, debounce));
38+
const onChangeDebounced = debounceFn(onChange, debounce);
39+
3640
const handleUpdate = useStableCallback((updated: string) => {
3741
setValue(updated);
3842
onChangeDebounced(updated);
3943
});
4044

45+
useEffect(() => {
46+
if (value && selection) {
47+
onChangeDebounced.cancel();
48+
setValue('');
49+
onChange('');
50+
}
51+
// Remove any search, if selection is passed
52+
// eslint-disable-next-line react-hooks/exhaustive-deps
53+
}, [selection]);
54+
4155
return (
4256
<div className={b(null, className)}>
4357
<TextInput

src/components/Settings/__stories__/SettingsDemo.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, {useReducer} from 'react';
22

3+
import {Gear} from '@gravity-ui/icons';
34
import {
45
Button,
56
Checkbox,
@@ -15,8 +16,6 @@ import {cn} from '../../utils/cn';
1516
import {SettingsSelection} from '../Selection/types';
1617
import {Settings} from '../index';
1718

18-
import featureIcon from '../../../../assets/icons/gear.svg';
19-
2019
import './SettingsDemo.scss';
2120

2221
export interface DemoProps {
@@ -108,7 +107,7 @@ export const SettingsComponent = React.memo(
108107
selection={selection}
109108
>
110109
<Settings.Group id="arcanum" groupTitle="Arcanum">
111-
<Settings.Page id="features" title="Features" icon={{data: featureIcon}}>
110+
<Settings.Page id="features" title="Features" icon={{data: Gear}}>
112111
<Settings.Section title="Beta functionality">
113112
<Settings.Item
114113
title="YFM markdown in md. files"
@@ -201,7 +200,7 @@ export const SettingsComponent = React.memo(
201200
</Settings.Item>
202201
</Settings.Section>
203202
</Settings.Page>
204-
<Settings.Page id="appearance" title="Appearance" icon={{data: featureIcon}}>
203+
<Settings.Page id="appearance" title="Appearance" icon={{data: Gear}}>
205204
<Settings.Section
206205
title="Appearance"
207206
header={
@@ -282,7 +281,7 @@ function renderGeneralSettings(
282281
) {
283282
return (
284283
<Settings.Group id="general" groupTitle="General">
285-
<Settings.Page id="appearance" title="Appearance" icon={{data: featureIcon}}>
284+
<Settings.Page id="appearance" title="Appearance" icon={{data: Gear}}>
286285
<Settings.Section title="Appearance">
287286
<Settings.Item title="Interface language">
288287
<SegmentedRadioGroup
@@ -308,7 +307,7 @@ function renderGeneralSettings(
308307
</Settings.Item>
309308
</Settings.Section>
310309
</Settings.Page>
311-
<Settings.Page id="communication" title="Communication" icon={{data: featureIcon}}>
310+
<Settings.Page id="communication" title="Communication" icon={{data: Gear}}>
312311
<Settings.Section title="Phone settings" withBadge={withBadge}>
313312
<Settings.Item
314313
title="Send notifications"

src/components/Settings/__stories__/SettingsMobileDemo.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import React, {useReducer} from 'react';
22

3+
import {Gear} from '@gravity-ui/icons';
34
import {Button, Radio, SegmentedRadioGroup, Select, Switch, useUniqId} from '@gravity-ui/uikit';
45

56
import {Settings} from '../../..';
67
import {cn} from '../../utils/cn';
78

8-
import featureIcon from '../../../../assets/icons/gear.svg';
9-
109
import './SettingsMobileDemo.scss';
1110

1211
const b = cn('settings-mobile-demo');
@@ -233,7 +232,7 @@ function renderGeneralSettings(
233232
) {
234233
return (
235234
<Settings.Group id="general" groupTitle="General">
236-
<Settings.Page id="appearance" title="General Appearance" icon={{data: featureIcon}}>
235+
<Settings.Page id="appearance" title="General Appearance" icon={{data: Gear}}>
237236
<Settings.Section title="Appearance">
238237
<Settings.Item title="Interface language">
239238
<SegmentedRadioGroup
@@ -261,7 +260,7 @@ function renderGeneralSettings(
261260
</Settings.Item>
262261
</Settings.Section>
263262
</Settings.Page>
264-
<Settings.Page id="communication" title="Communication" icon={{data: featureIcon}}>
263+
<Settings.Page id="communication" title="Communication" icon={{data: Gear}}>
265264
<Settings.Section title="Send notifications" withBadge={withBadge}>
266265
<Settings.Item
267266
title="Monitoring"

0 commit comments

Comments
 (0)