@sberbusiness/triplex-next-mcp-server 1.43.0-beta → 1.45.0-beta

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 (85) hide show
  1. package/README.md +7 -6
  2. package/bundle/components/AlertContext.md +11 -8
  3. package/bundle/components/AlertProcess.md +16 -12
  4. package/bundle/components/Badge.md +6 -3
  5. package/bundle/components/BodyPage.md +72 -4
  6. package/bundle/components/Button.md +46 -43
  7. package/bundle/components/ButtonDropdownExtended.md +3 -2
  8. package/bundle/components/ButtonIcon.md +7 -4
  9. package/bundle/components/Calendar.md +62 -59
  10. package/bundle/components/Caption.md +26 -23
  11. package/bundle/components/Checkbox.md +32 -29
  12. package/bundle/components/CheckboxTreeExtended.md +9 -5
  13. package/bundle/components/Chip.md +44 -41
  14. package/bundle/components/CodeText.md +26 -23
  15. package/bundle/components/Col.md +1 -2
  16. package/bundle/components/CollapsibleTree.md +8 -5
  17. package/bundle/components/CollapsibleTreeExtended.md +6 -1
  18. package/bundle/components/DatePickerExtended.md +409 -262
  19. package/bundle/components/Dropdown.md +33 -28
  20. package/bundle/components/EmptyView.md +12 -5
  21. package/bundle/components/ExpandAnimation.md +263 -218
  22. package/bundle/components/Footer.md +235 -278
  23. package/bundle/components/FooterPage.md +6 -2
  24. package/bundle/components/FormField.md +44 -41
  25. package/bundle/components/FormGroup.md +192 -52
  26. package/bundle/components/Header.md +491 -211
  27. package/bundle/components/HeaderPage.md +6 -2
  28. package/bundle/components/ImageGallery.md +33 -34
  29. package/bundle/components/ImageGalleryExtended.md +33 -34
  30. package/bundle/components/Island.md +52 -14
  31. package/bundle/components/LightBox.md +25 -18
  32. package/bundle/components/Link.md +211 -96
  33. package/bundle/components/ListActionItem.md +7 -4
  34. package/bundle/components/ListItem.md +11 -8
  35. package/bundle/components/ListItemControlsButton.md +16 -13
  36. package/bundle/components/ListItemControlsButtonDropdown.md +9 -6
  37. package/bundle/components/ListItemSelectable.md +7 -4
  38. package/bundle/components/ListMasterFooter.md +8 -5
  39. package/bundle/components/ListMasterHeader.md +8 -5
  40. package/bundle/components/ListSortableItem.md +11 -8
  41. package/bundle/components/ListTableItem.md +7 -4
  42. package/bundle/components/LoaderMiddle.md +7 -4
  43. package/bundle/components/LoaderScreen.md +11 -8
  44. package/bundle/components/LoaderSmall.md +7 -4
  45. package/bundle/components/Marker.md +184 -69
  46. package/bundle/components/MarkerStatus.md +206 -88
  47. package/bundle/components/MaskedField.md +2 -4
  48. package/bundle/components/MasterTable.md +602 -0
  49. package/bundle/components/ModalWindow.md +7 -4
  50. package/bundle/components/MultiselectField.md +2183 -132
  51. package/bundle/components/Notification.md +12 -9
  52. package/bundle/components/Overlay.md +33 -5
  53. package/bundle/components/Page.md +2 -2
  54. package/bundle/components/Pagination.md +27 -21
  55. package/bundle/components/Row.md +1 -1
  56. package/bundle/components/SelectExtendedField.md +3 -3
  57. package/bundle/components/SelectField.md +466 -148
  58. package/bundle/components/SliderExtended.md +39 -36
  59. package/bundle/components/SmallInput.md +194 -51
  60. package/bundle/components/Suggest.md +535 -99
  61. package/bundle/components/SwipeableArea.md +428 -79
  62. package/bundle/components/TableBasic.md +2302 -0
  63. package/bundle/components/TabsExtended.md +24 -21
  64. package/bundle/components/Tag.md +263 -78
  65. package/bundle/components/Text.md +28 -25
  66. package/bundle/components/TextField.md +3 -4
  67. package/bundle/components/ThemeProvider.md +50 -1
  68. package/bundle/components/Title.md +26 -23
  69. package/bundle/components/Tooltip.md +24 -21
  70. package/bundle/components/TopOverlay.md +368 -89
  71. package/bundle/components/TreeView.md +343 -166
  72. package/bundle/components/TreeViewNode.md +436 -0
  73. package/bundle/guides/codestyle.md +3 -0
  74. package/bundle/guides/commits.md +106 -26
  75. package/bundle/guides/context.md +84 -12
  76. package/bundle/guides/guidelines.md +321 -0
  77. package/bundle/guides/template.md +22 -12
  78. package/bundle/manifest.json +54 -20
  79. package/bundle/release-notes/1.44.0.mdx +113 -0
  80. package/bundle/release-notes/1.45.0.mdx +124 -0
  81. package/bundle/release-notes/1.46.0.mdx +7 -0
  82. package/bundle/tokens.json +6362 -0
  83. package/dist/index.js +177 -28
  84. package/dist/index.js.map +1 -1
  85. package/package.json +1 -1
@@ -0,0 +1,2302 @@
1
+ ---
2
+ component: TableBasic
3
+ category: Table
4
+ related: [MasterTable, ListTableItem, LoaderScreen, LoaderMiddle, Text]
5
+ tokens:
6
+ - TableBasic.Header_Background
7
+ - TableBasic.Color
8
+ - TableBasic.Background_Hover
9
+ - TableBasic.Background_Selected
10
+ stories: stories/TableBasic/TableBasic.stories.tsx
11
+ version: "1.0"
12
+ ---
13
+
14
+ # TableBasic
15
+
16
+ ## Назначение
17
+
18
+ Табличное представление данных: `<table>` с шапкой из `columns`, строками из
19
+ `data` и подвалом, в котором показывается заглушка пустого состояния или лоадер.
20
+ Обвязку вокруг таблицы — панель чипов, фильтры, настройки колонок, футер
21
+ выделения и пагинацию — добавляет родительский `MasterTable`; из его контекста
22
+ `TableBasic` берёт состояние загрузки и публикует в него свой набор колонок.
23
+
24
+ Используй когда: нужен структурированный набор данных в строках и столбцах —
25
+ реестр документов, выписка, список операций.
26
+
27
+ Не используй когда:
28
+ - нужен список карточек с собственной вёрсткой строки, свайпом или
29
+ drag-and-drop — бери `List` / `ListTableItem`;
30
+ - таблица должна работать без `MasterTable`. Формально это возможно (контекст
31
+ имеет значения по умолчанию), но тогда не работают `loading` и настройки
32
+ колонок — это не поддерживаемый сценарий.
33
+
34
+ ---
35
+
36
+ ## Варианты и props
37
+
38
+ `TableBasic` доступен и напрямую, и как `MasterTable.TableBasic` — это один и
39
+ тот же компонент.
40
+
41
+ ### Обязательные props
42
+
43
+ | Prop | Тип | Описание |
44
+ |---|---|---|
45
+ | `columns` | `ITableBasicColumn[]` | Описание столбцов: ключ поля, заголовок, выравнивание, ширина, сортировка |
46
+ | `data` | `ITableBasicRow[]` | Строки таблицы. Пустой массив — рендерится `renderNoData` |
47
+ | `renderNoData` | `() => React.JSX.Element` | Заглушка для пустой таблицы. Обязателен даже там, где данные всегда есть |
48
+
49
+ ### Опциональные props
50
+
51
+ | Prop | Тип | По умолчанию | Описание |
52
+ |---|---|---|---|
53
+ | `renderNoColumns` | `() => React.ReactNode` | — | Заглушка вместо таблицы, когда все `columns` скрыты (`hidden`) |
54
+ | `highlightRowOnHover` | `boolean` | `false` | Подсветка строки под курсором |
55
+ | `onOrderBy` | `(order: ISortOrder) => void` | — | Обработчик сортировки. Включает кнопку сортировки у колонок с `orderDirection` |
56
+ | `onClickRow` | `(rowKey: string) => void` | — | Клик по строке. Включает `cursor: pointer` и подсветку при наведении |
57
+ | `headless` | `boolean` | `false` | Скрыть шапку таблицы |
58
+ | `...HTMLTableElementAttributes` | — | — | Стандартные атрибуты `<table>`, включая `className`; `ref` — на `<table>` (в ветке `renderNoColumns` таблицы нет, ref пустой) |
59
+
60
+ ### `ITableBasicColumn`
61
+
62
+ | Поле | Тип | По умолчанию | Описание |
63
+ |---|---|---|---|
64
+ | `fieldKey` | `string` | — | Ключ в `rowData`, значение которого выводится в ячейке |
65
+ | `label` | `string \| React.JSX.Element` | — | Содержимое заголовка |
66
+ | `title` | `string` | — | Нативный `title` у `<th>` |
67
+ | `orderDirection` | `EOrderDirection` | — | Текущее направление. Наличие значения + `onOrderBy` включают сортировку |
68
+ | `horizontalAlign` | `EHorizontalAlign` | `LEFT` | Горизонтальное выравнивание |
69
+ | `verticalAlign` | `EVerticalAlign` | `BASELINE` для `TEXT`, `TOP` для остальных | Вертикальное выравнивание |
70
+ | `cellType` | `ECellType` | `TEXT` | Отступы ячейки и способ вывода контента |
71
+ | `width` | `string \| number` | — | У `<th>` задаёт `min/max/width`, у `<td>` — только `width` |
72
+ | `hidden` | `boolean` | `false` | Столбец не рендерится ни в шапке, ни в строках |
73
+ | `renderCell` | `(param: any) => React.ReactNode` | — | Кастомный рендер значения ячейки |
74
+ | `dataAttributes` / `ariaAttributes` | `TDataHTMLAttributes` / `TAriaHTMLAttributes` | — | Ключи без префикса: `{label: "..."}` → `aria-label="..."` |
75
+
76
+ ### `ITableBasicRow`
77
+
78
+ | Поле | Тип | По умолчанию | Описание |
79
+ |---|---|---|---|
80
+ | `rowKey` | `string` | — | React-key строки, он же аргумент `onClickRow` |
81
+ | `rowData` | `any` | — | Объект «ключ колонки → значение ячейки» |
82
+ | `rowLayout` | `Record<string, ITableRowCellSpanProps>` | — | `rowSpan` / `colSpan` по ключу колонки |
83
+ | `selected` | `boolean` | `false` | Подсветка выбранной строки. Компонент сам флаг не проставляет |
84
+ | `dataAttributes` / `ariaAttributes` | `TDataHTMLAttributes` / `TAriaHTMLAttributes` | — | Атрибуты `<tr>`; `test-id` дополнительно строит `data-test-id` ячеек |
85
+
86
+ ### Ограничения по типам ячеек
87
+
88
+ - `ECellType.TEXT` (по умолчанию) — значение оборачивается в `Text` размера `B3`.
89
+ - `ECellType.COMPONENTS` и `ECellType.CHECKBOX` — разметка выводится как есть,
90
+ меняются только внутренние отступы ячейки.
91
+
92
+ ---
93
+
94
+ ## Дизайн-токены
95
+
96
+ Переопределяются через `ThemeProvider` (prop `tokens`) — см. `ThemeProvider-ai.md` →
97
+ «Как переопределять токены». Значения по умолчанию — `src/components/DesignTokens/components/TableBasic.ts`.
98
+
99
+ ```text
100
+ TableBasic.Header_Background
101
+ TableBasic.Color
102
+ TableBasic.Background_Hover
103
+ TableBasic.Background_Selected
104
+ ```
105
+
106
+ В группе `TableBasic` лежат и токены соседних компонентов семейства —
107
+ `TableFooter_*` применяются в `TableFooter`, `TableBasicSettings_*` — в
108
+ `TableBasicSettings`. Переопределяются они там же, в группе `TableBasic`:
109
+ имя группы — не имя компонента. Здесь они не перечислены, потому что сам
110
+ `TableBasic` их не использует, а собственных AI.md у этих компонентов пока нет —
111
+ поэтому `syncAiMdTokens` и предупреждает, что токены есть в LESS директории,
112
+ но не описаны ни в одном документе.
113
+
114
+ ---
115
+
116
+ ## Инварианты
117
+
118
+ - **Корневой DOM — `<div>` со стилями таблицы, а `ref` и остальные HTML-атрибуты
119
+ идут на вложенный `<table>`.** Так исторически ведёт себя `className`, и
120
+ `forwardRef` добавлен туда же. Переносить их на корневой `<div>` — breaking
121
+ change. Проброс не безусловный: когда все колонки скрыты и задан
122
+ `renderNoColumns`, вместо таблицы рендерится заглушка и `ref.current` остаётся
123
+ `null` — измерять таблицу по ref в этом состоянии нельзя.
124
+ - **`renderNoData` обязателен.** Сделать его опциональным можно только вместе с
125
+ решением, что рендерится при пустых данных без него.
126
+ - **Ячейка рендерится, только если ключ колонки есть в `rowData`.** Колонка без
127
+ соответствующего ключа не даёт `<td>` вовсе — строка окажется короче шапки.
128
+ Для пустой ячейки передавай `null`, а не отсутствие ключа.
129
+ - **Пустое значение ячейки заменяется плейсхолдером `---`.** Проверка falsy,
130
+ поэтому `0`, `""` и `false` тоже дают `---`. Менять это правило —
131
+ наблюдаемое изменение поведения.
132
+ - **Компонент не сортирует данные.** `onOrderBy` вызывается со **следующим**
133
+ направлением по циклу `none → asc → desc → none`; сортировку выполняет
134
+ потребитель и возвращает новый `orderDirection` в колонке.
135
+ - **Колонки публикуются в `MasterTableContext`** через `useEffect` с
136
+ `isEqual`-сравнением — на нём держатся настройки колонок
137
+ (`TableBasicSettings` / `ColumnSettings`). Собирай `columns` стабильно
138
+ (по возможности вне рендера), иначе таблица будет лишний раз обновлять
139
+ контекст.
140
+ - **`loading` приходит только из контекста `MasterTable`**, собственного prop'а
141
+ у таблицы нет.
142
+ - **Класс `selected` на `<tr>` выставляется дважды** — хешированный из
143
+ CSS-модуля и глобальный `"selected"`. Глобальный оставлен для внешних стилей
144
+ и тестов, удалять нельзя.
145
+
146
+ ---
147
+
148
+ ## Accessibility
149
+
150
+ - Разметка нативная: `<table>` / `<thead>` / `<tbody>` / `<th>` / `<td>`,
151
+ скринридер читает её как таблицу без дополнительных ролей.
152
+ - Собственных `aria-*` компонент не хардкодит — библиотека мультиязычная.
153
+ `aria-label` таблицы передаётся через props (уходит на `<table>`), атрибуты
154
+ столбца и строки — через `ariaAttributes` (ключи без префикса `aria-`).
155
+ - **Сортировка не доступна с клавиатуры.** Заголовок с сортировкой — это
156
+ `<span>` с `onClick`, без `role="button"`, `tabIndex` и обработки
157
+ `Enter` / `Space`; `aria-sort` не выставляется. То же с `onClickRow`: клик по
158
+ `<tr>` не имеет клавиатурного эквивалента. Если нужен доступный вариант —
159
+ клади в ячейку интерактивный элемент через `renderCell`.
160
+ - Иконка направления сортировки в состоянии `NONE` видна только при наведении на
161
+ заголовок — состояние сортировки передаётся только цветом и формой иконки.
162
+
163
+ ---
164
+
165
+ ## Связанные компоненты
166
+
167
+ - `MasterTable` — родитель семейства: провайдер контекста (`loading`, `columns`)
168
+ и контейнер для панелей. Предоставляет `MasterTable.TableBasic`,
169
+ `MasterTable.FilterPanel`, `MasterTable.ChipPanel`,
170
+ `MasterTable.TableBasicSettings`, `MasterTable.TableFooter`,
171
+ `MasterTable.PaginationPanel`, `MasterTable.NoColumns`.
172
+ - `ListTableItem` — альтернатива для табличного списка, когда строка это
173
+ карточка со своей вёрсткой, свайпом и контролами.
174
+ - `LoaderScreen` — рендерится поверх таблицы, когда `loading` пришёл из
175
+ контекста, а данные уже есть.
176
+ - `LoaderMiddle` — рендерится в подвале, когда идёт загрузка и данных ещё нет.
177
+ - `Text` — в него оборачивается содержимое заголовков и ячеек типа
178
+ `ECellType.TEXT`.
179
+
180
+ ---
181
+
182
+ ## Stories
183
+
184
+ Основные истории: `stories/TableBasic/TableBasic.stories.tsx`
185
+ Файлы примеров: `stories/TableBasic/examples/`
186
+
187
+ | Story | Example file | Что демонстрирует |
188
+ |---|---|---|
189
+ | `Playground` | `Playground.tsx` | Интерактивный контроль загрузки, шапки, подсветки, данных, скролла и кнопок |
190
+ | `Default` | `Default.tsx` | Минимальная таблица: `columns`, `data`, `renderNoData` |
191
+ | `Alignments` | `Alignments.tsx` | `horizontalAlign` и `verticalAlign` и их значения по умолчанию |
192
+ | `CellTypes` | `CellTypes.tsx` | `ECellType.TEXT` / `COMPONENTS` / `CHECKBOX` |
193
+ | `Sorting` | `Sorting.tsx` | Цикл `none → asc → desc → none`, сортировка на стороне потребителя |
194
+ | `Headless` | `Headless.tsx` | Таблица без шапки |
195
+ | `HighlightRowOnHover` | `HighlightRowOnHover.tsx` | Подсветка строки под курсором |
196
+ | `ClickableRows` | `ClickableRows.tsx` | `onClickRow` и флаг `selected` в данных строки |
197
+ | `NoData` | `NoData.tsx` | Заглушка пустой таблицы |
198
+ | `NoColumns` | `NoColumns.tsx` | Заглушка, когда все колонки скрыты |
199
+ | `Loading` | `Loading.tsx` | Загрузка поверх данных и загрузка при пустой таблице |
200
+ | `TableSpan` | `TableSpan.tsx` | Объединение ячеек через `rowLayout` |
201
+ | `TableSettingsColumn` | `TableSettingsColumn.tsx` | Настройка видимости и порядка колонок |
202
+ | `TableSettingsColumnExtended` | `TableSettingsColumnExtended.tsx` | Настройка не только колонок, но и блоков внутри колонки |
203
+ | `TableWithPagination` | `TableWithPagination.tsx` | Таблица с `Pagination` при известном количестве данных |
204
+ | `TableWithPaginationLoading` | `TableWithPaginationLoading.tsx` | Фильтры, таблица и пагинация в состоянии загрузки |
205
+ | `TableWithPaginationExtended` | `TableWithPaginationExtended.tsx` | Таблица с `PaginationExtended` при неизвестном количестве данных |
206
+ | `Example: production` | `ExampleProduction.tsx` | Композиция, приближённая к production: чипы-сегменты, фильтры, теги |
207
+ | `VisualTests` | `VisualTests.tsx` | Статичные визуальные состояния в одном кадре |
208
+
209
+ ---
210
+
211
+ ## История изменений
212
+
213
+ | Дата | Изменение |
214
+ |---|---|
215
+ | 2026-08-27 | Создан документ. AI-рефакторинг: `TableBasic` получил `forwardRef` на `<table>`, JSDoc на всех публичных полях, разбор подвала по состояниям, удалён неиспользуемый `TableBasicAdaptive.module.less`. Stories переведены на modern pattern, добавлены unit-тесты (46) |
216
+
217
+ ---
218
+
219
+ ## Примеры использования
220
+
221
+ ### Default
222
+
223
+ `stories/TableBasic/examples/Default.tsx`
224
+
225
+ ```tsx
226
+ import React from "react";
227
+ import { ITableBasicColumn, ITableBasicRow, MasterTable } from "@sberbusiness/triplex-next";
228
+
229
+ const columns: ITableBasicColumn[] = [
230
+ { fieldKey: "number", label: "Номер", title: "Номер", width: 100 },
231
+ { fieldKey: "recipient", label: "Получатель", title: "Получатель" },
232
+ { fieldKey: "sum", label: "Сумма", title: "Сумма", width: 140 },
233
+ { fieldKey: "status", label: "Статус", title: "Статус", width: 140 },
234
+ ];
235
+
236
+ const data: ITableBasicRow[] = [
237
+ { rowKey: "1350", rowData: { number: "1350", recipient: "ООО Ромашка", sum: "9 450,00", status: "Создан" } },
238
+ { rowKey: "1351", rowData: { number: "1351", recipient: "ИП Иванов И. И.", sum: "18 914,00", status: "Подписан" } },
239
+ { rowKey: "1352", rowData: { number: "1352", recipient: "ООО Росинка", sum: "28 392,00", status: "Оплачен" } },
240
+ ];
241
+
242
+ export const Default = () => (
243
+ <MasterTable>
244
+ <MasterTable.TableBasic columns={columns} data={data} renderNoData={() => <div>Нет данных</div>} />
245
+ </MasterTable>
246
+ );
247
+ ```
248
+
249
+ ### Alignments
250
+
251
+ `stories/TableBasic/examples/Alignments.tsx`
252
+
253
+ ```tsx
254
+ import React from "react";
255
+ import {
256
+ EHorizontalAlign,
257
+ EVerticalAlign,
258
+ ITableBasicColumn,
259
+ ITableBasicRow,
260
+ MasterTable,
261
+ } from "@sberbusiness/triplex-next";
262
+
263
+ const columns: ITableBasicColumn[] = [
264
+ {
265
+ fieldKey: "left",
266
+ label: "Слева (по умолчанию)",
267
+ horizontalAlign: EHorizontalAlign.LEFT,
268
+ verticalAlign: EVerticalAlign.TOP,
269
+ },
270
+ {
271
+ fieldKey: "center",
272
+ label: "По центру",
273
+ horizontalAlign: EHorizontalAlign.CENTER,
274
+ verticalAlign: EVerticalAlign.BOTTOM,
275
+ },
276
+ ];
277
+
278
+ const data: ITableBasicRow[] = [
279
+ {
280
+ rowKey: "1",
281
+ rowData: {
282
+ left: "Выравнивание по верхнему краю ячейки. Текст занимает несколько строк, чтобы разница по вертикали была видна.",
283
+ center: "По нижнему краю ячейки",
284
+ },
285
+ },
286
+ {
287
+ rowKey: "2",
288
+ rowData: {
289
+ left: "Ещё одна многострочная ячейка, задающая высоту строки таблицы для наглядности примера.",
290
+ center: "По нижнему краю ячейки",
291
+ },
292
+ },
293
+ ];
294
+
295
+ export const Alignments = () => (
296
+ <MasterTable>
297
+ <MasterTable.TableBasic columns={columns} data={data} renderNoData={() => <div>Нет данных</div>} />
298
+ </MasterTable>
299
+ );
300
+ ```
301
+
302
+ ### CellTypes
303
+
304
+ `stories/TableBasic/examples/CellTypes.tsx`
305
+
306
+ ```tsx
307
+ import React, { useState } from "react";
308
+ import {
309
+ Button,
310
+ Checkbox,
311
+ ECellType,
312
+ EButtonTheme,
313
+ EComponentSize,
314
+ ITableBasicColumn,
315
+ ITableBasicRow,
316
+ MasterTable,
317
+ } from "@sberbusiness/triplex-next";
318
+
319
+ const rows = [
320
+ { rowKey: "1350", number: "1350", recipient: "ООО Ромашка" },
321
+ { rowKey: "1351", number: "1351", recipient: "ИП Иванов И. И." },
322
+ ];
323
+
324
+ // Колонки собираются вне рендера, чтобы таблица не обновляла контекст MasterTable на каждый рендер.
325
+ const columns: ITableBasicColumn[] = [
326
+ { fieldKey: "checkbox", cellType: ECellType.CHECKBOX, label: "", width: 48 },
327
+ { fieldKey: "number", cellType: ECellType.TEXT, label: "Номер (text)", width: 120 },
328
+ { fieldKey: "recipient", cellType: ECellType.TEXT, label: "Получатель (text)" },
329
+ { fieldKey: "action", cellType: ECellType.COMPONENTS, label: "Действие (components)", width: 180 },
330
+ ];
331
+
332
+ export const CellTypes = () => {
333
+ const [checkedRows, setCheckedRows] = useState<string[]>([]);
334
+
335
+ const toggleRow = (rowKey: string, checked: boolean) => {
336
+ setCheckedRows((prev) => (checked ? [...prev, rowKey] : prev.filter((key) => key !== rowKey)));
337
+ };
338
+
339
+ const data: ITableBasicRow[] = rows.map((row) => ({
340
+ rowKey: row.rowKey,
341
+ selected: checkedRows.includes(row.rowKey),
342
+ rowData: {
343
+ checkbox: (
344
+ <Checkbox
345
+ checked={checkedRows.includes(row.rowKey)}
346
+ onChange={(event) => toggleRow(row.rowKey, event.target.checked)}
347
+ aria-label={`Выбрать строку ${row.number}`}
348
+ />
349
+ ),
350
+ number: row.number,
351
+ recipient: row.recipient,
352
+ action: (
353
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={() => {}}>
354
+ Напечатать
355
+ </Button>
356
+ ),
357
+ },
358
+ }));
359
+
360
+ return (
361
+ <MasterTable>
362
+ <MasterTable.TableBasic columns={columns} data={data} renderNoData={() => <div>Нет данных</div>} />
363
+ </MasterTable>
364
+ );
365
+ };
366
+ ```
367
+
368
+ ### Sorting
369
+
370
+ `stories/TableBasic/examples/Sorting.tsx`
371
+
372
+ ```tsx
373
+ import React, { useState } from "react";
374
+ import {
375
+ EHorizontalAlign,
376
+ EOrderDirection,
377
+ ISortOrder,
378
+ ITableBasicColumn,
379
+ ITableBasicRow,
380
+ MasterTable,
381
+ } from "@sberbusiness/triplex-next";
382
+
383
+ const rows = [
384
+ { rowKey: "1350", number: "1350", recipient: "ООО Ромашка", sum: 9450 },
385
+ { rowKey: "1351", number: "1351", recipient: "ИП Иванов И. И.", sum: 18914 },
386
+ { rowKey: "1352", number: "1352", recipient: "ООО Росинка", sum: 4200 },
387
+ ];
388
+
389
+ export const Sorting = () => {
390
+ // Направление переключается по циклу none → asc → desc → none, следующее значение приходит в onOrderBy.
391
+ const [order, setOrder] = useState<ISortOrder>({ fieldKey: "sum", direction: EOrderDirection.NONE });
392
+
393
+ const getDirection = (fieldKey: string) => (order.fieldKey === fieldKey ? order.direction : EOrderDirection.NONE);
394
+
395
+ const columns: ITableBasicColumn[] = [
396
+ { fieldKey: "number", label: "Номер", width: 120, orderDirection: getDirection("number") },
397
+ { fieldKey: "recipient", label: "Получатель (без сортировки)" },
398
+ {
399
+ fieldKey: "sum",
400
+ label: "Сумма",
401
+ width: 160,
402
+ horizontalAlign: EHorizontalAlign.RIGHT,
403
+ orderDirection: getDirection("sum"),
404
+ },
405
+ ];
406
+
407
+ const sortedRows = [...rows].sort((a, b) => {
408
+ if (order.direction === EOrderDirection.NONE) {
409
+ return 0;
410
+ }
411
+
412
+ const sign = order.direction === EOrderDirection.ASC ? 1 : -1;
413
+
414
+ return order.fieldKey === "sum" ? sign * (a.sum - b.sum) : sign * a.number.localeCompare(b.number);
415
+ });
416
+
417
+ const data: ITableBasicRow[] = sortedRows.map((row) => ({
418
+ rowKey: row.rowKey,
419
+ rowData: { number: row.number, recipient: row.recipient, sum: row.sum.toLocaleString("ru-RU") },
420
+ }));
421
+
422
+ return (
423
+ <MasterTable>
424
+ <MasterTable.TableBasic
425
+ columns={columns}
426
+ data={data}
427
+ onOrderBy={setOrder}
428
+ renderNoData={() => <div>Нет данных</div>}
429
+ />
430
+ </MasterTable>
431
+ );
432
+ };
433
+ ```
434
+
435
+ ### Headless
436
+
437
+ `stories/TableBasic/examples/Headless.tsx`
438
+
439
+ ```tsx
440
+ import React from "react";
441
+ import { ITableBasicColumn, ITableBasicRow, MasterTable } from "@sberbusiness/triplex-next";
442
+
443
+ const columns: ITableBasicColumn[] = [
444
+ { fieldKey: "number", label: "Номер", width: 100 },
445
+ { fieldKey: "recipient", label: "Получатель" },
446
+ { fieldKey: "sum", label: "Сумма", width: 140 },
447
+ ];
448
+
449
+ const data: ITableBasicRow[] = [
450
+ { rowKey: "1350", rowData: { number: "1350", recipient: "ООО Ромашка", sum: "9 450,00" } },
451
+ { rowKey: "1351", rowData: { number: "1351", recipient: "ИП Иванов И. И.", sum: "18 914,00" } },
452
+ ];
453
+
454
+ export const Headless = () => (
455
+ <MasterTable>
456
+ <MasterTable.TableBasic headless columns={columns} data={data} renderNoData={() => <div>Нет данных</div>} />
457
+ </MasterTable>
458
+ );
459
+ ```
460
+
461
+ ### HighlightRowOnHover
462
+
463
+ `stories/TableBasic/examples/HighlightRowOnHover.tsx`
464
+
465
+ ```tsx
466
+ import React from "react";
467
+ import { ITableBasicColumn, ITableBasicRow, MasterTable } from "@sberbusiness/triplex-next";
468
+
469
+ const columns: ITableBasicColumn[] = [
470
+ { fieldKey: "number", label: "Номер", width: 100 },
471
+ { fieldKey: "recipient", label: "Получатель" },
472
+ { fieldKey: "sum", label: "Сумма", width: 140 },
473
+ ];
474
+
475
+ const data: ITableBasicRow[] = [
476
+ { rowKey: "1350", rowData: { number: "1350", recipient: "ООО Ромашка", sum: "9 450,00" } },
477
+ { rowKey: "1351", rowData: { number: "1351", recipient: "ИП Иванов И. И.", sum: "18 914,00" } },
478
+ { rowKey: "1352", rowData: { number: "1352", recipient: "ООО Росинка", sum: "28 392,00" } },
479
+ ];
480
+
481
+ export const HighlightRowOnHover = () => (
482
+ <MasterTable>
483
+ <MasterTable.TableBasic
484
+ highlightRowOnHover
485
+ columns={columns}
486
+ data={data}
487
+ renderNoData={() => <div>Нет данных</div>}
488
+ />
489
+ </MasterTable>
490
+ );
491
+ ```
492
+
493
+ ### ClickableRows
494
+
495
+ `stories/TableBasic/examples/ClickableRows.tsx`
496
+
497
+ ```tsx
498
+ import React, { useState } from "react";
499
+ import { ITableBasicColumn, ITableBasicRow, MasterTable } from "@sberbusiness/triplex-next";
500
+
501
+ const columns: ITableBasicColumn[] = [
502
+ { fieldKey: "number", label: "Номер", width: 100 },
503
+ { fieldKey: "recipient", label: "Получатель" },
504
+ { fieldKey: "sum", label: "Сумма", width: 140 },
505
+ ];
506
+
507
+ const rows = [
508
+ { rowKey: "1350", number: "1350", recipient: "ООО Ромашка", sum: "9 450,00" },
509
+ { rowKey: "1351", number: "1351", recipient: "ИП Иванов И. И.", sum: "18 914,00" },
510
+ { rowKey: "1352", number: "1352", recipient: "ООО Росинка", sum: "28 392,00" },
511
+ ];
512
+
513
+ export const ClickableRows = () => {
514
+ const [selectedRowKey, setSelectedRowKey] = useState("1351");
515
+
516
+ // Заданный onClickRow сам включает подсветку строки при наведении, highlightRowOnHover не нужен.
517
+ const data: ITableBasicRow[] = rows.map((row) => ({
518
+ rowKey: row.rowKey,
519
+ selected: row.rowKey === selectedRowKey,
520
+ rowData: { number: row.number, recipient: row.recipient, sum: row.sum },
521
+ }));
522
+
523
+ return (
524
+ <MasterTable>
525
+ <MasterTable.TableBasic
526
+ columns={columns}
527
+ data={data}
528
+ onClickRow={setSelectedRowKey}
529
+ renderNoData={() => <div>Нет данных</div>}
530
+ />
531
+ </MasterTable>
532
+ );
533
+ };
534
+ ```
535
+
536
+ ### NoData
537
+
538
+ `stories/TableBasic/examples/NoData.tsx`
539
+
540
+ ```tsx
541
+ import React from "react";
542
+ import { EmptytableSysIcon96 } from "@sberbusiness/icons-next";
543
+ import {
544
+ EFontType,
545
+ EFontWeightTitle,
546
+ ETextSize,
547
+ ETitleSize,
548
+ Gap,
549
+ ITableBasicColumn,
550
+ MasterTable,
551
+ Text,
552
+ Title,
553
+ } from "@sberbusiness/triplex-next";
554
+
555
+ const columns: ITableBasicColumn[] = [
556
+ { fieldKey: "number", label: "Номер", width: 100 },
557
+ { fieldKey: "recipient", label: "Получатель" },
558
+ { fieldKey: "sum", label: "Сумма", width: 140 },
559
+ ];
560
+
561
+ const renderNoData = () => (
562
+ <>
563
+ <EmptytableSysIcon96 />
564
+ <Gap size={8} />
565
+ <Title size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
566
+ Пока нет документов
567
+ </Title>
568
+ <Gap size={12} />
569
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
570
+ Здесь появятся платёжные поручения, когда вы их создадите.
571
+ </Text>
572
+ </>
573
+ );
574
+
575
+ export const NoData = () => (
576
+ <MasterTable>
577
+ <MasterTable.TableBasic columns={columns} data={[]} renderNoData={renderNoData} />
578
+ </MasterTable>
579
+ );
580
+ ```
581
+
582
+ ### NoColumns
583
+
584
+ `stories/TableBasic/examples/NoColumns.tsx`
585
+
586
+ ```tsx
587
+ import React, { useState } from "react";
588
+ import {
589
+ Button,
590
+ EButtonTheme,
591
+ EComponentSize,
592
+ EFontType,
593
+ EFontWeightTitle,
594
+ ETextSize,
595
+ ETitleSize,
596
+ Gap,
597
+ ITableBasicColumn,
598
+ ITableBasicRow,
599
+ MasterTable,
600
+ Text,
601
+ Title,
602
+ } from "@sberbusiness/triplex-next";
603
+
604
+ const defaultColumns: ITableBasicColumn[] = [
605
+ { fieldKey: "number", label: "Номер", width: 100 },
606
+ { fieldKey: "recipient", label: "Получатель" },
607
+ { fieldKey: "sum", label: "Сумма", width: 140 },
608
+ ];
609
+
610
+ const data: ITableBasicRow[] = [
611
+ { rowKey: "1350", rowData: { number: "1350", recipient: "ООО Ромашка", sum: "9 450,00" } },
612
+ ];
613
+
614
+ export const NoColumns = () => {
615
+ // Все колонки скрыты: renderNoColumns вызывается вместо таблицы.
616
+ const [columns, setColumns] = useState<ITableBasicColumn[]>(
617
+ defaultColumns.map((column) => ({ ...column, hidden: true })),
618
+ );
619
+
620
+ const renderNoColumns = () => (
621
+ <MasterTable.NoColumns>
622
+ <Title size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
623
+ Все колонки таблицы скрыты
624
+ </Title>
625
+ <Gap size={12} />
626
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
627
+ Выберите нужные вам для отображения колонки в настройках таблицы.
628
+ </Text>
629
+ <Gap size={24} />
630
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={() => setColumns(defaultColumns)}>
631
+ Сбросить настройки
632
+ </Button>
633
+ </MasterTable.NoColumns>
634
+ );
635
+
636
+ return (
637
+ <MasterTable>
638
+ <MasterTable.TableBasic
639
+ columns={columns}
640
+ data={data}
641
+ renderNoData={() => <div>Нет данных</div>}
642
+ renderNoColumns={renderNoColumns}
643
+ />
644
+ </MasterTable>
645
+ );
646
+ };
647
+ ```
648
+
649
+ ### Loading
650
+
651
+ `stories/TableBasic/examples/Loading.tsx`
652
+
653
+ ```tsx
654
+ import React from "react";
655
+ import { ETextSize, Gap, ITableBasicColumn, ITableBasicRow, MasterTable, Text } from "@sberbusiness/triplex-next";
656
+
657
+ const columns: ITableBasicColumn[] = [
658
+ { fieldKey: "number", label: "Номер", width: 100 },
659
+ { fieldKey: "recipient", label: "Получатель" },
660
+ { fieldKey: "sum", label: "Сумма", width: 140 },
661
+ ];
662
+
663
+ const data: ITableBasicRow[] = [
664
+ { rowKey: "1350", rowData: { number: "1350", recipient: "ООО Ромашка", sum: "9 450,00" } },
665
+ { rowKey: "1351", rowData: { number: "1351", recipient: "ИП Иванов И. И.", sum: "18 914,00" } },
666
+ ];
667
+
668
+ // Состояние загрузки задаётся на MasterTable, TableBasic берёт его из контекста.
669
+ export const Loading = () => (
670
+ <>
671
+ <Text tag="div" size={ETextSize.B3}>
672
+ Загрузка поверх уже показанных данных
673
+ </Text>
674
+ <Gap size={12} />
675
+ <MasterTable loading>
676
+ <MasterTable.TableBasic columns={columns} data={data} renderNoData={() => <div>Нет данных</div>} />
677
+ </MasterTable>
678
+ <Gap size={24} />
679
+ <Text tag="div" size={ETextSize.B3}>
680
+ Загрузка при пустой таблице
681
+ </Text>
682
+ <Gap size={12} />
683
+ <MasterTable loading>
684
+ <MasterTable.TableBasic columns={columns} data={[]} renderNoData={() => <div>Нет данных</div>} />
685
+ </MasterTable>
686
+ </>
687
+ );
688
+ ```
689
+
690
+ ### TableSpan
691
+
692
+ `stories/TableBasic/examples/TableSpan.tsx`
693
+
694
+ ```tsx
695
+ import React from "react";
696
+ import {
697
+ Amount,
698
+ EHorizontalAlign,
699
+ EVerticalAlign,
700
+ MasterTable,
701
+ ITableBasicColumn,
702
+ ITableBasicRow,
703
+ } from "@sberbusiness/triplex-next";
704
+
705
+ // Колонки и данные собираются вне рендера, чтобы таблица не обновляла контекст MasterTable на каждый рендер.
706
+ const backendData: ITableBasicRow[] = [
707
+ {
708
+ rowKey: "637",
709
+ rowData: {
710
+ number: "637",
711
+ sender: "ООО Ромашка",
712
+ sum: "1337,00",
713
+ status: "Ячейки первой и второй строки объединены по вертикали.",
714
+ },
715
+ rowLayout: {
716
+ status: { rowSpan: 2 },
717
+ },
718
+ },
719
+ {
720
+ rowKey: "638",
721
+ rowData: {
722
+ number: "638",
723
+ sender: "ООО Росинка",
724
+ sum: "420,00",
725
+ },
726
+ },
727
+ {
728
+ rowKey: "639",
729
+ rowData: {
730
+ number: "639",
731
+ sender: "Ячейки второго и третьего столбца объединены по горизонтали.",
732
+ status: "Исполнено",
733
+ },
734
+ rowLayout: {
735
+ sender: { colSpan: 2 },
736
+ },
737
+ },
738
+ ];
739
+
740
+ const columns: ITableBasicColumn[] = [
741
+ {
742
+ fieldKey: "number",
743
+ label: "Номер",
744
+ width: 65,
745
+ },
746
+ {
747
+ fieldKey: "sender",
748
+ label: "Отправитель",
749
+ },
750
+ {
751
+ fieldKey: "sum",
752
+ label: "Сумма",
753
+ horizontalAlign: EHorizontalAlign.RIGHT,
754
+ renderCell: (fieldValue) => fieldValue && <Amount value={fieldValue} currency={"RUB"} />,
755
+ },
756
+ {
757
+ fieldKey: "status",
758
+ label: "Статус",
759
+ verticalAlign: EVerticalAlign.MIDDLE,
760
+ width: 150,
761
+ },
762
+ ];
763
+
764
+ export const TableSpan = () => (
765
+ <div style={{ maxWidth: "600px" }}>
766
+ <MasterTable>
767
+ <MasterTable.TableBasic columns={columns} data={backendData} renderNoData={() => <div>Нет данных</div>} />
768
+ </MasterTable>
769
+ </div>
770
+ );
771
+ ```
772
+
773
+ ### TableSettingsColumn
774
+
775
+ `stories/TableBasic/examples/TableSettingsColumn.tsx`
776
+
777
+ ```tsx
778
+ import React, { useState } from "react";
779
+ import { EmptytableSysIcon96 } from "@sberbusiness/icons-next";
780
+ import {
781
+ Amount,
782
+ Button,
783
+ Checkbox,
784
+ ColumnSettings,
785
+ EButtonTheme,
786
+ EComponentSize,
787
+ EFontType,
788
+ EFontWeightTitle,
789
+ EHorizontalAlign,
790
+ ETextSize,
791
+ ETitleSize,
792
+ Gap,
793
+ MasterTable,
794
+ Text,
795
+ Title,
796
+ ITableBasicColumn,
797
+ ITableBasicRow,
798
+ } from "@sberbusiness/triplex-next";
799
+
800
+ const renderCounterpartyDetails = (purpose: string, account: string, tax: string) => (
801
+ <>
802
+ <div>
803
+ {purpose}
804
+ <br />
805
+ {account}
806
+ </div>
807
+ <Gap size={4} />
808
+ <Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
809
+ {tax}
810
+ </Text>
811
+ </>
812
+ );
813
+
814
+ const renderNoData = () => (
815
+ <>
816
+ <EmptytableSysIcon96 />
817
+ <Gap size={8} />
818
+ <Title size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
819
+ Текст заголовка
820
+ </Title>
821
+ <Gap size={12} />
822
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
823
+ Нет данных, но можно предложить какие-то действия для заполнения таблицы
824
+ </Text>
825
+ <Gap size={24} />
826
+ <div>
827
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
828
+ Button text
829
+ </Button>
830
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
831
+ Button text
832
+ </Button>
833
+ </div>
834
+ </>
835
+ );
836
+
837
+ const renderNoColumns = (onReset: () => void) => (
838
+ <MasterTable.NoColumns>
839
+ <Title size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
840
+ Все колонки таблицы скрыты
841
+ </Title>
842
+ <Gap size={12} />
843
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
844
+ Выберите нужные вам для отображения колонки в настройках таблицы.
845
+ </Text>
846
+ <Gap size={24} />
847
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={onReset}>
848
+ Сбросить настройки
849
+ </Button>
850
+ </MasterTable.NoColumns>
851
+ );
852
+
853
+ // Колонки и данные собираются вне рендера, чтобы таблица не обновляла контекст MasterTable на каждый рендер.
854
+ const defaultColumns: ITableBasicColumn[] = [
855
+ {
856
+ fieldKey: "number",
857
+ label: "Номер",
858
+ },
859
+ {
860
+ fieldKey: "value",
861
+ label: "Получатель",
862
+ },
863
+ {
864
+ fieldKey: "sum",
865
+ horizontalAlign: EHorizontalAlign.RIGHT,
866
+ label: "Сумма",
867
+ renderCell: (fieldValue) => fieldValue && <Amount value={fieldValue} currency="RUB" />,
868
+ },
869
+ {
870
+ fieldKey: "status",
871
+ label: "Статус",
872
+ },
873
+ ];
874
+
875
+ const data: ITableBasicRow[] = Array.from({ length: 5 }, (_, index) => ({
876
+ rowData: {
877
+ number: 1397450 + index,
878
+ status: "Исполнено",
879
+ sum: "1220000000",
880
+ value: renderCounterpartyDetails(
881
+ "Платежное поручение ООО Ромашка",
882
+ "40702 810 2 0527 5000000",
883
+ "В том числе НДС 20%",
884
+ ),
885
+ },
886
+ rowKey: `table-basic-row-${index}`,
887
+ }));
888
+
889
+ export const TableSettingsColumn = () => {
890
+ const [columns, setColumns] = useState(defaultColumns);
891
+ const [settingsDropdownOpened, setSettingsDropdownOpened] = useState(false);
892
+
893
+ const resetColumns = () => {
894
+ setColumns(defaultColumns);
895
+ };
896
+
897
+ const showAllColumns = () => {
898
+ setColumns(
899
+ columns.map((column) => {
900
+ if (column.hidden) {
901
+ return { ...column, hidden: false };
902
+ }
903
+ return column;
904
+ }),
905
+ );
906
+ };
907
+
908
+ const toggleColumn = (fieldKey: string, checked: boolean) => {
909
+ setColumns(
910
+ columns.map((column) => {
911
+ if (column.fieldKey === fieldKey) {
912
+ return { ...column, hidden: !checked };
913
+ }
914
+ return column;
915
+ }),
916
+ );
917
+ };
918
+
919
+ const renderColumns = () => (
920
+ <ColumnSettings.SortableList columns={columns} onColumnsChange={setColumns}>
921
+ {columns.map((column) => (
922
+ <ColumnSettings.SortableList.Item key={column.fieldKey} id={column.fieldKey}>
923
+ <Checkbox
924
+ checked={column.hidden !== true}
925
+ onChange={(event) => toggleColumn(column.fieldKey, event.target.checked)}
926
+ >
927
+ {column.label}
928
+ </Checkbox>
929
+ </ColumnSettings.SortableList.Item>
930
+ ))}
931
+ </ColumnSettings.SortableList>
932
+ );
933
+
934
+ return (
935
+ <MasterTable>
936
+ <MasterTable.ChipPanel>
937
+ <MasterTable.ChipPanel.Links>
938
+ <MasterTable.TableBasicSettings
939
+ linkTitle={settingsDropdownOpened ? "Скрыть настройки" : "Настройки"}
940
+ opened={settingsDropdownOpened}
941
+ setOpened={setSettingsDropdownOpened}
942
+ >
943
+ <MasterTable.TableBasicSettings.Header>Показать столбцы</MasterTable.TableBasicSettings.Header>
944
+
945
+ <MasterTable.TableBasicSettings.Body>
946
+ <ColumnSettings>{renderColumns()}</ColumnSettings>
947
+ </MasterTable.TableBasicSettings.Body>
948
+
949
+ <MasterTable.TableBasicSettings.Footer>
950
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD} onClick={showAllColumns}>
951
+ Выбрать все
952
+ </Button>
953
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD} onClick={resetColumns}>
954
+ Сбросить настройки
955
+ </Button>
956
+ </MasterTable.TableBasicSettings.Footer>
957
+ </MasterTable.TableBasicSettings>
958
+ </MasterTable.ChipPanel.Links>
959
+ </MasterTable.ChipPanel>
960
+ <MasterTable.TableBasic
961
+ columns={columns}
962
+ data={data}
963
+ renderNoData={renderNoData}
964
+ renderNoColumns={() => renderNoColumns(resetColumns)}
965
+ />
966
+ </MasterTable>
967
+ );
968
+ };
969
+ ```
970
+
971
+ ### TableSettingsColumnExtended
972
+
973
+ `stories/TableBasic/examples/TableSettingsColumnExtended.tsx`
974
+
975
+ ```tsx
976
+ import React, { useState } from "react";
977
+ import { EmptytableSysIcon96 } from "@sberbusiness/icons-next";
978
+ import {
979
+ Amount,
980
+ Button,
981
+ Checkbox,
982
+ ColumnSettings,
983
+ EButtonTheme,
984
+ EComponentSize,
985
+ EFontType,
986
+ EFontWeightTitle,
987
+ EHorizontalAlign,
988
+ ETextSize,
989
+ ETitleSize,
990
+ Gap,
991
+ MasterTable,
992
+ Text,
993
+ Title,
994
+ ITableBasicColumn,
995
+ ITableBasicRow,
996
+ } from "@sberbusiness/triplex-next";
997
+
998
+ const renderNoData = () => (
999
+ <>
1000
+ <EmptytableSysIcon96 />
1001
+ <Gap size={8} />
1002
+ <Title size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
1003
+ Текст заголовка
1004
+ </Title>
1005
+ <Gap size={12} />
1006
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
1007
+ Нет данных, но можно предложить какие-то действия для заполнения таблицы
1008
+ </Text>
1009
+ <Gap size={24} />
1010
+ <div>
1011
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
1012
+ Button text
1013
+ </Button>
1014
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
1015
+ Button text
1016
+ </Button>
1017
+ </div>
1018
+ </>
1019
+ );
1020
+
1021
+ const renderNoColumns = (onReset: () => void) => (
1022
+ <MasterTable.NoColumns>
1023
+ <Title size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
1024
+ Все колонки таблицы скрыты
1025
+ </Title>
1026
+ <Gap size={12} />
1027
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
1028
+ Выберите нужные вам для отображения колонки в настройках таблицы.
1029
+ </Text>
1030
+ <Gap size={24} />
1031
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={onReset}>
1032
+ Сбросить настройки
1033
+ </Button>
1034
+ </MasterTable.NoColumns>
1035
+ );
1036
+
1037
+ // Колонки собираются вне рендера, чтобы таблица не обновляла контекст MasterTable на каждый рендер.
1038
+ const defaultColumns: ITableBasicColumn[] = [
1039
+ {
1040
+ fieldKey: "number",
1041
+ label: "Номер",
1042
+ },
1043
+ {
1044
+ fieldKey: "value",
1045
+ label: "Получатель",
1046
+ },
1047
+ {
1048
+ fieldKey: "sum",
1049
+ horizontalAlign: EHorizontalAlign.RIGHT,
1050
+ label: "Сумма",
1051
+ renderCell: (fieldValue) => fieldValue && <Amount value={fieldValue} currency="RUB" />,
1052
+ },
1053
+ {
1054
+ fieldKey: "status",
1055
+ label: "Статус",
1056
+ },
1057
+ ];
1058
+
1059
+ interface IValueColumnOptions {
1060
+ showCounterparty: boolean;
1061
+ showNumber: boolean;
1062
+ showNDS: boolean;
1063
+ }
1064
+
1065
+ const initiateValueColumnOptions = (defaultChecked: boolean): IValueColumnOptions => ({
1066
+ showCounterparty: defaultChecked,
1067
+ showNumber: defaultChecked,
1068
+ showNDS: defaultChecked,
1069
+ });
1070
+
1071
+ const defaultValueColumnOptions = initiateValueColumnOptions(true);
1072
+
1073
+ const mapValueColumnOptionKeyToName: Record<string, string> = {
1074
+ showCounterparty: "Получатель",
1075
+ showNumber: "Номер счета",
1076
+ showNDS: "НДС",
1077
+ };
1078
+
1079
+ /**
1080
+ * Возвращает данные для таблицы.
1081
+ * @param showCounterparty - показать блок Получатель в столбце Значение.
1082
+ * @param showNumber - показать блок Номер счета в столбце Значение.
1083
+ * @param showNDS - показать блок НДС счета в столбце Значение.
1084
+ */
1085
+ const getData = ({ showCounterparty, showNumber, showNDS }: IValueColumnOptions): ITableBasicRow[] =>
1086
+ Array.from({ length: 5 }, (_, index) => ({
1087
+ rowData: {
1088
+ number: 1397450 + index,
1089
+ status: "Исполнено",
1090
+ sum: "1220000000",
1091
+ value: (
1092
+ <>
1093
+ {(showCounterparty || showNumber) && (
1094
+ <>
1095
+ <div>
1096
+ {showCounterparty && (
1097
+ <>
1098
+ Платежное поручение ООО Ромашка <br />
1099
+ </>
1100
+ )}
1101
+ {showNumber && "40702 810 2 0527 5000000"}
1102
+ </div>
1103
+ <Gap size={4} />
1104
+ </>
1105
+ )}
1106
+
1107
+ {showNDS && (
1108
+ <Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
1109
+ В том числе НДС 20%
1110
+ </Text>
1111
+ )}
1112
+ </>
1113
+ ),
1114
+ },
1115
+ rowKey: `table-basic-row-${index}`,
1116
+ }));
1117
+
1118
+ interface IValueColumnSettingsProps {
1119
+ column: ITableBasicColumn;
1120
+ options: IValueColumnOptions;
1121
+ setColumn: (checked: boolean) => void;
1122
+ setOptions: (option: IValueColumnOptions) => void;
1123
+ }
1124
+
1125
+ const ValueColumnSettings = ({ column, options, setColumn, setOptions }: IValueColumnSettingsProps) => {
1126
+ const handleChildCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>, columnKey: string) => {
1127
+ const newOptions = { ...options, [columnKey]: event.target.checked };
1128
+
1129
+ if (event.target.checked !== !column.hidden) {
1130
+ if (event.target.checked) {
1131
+ if (Object.values(options).every((value) => value === false)) {
1132
+ setColumn(event.target.checked);
1133
+ }
1134
+ } else {
1135
+ if (Object.values(newOptions).every((value) => value === false)) {
1136
+ setColumn(event.target.checked);
1137
+ }
1138
+ }
1139
+ }
1140
+
1141
+ setOptions(newOptions);
1142
+ };
1143
+
1144
+ const handleParentCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
1145
+ setOptions(initiateValueColumnOptions(event.target.checked));
1146
+ setColumn(event.target.checked);
1147
+ };
1148
+
1149
+ const renderOptions = () => (
1150
+ <ColumnSettings.StaticList depth={1}>
1151
+ {Object.entries(options).map(([key, value], index) => (
1152
+ <ColumnSettings.StaticList.Item key={index}>
1153
+ <Checkbox checked={Boolean(value)} onChange={(event) => handleChildCheckboxChange(event, key)}>
1154
+ {mapValueColumnOptionKeyToName[key]}
1155
+ </Checkbox>
1156
+ </ColumnSettings.StaticList.Item>
1157
+ ))}
1158
+ </ColumnSettings.StaticList>
1159
+ );
1160
+
1161
+ return (
1162
+ <ColumnSettings.SortableList.Item id={column.fieldKey} staticContent={renderOptions()}>
1163
+ <Checkbox
1164
+ checked={!column.hidden}
1165
+ bulk={Object.values(options).some((value) => value !== !column.hidden)}
1166
+ onChange={handleParentCheckboxChange}
1167
+ >
1168
+ {column.label}
1169
+ </Checkbox>
1170
+ </ColumnSettings.SortableList.Item>
1171
+ );
1172
+ };
1173
+
1174
+ export const TableSettingsColumnExtended = () => {
1175
+ const [columns, setColumns] = useState(defaultColumns);
1176
+ const [valueColumnOptions, setValueColumnOptions] = useState(defaultValueColumnOptions);
1177
+ const [settingsDropdownOpened, setSettingsDropdownOpened] = useState(false);
1178
+
1179
+ // Содержимое колонки «Получатель» выводится из настроек — отдельного состояния для данных не нужно.
1180
+ const data = getData(valueColumnOptions);
1181
+
1182
+ const resetColumns = () => {
1183
+ setColumns(defaultColumns);
1184
+ setValueColumnOptions(defaultValueColumnOptions);
1185
+ };
1186
+
1187
+ const showAllColumns = () => {
1188
+ setColumns(
1189
+ columns.map((column) => {
1190
+ if (column.hidden) {
1191
+ return { ...column, hidden: false };
1192
+ }
1193
+ return column;
1194
+ }),
1195
+ );
1196
+ setValueColumnOptions(defaultValueColumnOptions);
1197
+ };
1198
+
1199
+ const toggleColumn = (fieldKey: string, checked: boolean): void => {
1200
+ setColumns(
1201
+ columns.map((column) => {
1202
+ if (column.fieldKey === fieldKey) {
1203
+ return { ...column, hidden: !checked };
1204
+ }
1205
+ return column;
1206
+ }),
1207
+ );
1208
+ };
1209
+
1210
+ const renderColumns = () => (
1211
+ <ColumnSettings.SortableList columns={columns} onColumnsChange={setColumns}>
1212
+ {columns.map((column) => {
1213
+ if (column.fieldKey === "value") {
1214
+ return (
1215
+ <ValueColumnSettings
1216
+ key={column.fieldKey}
1217
+ column={column}
1218
+ options={valueColumnOptions}
1219
+ setColumn={(checked: boolean) => toggleColumn(column.fieldKey, checked)}
1220
+ setOptions={(options) => {
1221
+ setValueColumnOptions(options);
1222
+ }}
1223
+ />
1224
+ );
1225
+ }
1226
+
1227
+ return (
1228
+ <ColumnSettings.SortableList.Item key={column.fieldKey} id={column.fieldKey}>
1229
+ <Checkbox
1230
+ checked={column.hidden !== true}
1231
+ onChange={(event) => toggleColumn(column.fieldKey, event.target.checked)}
1232
+ >
1233
+ {column.label}
1234
+ </Checkbox>
1235
+ </ColumnSettings.SortableList.Item>
1236
+ );
1237
+ })}
1238
+ </ColumnSettings.SortableList>
1239
+ );
1240
+
1241
+ return (
1242
+ <MasterTable>
1243
+ <MasterTable.ChipPanel>
1244
+ <MasterTable.ChipPanel.Links>
1245
+ <MasterTable.TableBasicSettings
1246
+ linkTitle={settingsDropdownOpened ? "Скрыть настройки" : "Настройки"}
1247
+ opened={settingsDropdownOpened}
1248
+ setOpened={setSettingsDropdownOpened}
1249
+ >
1250
+ <MasterTable.TableBasicSettings.Header>Показать столбцы</MasterTable.TableBasicSettings.Header>
1251
+
1252
+ <MasterTable.TableBasicSettings.Body>
1253
+ <ColumnSettings>{renderColumns()}</ColumnSettings>
1254
+ </MasterTable.TableBasicSettings.Body>
1255
+
1256
+ <MasterTable.TableBasicSettings.Footer>
1257
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD} onClick={showAllColumns}>
1258
+ Выбрать все
1259
+ </Button>
1260
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD} onClick={resetColumns}>
1261
+ Сбросить настройки
1262
+ </Button>
1263
+ </MasterTable.TableBasicSettings.Footer>
1264
+ </MasterTable.TableBasicSettings>
1265
+ </MasterTable.ChipPanel.Links>
1266
+ </MasterTable.ChipPanel>
1267
+ <MasterTable.TableBasic
1268
+ columns={columns}
1269
+ data={data}
1270
+ renderNoData={renderNoData}
1271
+ renderNoColumns={() => renderNoColumns(resetColumns)}
1272
+ />
1273
+ </MasterTable>
1274
+ );
1275
+ };
1276
+ ```
1277
+
1278
+ ### TableWithPagination
1279
+
1280
+ `stories/TableBasic/examples/TableWithPagination.tsx`
1281
+
1282
+ ```tsx
1283
+ import React, { useState } from "react";
1284
+ import {
1285
+ Amount,
1286
+ EFontType,
1287
+ EHorizontalAlign,
1288
+ ETextSize,
1289
+ Gap,
1290
+ MasterTable,
1291
+ Pagination,
1292
+ Text,
1293
+ ISelectExtendedFieldDefaultOption,
1294
+ ITableBasicColumn,
1295
+ ITableBasicRow,
1296
+ } from "@sberbusiness/triplex-next";
1297
+
1298
+ const renderCounterpartyDetails = (purpose: string, account: string, tax: string) => (
1299
+ <>
1300
+ <div>
1301
+ {purpose}
1302
+ <br />
1303
+ {account}
1304
+ </div>
1305
+ <Gap size={4} />
1306
+ <Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
1307
+ {tax}
1308
+ </Text>
1309
+ </>
1310
+ );
1311
+
1312
+ // Колонки и данные собираются вне рендера, чтобы таблица не обновляла контекст MasterTable на каждый рендер.
1313
+ const columns: ITableBasicColumn[] = [
1314
+ {
1315
+ fieldKey: "number",
1316
+ label: "Номер",
1317
+ },
1318
+ {
1319
+ fieldKey: "value",
1320
+ label: "Получатель",
1321
+ },
1322
+ {
1323
+ fieldKey: "sum",
1324
+ horizontalAlign: EHorizontalAlign.RIGHT,
1325
+ label: "Сумма",
1326
+ renderCell: (fieldValue) => fieldValue && <Amount value={fieldValue} currency="RUB" />,
1327
+ },
1328
+ {
1329
+ fieldKey: "status",
1330
+ label: "Статус",
1331
+ },
1332
+ ];
1333
+
1334
+ const data: ITableBasicRow[] = Array.from({ length: 300 }, (_, index) => ({
1335
+ rowData: {
1336
+ number: 1000 + index,
1337
+ status: "Исполнено",
1338
+ sum: "1220000000",
1339
+ value: renderCounterpartyDetails(
1340
+ "Платежное поручение ООО Ромашка",
1341
+ "40702 810 2 0527 5000000",
1342
+ "В том числе НДС 20%",
1343
+ ),
1344
+ },
1345
+ rowKey: `table-basic-row-${index}`,
1346
+ }));
1347
+
1348
+ const options: ISelectExtendedFieldDefaultOption[] = [
1349
+ { id: "0", value: "10", label: "10" },
1350
+ { id: "1", value: "20", label: "20" },
1351
+ { id: "2", value: "50", label: "50" },
1352
+ { id: "3", value: "100", label: "100" },
1353
+ ];
1354
+
1355
+ export const TableWithPagination = () => {
1356
+ const [page, setPage] = useState(1);
1357
+ const [pageSize, setPageSize] = useState(10);
1358
+ const totalItems = data.length;
1359
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
1360
+
1361
+ const handlePageSizeChange = (option: ISelectExtendedFieldDefaultOption) => {
1362
+ setPage(1);
1363
+ setPageSize(Number(option.value));
1364
+ };
1365
+
1366
+ const selectedOption = options.find((option) => option.value === String(pageSize));
1367
+
1368
+ const getPaginatedData = () => {
1369
+ return data.slice((page - 1) * pageSize, page * pageSize);
1370
+ };
1371
+
1372
+ return (
1373
+ <MasterTable>
1374
+ <MasterTable.TableBasic
1375
+ columns={columns}
1376
+ data={getPaginatedData()}
1377
+ renderNoData={() => <div>Нет данных</div>}
1378
+ />
1379
+ <MasterTable.PaginationPanel>
1380
+ <Pagination
1381
+ paginationNavigationProps={{
1382
+ currentPage: page,
1383
+ totalPages,
1384
+ boundaryCount: 1,
1385
+ siblingCount: 1,
1386
+ onCurrentPageChange: setPage,
1387
+ }}
1388
+ paginationSelectProps={{
1389
+ paginationLabel: "Показать на странице:",
1390
+ options,
1391
+ value: selectedOption || options[0],
1392
+ onChange: handlePageSizeChange,
1393
+ targetProps: {
1394
+ fieldLabel: "",
1395
+ },
1396
+ }}
1397
+ />
1398
+ </MasterTable.PaginationPanel>
1399
+ </MasterTable>
1400
+ );
1401
+ };
1402
+ ```
1403
+
1404
+ ### TableWithPaginationLoading
1405
+
1406
+ `stories/TableBasic/examples/TableWithPaginationLoading.tsx`
1407
+
1408
+ ```tsx
1409
+ import React, { useState } from "react";
1410
+ import { EmptytableSysIcon96, NotfoundSysIcon96 } from "@sberbusiness/icons-next";
1411
+ import {
1412
+ Amount,
1413
+ Button,
1414
+ Col,
1415
+ EButtonTheme,
1416
+ EComponentSize,
1417
+ EFontType,
1418
+ EFontWeightTitle,
1419
+ EHorizontalAlign,
1420
+ ETextSize,
1421
+ ETitleSize,
1422
+ Gap,
1423
+ MasterTable,
1424
+ Pagination,
1425
+ Row,
1426
+ Text,
1427
+ TextField,
1428
+ Title,
1429
+ ISelectExtendedFieldDefaultOption,
1430
+ ITableBasicColumn,
1431
+ ITableBasicRow,
1432
+ } from "@sberbusiness/triplex-next";
1433
+
1434
+ const renderCounterpartyDetails = (purpose: string, account: string, tax: string) => (
1435
+ <>
1436
+ <div>
1437
+ {purpose}
1438
+ <br />
1439
+ {account}
1440
+ </div>
1441
+ <Gap size={4} />
1442
+ <Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
1443
+ {tax}
1444
+ </Text>
1445
+ </>
1446
+ );
1447
+
1448
+ const renderNoData = (isFiltered: boolean) => (
1449
+ <>
1450
+ {isFiltered ? <NotfoundSysIcon96 /> : <EmptytableSysIcon96 />}
1451
+ <Gap size={8} />
1452
+ <Title size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
1453
+ Текст заголовка
1454
+ </Title>
1455
+ <Gap size={12} />
1456
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
1457
+ Нет данных, но можно предложить какие-то действия для заполнения таблицы
1458
+ </Text>
1459
+ <Gap size={24} />
1460
+ <div>
1461
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
1462
+ Button text
1463
+ </Button>
1464
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
1465
+ Button text
1466
+ </Button>
1467
+ </div>
1468
+ </>
1469
+ );
1470
+
1471
+ // Колонки собираются вне рендера, чтобы таблица не обновляла контекст MasterTable на каждый рендер.
1472
+ const columns: ITableBasicColumn[] = [
1473
+ {
1474
+ fieldKey: "number",
1475
+ label: "Номер",
1476
+ },
1477
+ {
1478
+ fieldKey: "value",
1479
+ label: "Получатель",
1480
+ },
1481
+ {
1482
+ fieldKey: "sum",
1483
+ horizontalAlign: EHorizontalAlign.RIGHT,
1484
+ label: "Сумма",
1485
+ renderCell: (fieldValue) => fieldValue && <Amount value={fieldValue} currency="RUB" />,
1486
+ },
1487
+ {
1488
+ fieldKey: "status",
1489
+ label: "Статус",
1490
+ },
1491
+ ];
1492
+
1493
+ const filtersAndPaginationRows = Array.from({ length: 30 }, (_, index) => ({
1494
+ docNumber: String(1000 + index),
1495
+ recipient: index % 2 === 0 ? "ООО Ромашка" : "ИП Иванов Иван Иванович",
1496
+ status: ["Создан", "Подписан", "Оплачен"][index % 3],
1497
+ sum: String((index + 1) * 10000),
1498
+ }));
1499
+
1500
+ const pageSizeOptions: ISelectExtendedFieldDefaultOption[] = [
1501
+ { id: "0", value: "10", label: "10" },
1502
+ { id: "1", value: "20", label: "20" },
1503
+ { id: "2", value: "50", label: "50" },
1504
+ ];
1505
+
1506
+ export const TableWithPaginationLoading = () => {
1507
+ const [docNumberFilter, setDocNumberFilter] = useState("");
1508
+ const [recipientFilter, setRecipientFilter] = useState("");
1509
+ const [page, setPage] = useState(1);
1510
+ const [pageSize, setPageSize] = useState(10);
1511
+
1512
+ const isFiltered = docNumberFilter !== "" || recipientFilter !== "";
1513
+
1514
+ const filteredRows = filtersAndPaginationRows.filter(
1515
+ (row) =>
1516
+ row.docNumber.includes(docNumberFilter) &&
1517
+ row.recipient.toLowerCase().includes(recipientFilter.toLowerCase()),
1518
+ );
1519
+
1520
+ const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
1521
+ // Фильтр мог сократить выборку сильнее, чем текущая страница — страховка от выхода за диапазон.
1522
+ const currentPage = Math.min(page, totalPages);
1523
+
1524
+ const data: ITableBasicRow[] = filteredRows
1525
+ .slice((currentPage - 1) * pageSize, currentPage * pageSize)
1526
+ .map((row) => ({
1527
+ rowData: {
1528
+ number: row.docNumber,
1529
+ status: row.status,
1530
+ sum: row.sum,
1531
+ value: renderCounterpartyDetails(
1532
+ `Платежное поручение ${row.recipient}`,
1533
+ "40702 810 2 0527 5000000",
1534
+ "В том числе НДС 20%",
1535
+ ),
1536
+ },
1537
+ rowKey: `table-with-filters-row-${row.docNumber}`,
1538
+ }));
1539
+
1540
+ const handleChangeDocNumber = (event: React.ChangeEvent<HTMLInputElement>) => {
1541
+ setDocNumberFilter(event.target.value);
1542
+ setPage(1);
1543
+ };
1544
+
1545
+ const handleChangeRecipient = (event: React.ChangeEvent<HTMLInputElement>) => {
1546
+ setRecipientFilter(event.target.value);
1547
+ setPage(1);
1548
+ };
1549
+
1550
+ const handlePageSizeChange = (option: ISelectExtendedFieldDefaultOption) => {
1551
+ setPageSize(Number(option.value));
1552
+ setPage(1);
1553
+ };
1554
+
1555
+ return (
1556
+ <MasterTable loading>
1557
+ <MasterTable.FilterPanel>
1558
+ <Row paddingBottom={false}>
1559
+ <Col size={6}>
1560
+ <TextField
1561
+ size={EComponentSize.MD}
1562
+ inputProps={{
1563
+ value: docNumberFilter,
1564
+ onChange: handleChangeDocNumber,
1565
+ placeholder: "Введите номер документа",
1566
+ }}
1567
+ label="Номер документа"
1568
+ />
1569
+ </Col>
1570
+ <Col size={6}>
1571
+ <TextField
1572
+ size={EComponentSize.MD}
1573
+ inputProps={{
1574
+ value: recipientFilter,
1575
+ onChange: handleChangeRecipient,
1576
+ placeholder: "Введите получателя",
1577
+ }}
1578
+ label="Получатель"
1579
+ />
1580
+ </Col>
1581
+ </Row>
1582
+ </MasterTable.FilterPanel>
1583
+ <MasterTable.TableBasic columns={columns} data={data} renderNoData={() => renderNoData(isFiltered)} />
1584
+ <MasterTable.PaginationPanel>
1585
+ <Pagination
1586
+ paginationNavigationProps={{
1587
+ currentPage,
1588
+ totalPages,
1589
+ boundaryCount: 1,
1590
+ siblingCount: 1,
1591
+ onCurrentPageChange: setPage,
1592
+ }}
1593
+ paginationSelectProps={{
1594
+ paginationLabel: "Показать на странице:",
1595
+ options: pageSizeOptions,
1596
+ value:
1597
+ pageSizeOptions.find((option) => option.value === String(pageSize)) || pageSizeOptions[0],
1598
+ onChange: handlePageSizeChange,
1599
+ targetProps: {
1600
+ fieldLabel: "",
1601
+ },
1602
+ }}
1603
+ />
1604
+ </MasterTable.PaginationPanel>
1605
+ </MasterTable>
1606
+ );
1607
+ };
1608
+ ```
1609
+
1610
+ ### TableWithPaginationExtended
1611
+
1612
+ `stories/TableBasic/examples/TableWithPaginationExtended.tsx`
1613
+
1614
+ ```tsx
1615
+ import React, { useState } from "react";
1616
+ import {
1617
+ Amount,
1618
+ EFontType,
1619
+ EHorizontalAlign,
1620
+ EPaginationNavigationIconDirection,
1621
+ ETextSize,
1622
+ Gap,
1623
+ MasterTable,
1624
+ PaginationExtended,
1625
+ PaginationNavigationButton,
1626
+ PaginationNavigationExtended,
1627
+ PaginationNavigationExtendedItem,
1628
+ PaginationPageButton,
1629
+ PaginationSelect,
1630
+ Text,
1631
+ ISelectExtendedFieldDefaultOption,
1632
+ ITableBasicColumn,
1633
+ ITableBasicRow,
1634
+ } from "@sberbusiness/triplex-next";
1635
+
1636
+ const renderCounterpartyDetails = (purpose: string, account: string, tax: string) => (
1637
+ <>
1638
+ <div>
1639
+ {purpose}
1640
+ <br />
1641
+ {account}
1642
+ </div>
1643
+ <Gap size={4} />
1644
+ <Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
1645
+ {tax}
1646
+ </Text>
1647
+ </>
1648
+ );
1649
+
1650
+ // Колонки и данные собираются вне рендера, чтобы таблица не обновляла контекст MasterTable на каждый рендер.
1651
+ const columns: ITableBasicColumn[] = [
1652
+ {
1653
+ fieldKey: "number",
1654
+ label: "Номер",
1655
+ },
1656
+ {
1657
+ fieldKey: "value",
1658
+ label: "Получатель",
1659
+ },
1660
+ {
1661
+ fieldKey: "sum",
1662
+ horizontalAlign: EHorizontalAlign.RIGHT,
1663
+ label: "Сумма",
1664
+ renderCell: (fieldValue) => fieldValue && <Amount value={fieldValue} currency="RUB" />,
1665
+ },
1666
+ {
1667
+ fieldKey: "status",
1668
+ label: "Статус",
1669
+ },
1670
+ ];
1671
+
1672
+ const data: ITableBasicRow[] = Array.from({ length: 300 }, (_, index) => ({
1673
+ rowData: {
1674
+ number: 1000 + index,
1675
+ status: "Исполнено",
1676
+ sum: "1220000000",
1677
+ value: renderCounterpartyDetails(
1678
+ "Платежное поручение ООО Ромашка",
1679
+ "40702 810 2 0527 5000000",
1680
+ "В том числе НДС 20%",
1681
+ ),
1682
+ },
1683
+ rowKey: `table-basic-row-${index}`,
1684
+ }));
1685
+
1686
+ const options: ISelectExtendedFieldDefaultOption[] = [
1687
+ { id: "0", value: "10", label: "10" },
1688
+ { id: "1", value: "20", label: "20" },
1689
+ { id: "2", value: "50", label: "50" },
1690
+ { id: "3", value: "100", label: "100" },
1691
+ ];
1692
+
1693
+ export const TableWithPaginationExtended = () => {
1694
+ const [page, setPage] = useState(1);
1695
+ const [pageSize, setPageSize] = useState(10);
1696
+ const totalItems = data.length;
1697
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
1698
+
1699
+ const handlePageSizeChange = (option: ISelectExtendedFieldDefaultOption) => {
1700
+ setPage(1);
1701
+ setPageSize(Number(option.value));
1702
+ };
1703
+
1704
+ const selectedOption = options.find((option) => option.value === String(pageSize));
1705
+
1706
+ const getPaginatedData = () => {
1707
+ return data.slice((page - 1) * pageSize, page * pageSize);
1708
+ };
1709
+
1710
+ return (
1711
+ <MasterTable>
1712
+ <MasterTable.TableBasic
1713
+ columns={columns}
1714
+ data={getPaginatedData()}
1715
+ renderNoData={() => <div>Нет данных</div>}
1716
+ />
1717
+ <MasterTable.PaginationPanel>
1718
+ <PaginationExtended>
1719
+ {totalPages > 1 && (
1720
+ <PaginationNavigationExtended>
1721
+ <PaginationNavigationExtendedItem>
1722
+ <PaginationNavigationButton
1723
+ direction={EPaginationNavigationIconDirection.BACK}
1724
+ onClick={() => setPage((prevPage) => Math.max(1, prevPage - 1))}
1725
+ disabled={page === 1}
1726
+ />
1727
+ </PaginationNavigationExtendedItem>
1728
+
1729
+ <PaginationNavigationExtendedItem key="current-page">
1730
+ <PaginationPageButton isCurrent onClick={() => {}}>
1731
+ {page}
1732
+ </PaginationPageButton>
1733
+ </PaginationNavigationExtendedItem>
1734
+
1735
+ <PaginationNavigationExtendedItem>
1736
+ <PaginationNavigationButton
1737
+ direction={EPaginationNavigationIconDirection.NEXT}
1738
+ onClick={() => setPage((prevPage) => Math.min(totalPages, prevPage + 1))}
1739
+ disabled={page === totalPages}
1740
+ />
1741
+ </PaginationNavigationExtendedItem>
1742
+ </PaginationNavigationExtended>
1743
+ )}
1744
+ <PaginationSelect
1745
+ paginationLabel="Показать на странице:"
1746
+ value={selectedOption || options[0]}
1747
+ options={options}
1748
+ onChange={handlePageSizeChange}
1749
+ targetProps={{
1750
+ fieldLabel: "",
1751
+ }}
1752
+ />
1753
+ </PaginationExtended>
1754
+ </MasterTable.PaginationPanel>
1755
+ </MasterTable>
1756
+ );
1757
+ };
1758
+ ```
1759
+
1760
+ ### Example: production
1761
+
1762
+ `stories/TableBasic/examples/ExampleProduction.tsx`
1763
+
1764
+ ```tsx
1765
+ import React, { useState } from "react";
1766
+ import { EmptytableSysIcon96, NotfoundSysIcon96 } from "@sberbusiness/icons-next";
1767
+ import {
1768
+ Amount,
1769
+ Button,
1770
+ Checkbox,
1771
+ CheckboxYGroup,
1772
+ Chip,
1773
+ ChipGroup,
1774
+ ChipMultiselect,
1775
+ Col,
1776
+ DropdownMobileBody,
1777
+ DropdownMobileClose,
1778
+ DropdownMobileFooter,
1779
+ DropdownMobileHeader,
1780
+ DropdownMobileInput,
1781
+ EButtonTheme,
1782
+ EComponentSize,
1783
+ EDropdownAlignment,
1784
+ EFontType,
1785
+ EFontWeightTitle,
1786
+ EHorizontalAlign,
1787
+ EMarkerStatus,
1788
+ ETextSize,
1789
+ ETitleSize,
1790
+ FormField,
1791
+ FormFieldClear,
1792
+ FormFieldInput,
1793
+ FormFieldLabel,
1794
+ FormFieldPostfix,
1795
+ Gap,
1796
+ Link,
1797
+ MarkerStatus,
1798
+ MasterTable,
1799
+ MultiselectField,
1800
+ Row,
1801
+ SelectField,
1802
+ Tag,
1803
+ TagGroup,
1804
+ Text,
1805
+ TextField,
1806
+ Title,
1807
+ ISelectFieldOption,
1808
+ ITableBasicColumn,
1809
+ ITableBasicRow,
1810
+ } from "@sberbusiness/triplex-next";
1811
+
1812
+ /** Ключи полей таблицы. */
1813
+ enum ETableField {
1814
+ Number = "Number",
1815
+ Recipient = "Recipient",
1816
+ Sum = "Sum",
1817
+ Status = "Status",
1818
+ }
1819
+
1820
+ interface IExampleExtraFilters {
1821
+ recipientOption: ISelectFieldOption | undefined;
1822
+ docNumber: string;
1823
+ }
1824
+
1825
+ const renderCounterpartyDetails = (purpose: string, account: string, tax: string) => (
1826
+ <>
1827
+ <div>
1828
+ {purpose}
1829
+ <br />
1830
+ {account}
1831
+ </div>
1832
+ <Gap size={4} />
1833
+ <Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
1834
+ {tax}
1835
+ </Text>
1836
+ </>
1837
+ );
1838
+
1839
+ const renderNoData = (isFiltered: boolean) => (
1840
+ <>
1841
+ {isFiltered ? <NotfoundSysIcon96 /> : <EmptytableSysIcon96 />}
1842
+ <Gap size={8} />
1843
+ <Title size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
1844
+ Текст заголовка
1845
+ </Title>
1846
+ <Gap size={12} />
1847
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
1848
+ Нет данных, но можно предложить какие-то действия для заполнения таблицы
1849
+ </Text>
1850
+ <Gap size={24} />
1851
+ <div>
1852
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
1853
+ Button text
1854
+ </Button>
1855
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
1856
+ Button text
1857
+ </Button>
1858
+ </div>
1859
+ </>
1860
+ );
1861
+
1862
+ const segments: { id: string; label: string; status: string | null; withNotification?: boolean }[] = [
1863
+ { id: "all", label: "Все", status: null },
1864
+ { id: "drafts", label: "Черновики", status: "Создан" },
1865
+ { id: "toSign", label: "На подпись и отправку", status: "Подписан", withNotification: true },
1866
+ { id: "executed", label: "Исполненные", status: "Оплачен" },
1867
+ ];
1868
+
1869
+ const statusFilterOptions = [
1870
+ { id: "created", label: "Создан" },
1871
+ { id: "signed", label: "Подписан" },
1872
+ { id: "paid", label: "Оплачен" },
1873
+ ];
1874
+
1875
+ const statusToMarker: Record<string, EMarkerStatus> = {
1876
+ Создан: EMarkerStatus.WAITING,
1877
+ Подписан: EMarkerStatus.WARNING,
1878
+ Оплачен: EMarkerStatus.SUCCESS,
1879
+ };
1880
+
1881
+ const counterpartyOptions: ISelectFieldOption[] = [
1882
+ { id: "0", value: "", label: "Не указан" },
1883
+ { id: "1", value: "ООО Ромашка", label: "ООО Ромашка" },
1884
+ { id: "2", value: "ИП Иванов Иван Иванович", label: "ИП Иванов Иван Иванович" },
1885
+ ];
1886
+
1887
+ const documents = [
1888
+ {
1889
+ docNumber: "1350",
1890
+ recipient: { account: "40702 810 2 9045 8100009", name: "ИП Иванов Иван Иванович" },
1891
+ purpose: "Платёжное поручение ИП Иванов Иван Иванович за оказание услуг по акту №67834259-ТНГК2356-345",
1892
+ tax: "В том числе НДС 20%",
1893
+ sum: "9450.00",
1894
+ status: "Создан",
1895
+ },
1896
+ {
1897
+ docNumber: "1351",
1898
+ recipient: { account: "40702 810 2 0527 5520080", name: "ООО Ромашка" },
1899
+ purpose: "Платёжное поручение ООО Ромашка за оказание услуг по акту №67834259-ТНГК2356-345",
1900
+ tax: "В том числе НДС 20%",
1901
+ sum: "18914.00",
1902
+ status: "Создан",
1903
+ },
1904
+ {
1905
+ docNumber: "1352",
1906
+ recipient: { account: "40702 810 2 2581 6403700", name: "ИП Иванов Иван Иванович" },
1907
+ purpose: "Платёжное поручение ИП Иванов Иван Иванович за оказание услуг по акту №67834259-ТНГК2356-345",
1908
+ tax: "В том числе НДС 20%",
1909
+ sum: "28392.00",
1910
+ status: "Подписан",
1911
+ },
1912
+ {
1913
+ docNumber: "1353",
1914
+ recipient: { account: "40702 810 2 8302 7306400", name: "ООО Ромашка" },
1915
+ purpose: "Платёжное поручение ООО Ромашка за оказание услуг по акту №67834259-ТНГК2356-345",
1916
+ tax: "В том числе НДС 20%",
1917
+ sum: "37884.00",
1918
+ status: "Подписан",
1919
+ },
1920
+ {
1921
+ docNumber: "1354",
1922
+ recipient: { account: "40702 810 2 9947 8250050", name: "ИП Иванов Иван Иванович" },
1923
+ purpose: "Платёжное поручение ИП Иванов Иван Иванович за оказание услуг по акту №67834259-ТНГК2356-345",
1924
+ tax: "В том числе НДС 20%",
1925
+ sum: "47390.00",
1926
+ status: "Оплачен",
1927
+ },
1928
+ {
1929
+ docNumber: "1355",
1930
+ recipient: { account: "40702 810 2 0290 9100406", name: "ООО Ромашка" },
1931
+ purpose: "Платёжное поручение ООО Ромашка за оказание услуг по акту №67834259-ТНГК2356-345",
1932
+ tax: "В том числе НДС 20%",
1933
+ sum: "56910.00",
1934
+ status: "Оплачен",
1935
+ },
1936
+ ];
1937
+
1938
+ // Колонки собираются вне рендера, чтобы таблица не обновляла контекст MasterTable на каждый рендер.
1939
+ const columns: ITableBasicColumn[] = [
1940
+ { fieldKey: ETableField.Number, label: "Номер", title: "Номер", width: 100 },
1941
+ { fieldKey: ETableField.Recipient, label: "Получатель", title: "Получатель", width: 320 },
1942
+ {
1943
+ fieldKey: ETableField.Sum,
1944
+ label: "Сумма",
1945
+ title: "Сумма",
1946
+ horizontalAlign: EHorizontalAlign.RIGHT,
1947
+ renderCell: (fieldValue) => fieldValue && <Amount value={fieldValue} currency="RUB" />,
1948
+ },
1949
+ {
1950
+ fieldKey: ETableField.Status,
1951
+ label: "Статус",
1952
+ title: "Статус",
1953
+ width: 140,
1954
+ renderCell: (status: string) => (
1955
+ <MarkerStatus size={EComponentSize.LG} status={statusToMarker[status] ?? EMarkerStatus.WAITING}>
1956
+ {status}
1957
+ </MarkerStatus>
1958
+ ),
1959
+ },
1960
+ ];
1961
+
1962
+ const defaultExtraFilters: IExampleExtraFilters = { recipientOption: undefined, docNumber: "" };
1963
+
1964
+ export const ExampleProduction = () => {
1965
+ const [activeSegment, setActiveSegment] = useState("all");
1966
+ const [selectedStatusIds, setSelectedStatusIds] = useState<string[]>([]);
1967
+ const [statusFilter, setStatusFilter] = useState("");
1968
+ const [recipientQuery, setRecipientQuery] = useState("");
1969
+ const [showExtraFilters, setShowExtraFilters] = useState(false);
1970
+ const [appliedExtraFilters, setAppliedExtraFilters] = useState<IExampleExtraFilters>(defaultExtraFilters);
1971
+ const [tempExtraFilters, setTempExtraFilters] = useState<IExampleExtraFilters>(defaultExtraFilters);
1972
+
1973
+ const selectedStatusLabels = statusFilterOptions
1974
+ .filter((option) => selectedStatusIds.includes(option.id))
1975
+ .map((option) => option.label);
1976
+
1977
+ const segmentStatus = segments.find((segment) => segment.id === activeSegment)?.status ?? null;
1978
+
1979
+ const isExtraFilterApplied =
1980
+ Boolean(appliedExtraFilters.recipientOption?.value) || Boolean(appliedExtraFilters.docNumber);
1981
+
1982
+ const isAnyFilterApplied = selectedStatusLabels.length > 0 || Boolean(recipientQuery) || isExtraFilterApplied;
1983
+
1984
+ const filteredData = documents.filter((doc) => {
1985
+ if (segmentStatus && doc.status !== segmentStatus) {
1986
+ return false;
1987
+ }
1988
+ if (selectedStatusLabels.length > 0 && !selectedStatusLabels.includes(doc.status)) {
1989
+ return false;
1990
+ }
1991
+ if (recipientQuery && !doc.recipient.name.toLowerCase().includes(recipientQuery.toLowerCase())) {
1992
+ return false;
1993
+ }
1994
+ if (
1995
+ appliedExtraFilters.recipientOption?.value &&
1996
+ doc.recipient.name !== appliedExtraFilters.recipientOption.value
1997
+ ) {
1998
+ return false;
1999
+ }
2000
+ if (appliedExtraFilters.docNumber && !doc.docNumber.includes(appliedExtraFilters.docNumber)) {
2001
+ return false;
2002
+ }
2003
+ return true;
2004
+ });
2005
+
2006
+ const data: ITableBasicRow[] = filteredData.map((doc) => ({
2007
+ rowKey: doc.docNumber,
2008
+ rowData: {
2009
+ [ETableField.Number]: doc.docNumber,
2010
+ [ETableField.Recipient]: renderCounterpartyDetails(doc.purpose, doc.recipient.account, doc.tax),
2011
+ [ETableField.Sum]: doc.sum,
2012
+ [ETableField.Status]: doc.status,
2013
+ },
2014
+ }));
2015
+
2016
+ const visibleStatusOptions = statusFilterOptions.filter((option) =>
2017
+ option.label.toLowerCase().includes(statusFilter.trim().toLowerCase()),
2018
+ );
2019
+
2020
+ const toggleStatus = (optionId: string, checked: boolean) => {
2021
+ setSelectedStatusIds((prev) => (checked ? [...prev, optionId] : prev.filter((id) => id !== optionId)));
2022
+ };
2023
+
2024
+ const unselectAllStatuses = () => {
2025
+ setSelectedStatusIds([]);
2026
+ setStatusFilter("");
2027
+ };
2028
+
2029
+ const handleStatusFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
2030
+ setStatusFilter(event.target.value);
2031
+ };
2032
+
2033
+ const handleClickToggleExtraFilters = () => {
2034
+ setTempExtraFilters(appliedExtraFilters);
2035
+ setShowExtraFilters((prev) => !prev);
2036
+ };
2037
+
2038
+ const handleClickResetTempFilters = () => {
2039
+ setTempExtraFilters(defaultExtraFilters);
2040
+ };
2041
+
2042
+ const handleClickApplyTempFilters = () => {
2043
+ setAppliedExtraFilters(tempExtraFilters);
2044
+ setShowExtraFilters(false);
2045
+ };
2046
+
2047
+ const handleRemoveTag = (newFilters: IExampleExtraFilters) => () => {
2048
+ setTempExtraFilters(newFilters);
2049
+ setAppliedExtraFilters(newFilters);
2050
+ };
2051
+
2052
+ const handleClickResetAllFilters = () => {
2053
+ unselectAllStatuses();
2054
+ setRecipientQuery("");
2055
+ setTempExtraFilters(defaultExtraFilters);
2056
+ setAppliedExtraFilters(defaultExtraFilters);
2057
+ };
2058
+
2059
+ const renderStatusCheckboxList = () => (
2060
+ <CheckboxYGroup aria-label="Статус">
2061
+ {visibleStatusOptions.map((option) => (
2062
+ <Checkbox
2063
+ key={option.id}
2064
+ id={option.id}
2065
+ size={EComponentSize.MD}
2066
+ checked={selectedStatusIds.includes(option.id)}
2067
+ onChange={(event) => toggleStatus(option.id, event.target.checked)}
2068
+ >
2069
+ {option.label}
2070
+ </Checkbox>
2071
+ ))}
2072
+ </CheckboxYGroup>
2073
+ );
2074
+
2075
+ const renderStatusDropdownContent = () => {
2076
+ const hasResults = !statusFilter.trim() || visibleStatusOptions.length > 0;
2077
+
2078
+ return hasResults ? (
2079
+ renderStatusCheckboxList()
2080
+ ) : (
2081
+ <div style={{ padding: "8px 16px" }}>
2082
+ <Text size={ETextSize.B3}>Ничего не найдено</Text>
2083
+ </div>
2084
+ );
2085
+ };
2086
+
2087
+ const renderStatusMultiselect = () => (
2088
+ <ChipMultiselect
2089
+ size={EComponentSize.MD}
2090
+ clearSelected={unselectAllStatuses}
2091
+ selected={selectedStatusIds.length > 0}
2092
+ label="Статус"
2093
+ displayedValue={`Статус: ${selectedStatusLabels.join(", ")}`}
2094
+ >
2095
+ {({ opened, setOpened, targetRef, dropdownRef }) => (
2096
+ <MultiselectField.Dropdown
2097
+ opened={opened}
2098
+ setOpened={setOpened}
2099
+ targetRef={targetRef}
2100
+ ref={dropdownRef}
2101
+ alignment={EDropdownAlignment.LEFT}
2102
+ focusTrapProps={{ focusTrapOptions: { initialFocus: false } }}
2103
+ mobileViewProps={{
2104
+ children: (
2105
+ <>
2106
+ <DropdownMobileHeader
2107
+ controlButtons={<DropdownMobileClose onClick={() => setOpened(false)} />}
2108
+ >
2109
+ <DropdownMobileInput
2110
+ autoFocus
2111
+ placeholder="Поиск по статусу"
2112
+ value={statusFilter}
2113
+ onChange={handleStatusFilterChange}
2114
+ />
2115
+ </DropdownMobileHeader>
2116
+ <DropdownMobileBody>{renderStatusDropdownContent()}</DropdownMobileBody>
2117
+ <DropdownMobileFooter>
2118
+ <Button
2119
+ theme={EButtonTheme.GENERAL}
2120
+ size={EComponentSize.SM}
2121
+ onClick={() => setOpened(false)}
2122
+ >
2123
+ Применить
2124
+ </Button>
2125
+ <Button
2126
+ theme={EButtonTheme.LINK}
2127
+ size={EComponentSize.SM}
2128
+ onClick={unselectAllStatuses}
2129
+ >
2130
+ Сбросить
2131
+ </Button>
2132
+ </DropdownMobileFooter>
2133
+ </>
2134
+ ),
2135
+ }}
2136
+ >
2137
+ <MultiselectField.Dropdown.Header>
2138
+ <FormField size={EComponentSize.SM}>
2139
+ <FormFieldLabel>Поиск по статусу</FormFieldLabel>
2140
+ <FormFieldInput value={statusFilter} onChange={handleStatusFilterChange} />
2141
+ <FormFieldPostfix>
2142
+ <FormFieldClear onClick={() => setStatusFilter("")} />
2143
+ </FormFieldPostfix>
2144
+ </FormField>
2145
+ </MultiselectField.Dropdown.Header>
2146
+ <MultiselectField.Dropdown.Content>
2147
+ {renderStatusDropdownContent()}
2148
+ </MultiselectField.Dropdown.Content>
2149
+ <MultiselectField.Dropdown.Footer>
2150
+ <Button
2151
+ theme={EButtonTheme.SECONDARY}
2152
+ size={EComponentSize.SM}
2153
+ onClick={() => setOpened(false)}
2154
+ >
2155
+ Применить
2156
+ </Button>
2157
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.SM} onClick={unselectAllStatuses}>
2158
+ Сбросить
2159
+ </Button>
2160
+ </MultiselectField.Dropdown.Footer>
2161
+ </MultiselectField.Dropdown>
2162
+ )}
2163
+ </ChipMultiselect>
2164
+ );
2165
+
2166
+ const renderExtraFilters = () => (
2167
+ <>
2168
+ <Gap size={12} />
2169
+ <Row paddingBottom={false}>
2170
+ <Col size={6}>
2171
+ <SelectField
2172
+ size={EComponentSize.MD}
2173
+ value={tempExtraFilters.recipientOption}
2174
+ options={counterpartyOptions}
2175
+ onChange={(option) =>
2176
+ setTempExtraFilters((prev) => ({
2177
+ ...prev,
2178
+ recipientOption: option.value ? option : undefined,
2179
+ }))
2180
+ }
2181
+ placeholder="Выберите получателя из списка"
2182
+ targetProps={{ fieldLabel: "Получатель" }}
2183
+ />
2184
+ </Col>
2185
+ <Col size={6}>
2186
+ <TextField
2187
+ size={EComponentSize.MD}
2188
+ inputProps={{
2189
+ value: tempExtraFilters.docNumber,
2190
+ onChange: (event) =>
2191
+ setTempExtraFilters((prev) => ({ ...prev, docNumber: event.target.value })),
2192
+ placeholder: "Введите номер документа",
2193
+ }}
2194
+ label="Номер документа"
2195
+ />
2196
+ </Col>
2197
+ </Row>
2198
+ <Gap size={12} />
2199
+ <div style={{ display: "flex", justifyContent: "end" }}>
2200
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleClickResetTempFilters}>
2201
+ Сбросить
2202
+ </Button>
2203
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={handleClickApplyTempFilters}>
2204
+ Применить
2205
+ </Button>
2206
+ </div>
2207
+ </>
2208
+ );
2209
+
2210
+ const renderExtraFilterTags = () => {
2211
+ const tags: React.JSX.Element[] = [];
2212
+
2213
+ if (appliedExtraFilters.recipientOption) {
2214
+ tags.push(
2215
+ <Tag
2216
+ key="tag-filter-recipient"
2217
+ id="tag-filter-recipient"
2218
+ size={EComponentSize.MD}
2219
+ onRemove={handleRemoveTag({ ...appliedExtraFilters, recipientOption: undefined })}
2220
+ >
2221
+ {`Получатель: ${appliedExtraFilters.recipientOption.value}`}
2222
+ </Tag>,
2223
+ );
2224
+ }
2225
+
2226
+ if (appliedExtraFilters.docNumber) {
2227
+ tags.push(
2228
+ <Tag
2229
+ key="tag-filter-doc-number"
2230
+ id="tag-filter-doc-number"
2231
+ size={EComponentSize.MD}
2232
+ onRemove={handleRemoveTag({ ...appliedExtraFilters, docNumber: "" })}
2233
+ >
2234
+ {`Номер документа: ${appliedExtraFilters.docNumber}`}
2235
+ </Tag>,
2236
+ );
2237
+ }
2238
+
2239
+ return (
2240
+ <>
2241
+ <Gap size={12} />
2242
+ <TagGroup size={EComponentSize.MD}>{tags}</TagGroup>
2243
+ </>
2244
+ );
2245
+ };
2246
+
2247
+ const renderAdditionalFilterArea = () => {
2248
+ if (showExtraFilters) {
2249
+ return renderExtraFilters();
2250
+ }
2251
+
2252
+ return isExtraFilterApplied ? renderExtraFilterTags() : null;
2253
+ };
2254
+
2255
+ return (
2256
+ <MasterTable>
2257
+ <MasterTable.ChipPanel>
2258
+ <ChipGroup size={EComponentSize.SM} oneLine>
2259
+ {segments.map((segment) => (
2260
+ <Chip
2261
+ key={segment.id}
2262
+ size={EComponentSize.SM}
2263
+ selected={activeSegment === segment.id}
2264
+ showNotificationIcon={segment.withNotification}
2265
+ onClick={() => setActiveSegment(segment.id)}
2266
+ >
2267
+ {segment.label}
2268
+ </Chip>
2269
+ ))}
2270
+ </ChipGroup>
2271
+ <MasterTable.ChipPanel.Links>
2272
+ {isExtraFilterApplied && <Link onClick={handleClickResetAllFilters}>Сбросить всё</Link>}
2273
+ <Link onClick={handleClickToggleExtraFilters}>
2274
+ {showExtraFilters ? "Скрыть фильтры" : isExtraFilterApplied ? "Изменить фильтры" : "Фильтры"}
2275
+ </Link>
2276
+ </MasterTable.ChipPanel.Links>
2277
+ </MasterTable.ChipPanel>
2278
+ <MasterTable.FilterPanel>
2279
+ <div style={{ display: "flex", alignItems: "flex-start", gap: 12, flexWrap: "wrap" }}>
2280
+ {renderStatusMultiselect()}
2281
+ <TextField
2282
+ size={EComponentSize.MD}
2283
+ inputProps={{
2284
+ value: recipientQuery,
2285
+ onChange: (event) => setRecipientQuery(event.target.value),
2286
+ placeholder: "Введите получателя",
2287
+ }}
2288
+ label="Получатель"
2289
+ />
2290
+ </div>
2291
+ {renderAdditionalFilterArea()}
2292
+ </MasterTable.FilterPanel>
2293
+ <MasterTable.TableBasic
2294
+ columns={columns}
2295
+ data={data}
2296
+ renderNoData={() => renderNoData(isAnyFilterApplied)}
2297
+ />
2298
+ </MasterTable>
2299
+ );
2300
+ };
2301
+ ```
2302
+