@cloud-ru/uikit-product-mobile-toolbar 0.4.13 → 0.5.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cjs/components/MobileToolbar/MobileToolbar.d.ts +6 -2
  3. package/dist/cjs/components/MobileToolbar/MobileToolbar.js +2 -1
  4. package/dist/cjs/components/MobileToolbar/hooks/index.d.ts +2 -0
  5. package/dist/cjs/components/MobileToolbar/hooks/index.js +18 -0
  6. package/dist/cjs/components/MobileToolbar/{hooks.d.ts → hooks/useFilters.d.ts} +2 -2
  7. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/index.d.ts +1 -0
  8. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/index.js +17 -0
  9. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/usePersistState.d.ts +9 -0
  10. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/usePersistState.js +61 -0
  11. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/utils/index.d.ts +2 -0
  12. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/utils/index.js +18 -0
  13. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/utils/parser.d.ts +4 -0
  14. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/utils/parser.js +46 -0
  15. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/utils/serializer.d.ts +11 -0
  16. package/dist/cjs/components/MobileToolbar/hooks/usePersistState/utils/serializer.js +45 -0
  17. package/dist/cjs/components/MobileToolbar/types.d.ts +23 -0
  18. package/dist/esm/components/MobileToolbar/MobileToolbar.d.ts +6 -2
  19. package/dist/esm/components/MobileToolbar/MobileToolbar.js +3 -2
  20. package/dist/esm/components/MobileToolbar/hooks/index.d.ts +2 -0
  21. package/dist/esm/components/MobileToolbar/hooks/index.js +2 -0
  22. package/dist/esm/components/MobileToolbar/{hooks.d.ts → hooks/useFilters.d.ts} +2 -2
  23. package/dist/esm/components/MobileToolbar/hooks/usePersistState/index.d.ts +1 -0
  24. package/dist/esm/components/MobileToolbar/hooks/usePersistState/index.js +1 -0
  25. package/dist/esm/components/MobileToolbar/hooks/usePersistState/usePersistState.d.ts +9 -0
  26. package/dist/esm/components/MobileToolbar/hooks/usePersistState/usePersistState.js +58 -0
  27. package/dist/esm/components/MobileToolbar/hooks/usePersistState/utils/index.d.ts +2 -0
  28. package/dist/esm/components/MobileToolbar/hooks/usePersistState/utils/index.js +2 -0
  29. package/dist/esm/components/MobileToolbar/hooks/usePersistState/utils/parser.d.ts +4 -0
  30. package/dist/esm/components/MobileToolbar/hooks/usePersistState/utils/parser.js +41 -0
  31. package/dist/esm/components/MobileToolbar/hooks/usePersistState/utils/serializer.d.ts +11 -0
  32. package/dist/esm/components/MobileToolbar/hooks/usePersistState/utils/serializer.js +40 -0
  33. package/dist/esm/components/MobileToolbar/types.d.ts +23 -0
  34. package/package.json +11 -9
  35. package/src/components/MobileToolbar/MobileToolbar.tsx +9 -2
  36. package/src/components/MobileToolbar/hooks/index.ts +2 -0
  37. package/src/components/MobileToolbar/{hooks.ts → hooks/useFilters.ts} +2 -2
  38. package/src/components/MobileToolbar/hooks/usePersistState/index.ts +1 -0
  39. package/src/components/MobileToolbar/hooks/usePersistState/usePersistState.ts +87 -0
  40. package/src/components/MobileToolbar/hooks/usePersistState/utils/index.ts +2 -0
  41. package/src/components/MobileToolbar/hooks/usePersistState/utils/parser.ts +61 -0
  42. package/src/components/MobileToolbar/hooks/usePersistState/utils/serializer.ts +54 -0
  43. package/src/components/MobileToolbar/types.ts +25 -0
  44. /package/dist/cjs/components/MobileToolbar/{hooks.js → hooks/useFilters.js} +0 -0
  45. /package/dist/esm/components/MobileToolbar/{hooks.js → hooks/useFilters.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 0.5.0 (2026-01-21)
7
+
8
+
9
+ ### Features
10
+
11
+ * **FF-7319:** persist state functionality in mobile toolbar ([95099d3](https://gitverse.ru/cloud-ru-tech/uikit-product/commits/95099d3de3883152d2eb8066537c4bb8ad74ed92))
12
+
13
+
14
+
15
+
16
+
17
+ ## 0.4.14 (2025-12-17)
18
+
19
+ ### Only dependencies have been changed
20
+ * [@cloud-ru/uikit-product-icons@16.1.1](https://gitverse.ru/cloud-ru-tech/uikit-product/-/blob/master/packages/icons/CHANGELOG.md)
21
+ * [@cloud-ru/uikit-product-mobile-chips@0.8.47](https://gitverse.ru/cloud-ru-tech/uikit-product/-/blob/master/packages/mobile-chips/CHANGELOG.md)
22
+ * [@cloud-ru/uikit-product-mobile-dropdown@0.9.31](https://gitverse.ru/cloud-ru-tech/uikit-product/-/blob/master/packages/mobile-dropdown/CHANGELOG.md)
23
+ * [@cloud-ru/uikit-product-mobile-tooltip@0.5.4](https://gitverse.ru/cloud-ru-tech/uikit-product/-/blob/master/packages/mobile-tooltip/CHANGELOG.md)
24
+ * [@cloud-ru/uikit-product-utils@8.1.0](https://gitverse.ru/cloud-ru-tech/uikit-product/-/blob/master/packages/utils/CHANGELOG.md)
25
+
26
+
27
+
28
+
29
+
6
30
  ## 0.4.13 (2025-12-12)
7
31
 
8
32
  ### Only dependencies have been changed
@@ -1,7 +1,11 @@
1
1
  import { FiltersState } from '@cloud-ru/uikit-product-mobile-chips';
2
2
  import { WithSupportProps } from '@snack-uikit/utils';
3
- import { CheckedToolbarProps, DefaultToolbarProps, FilterRow } from './types';
3
+ import { CheckedToolbarProps, DefaultToolbarProps, FilterRow, ToolbarPersistConfig } from './types';
4
4
  export type MobileToolbarProps<TState extends FiltersState = Record<string, unknown>> = WithSupportProps<DefaultToolbarProps | CheckedToolbarProps> & {
5
5
  filterRow?: FilterRow<TState>;
6
+ /** Конфиг для сохранения состояния в localStorage и queryParams. <br>
7
+ * Поле id должно быть уникальным для каждого инстанса компонента. <br>
8
+ * */
9
+ persist?: ToolbarPersistConfig<TState>;
6
10
  };
7
- export declare function MobileToolbar<TState extends FiltersState = Record<string, unknown>>({ className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps, ...rest }: MobileToolbarProps<TState>): import("react/jsx-runtime").JSX.Element;
11
+ export declare function MobileToolbar<TState extends FiltersState = Record<string, unknown>>({ className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps, persist, ...rest }: MobileToolbarProps<TState>): import("react/jsx-runtime").JSX.Element;
@@ -32,12 +32,13 @@ const hooks_1 = require("./hooks");
32
32
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
33
33
  function MobileToolbar(_a) {
34
34
  var _b, _c;
35
- var { className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps } = _a, rest = __rest(_a, ["className", "after", "outline", "moreActions", "onRefresh", "search", "filterRow"]);
35
+ var { className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps, persist } = _a, rest = __rest(_a, ["className", "after", "outline", "moreActions", "onRefresh", "search", "filterRow", "persist"]);
36
36
  const needsBulkActions = (0, helpers_1.isBulkActionsProps)(rest);
37
37
  const hasVisibleRefresh = onRefresh && !(moreActions === null || moreActions === void 0 ? void 0 : moreActions.length);
38
38
  const hasLeftSideElements = Boolean(needsBulkActions || hasVisibleRefresh);
39
39
  const resizingContainerRef = (0, react_1.useRef)(null);
40
40
  const { t } = (0, uikit_product_locale_1.useLocale)('MobileToolbar');
41
+ (0, hooks_1.usePersistState)({ persist, filter: filterRowProps === null || filterRowProps === void 0 ? void 0 : filterRowProps.value, search: search === null || search === void 0 ? void 0 : search.value });
41
42
  const moreActionsProps = (0, react_1.useMemo)(() => onRefresh
42
43
  ? {
43
44
  pinTop: [{ content: { option: t('refresh') }, icon: (0, jsx_runtime_1.jsx)(uikit_product_icons_1.UpdateSVG, {}), onClick: onRefresh }],
@@ -0,0 +1,2 @@
1
+ export * from './useFilters';
2
+ export * from './usePersistState';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./useFilters"), exports);
18
+ __exportStar(require("./usePersistState"), exports);
@@ -1,6 +1,6 @@
1
1
  import { FiltersState } from '@cloud-ru/uikit-product-mobile-chips';
2
- import { FilterButtonProps } from '../../helperComponents';
3
- import { FilterRow } from './types';
2
+ import { FilterButtonProps } from '../../../helperComponents';
3
+ import { FilterRow } from '../types';
4
4
  type UseFiltersProps<TState extends FiltersState> = {
5
5
  filterRow?: FilterRow<TState>;
6
6
  };
@@ -0,0 +1 @@
1
+ export * from './usePersistState';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./usePersistState"), exports);
@@ -0,0 +1,9 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
2
+ import { ToolbarPersistConfig } from '../../types';
3
+ type usePersistStateProps<TState extends FiltersState = Record<string, unknown>> = {
4
+ persist?: ToolbarPersistConfig<TState>;
5
+ filter?: TState;
6
+ search?: string;
7
+ };
8
+ export declare function usePersistState<TState extends FiltersState = Record<string, unknown>>({ persist, filter, search, }: usePersistStateProps<TState>): void;
9
+ export {};
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usePersistState = usePersistState;
4
+ const react_1 = require("react");
5
+ const utils_1 = require("@snack-uikit/utils");
6
+ const utils_2 = require("./utils");
7
+ function usePersistState({ persist, filter, search, }) {
8
+ const hasHydratedRef = (0, react_1.useRef)(false);
9
+ const dataPersistOptions = (0, react_1.useMemo)(() => {
10
+ if (!(persist === null || persist === void 0 ? void 0 : persist.filterQueryKey) || !(persist === null || persist === void 0 ? void 0 : persist.id))
11
+ return undefined;
12
+ const defaultValidate = (value) => {
13
+ const filterOk = (value === null || value === void 0 ? void 0 : value.filter) === undefined || (typeof value.filter === 'object' && value.filter !== null);
14
+ const searchOk = (value === null || value === void 0 ? void 0 : value.search) === undefined || typeof value.search === 'string';
15
+ return Boolean(filterOk && searchOk);
16
+ };
17
+ const combinedValidate = (value) => {
18
+ const baseValid = defaultValidate(value);
19
+ return (persist === null || persist === void 0 ? void 0 : persist.validateData) ? baseValid && persist.validateData(value) : baseValid;
20
+ };
21
+ return {
22
+ queryKey: persist.filterQueryKey,
23
+ localStorageKey: `${persist.id}_filter`,
24
+ validateData: combinedValidate,
25
+ };
26
+ }, [persist]);
27
+ const { getDefaultData, setDataToStorages } = (0, utils_1.useDataPersist)({
28
+ options: dataPersistOptions,
29
+ serializer: value => ((persist === null || persist === void 0 ? void 0 : persist.serializer) ? persist.serializer(value) : (0, utils_2.defaultSerializer)(value)),
30
+ parser: (value) => ((persist === null || persist === void 0 ? void 0 : persist.parser) ? persist.parser(value) : (0, utils_2.defaultParser)(value)),
31
+ });
32
+ const defaultState = (0, react_1.useMemo)(getDefaultData, [getDefaultData]);
33
+ (0, react_1.useEffect)(() => {
34
+ var _a, _b;
35
+ if (hasHydratedRef.current) {
36
+ return;
37
+ }
38
+ if (defaultState) {
39
+ (_a = persist === null || persist === void 0 ? void 0 : persist.onLoad) === null || _a === void 0 ? void 0 : _a.call(persist, Object.assign(Object.assign({}, defaultState), { filter: (0, utils_2.prepareDataForFilter)((_b = defaultState.filter) !== null && _b !== void 0 ? _b : {}) }));
40
+ }
41
+ // mark as hydrated after attempting to load defaultState
42
+ hasHydratedRef.current = true;
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, [defaultState]);
45
+ (0, react_1.useEffect)(() => {
46
+ var _a, _b;
47
+ if (!(persist === null || persist === void 0 ? void 0 : persist.id) || !(persist === null || persist === void 0 ? void 0 : persist.filterQueryKey) || !hasHydratedRef.current) {
48
+ return;
49
+ }
50
+ const snapshot = (_b = (_a = persist.state) !== null && _a !== void 0 ? _a : getDefaultData()) !== null && _b !== void 0 ? _b : {};
51
+ if (filter) {
52
+ snapshot.filter = filter;
53
+ }
54
+ if (search !== undefined) {
55
+ snapshot.search = search;
56
+ }
57
+ if (Object.keys(snapshot).length === 0)
58
+ return;
59
+ setDataToStorages(snapshot);
60
+ }, [persist === null || persist === void 0 ? void 0 : persist.id, persist === null || persist === void 0 ? void 0 : persist.filterQueryKey, persist === null || persist === void 0 ? void 0 : persist.state, filter, search, setDataToStorages, getDefaultData]);
61
+ }
@@ -0,0 +1,2 @@
1
+ export * from './parser';
2
+ export * from './serializer';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./parser"), exports);
18
+ __exportStar(require("./serializer"), exports);
@@ -0,0 +1,4 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
2
+ import { PersistedFilterState } from '../../../types';
3
+ export declare const defaultParser: <T extends FiltersState>(value: string) => PersistedFilterState<T>;
4
+ export declare const prepareDataForFilter: <T>(filter: Record<string, unknown>) => T;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareDataForFilter = exports.defaultParser = void 0;
4
+ const ft_request_payload_transform_1 = require("@cloud-ru/ft-request-payload-transform");
5
+ const toolbar_1 = require("@snack-uikit/toolbar");
6
+ const parseValue = (value) => {
7
+ if ((0, toolbar_1.isDateString)(value)) {
8
+ return new Date(value);
9
+ }
10
+ if (typeof value === 'boolean') {
11
+ return String(value);
12
+ }
13
+ return value;
14
+ };
15
+ const mapFilterFromPayload = (value) => {
16
+ if (!value) {
17
+ return undefined;
18
+ }
19
+ return Object.fromEntries(value.map(filter => {
20
+ if (Array.isArray(filter.value)) {
21
+ return [filter.field, filter.value.map(parseValue)];
22
+ }
23
+ return [filter.field, parseValue(filter.value)];
24
+ }));
25
+ };
26
+ const defaultParser = (value) => {
27
+ var _a, _b;
28
+ const parsed = (0, ft_request_payload_transform_1.parseQueryParamsString)(value);
29
+ return {
30
+ pagination: parsed === null || parsed === void 0 ? void 0 : parsed.pagination,
31
+ ordering: parsed === null || parsed === void 0 ? void 0 : parsed.ordering,
32
+ search: ((_a = parsed === null || parsed === void 0 ? void 0 : parsed.search) === null || _a === void 0 ? void 0 : _a.toString()) || '',
33
+ filter: (_b = mapFilterFromPayload(parsed === null || parsed === void 0 ? void 0 : parsed.filter)) !== null && _b !== void 0 ? _b : {},
34
+ };
35
+ };
36
+ exports.defaultParser = defaultParser;
37
+ const prepareDataForFilter = (filter) => Object.entries(filter).reduce((acc, [key, value]) => {
38
+ if (Array.isArray(value)) {
39
+ acc[key] = value.map(parseValue);
40
+ }
41
+ else {
42
+ acc[key] = parseValue(value);
43
+ }
44
+ return acc;
45
+ }, Object.assign({}, filter));
46
+ exports.prepareDataForFilter = prepareDataForFilter;
@@ -0,0 +1,11 @@
1
+ import { RequestPayloadParams } from '@cloud-ru/ft-request-payload-transform';
2
+ import { FiltersState } from '@snack-uikit/chips';
3
+ import { PersistedFilterState } from '../../../types';
4
+ /** Вспомогательная функция для преобразования состояния тулбара к формату RequestPayloadParams */
5
+ export declare const formatFilterStateToRequestPayload: <T extends FiltersState>(value?: PersistedFilterState<T> | null) => {
6
+ toObject(): Partial<RequestPayloadParams>;
7
+ toString(params?: {
8
+ encode?: boolean;
9
+ }): string;
10
+ };
11
+ export declare const defaultSerializer: <T extends FiltersState>(value: PersistedFilterState<T>) => string;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultSerializer = exports.formatFilterStateToRequestPayload = void 0;
4
+ const ft_request_payload_transform_1 = require("@cloud-ru/ft-request-payload-transform");
5
+ const toolbar_1 = require("@snack-uikit/toolbar");
6
+ const mapValueToString = (filter) => {
7
+ if (filter instanceof Date) {
8
+ return filter.toISOString();
9
+ }
10
+ if ((0, toolbar_1.isDateString)(filter)) {
11
+ return filter;
12
+ }
13
+ return filter;
14
+ };
15
+ const mapFilterToPayload = (value) => {
16
+ if (!value) {
17
+ return undefined;
18
+ }
19
+ return Object.entries(value)
20
+ .filter(([, v]) => v !== undefined)
21
+ .map(([key, v]) => Array.isArray(v)
22
+ ? {
23
+ value: v.map(mapValueToString),
24
+ condition: 'in',
25
+ field: key,
26
+ }
27
+ : {
28
+ value: mapValueToString(v),
29
+ condition: 'eq',
30
+ field: key,
31
+ });
32
+ };
33
+ /** Вспомогательная функция для преобразования состояния тулбара к формату RequestPayloadParams */
34
+ const formatFilterStateToRequestPayload = (value) => {
35
+ var _a;
36
+ return (0, ft_request_payload_transform_1.createRequestPayload)({
37
+ pagination: value === null || value === void 0 ? void 0 : value.pagination,
38
+ search: (_a = value === null || value === void 0 ? void 0 : value.search) !== null && _a !== void 0 ? _a : '',
39
+ ordering: value === null || value === void 0 ? void 0 : value.ordering,
40
+ filter: mapFilterToPayload(value === null || value === void 0 ? void 0 : value.filter),
41
+ });
42
+ };
43
+ exports.formatFilterStateToRequestPayload = formatFilterStateToRequestPayload;
44
+ const defaultSerializer = (value) => (0, exports.formatFilterStateToRequestPayload)(value).toString();
45
+ exports.defaultSerializer = defaultSerializer;
@@ -1,3 +1,4 @@
1
+ import { RequestPayloadParams } from '@cloud-ru/ft-request-payload-transform';
1
2
  import { ReactNode } from 'react';
2
3
  import { FiltersState, MobileChipChoiceRowProps } from '@cloud-ru/uikit-product-mobile-chips';
3
4
  import { BulkActionsProps, MoreActionsProps } from '../../helperComponents';
@@ -41,4 +42,26 @@ export type FilterRow<TState extends FiltersState> = Omit<MobileChipChoiceRowPro
41
42
  initialOpen?: boolean;
42
43
  onOpenChange?(isOpen: boolean): void;
43
44
  };
45
+ export type PersistedFilterState<T extends FiltersState> = {
46
+ pagination?: RequestPayloadParams['pagination'];
47
+ ordering?: RequestPayloadParams['ordering'];
48
+ search?: string;
49
+ filter?: T;
50
+ };
51
+ export type ToolbarPersistConfig<T extends FiltersState> = {
52
+ /** Уникальный id для текущего инстанса компонента */
53
+ id?: string;
54
+ /** Ключ для queryParams */
55
+ filterQueryKey?: string;
56
+ /** Валидатор сохраненных */
57
+ validateData?(value: unknown): value is PersistedFilterState<T>;
58
+ /** Custom-сериализация состояния перед сохранением в queryParams */
59
+ serializer?(value: PersistedFilterState<T>): string;
60
+ /** Custom-парсер queryParams для преобразования в данные состояния */
61
+ parser?(value: string): PersistedFilterState<T>;
62
+ /** Состояние для сохранения */
63
+ state?: PersistedFilterState<T>;
64
+ /** Колбэк при первом рендере для получения сохраненных данных и установки их в стейт */
65
+ onLoad?(state: PersistedFilterState<T>): void;
66
+ };
44
67
  export {};
@@ -1,7 +1,11 @@
1
1
  import { FiltersState } from '@cloud-ru/uikit-product-mobile-chips';
2
2
  import { WithSupportProps } from '@snack-uikit/utils';
3
- import { CheckedToolbarProps, DefaultToolbarProps, FilterRow } from './types';
3
+ import { CheckedToolbarProps, DefaultToolbarProps, FilterRow, ToolbarPersistConfig } from './types';
4
4
  export type MobileToolbarProps<TState extends FiltersState = Record<string, unknown>> = WithSupportProps<DefaultToolbarProps | CheckedToolbarProps> & {
5
5
  filterRow?: FilterRow<TState>;
6
+ /** Конфиг для сохранения состояния в localStorage и queryParams. <br>
7
+ * Поле id должно быть уникальным для каждого инстанса компонента. <br>
8
+ * */
9
+ persist?: ToolbarPersistConfig<TState>;
6
10
  };
7
- export declare function MobileToolbar<TState extends FiltersState = Record<string, unknown>>({ className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps, ...rest }: MobileToolbarProps<TState>): import("react/jsx-runtime").JSX.Element;
11
+ export declare function MobileToolbar<TState extends FiltersState = Record<string, unknown>>({ className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps, persist, ...rest }: MobileToolbarProps<TState>): import("react/jsx-runtime").JSX.Element;
@@ -22,16 +22,17 @@ import { TEST_IDS } from '../../constants';
22
22
  import { BulkActions, FilterButton, MoreActions, Separator } from '../../helperComponents';
23
23
  import { BulkActionsCheckbox } from '../../helperComponents/BulkActionsCheckbox';
24
24
  import { isBulkActionsProps } from './helpers';
25
- import { useFilters } from './hooks';
25
+ import { useFilters, usePersistState } from './hooks';
26
26
  import styles from './styles.module.css';
27
27
  export function MobileToolbar(_a) {
28
28
  var _b, _c;
29
- var { className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps } = _a, rest = __rest(_a, ["className", "after", "outline", "moreActions", "onRefresh", "search", "filterRow"]);
29
+ var { className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps, persist } = _a, rest = __rest(_a, ["className", "after", "outline", "moreActions", "onRefresh", "search", "filterRow", "persist"]);
30
30
  const needsBulkActions = isBulkActionsProps(rest);
31
31
  const hasVisibleRefresh = onRefresh && !(moreActions === null || moreActions === void 0 ? void 0 : moreActions.length);
32
32
  const hasLeftSideElements = Boolean(needsBulkActions || hasVisibleRefresh);
33
33
  const resizingContainerRef = useRef(null);
34
34
  const { t } = useLocale('MobileToolbar');
35
+ usePersistState({ persist, filter: filterRowProps === null || filterRowProps === void 0 ? void 0 : filterRowProps.value, search: search === null || search === void 0 ? void 0 : search.value });
35
36
  const moreActionsProps = useMemo(() => onRefresh
36
37
  ? {
37
38
  pinTop: [{ content: { option: t('refresh') }, icon: _jsx(UpdateSVG, {}), onClick: onRefresh }],
@@ -0,0 +1,2 @@
1
+ export * from './useFilters';
2
+ export * from './usePersistState';
@@ -0,0 +1,2 @@
1
+ export * from './useFilters';
2
+ export * from './usePersistState';
@@ -1,6 +1,6 @@
1
1
  import { FiltersState } from '@cloud-ru/uikit-product-mobile-chips';
2
- import { FilterButtonProps } from '../../helperComponents';
3
- import { FilterRow } from './types';
2
+ import { FilterButtonProps } from '../../../helperComponents';
3
+ import { FilterRow } from '../types';
4
4
  type UseFiltersProps<TState extends FiltersState> = {
5
5
  filterRow?: FilterRow<TState>;
6
6
  };
@@ -0,0 +1 @@
1
+ export * from './usePersistState';
@@ -0,0 +1 @@
1
+ export * from './usePersistState';
@@ -0,0 +1,9 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
2
+ import { ToolbarPersistConfig } from '../../types';
3
+ type usePersistStateProps<TState extends FiltersState = Record<string, unknown>> = {
4
+ persist?: ToolbarPersistConfig<TState>;
5
+ filter?: TState;
6
+ search?: string;
7
+ };
8
+ export declare function usePersistState<TState extends FiltersState = Record<string, unknown>>({ persist, filter, search, }: usePersistStateProps<TState>): void;
9
+ export {};
@@ -0,0 +1,58 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { useDataPersist } from '@snack-uikit/utils';
3
+ import { defaultParser, defaultSerializer, prepareDataForFilter } from './utils';
4
+ export function usePersistState({ persist, filter, search, }) {
5
+ const hasHydratedRef = useRef(false);
6
+ const dataPersistOptions = useMemo(() => {
7
+ if (!(persist === null || persist === void 0 ? void 0 : persist.filterQueryKey) || !(persist === null || persist === void 0 ? void 0 : persist.id))
8
+ return undefined;
9
+ const defaultValidate = (value) => {
10
+ const filterOk = (value === null || value === void 0 ? void 0 : value.filter) === undefined || (typeof value.filter === 'object' && value.filter !== null);
11
+ const searchOk = (value === null || value === void 0 ? void 0 : value.search) === undefined || typeof value.search === 'string';
12
+ return Boolean(filterOk && searchOk);
13
+ };
14
+ const combinedValidate = (value) => {
15
+ const baseValid = defaultValidate(value);
16
+ return (persist === null || persist === void 0 ? void 0 : persist.validateData) ? baseValid && persist.validateData(value) : baseValid;
17
+ };
18
+ return {
19
+ queryKey: persist.filterQueryKey,
20
+ localStorageKey: `${persist.id}_filter`,
21
+ validateData: combinedValidate,
22
+ };
23
+ }, [persist]);
24
+ const { getDefaultData, setDataToStorages } = useDataPersist({
25
+ options: dataPersistOptions,
26
+ serializer: value => ((persist === null || persist === void 0 ? void 0 : persist.serializer) ? persist.serializer(value) : defaultSerializer(value)),
27
+ parser: (value) => ((persist === null || persist === void 0 ? void 0 : persist.parser) ? persist.parser(value) : defaultParser(value)),
28
+ });
29
+ const defaultState = useMemo(getDefaultData, [getDefaultData]);
30
+ useEffect(() => {
31
+ var _a, _b;
32
+ if (hasHydratedRef.current) {
33
+ return;
34
+ }
35
+ if (defaultState) {
36
+ (_a = persist === null || persist === void 0 ? void 0 : persist.onLoad) === null || _a === void 0 ? void 0 : _a.call(persist, Object.assign(Object.assign({}, defaultState), { filter: prepareDataForFilter((_b = defaultState.filter) !== null && _b !== void 0 ? _b : {}) }));
37
+ }
38
+ // mark as hydrated after attempting to load defaultState
39
+ hasHydratedRef.current = true;
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ }, [defaultState]);
42
+ useEffect(() => {
43
+ var _a, _b;
44
+ if (!(persist === null || persist === void 0 ? void 0 : persist.id) || !(persist === null || persist === void 0 ? void 0 : persist.filterQueryKey) || !hasHydratedRef.current) {
45
+ return;
46
+ }
47
+ const snapshot = (_b = (_a = persist.state) !== null && _a !== void 0 ? _a : getDefaultData()) !== null && _b !== void 0 ? _b : {};
48
+ if (filter) {
49
+ snapshot.filter = filter;
50
+ }
51
+ if (search !== undefined) {
52
+ snapshot.search = search;
53
+ }
54
+ if (Object.keys(snapshot).length === 0)
55
+ return;
56
+ setDataToStorages(snapshot);
57
+ }, [persist === null || persist === void 0 ? void 0 : persist.id, persist === null || persist === void 0 ? void 0 : persist.filterQueryKey, persist === null || persist === void 0 ? void 0 : persist.state, filter, search, setDataToStorages, getDefaultData]);
58
+ }
@@ -0,0 +1,2 @@
1
+ export * from './parser';
2
+ export * from './serializer';
@@ -0,0 +1,2 @@
1
+ export * from './parser';
2
+ export * from './serializer';
@@ -0,0 +1,4 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
2
+ import { PersistedFilterState } from '../../../types';
3
+ export declare const defaultParser: <T extends FiltersState>(value: string) => PersistedFilterState<T>;
4
+ export declare const prepareDataForFilter: <T>(filter: Record<string, unknown>) => T;
@@ -0,0 +1,41 @@
1
+ import { parseQueryParamsString } from '@cloud-ru/ft-request-payload-transform';
2
+ import { isDateString } from '@snack-uikit/toolbar';
3
+ const parseValue = (value) => {
4
+ if (isDateString(value)) {
5
+ return new Date(value);
6
+ }
7
+ if (typeof value === 'boolean') {
8
+ return String(value);
9
+ }
10
+ return value;
11
+ };
12
+ const mapFilterFromPayload = (value) => {
13
+ if (!value) {
14
+ return undefined;
15
+ }
16
+ return Object.fromEntries(value.map(filter => {
17
+ if (Array.isArray(filter.value)) {
18
+ return [filter.field, filter.value.map(parseValue)];
19
+ }
20
+ return [filter.field, parseValue(filter.value)];
21
+ }));
22
+ };
23
+ export const defaultParser = (value) => {
24
+ var _a, _b;
25
+ const parsed = parseQueryParamsString(value);
26
+ return {
27
+ pagination: parsed === null || parsed === void 0 ? void 0 : parsed.pagination,
28
+ ordering: parsed === null || parsed === void 0 ? void 0 : parsed.ordering,
29
+ search: ((_a = parsed === null || parsed === void 0 ? void 0 : parsed.search) === null || _a === void 0 ? void 0 : _a.toString()) || '',
30
+ filter: (_b = mapFilterFromPayload(parsed === null || parsed === void 0 ? void 0 : parsed.filter)) !== null && _b !== void 0 ? _b : {},
31
+ };
32
+ };
33
+ export const prepareDataForFilter = (filter) => Object.entries(filter).reduce((acc, [key, value]) => {
34
+ if (Array.isArray(value)) {
35
+ acc[key] = value.map(parseValue);
36
+ }
37
+ else {
38
+ acc[key] = parseValue(value);
39
+ }
40
+ return acc;
41
+ }, Object.assign({}, filter));
@@ -0,0 +1,11 @@
1
+ import { RequestPayloadParams } from '@cloud-ru/ft-request-payload-transform';
2
+ import { FiltersState } from '@snack-uikit/chips';
3
+ import { PersistedFilterState } from '../../../types';
4
+ /** Вспомогательная функция для преобразования состояния тулбара к формату RequestPayloadParams */
5
+ export declare const formatFilterStateToRequestPayload: <T extends FiltersState>(value?: PersistedFilterState<T> | null) => {
6
+ toObject(): Partial<RequestPayloadParams>;
7
+ toString(params?: {
8
+ encode?: boolean;
9
+ }): string;
10
+ };
11
+ export declare const defaultSerializer: <T extends FiltersState>(value: PersistedFilterState<T>) => string;
@@ -0,0 +1,40 @@
1
+ import { createRequestPayload } from '@cloud-ru/ft-request-payload-transform';
2
+ import { isDateString } from '@snack-uikit/toolbar';
3
+ const mapValueToString = (filter) => {
4
+ if (filter instanceof Date) {
5
+ return filter.toISOString();
6
+ }
7
+ if (isDateString(filter)) {
8
+ return filter;
9
+ }
10
+ return filter;
11
+ };
12
+ const mapFilterToPayload = (value) => {
13
+ if (!value) {
14
+ return undefined;
15
+ }
16
+ return Object.entries(value)
17
+ .filter(([, v]) => v !== undefined)
18
+ .map(([key, v]) => Array.isArray(v)
19
+ ? {
20
+ value: v.map(mapValueToString),
21
+ condition: 'in',
22
+ field: key,
23
+ }
24
+ : {
25
+ value: mapValueToString(v),
26
+ condition: 'eq',
27
+ field: key,
28
+ });
29
+ };
30
+ /** Вспомогательная функция для преобразования состояния тулбара к формату RequestPayloadParams */
31
+ export const formatFilterStateToRequestPayload = (value) => {
32
+ var _a;
33
+ return createRequestPayload({
34
+ pagination: value === null || value === void 0 ? void 0 : value.pagination,
35
+ search: (_a = value === null || value === void 0 ? void 0 : value.search) !== null && _a !== void 0 ? _a : '',
36
+ ordering: value === null || value === void 0 ? void 0 : value.ordering,
37
+ filter: mapFilterToPayload(value === null || value === void 0 ? void 0 : value.filter),
38
+ });
39
+ };
40
+ export const defaultSerializer = (value) => formatFilterStateToRequestPayload(value).toString();
@@ -1,3 +1,4 @@
1
+ import { RequestPayloadParams } from '@cloud-ru/ft-request-payload-transform';
1
2
  import { ReactNode } from 'react';
2
3
  import { FiltersState, MobileChipChoiceRowProps } from '@cloud-ru/uikit-product-mobile-chips';
3
4
  import { BulkActionsProps, MoreActionsProps } from '../../helperComponents';
@@ -41,4 +42,26 @@ export type FilterRow<TState extends FiltersState> = Omit<MobileChipChoiceRowPro
41
42
  initialOpen?: boolean;
42
43
  onOpenChange?(isOpen: boolean): void;
43
44
  };
45
+ export type PersistedFilterState<T extends FiltersState> = {
46
+ pagination?: RequestPayloadParams['pagination'];
47
+ ordering?: RequestPayloadParams['ordering'];
48
+ search?: string;
49
+ filter?: T;
50
+ };
51
+ export type ToolbarPersistConfig<T extends FiltersState> = {
52
+ /** Уникальный id для текущего инстанса компонента */
53
+ id?: string;
54
+ /** Ключ для queryParams */
55
+ filterQueryKey?: string;
56
+ /** Валидатор сохраненных */
57
+ validateData?(value: unknown): value is PersistedFilterState<T>;
58
+ /** Custom-сериализация состояния перед сохранением в queryParams */
59
+ serializer?(value: PersistedFilterState<T>): string;
60
+ /** Custom-парсер queryParams для преобразования в данные состояния */
61
+ parser?(value: string): PersistedFilterState<T>;
62
+ /** Состояние для сохранения */
63
+ state?: PersistedFilterState<T>;
64
+ /** Колбэк при первом рендере для получения сохраненных данных и установки их в стейт */
65
+ onLoad?(state: PersistedFilterState<T>): void;
66
+ };
44
67
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloud-ru/uikit-product-mobile-toolbar",
3
3
  "title": "Mobile Toolbar",
4
- "version": "0.4.13",
4
+ "version": "0.5.0",
5
5
  "sideEffects": [
6
6
  "*.css",
7
7
  "*.woff",
@@ -36,23 +36,25 @@
36
36
  },
37
37
  "scripts": {},
38
38
  "dependencies": {
39
- "@cloud-ru/uikit-product-icons": "16.1.0",
40
- "@cloud-ru/uikit-product-mobile-chips": "0.8.46",
41
- "@cloud-ru/uikit-product-mobile-dropdown": "0.9.30",
42
- "@cloud-ru/uikit-product-mobile-tooltip": "0.5.3",
43
- "@cloud-ru/uikit-product-utils": "8.0.2",
39
+ "@cloud-ru/ft-request-payload-transform": "0.3.0",
40
+ "@cloud-ru/uikit-product-icons": "16.1.1",
41
+ "@cloud-ru/uikit-product-mobile-chips": "0.8.47",
42
+ "@cloud-ru/uikit-product-mobile-dropdown": "0.9.31",
43
+ "@cloud-ru/uikit-product-mobile-tooltip": "0.5.4",
44
+ "@cloud-ru/uikit-product-utils": "8.1.0",
44
45
  "@snack-uikit/button": "0.19.15",
46
+ "@snack-uikit/chips": "0.28.12",
45
47
  "@snack-uikit/counter": "0.8.9",
46
48
  "@snack-uikit/search-private": "0.4.29",
47
49
  "@snack-uikit/tag": "0.15.7",
48
50
  "@snack-uikit/toggles": "0.13.20",
49
- "@snack-uikit/toolbar": "0.13.22",
50
- "@snack-uikit/utils": "3.10.1",
51
+ "@snack-uikit/toolbar": "0.14.16",
52
+ "@snack-uikit/utils": "4.0.0",
51
53
  "classnames": "2.5.1",
52
54
  "uncontrollable": "8.0.4"
53
55
  },
54
56
  "peerDependencies": {
55
57
  "@cloud-ru/uikit-product-locale": "*"
56
58
  },
57
- "gitHead": "7303f734eef30f1ac7ac3a279bcf324dcd549059"
59
+ "gitHead": "b8c76e1a66bd22f7c266466eb340860b06581d10"
58
60
  }
@@ -12,14 +12,18 @@ import { TEST_IDS } from '../../constants';
12
12
  import { BulkActions, FilterButton, MoreActions, Separator } from '../../helperComponents';
13
13
  import { BulkActionsCheckbox } from '../../helperComponents/BulkActionsCheckbox';
14
14
  import { isBulkActionsProps } from './helpers';
15
- import { useFilters } from './hooks';
15
+ import { useFilters, usePersistState } from './hooks';
16
16
  import styles from './styles.module.scss';
17
- import { CheckedToolbarProps, DefaultToolbarProps, FilterRow } from './types';
17
+ import { CheckedToolbarProps, DefaultToolbarProps, FilterRow, ToolbarPersistConfig } from './types';
18
18
 
19
19
  export type MobileToolbarProps<TState extends FiltersState = Record<string, unknown>> = WithSupportProps<
20
20
  DefaultToolbarProps | CheckedToolbarProps
21
21
  > & {
22
22
  filterRow?: FilterRow<TState>;
23
+ /** Конфиг для сохранения состояния в localStorage и queryParams. <br>
24
+ * Поле id должно быть уникальным для каждого инстанса компонента. <br>
25
+ * */
26
+ persist?: ToolbarPersistConfig<TState>;
23
27
  };
24
28
 
25
29
  export function MobileToolbar<TState extends FiltersState = Record<string, unknown>>({
@@ -30,6 +34,7 @@ export function MobileToolbar<TState extends FiltersState = Record<string, unkno
30
34
  onRefresh,
31
35
  search,
32
36
  filterRow: filterRowProps,
37
+ persist,
33
38
  ...rest
34
39
  }: MobileToolbarProps<TState>) {
35
40
  const needsBulkActions = isBulkActionsProps(rest);
@@ -39,6 +44,8 @@ export function MobileToolbar<TState extends FiltersState = Record<string, unkno
39
44
 
40
45
  const { t } = useLocale('MobileToolbar');
41
46
 
47
+ usePersistState({ persist, filter: filterRowProps?.value, search: search?.value });
48
+
42
49
  const moreActionsProps = useMemo(
43
50
  () =>
44
51
  onRefresh
@@ -0,0 +1,2 @@
1
+ export * from './useFilters';
2
+ export * from './usePersistState';
@@ -3,8 +3,8 @@ import { useUncontrolledProp } from 'uncontrollable';
3
3
 
4
4
  import { FiltersState, hasFilterBeenApplied } from '@cloud-ru/uikit-product-mobile-chips';
5
5
 
6
- import { FilterButtonProps } from '../../helperComponents';
7
- import { FilterRow } from './types';
6
+ import { FilterButtonProps } from '../../../helperComponents';
7
+ import { FilterRow } from '../types';
8
8
 
9
9
  type UseFiltersProps<TState extends FiltersState> = {
10
10
  filterRow?: FilterRow<TState>;
@@ -0,0 +1 @@
1
+ export * from './usePersistState';
@@ -0,0 +1,87 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+
3
+ import { FiltersState } from '@snack-uikit/chips';
4
+ import { DataPersistOptions, useDataPersist } from '@snack-uikit/utils';
5
+
6
+ import { PersistedFilterState, ToolbarPersistConfig } from '../../types';
7
+ import { defaultParser, defaultSerializer, prepareDataForFilter } from './utils';
8
+
9
+ type usePersistStateProps<TState extends FiltersState = Record<string, unknown>> = {
10
+ persist?: ToolbarPersistConfig<TState>;
11
+ filter?: TState;
12
+ search?: string;
13
+ };
14
+
15
+ export function usePersistState<TState extends FiltersState = Record<string, unknown>>({
16
+ persist,
17
+ filter,
18
+ search,
19
+ }: usePersistStateProps<TState>) {
20
+ const hasHydratedRef = useRef(false);
21
+
22
+ const dataPersistOptions = useMemo<DataPersistOptions<PersistedFilterState<TState>> | undefined>(() => {
23
+ if (!persist?.filterQueryKey || !persist?.id) return undefined;
24
+
25
+ const defaultValidate = (value: PersistedFilterState<TState>): boolean => {
26
+ const filterOk = value?.filter === undefined || (typeof value.filter === 'object' && value.filter !== null);
27
+ const searchOk = value?.search === undefined || typeof value.search === 'string';
28
+ return Boolean(filterOk && searchOk);
29
+ };
30
+
31
+ const combinedValidate = (value: PersistedFilterState<TState>): boolean => {
32
+ const baseValid = defaultValidate(value);
33
+ return persist?.validateData ? baseValid && persist.validateData(value) : baseValid;
34
+ };
35
+
36
+ return {
37
+ queryKey: persist.filterQueryKey,
38
+ localStorageKey: `${persist.id}_filter`,
39
+ validateData: combinedValidate as unknown as (value: unknown) => value is PersistedFilterState<TState>,
40
+ };
41
+ }, [persist]);
42
+
43
+ const { getDefaultData, setDataToStorages } = useDataPersist<PersistedFilterState<TState>>({
44
+ options: dataPersistOptions,
45
+ serializer: value => (persist?.serializer ? persist.serializer(value) : defaultSerializer<TState>(value)),
46
+ parser: (value: string) => (persist?.parser ? persist.parser(value) : defaultParser<TState>(value)),
47
+ });
48
+
49
+ const defaultState = useMemo(getDefaultData, [getDefaultData]);
50
+
51
+ useEffect(() => {
52
+ if (hasHydratedRef.current) {
53
+ return;
54
+ }
55
+
56
+ if (defaultState) {
57
+ persist?.onLoad?.({
58
+ ...defaultState,
59
+ filter: prepareDataForFilter(defaultState.filter ?? {}),
60
+ });
61
+ }
62
+
63
+ // mark as hydrated after attempting to load defaultState
64
+ hasHydratedRef.current = true;
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [defaultState]);
67
+
68
+ useEffect(() => {
69
+ if (!persist?.id || !persist?.filterQueryKey || !hasHydratedRef.current) {
70
+ return;
71
+ }
72
+
73
+ const snapshot = persist.state ?? getDefaultData() ?? ({} as PersistedFilterState<TState>);
74
+
75
+ if (filter) {
76
+ snapshot.filter = filter;
77
+ }
78
+
79
+ if (search !== undefined) {
80
+ snapshot.search = search;
81
+ }
82
+
83
+ if (Object.keys(snapshot).length === 0) return;
84
+
85
+ setDataToStorages(snapshot);
86
+ }, [persist?.id, persist?.filterQueryKey, persist?.state, filter, search, setDataToStorages, getDefaultData]);
87
+ }
@@ -0,0 +1,2 @@
1
+ export * from './parser';
2
+ export * from './serializer';
@@ -0,0 +1,61 @@
1
+ import { parseQueryParamsString, RequestPayloadParams } from '@cloud-ru/ft-request-payload-transform';
2
+
3
+ import { FiltersState } from '@snack-uikit/chips';
4
+ import { isDateString } from '@snack-uikit/toolbar';
5
+
6
+ import { PersistedFilterState } from '../../../types';
7
+
8
+ const parseValue = (value: unknown) => {
9
+ if (isDateString(value)) {
10
+ return new Date(value);
11
+ }
12
+
13
+ if (typeof value === 'boolean') {
14
+ return String(value);
15
+ }
16
+
17
+ return value;
18
+ };
19
+
20
+ const mapFilterFromPayload = <TFilters extends FiltersState = Record<string, unknown>>(
21
+ value?: RequestPayloadParams['filter'],
22
+ ): TFilters | undefined => {
23
+ if (!value) {
24
+ return undefined;
25
+ }
26
+
27
+ return Object.fromEntries(
28
+ value.map(filter => {
29
+ if (Array.isArray(filter.value)) {
30
+ return [filter.field, filter.value.map(parseValue)];
31
+ }
32
+
33
+ return [filter.field, parseValue(filter.value)];
34
+ }),
35
+ ) as TFilters;
36
+ };
37
+
38
+ export const defaultParser = <T extends FiltersState>(value: string): PersistedFilterState<T> => {
39
+ const parsed = parseQueryParamsString(value);
40
+
41
+ return {
42
+ pagination: parsed?.pagination,
43
+ ordering: parsed?.ordering,
44
+ search: parsed?.search?.toString() || '',
45
+ filter: mapFilterFromPayload<T>(parsed?.filter) ?? ({} as T),
46
+ };
47
+ };
48
+
49
+ export const prepareDataForFilter = <T>(filter: Record<string, unknown>): T =>
50
+ Object.entries(filter).reduce(
51
+ (acc, [key, value]) => {
52
+ if (Array.isArray(value)) {
53
+ acc[key] = value.map(parseValue);
54
+ } else {
55
+ acc[key] = parseValue(value);
56
+ }
57
+
58
+ return acc;
59
+ },
60
+ { ...filter },
61
+ ) as T;
@@ -0,0 +1,54 @@
1
+ import { createRequestPayload, RequestPayloadParams } from '@cloud-ru/ft-request-payload-transform';
2
+
3
+ import { FiltersState } from '@snack-uikit/chips';
4
+ import { isDateString } from '@snack-uikit/toolbar';
5
+
6
+ import { PersistedFilterState } from '../../../types';
7
+
8
+ type FilterValue = string | null | number | boolean | Date;
9
+
10
+ const mapValueToString = (filter: FilterValue): string | null | number | boolean => {
11
+ if (filter instanceof Date) {
12
+ return filter.toISOString();
13
+ }
14
+
15
+ if (isDateString(filter)) {
16
+ return filter;
17
+ }
18
+
19
+ return filter;
20
+ };
21
+
22
+ const mapFilterToPayload = (value?: FiltersState): RequestPayloadParams['filter'] => {
23
+ if (!value) {
24
+ return undefined;
25
+ }
26
+
27
+ return Object.entries(value)
28
+ .filter(([, v]) => v !== undefined)
29
+ .map(([key, v]) =>
30
+ Array.isArray(v)
31
+ ? {
32
+ value: (v as FilterValue[]).map(mapValueToString),
33
+ condition: 'in',
34
+ field: key,
35
+ }
36
+ : {
37
+ value: mapValueToString(v as FilterValue),
38
+ condition: 'eq',
39
+ field: key,
40
+ },
41
+ );
42
+ };
43
+
44
+ /** Вспомогательная функция для преобразования состояния тулбара к формату RequestPayloadParams */
45
+ export const formatFilterStateToRequestPayload = <T extends FiltersState>(value?: PersistedFilterState<T> | null) =>
46
+ createRequestPayload({
47
+ pagination: value?.pagination,
48
+ search: value?.search ?? '',
49
+ ordering: value?.ordering,
50
+ filter: mapFilterToPayload(value?.filter),
51
+ });
52
+
53
+ export const defaultSerializer = <T extends FiltersState>(value: PersistedFilterState<T>) =>
54
+ formatFilterStateToRequestPayload(value).toString();
@@ -1,3 +1,4 @@
1
+ import { RequestPayloadParams } from '@cloud-ru/ft-request-payload-transform';
1
2
  import { ReactNode } from 'react';
2
3
 
3
4
  import { FiltersState, MobileChipChoiceRowProps } from '@cloud-ru/uikit-product-mobile-chips';
@@ -51,3 +52,27 @@ export type FilterRow<TState extends FiltersState> = Omit<MobileChipChoiceRowPro
51
52
  initialOpen?: boolean;
52
53
  onOpenChange?(isOpen: boolean): void;
53
54
  };
55
+
56
+ export type PersistedFilterState<T extends FiltersState> = {
57
+ pagination?: RequestPayloadParams['pagination'];
58
+ ordering?: RequestPayloadParams['ordering'];
59
+ search?: string;
60
+ filter?: T;
61
+ };
62
+
63
+ export type ToolbarPersistConfig<T extends FiltersState> = {
64
+ /** Уникальный id для текущего инстанса компонента */
65
+ id?: string;
66
+ /** Ключ для queryParams */
67
+ filterQueryKey?: string;
68
+ /** Валидатор сохраненных */
69
+ validateData?(value: unknown): value is PersistedFilterState<T>;
70
+ /** Custom-сериализация состояния перед сохранением в queryParams */
71
+ serializer?(value: PersistedFilterState<T>): string;
72
+ /** Custom-парсер queryParams для преобразования в данные состояния */
73
+ parser?(value: string): PersistedFilterState<T>;
74
+ /** Состояние для сохранения */
75
+ state?: PersistedFilterState<T>;
76
+ /** Колбэк при первом рендере для получения сохраненных данных и установки их в стейт */
77
+ onLoad?(state: PersistedFilterState<T>): void;
78
+ };