@evergis/react 4.0.129 → 4.0.130

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 (25) hide show
  1. package/dist/components/Dashboard/componentTypes.d.ts +1 -1
  2. package/dist/components/Dashboard/components/AddButton/styled.d.ts +15 -0
  3. package/dist/components/Dashboard/components/index.d.ts +1 -0
  4. package/dist/components/Dashboard/containers/AttachmentContainer/styled.d.ts +0 -1
  5. package/dist/components/Dashboard/containers/StructuredDataContainer/constants.d.ts +2 -0
  6. package/dist/components/Dashboard/containers/StructuredDataContainer/hooks/useStructuredData.d.ts +4 -0
  7. package/dist/components/Dashboard/containers/StructuredDataContainer/styled.d.ts +3 -0
  8. package/dist/components/Dashboard/containers/StructuredDataContainer/utils/filterValue.d.ts +3 -2
  9. package/dist/components/Dashboard/containers/StructuredDataContainer/utils/viewScroll.d.ts +17 -0
  10. package/dist/components/Dashboard/elements/ElementTable/components/TableCell.d.ts +1 -1
  11. package/dist/components/Dashboard/elements/ElementTable/constants.d.ts +5 -0
  12. package/dist/components/Dashboard/elements/ElementTable/hooks/useCellEditing.d.ts +25 -9
  13. package/dist/components/Dashboard/elements/ElementTable/hooks/useHeadOverlap.d.ts +15 -0
  14. package/dist/components/Dashboard/elements/ElementTable/hooks/useSurfaceColor.d.ts +16 -0
  15. package/dist/components/Dashboard/elements/ElementTable/hooks/useTableView.d.ts +10 -1
  16. package/dist/components/Dashboard/elements/ElementTable/styled.d.ts +51 -4
  17. package/dist/components/Dashboard/elements/ElementTable/types.d.ts +5 -0
  18. package/dist/components/Dashboard/elements/ElementTable/utils/surfaceColor.d.ts +13 -0
  19. package/dist/components/Dashboard/elements/ElementTable/utils/tableBox.d.ts +20 -0
  20. package/dist/components/Dashboard/types.d.ts +0 -6
  21. package/dist/index.js +349 -113
  22. package/dist/index.js.map +1 -1
  23. package/dist/react.esm.js +350 -115
  24. package/dist/react.esm.js.map +1 -1
  25. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var jsxRuntime = require('react/jsx-runtime');
3
+ var styled = require('styled-components');
4
4
  var uilibGl = require('@evergis/uilib-gl');
5
+ var jsxRuntime = require('react/jsx-runtime');
5
6
  var React = require('react');
6
- var styled = require('styled-components');
7
7
  var charts = require('@evergis/charts');
8
8
  var api = require('@evergis/api');
9
9
  var lodash = require('lodash');
@@ -30,6 +30,45 @@ var xterm = require('@xterm/xterm');
30
30
  var addonFit = require('@xterm/addon-fit');
31
31
  require('@xterm/xterm/css/xterm.css');
32
32
 
33
+ /**
34
+ * Кнопка «добавить»: серая скруглённая, со значком и подписью. Общая на весь дашборд — вложения
35
+ * и таблица структурированных данных добавляют одинаковой кнопкой, разница только в значке.
36
+ *
37
+ * Размеры сняты с макета (все — в половину его пикселей, макет снят на удвоенной плотности):
38
+ * высота 24, поля 10, просвет между значком и подписью 6, значок 14, подпись 14.
39
+ *
40
+ * Скругление и ширина по содержимому — от самого `IconButton`: он скруглён по умолчанию, а с
41
+ * подписью в детях перестаёт держать фиксированную ширину. Значок ужат до 0.875rem: свой 1rem
42
+ * он заполняет краями, и на макете кружок мельче подписи, а не вровень с ней.
43
+ *
44
+ * Подпись темнее значка: `IconButton` красит и то и другое цветом иконки, но в макете подпись —
45
+ * обычный текст (`textPrimary`), а приглушён только значок.
46
+ */
47
+ const AddButtonRow = styled(uilibGl.IconButton).withConfig({ displayName: "AddButtonRow", componentId: "sc-3ytj1o" }) `
48
+ padding: 0 0.625rem;
49
+ height: 1.5rem;
50
+ background-color: ${({ theme }) => theme.palette.elementDark};
51
+ font-size: 0.875rem;
52
+
53
+ &:hover {
54
+ background-color: ${({ theme }) => theme.palette.elementDeep};
55
+ }
56
+
57
+ ${uilibGl.Icon} {
58
+ width: 0.875rem;
59
+ height: 0.875rem;
60
+
61
+ &:after {
62
+ font-size: 0.875rem;
63
+ }
64
+ }
65
+
66
+ ${uilibGl.IconButtonInnerChild} {
67
+ margin-left: 0.375rem;
68
+ color: ${({ theme }) => theme.palette.textPrimary};
69
+ }
70
+ `;
71
+
33
72
  const AddFeatureButton = ({ title, icon = "feature_add" /* , layerName, geometryType*/ }) => {
34
73
  // const [, handleAddFeature] = useFeatureCreator(layerName, geometryType);
35
74
  const handleAddFeature = () => { };
@@ -8735,19 +8774,10 @@ const ShowMoreButton$1 = styled.div.withConfig({ displayName: "ShowMoreButton",
8735
8774
  const AddButtonContainer = styled.div.withConfig({ displayName: "AddButtonContainer", componentId: "sc-1yphz1r" }) `
8736
8775
  margin-top: 0.75rem;
8737
8776
  `;
8738
- const AddButtonRow = styled(uilibGl.IconButton).withConfig({ displayName: "AddButtonRow", componentId: "sc-b7dwfu" }) `
8739
- padding: 0 0.75rem;
8740
- height: 1.5rem;
8741
- background-color: ${({ theme }) => theme.palette.elementDark};
8742
-
8743
- &:hover {
8744
- background-color: ${({ theme }) => theme.palette.elementDeep};
8745
- }
8746
- `;
8747
- const HiddenFileInput = styled.input.withConfig({ displayName: "HiddenFileInput", componentId: "sc-kgi0m7" }) `
8777
+ const HiddenFileInput = styled.input.withConfig({ displayName: "HiddenFileInput", componentId: "sc-3apzla" }) `
8748
8778
  display: none;
8749
8779
  `;
8750
- const LinkDialogContent = styled.div.withConfig({ displayName: "LinkDialogContent", componentId: "sc-vrozu8" }) `
8780
+ const LinkDialogContent = styled.div.withConfig({ displayName: "LinkDialogContent", componentId: "sc-1kihzpg" }) `
8751
8781
  display: flex;
8752
8782
  flex-direction: column;
8753
8783
  width: 100%;
@@ -10938,6 +10968,9 @@ const StructuredDataActions = styled(uilibGl.Flex).withConfig({ displayName: "St
10938
10968
  /**
10939
10969
  * Область представления. Прокручивается внутри себя, чтобы длинная таблица не растягивала
10940
10970
  * контейнер за пределы отведённой ему ячейки; `min-height: 0` разрешает flex-ребёнку сжиматься.
10971
+ *
10972
+ * Собственный бокс таблицы (`options.width`/`height` представления) живёт внутри и на эту
10973
+ * прокрутку не влияет: он ограничивает саму таблицу, а не отведённое контейнером место.
10941
10974
  */
10942
10975
  const StructuredDataView = styled(uilibGl.Flex).withConfig({ displayName: "StructuredDataView", componentId: "sc-1ws3t0d" }) `
10943
10976
  flex-direction: column;
@@ -10954,6 +10987,33 @@ const StructuredDataView = styled(uilibGl.Flex).withConfig({ displayName: "Struc
10954
10987
  */
10955
10988
  const StructuredDataContext = React.createContext(null);
10956
10989
 
10990
+ /** Слот представления: единственный ребёнок контейнера, рисующий данные (элемент `table`). */
10991
+ const STRUCTURED_DATA_VIEW_SLOT = "data";
10992
+ /**
10993
+ * Типы атрибутов с редактором ячейки. Атрибут любого другого типа показывается только на чтение,
10994
+ * даже если помечен `isEditable: true` — редактора для него нет.
10995
+ */
10996
+ const EDITABLE_ATTRIBUTE_TYPES = [
10997
+ api.AttributeType.String,
10998
+ api.AttributeType.Int32,
10999
+ api.AttributeType.Int64,
11000
+ api.AttributeType.Double,
11001
+ api.AttributeType.Boolean,
11002
+ api.AttributeType.DateTime,
11003
+ ];
11004
+ /** Типы, для которых ячейка редактируется числовым инпутом. */
11005
+ const NUMBER_EDITOR_TYPES = [
11006
+ api.AttributeType.Int32,
11007
+ api.AttributeType.Int64,
11008
+ api.AttributeType.Double,
11009
+ ];
11010
+ /** Типы, значение которых хранится целым числом — дробную часть в них не пускаем. */
11011
+ const INTEGER_ATTRIBUTE_TYPES = [api.AttributeType.Int32, api.AttributeType.Int64];
11012
+ /** Тип атрибута, когда его не задали ни конфиг, ни источник. */
11013
+ const DEFAULT_ATTRIBUTE_TYPE = api.AttributeType.String;
11014
+ /** Сколько миллисекунд висит нотификация об ошибке записи. */
11015
+ const SAVE_ERROR_DURATION = 5000;
11016
+
10957
11017
  /**
10958
11018
  * Оставляет от произвольного набора свойств только атрибуты схемы. Отсутствующие добавляет
10959
11019
  * как `null`: строка всегда имеет одинаковый набор ключей, иначе представление и
@@ -10991,8 +11051,9 @@ const getEditableProperties = (row, schema) => schema
10991
11051
  /**
10992
11052
  * Строки черновика → значение фильтра `valueType: "features"`.
10993
11053
  *
10994
- * В `properties` попадают ВСЕ атрибуты схемы, а не только показанные колонками: схема и вид
10995
- * разделены, и вид не должен резать данные. Геометрия не поддерживается `geometry: null`.
11054
+ * В `properties` попадает ровно схема: значения по её атрибутам, включая незаполненные. Лишние
11055
+ * ключи черновика (пришли из источника, остались от прежней схемы) наружу не уходят, а объявленный
11056
+ * атрибут уходит всегда. Геометрия не поддерживается — `geometry: null`.
10996
11057
  */
10997
11058
  const toFeaturesFilterValue = (rows, schema) => ({
10998
11059
  type: "FeatureCollection",
@@ -11041,14 +11102,17 @@ const useStructuredDataDraft = ({ schema, dataSource, hasDataSource, filterValue
11041
11102
  }, [applyBaseline, features, hasDataSource, schema]);
11042
11103
  // Ручная структура гидратируется из фильтра один раз: дальше значение фильтра пишем мы сами,
11043
11104
  // и повторная гидратация только пересоздавала бы ключи строк.
11105
+ //
11106
+ // Флаг взводится по факту гидратации, а не авансом: значение может приехать позже схемы
11107
+ // (восстановленный выбор пользователя — асинхронно, в отличие от `defaultValue` из конфига),
11108
+ // и окно не должно сгорать на пустом первом рендере. Грязный черновик значение не затирает.
11044
11109
  React.useEffect(() => {
11045
- if (hasDataSource || hydrated.current || !schema.length) {
11110
+ const canHydrate = !hasDataSource && !hydrated.current && !dirtyRef.current && !!schema.length;
11111
+ if (!canHydrate || !isFeaturesFilterValue(filterValue)) {
11046
11112
  return;
11047
11113
  }
11048
11114
  hydrated.current = true;
11049
- if (isFeaturesFilterValue(filterValue)) {
11050
- applyBaseline(fromFeaturesFilterValue(filterValue, schema));
11051
- }
11115
+ applyBaseline(fromFeaturesFilterValue(filterValue, schema));
11052
11116
  }, [applyBaseline, filterValue, hasDataSource, schema]);
11053
11117
  const changeCell = React.useCallback((key, attributeName, value) => {
11054
11118
  setRows(current => current.map(row => row.key === key
@@ -11071,36 +11135,16 @@ const useStructuredDataDraft = ({ schema, dataSource, hasDataSource, filterValue
11071
11135
  }, []));
11072
11136
  }, []);
11073
11137
  const reset = React.useCallback(() => setRows(baseline.current), []);
11074
- const commitSaved = React.useCallback((next) => applyBaseline(next), [applyBaseline]);
11138
+ const commitSaved = React.useCallback((next) => {
11139
+ // Сохранённое состояние — наше: значение фильтра теперь пишем мы, и вернувшееся из него
11140
+ // обновление не должно пересоздавать ключи уже показанных строк.
11141
+ hydrated.current = true;
11142
+ applyBaseline(next);
11143
+ }, [applyBaseline]);
11075
11144
  const visibleRows = React.useMemo(() => rows.filter(({ state }) => state !== "deleted"), [rows]);
11076
11145
  return { rows, visibleRows, dirty, changeCell, addRow, deleteRow, reset, commitSaved };
11077
11146
  };
11078
11147
 
11079
- /**
11080
- * Типы атрибутов с редактором ячейки. Атрибут любого другого типа показывается только на чтение,
11081
- * даже если помечен `isEditable: true` — редактора для него нет.
11082
- */
11083
- const EDITABLE_ATTRIBUTE_TYPES = [
11084
- api.AttributeType.String,
11085
- api.AttributeType.Int32,
11086
- api.AttributeType.Int64,
11087
- api.AttributeType.Double,
11088
- api.AttributeType.Boolean,
11089
- api.AttributeType.DateTime,
11090
- ];
11091
- /** Типы, для которых ячейка редактируется числовым инпутом. */
11092
- const NUMBER_EDITOR_TYPES = [
11093
- api.AttributeType.Int32,
11094
- api.AttributeType.Int64,
11095
- api.AttributeType.Double,
11096
- ];
11097
- /** Типы, значение которых хранится целым числом — дробную часть в них не пускаем. */
11098
- const INTEGER_ATTRIBUTE_TYPES = [api.AttributeType.Int32, api.AttributeType.Int64];
11099
- /** Тип атрибута, когда его не задали ни конфиг, ни источник. */
11100
- const DEFAULT_ATTRIBUTE_TYPE = api.AttributeType.String;
11101
- /** Сколько миллисекунд висит нотификация об ошибке записи. */
11102
- const SAVE_ERROR_DURATION = 5000;
11103
-
11104
11148
  /**
11105
11149
  * Применяет черновик к слою источника: удаление → обновление → создание.
11106
11150
  *
@@ -11267,13 +11311,19 @@ const useStructuredDataSchema = (type, elementConfig) => {
11267
11311
  * Вид источника на права правки не влияет — от него зависит только адресат «Сохранить». Слой
11268
11312
  * принимает запись через features-API, источник без `layerName` (EQL, python-скрипт, внешний
11269
11313
  * url) не принимает ничего, и правка такой таблицы уходит только в фильтр.
11314
+ *
11315
+ * Значение фильтра читается так же, как его читают остальные потребители дашборда: выбор
11316
+ * пользователя, а пока его нет — `defaultValue` из конфига фильтра. Общего стейта, залитого
11317
+ * дефолтами, в дашборде нет, поэтому фолбэк делает каждый потребитель сам.
11270
11318
  */
11271
11319
  const useStructuredData = (type, elementConfig) => {
11272
11320
  const { filters } = useWidgetContext(type);
11321
+ const { currentPage } = useWidgetPage(type);
11273
11322
  const { relatedDataSource, filterName, editMode } = elementConfig?.options || {};
11274
11323
  const { schema, dataSource, layerName } = useStructuredDataSchema(type, elementConfig);
11275
11324
  const hasDataSource = !!relatedDataSource;
11276
- const filterValue = filterName ? filters?.[filterName]?.value : undefined;
11325
+ const configFilter = React.useMemo(() => getConfigFilter(filterName, currentPage?.filters), [currentPage?.filters, filterName]);
11326
+ const filterValue = React.useMemo(() => (filterName ? filters?.[filterName]?.value ?? configFilter?.defaultValue : undefined), [configFilter?.defaultValue, filterName, filters]);
11277
11327
  const { rows, visibleRows, dirty, changeCell, addRow, deleteRow, reset, commitSaved } = useStructuredDataDraft({
11278
11328
  schema,
11279
11329
  dataSource,
@@ -11346,7 +11396,7 @@ const StructuredDataContainer = React.memo(({ type, elementConfig, isVisible, re
11346
11396
  if (hasError) {
11347
11397
  return jsxRuntime.jsx(DataSourceError, { name: elementConfig?.templateName });
11348
11398
  }
11349
- return (jsxRuntime.jsxs(ContainerRoot, { ...root, children: [jsxRuntime.jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxRuntime.jsxs(Container, { ...body, isColumn: true, children: [jsxRuntime.jsx(StructuredDataView, { children: jsxRuntime.jsx(StructuredDataContext.Provider, { value: contextValue, children: renderElement({ id: "data" }) }) }), canEdit && (jsxRuntime.jsxs(StructuredDataToolbar, { children: [jsxRuntime.jsx(StructuredDataActions, { children: jsxRuntime.jsx(uilibGl.IconButton, { kind: "plus", primary: true, disabled: saving, onClick: onAddRow, children: t("structuredData.addRow", { ns: "dashboard", defaultValue: "Добавить строку" }) }) }), dirty && (jsxRuntime.jsxs(StructuredDataActions, { children: [jsxRuntime.jsx(uilibGl.FlatButton, { disabled: saving, onClick: onCancel, children: t("actions.cancel", { ns: "common", defaultValue: "Отменить" }) }), jsxRuntime.jsx(uilibGl.RaisedButton, { primary: true, disabled: saving, onClick: onSave, children: t("actions.save", { ns: "common", defaultValue: "Сохранить" }) })] }))] }))] })), jsxRuntime.jsx(DeleteRowDialog, { isOpen: isDeleteConfirmOpen, isSourceRow: isDeletingSourceRow, onConfirm: onDeleteConfirm, onCancel: onDeleteCancel })] }));
11399
+ return (jsxRuntime.jsxs(ContainerRoot, { ...root, children: [jsxRuntime.jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxRuntime.jsxs(Container, { ...body, isColumn: true, children: [jsxRuntime.jsx(StructuredDataView, { children: jsxRuntime.jsx(StructuredDataContext.Provider, { value: contextValue, children: renderElement({ id: STRUCTURED_DATA_VIEW_SLOT }) }) }), canEdit && (jsxRuntime.jsxs(StructuredDataToolbar, { children: [jsxRuntime.jsx(StructuredDataActions, { children: jsxRuntime.jsx(AddButtonRow, { kind: "feature_add", tabIndex: 0, disabled: saving, onClick: onAddRow, children: t("actions.add", { ns: "common", defaultValue: "Добавить" }) }) }), dirty && (jsxRuntime.jsxs(StructuredDataActions, { children: [jsxRuntime.jsx(uilibGl.IconButton, { error: true, kind: "close", tabIndex: 0, disabled: saving, title: t("actions.cancel", { ns: "common", defaultValue: "Отменить" }), onClick: onCancel }), jsxRuntime.jsx(uilibGl.IconButton, { primary: true, kind: "success", tabIndex: 0, disabled: saving, title: t("actions.save", { ns: "common", defaultValue: "Сохранить" }), onClick: onSave })] }))] }))] })), jsxRuntime.jsx(DeleteRowDialog, { isOpen: isDeleteConfirmOpen, isSourceRow: isDeletingSourceRow, onConfirm: onDeleteConfirm, onCancel: onDeleteCancel })] }));
11350
11400
  });
11351
11401
 
11352
11402
  const TabAnchor = styled.div.withConfig({ displayName: "TabAnchor", componentId: "sc-emqf31" }) `
@@ -14297,28 +14347,86 @@ const ElementSvg = React.memo(({ type, elementConfig, ...rest }) => {
14297
14347
  return (jsxRuntime.jsx(SvgImage, { url: getSvgUrl({ elementConfig, layerInfo, attributes }), width: width, height: height, fontColor: fontColor }));
14298
14348
  });
14299
14349
 
14300
- const TableWrapper = styled.table.withConfig({ displayName: "TableWrapper", componentId: "sc-ymycsi" }) `
14301
- width: 100%;
14302
- border-collapse: collapse;
14350
+ /**
14351
+ * Насколько тело таблицы заехало под липкую шапку. Пишется на таблицу при прокрутке, читается
14352
+ * стилем тела: ровно на эту величину тело и отсекается.
14353
+ */
14354
+ const HEAD_OVERLAP_VARIABLE = "--table-head-overlap";
14355
+ /**
14356
+ * Признак времени в шаблоне `stringFormat.format`.
14357
+ *
14358
+ * Варианты времени в конфигурации атрибута — `hh:mm`, `hh:mm tt`, `hh:mm:ss`, `hh:mm:ss tt`
14359
+ * (см. `timeOptions` в `utils/format`), поэтому достаточно найти часы с минутами.
14360
+ */
14361
+ const TIME_FORMAT = /hh:mm/;
14362
+ /**
14363
+ * Контейнер порталов `@evergis/uilib-gl` — в него попадают выпадающие слои контролов,
14364
+ * в том числе календарь `DatePicker`. Клик по ним DOM-деревом лежит вне ячейки, поэтому
14365
+ * закрытие редактора по клику снаружи обязано этот контейнер пропускать.
14366
+ */
14367
+ const PORTAL_ROOT_SELECTOR = "#portal-root";
14368
+
14369
+ /**
14370
+ * Собственный бокс таблицы. Без размеров в `options` представления узел ничего не меняет:
14371
+ * таблица растёт под содержимое, а прокручивает лишнее область представления контейнера.
14372
+ *
14373
+ * С размерами бокс становится её скролл-контейнером — и липкая шапка липнет уже к нему.
14374
+ */
14375
+ const TableBox = styled.div.withConfig({ displayName: "TableBox", componentId: "sc-19vtvuq" }) `
14376
+ min-width: 0;
14377
+
14378
+ ${sizeCssMixin};
14379
+ `;
14380
+ /**
14381
+ * Таблица. По умолчанию втискивается в ширину, которую ей дали: колонкам достаётся то, что есть,
14382
+ * а длинные значения режутся многоточием внутри ячейки.
14383
+ *
14384
+ * `$contentWidth` (ширина бокса задана в `options`) переводит её на ширину по содержимому:
14385
+ * колонки встают по своим значениям, предел ячейки снимается, и всё, что не влезло, уходит
14386
+ * в горизонтальную прокрутку бокса. Иначе вышла бы бессмыслица — бокс с прокруткой, а значения
14387
+ * всё равно с многоточием. `min-width` оставляет таблицу растянутой на весь бокс, когда
14388
+ * содержимого на его ширину не набирается.
14389
+ *
14390
+ * Снятый предел ширины ячейки едет вниз css-переменной, а не правилом по `TableCellWrapper`:
14391
+ * ячейка объявлена ниже по файлу, и селектор по ней отсюда попал бы в TDZ.
14392
+ */
14393
+ const TableWrapper = styled.table.withConfig({ displayName: "TableWrapper", componentId: "sc-ih9qqq" }) `
14394
+ width: ${({ $contentWidth }) => ($contentWidth ? "max-content" : "100%")};
14395
+ min-width: 100%;
14396
+ /*
14397
+ * Рамки раздельные (со схлопнутым интервалом, поэтому выглядят как схлопнутые). Схлопнутые
14398
+ * рисует сама таблица, а не ячейки, и отсечка тела их не режет: линия строки, уехавшей под
14399
+ * шапку, продолжала бы рисоваться поверх заголовков.
14400
+ */
14401
+ border-collapse: separate;
14402
+ border-spacing: 0;
14303
14403
  font-size: 0.875rem;
14404
+
14405
+ ${({ $contentWidth }) => !!$contentWidth &&
14406
+ styled.css `
14407
+ --table-cell-max-width: none;
14408
+ `};
14304
14409
  `;
14305
14410
  /**
14306
14411
  * Шапка липнет к верху области прокрутки — при длинной таблице заголовки остаются видны.
14307
14412
  *
14308
- * Собственного фона у шапки нет: цвет из палитры не следовал за подложкой контейнера
14309
- * и выделялся белым прямоугольником на любом фоне, отличном от базового.
14413
+ * Своего фона у неё нет и не должно быть: контейнер дашборда красится цветом из конфига, и любой
14414
+ * назначенный шапке цвет остался бы прямоугольником чужого цвета, как только фон контейнера
14415
+ * поменяли. Под шапкой виден фон контейнера, а строки под неё не заезжают — тело отсечено
14416
+ * (см. TableBody и useHeadOverlap).
14310
14417
  */
14311
- const TableHead = styled.thead.withConfig({ displayName: "TableHead", componentId: "sc-pa89d3" }) `
14418
+ const TableHead = styled.thead.withConfig({ displayName: "TableHead", componentId: "sc-m8fpi" }) `
14312
14419
  position: sticky;
14313
14420
  top: 0;
14314
14421
  z-index: 1;
14315
14422
  `;
14316
- const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", componentId: "sc-1f151xj" }) `
14423
+ const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", componentId: "sc-1m6xil2" }) `
14317
14424
  padding: 0.375rem 0.5rem;
14318
14425
  text-align: left;
14319
14426
  font-weight: 600;
14320
14427
  white-space: nowrap;
14321
14428
  color: ${({ theme }) => theme.palette.textSecondary};
14429
+ /* Рамка принадлежит ячейке (рамки раздельные), поэтому едет вместе с липкой шапкой. */
14322
14430
  border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
14323
14431
 
14324
14432
  ${({ $sortable }) => $sortable &&
@@ -14327,25 +14435,50 @@ const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", compo
14327
14435
  user-select: none;
14328
14436
  `}
14329
14437
  `;
14330
- const TableHeadContent = styled.span.withConfig({ displayName: "TableHeadContent", componentId: "sc-19t325s" }) `
14438
+ const TableHeadContent = styled.span.withConfig({ displayName: "TableHeadContent", componentId: "sc-8hzcop" }) `
14331
14439
  display: inline-flex;
14332
14440
  align-items: center;
14333
14441
  gap: 0.25rem;
14334
14442
  `;
14335
- const TableRow = styled.tr.withConfig({ displayName: "TableRow", componentId: "sc-ios7ko" }) `
14336
- border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
14443
+ /**
14444
+ * Место под значок сортировки. Занято всегда, пока сортировка включена, а сам значок только
14445
+ * прячется: появляясь и исчезая, он менял бы ширину колонки — и таблицу дёргало бы на каждый
14446
+ * клик по заголовку, вместе с телом. Оба направления рисуются значком в 1rem, поэтому
14447
+ * переключение asc/des ширину тоже не трогает.
14448
+ */
14449
+ const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", componentId: "sc-z378vf" }) `
14450
+ display: inline-flex;
14451
+ visibility: ${({ $active }) => ($active ? "visible" : "hidden")};
14452
+ `;
14453
+ /**
14454
+ * Тело таблицы. Отсекается ровно на то, на сколько его накрыла липкая шапка: строки уходят под
14455
+ * её нижнюю кромку и сквозь прозрачную шапку не проступают. Величину пишет {@link useHeadOverlap},
14456
+ * без неё отсечка нулевая и разметка не меняется.
14457
+ */
14458
+ const TableBody = styled.tbody.withConfig({ displayName: "TableBody", componentId: "sc-1pxpl5v" }) `
14459
+ clip-path: inset(var(${HEAD_OVERLAP_VARIABLE}, 0px) 0 0 0);
14337
14460
  `;
14338
- const TableCellWrapper = styled.td.withConfig({ displayName: "TableCellWrapper", componentId: "sc-6nb6ny" }) `
14461
+ /**
14462
+ * Строка. Разделитель рисуют её ячейки: собственная рамка строки при раздельных рамках не
14463
+ * рисуется вовсе, а при схлопнутых её рисовала бы таблица — и отсечка тела её бы не резала.
14464
+ */
14465
+ const TableRow = styled.tr.withConfig({ displayName: "TableRow", componentId: "sc-j62zvd" }) `
14466
+ td {
14467
+ border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
14468
+ }
14469
+ `;
14470
+ const TableCellWrapper = styled.td.withConfig({ displayName: "TableCellWrapper", componentId: "sc-ziu5rk" }) `
14339
14471
  padding: 0.125rem 0.25rem;
14340
14472
  vertical-align: middle;
14341
- max-width: 20rem;
14473
+ /* Предел снимает заданная ширина: с колонками по содержимому обрезать значение незачем. */
14474
+ max-width: var(--table-cell-max-width, 20rem);
14342
14475
  `;
14343
14476
  /** Колонка действий: узкая, не растягивается содержимым. */
14344
- const TableActionsCell = styled(TableCellWrapper).withConfig({ displayName: "TableActionsCell", componentId: "sc-1txos2k" }) `
14477
+ const TableActionsCell = styled(TableCellWrapper).withConfig({ displayName: "TableActionsCell", componentId: "sc-18w13t3" }) `
14345
14478
  width: 2rem;
14346
14479
  text-align: right;
14347
14480
  `;
14348
- const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "sc-1a95gqm" }) `
14481
+ const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "sc-ok630s" }) `
14349
14482
  padding: 0.375rem 0.25rem;
14350
14483
  /* Та же высота строки, что у редактора — иначе строка подпрыгивала бы по вертикали. */
14351
14484
  line-height: 1.25rem;
@@ -14355,9 +14488,12 @@ const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "
14355
14488
  `;
14356
14489
  /**
14357
14490
  * Редактируемая ячейка вне фокуса. Показывает значение по `stringFormat`, а редактор
14358
- * подставляется по клику — иначе форматирование пришлось бы дублировать внутри инпута.
14491
+ * подставляется на фокус — иначе форматирование пришлось бы дублировать внутри инпута.
14492
+ *
14493
+ * Кнопка, а не просто текст, именно ради фокуса: она — таб-стоп ячейки, и Tab по таблице
14494
+ * открывает редактор с курсором внутри.
14359
14495
  */
14360
- const CellButton = styled.button.withConfig({ displayName: "CellButton", componentId: "sc-1rr68am" }) `
14496
+ const CellButton = styled.button.withConfig({ displayName: "CellButton", componentId: "sc-98k9cz" }) `
14361
14497
  width: 100%;
14362
14498
  padding: 0.375rem 0.25rem;
14363
14499
  line-height: 1.25rem;
@@ -14376,7 +14512,7 @@ const CellButton = styled.button.withConfig({ displayName: "CellButton", compone
14376
14512
  border-color: ${({ theme }) => theme.palette.elementDeep};
14377
14513
  }
14378
14514
  `;
14379
- const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder", componentId: "sc-1uhse2n" }) `
14515
+ const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder", componentId: "sc-7tff41" }) `
14380
14516
  color: ${({ theme }) => theme.palette.textSecondary};
14381
14517
  `;
14382
14518
  /**
@@ -14385,11 +14521,11 @@ const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder",
14385
14521
  * Копия остаётся в потоке и держит ширину колонки ровно такой, какой она была до клика, —
14386
14522
  * иначе таблица дёргалась бы на каждый вход в правку и выход из неё.
14387
14523
  */
14388
- const CellEditor = styled.div.withConfig({ displayName: "CellEditor", componentId: "sc-2pfo4f" }) `
14524
+ const CellEditor = styled.div.withConfig({ displayName: "CellEditor", componentId: "sc-qa22du" }) `
14389
14525
  position: relative;
14390
14526
  min-height: 2rem;
14391
14527
  `;
14392
- const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId: "sc-16p1jcy" }) `
14528
+ const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId: "sc-1i1yvb2" }) `
14393
14529
  display: block;
14394
14530
  padding: 0.375rem 0.25rem;
14395
14531
  /* Рамка повторяет CellButton — с ней высота ячейки в правке совпадает с высотой на чтении. */
@@ -14398,7 +14534,7 @@ const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId
14398
14534
  white-space: nowrap;
14399
14535
  visibility: hidden;
14400
14536
  `;
14401
- const CellField = styled.div.withConfig({ displayName: "CellField", componentId: "sc-1wuahyj" }) `
14537
+ const CellField = styled.div.withConfig({ displayName: "CellField", componentId: "sc-1ww3875" }) `
14402
14538
  position: absolute;
14403
14539
  inset: 0;
14404
14540
  display: flex;
@@ -14413,7 +14549,7 @@ const CellField = styled.div.withConfig({ displayName: "CellField", componentId:
14413
14549
  min-width: 0;
14414
14550
  }
14415
14551
  `;
14416
- const TableEmpty = styled.div.withConfig({ displayName: "TableEmpty", componentId: "sc-g64r9u" }) `
14552
+ const TableEmpty = styled.div.withConfig({ displayName: "TableEmpty", componentId: "sc-1vamijg" }) `
14417
14553
  padding: 0.75rem 0.25rem;
14418
14554
  color: ${({ theme }) => theme.palette.textSecondary};
14419
14555
  `;
@@ -14433,38 +14569,60 @@ const isSameCellValue = (next, current, type) => {
14433
14569
  };
14434
14570
 
14435
14571
  /**
14436
- * Признак времени в шаблоне `stringFormat.format`.
14572
+ * Правка одной ячейки.
14437
14573
  *
14438
- * Варианты времени в конфигурации атрибута `hh:mm`, `hh:mm tt`, `hh:mm:ss`, `hh:mm:ss tt`
14439
- * (см. `timeOptions` в `utils/format`), поэтому достаточно найти часы с минутами.
14440
- */
14441
- const TIME_FORMAT = /hh:mm/;
14442
- /**
14443
- * Контейнер порталов `@evergis/uilib-gl` — в него попадают выпадающие слои контролов,
14444
- * в том числе календарь `DatePicker`. Клик по ним DOM-деревом лежит вне ячейки, поэтому
14445
- * закрытие редактора по клику снаружи обязано этот контейнер пропускать.
14446
- */
14447
- const PORTAL_ROOT_SELECTOR = "#portal-root";
14448
-
14449
- /**
14450
- * Режим правки одной ячейки: открывается кликом по значению, закрывается кликом снаружи.
14574
+ * Открывает её фокус, а не клик: по таблице ходят Tab-ом, и на каждой ячейке сразу нужно поле
14575
+ * с курсором кнопка, требующая ещё и Enter, ломала бы весь проход.
14451
14576
  *
14452
- * Закрывать по `blur` нельзя: `DatePicker` уводит фокус из поля уже на клике по иконке
14453
- * календаря редактор исчезал бы раньше, чем календарь успевал открыться. Поэтому слушаем
14454
- * `mousedown` на документе, а клики внутри портала (`#portal-root`) считаем «своими»:
14455
- * выпадающие слои контролов uilib живут именно там, вне DOM-поддерева ячейки.
14577
+ * Поэтому состояний три. `focused` ячейка под фокусом с закрытым редактором, состояние после
14578
+ * Enter/Escape: фокус возвращается на кнопку, но правку заново не открывает (иначе выйти из неё
14579
+ * было бы нечем), а Tab с неё идёт дальше по таблице.
14580
+ *
14581
+ * Закрывать правку по любому уходу фокуса нельзя: DatePicker уводит его из поля уже на клике по
14582
+ * иконке календаря — редактор исчезал бы раньше, чем календарь успевал открыться. Клавиатурный
14583
+ * уход видно по `relatedTarget` (фокус ушёл на соседнюю ячейку), мышиный ловит `mousedown`
14584
+ * снаружи. Клики внутри портала (`#portal-root`) считаем «своими»: выпадающие слои контролов
14585
+ * uilib живут именно там, вне DOM-поддерева ячейки.
14456
14586
  */
14457
14587
  const useCellEditing = () => {
14458
- const [editing, setEditing] = React.useState(false);
14588
+ const [mode, setMode] = React.useState("idle");
14589
+ const buttonRef = React.useRef(null);
14459
14590
  const editorRef = React.useRef(null);
14460
- const startEditing = React.useCallback(() => setEditing(true), []);
14461
- // С клавиатуры правка закрывается там же, где и мышью: Enter подтверждает, Escape уходит
14462
- // из поля. Значение уже записано в черновик на каждый ввод, отдельного «применить» нет.
14463
- const onKeyDown = React.useCallback(({ key }) => {
14464
- if (key === "Enter" || key === "Escape") {
14465
- setEditing(false);
14591
+ const editing = mode === "editing";
14592
+ // Фокус на закрытой ячейке это уже правка. Из `focused` не открываем: фокус туда только что
14593
+ // вернули из редактора, и Escape перестал бы работать.
14594
+ const onFocus = React.useCallback(() => setMode(current => (current === "idle" ? "editing" : current)), []);
14595
+ // Переход в правку убирает саму кнопку её прощальный blur эту правку закрывать не должен.
14596
+ const onBlur = React.useCallback(() => setMode(current => (current === "editing" ? current : "idle")), []);
14597
+ // Клик открывает правку и тогда, когда фокус на ячейке уже стоит — то есть после Enter/Escape.
14598
+ const onClick = React.useCallback(() => setMode("editing"), []);
14599
+ // Enter и Escape закрывают редактор, оставляя фокус на ячейке: значение записано в черновик
14600
+ // на каждый ввод, отдельного «применить» нет.
14601
+ const onEditorKeyDown = React.useCallback((event) => {
14602
+ if (event.key !== "Enter" && event.key !== "Escape") {
14603
+ return;
14604
+ }
14605
+ // Гасим действие по умолчанию: у Enter это «нажать то, что в фокусе», а фокус браузер смотрит
14606
+ // уже после обработчиков — к тому моменту он вернулся на кнопку ячейки, и правка открылась бы
14607
+ // заново тем же нажатием.
14608
+ event.preventDefault();
14609
+ setMode("focused");
14610
+ }, []);
14611
+ const onEditorBlur = React.useCallback(({ relatedTarget }) => {
14612
+ const inside = editorRef.current?.contains(relatedTarget) || !!relatedTarget?.closest(PORTAL_ROOT_SELECTOR);
14613
+ // Фокус «в никуда» уходом не считаем: так ведут себя внутренности DatePicker.
14614
+ if (!relatedTarget || inside) {
14615
+ return;
14466
14616
  }
14617
+ setMode("idle");
14467
14618
  }, []);
14619
+ // Выход из редактора возвращает фокус ячейке: иначе он падал бы в body, и следующий Tab пошёл
14620
+ // бы с начала страницы.
14621
+ React.useEffect(() => {
14622
+ if (mode === "focused") {
14623
+ buttonRef.current?.focus();
14624
+ }
14625
+ }, [mode]);
14468
14626
  React.useEffect(() => {
14469
14627
  if (!editing) {
14470
14628
  return undefined;
@@ -14474,24 +14632,26 @@ const useCellEditing = () => {
14474
14632
  if (editorRef.current?.contains(node) || node?.closest?.(PORTAL_ROOT_SELECTOR)) {
14475
14633
  return;
14476
14634
  }
14477
- setEditing(false);
14635
+ setMode("idle");
14478
14636
  };
14479
14637
  document.addEventListener("mousedown", onDocumentMouseDown);
14480
14638
  return () => document.removeEventListener("mousedown", onDocumentMouseDown);
14481
14639
  }, [editing]);
14482
- return { editing, editorRef, startEditing, onKeyDown };
14640
+ const buttonProps = React.useMemo(() => ({ ref: buttonRef, onFocus, onBlur, onClick }), [onBlur, onClick, onFocus]);
14641
+ const editorProps = React.useMemo(() => ({ ref: editorRef, onBlur: onEditorBlur, onKeyDown: onEditorKeyDown }), [onEditorBlur, onEditorKeyDown]);
14642
+ return { editing, buttonProps, editorProps };
14483
14643
  };
14484
14644
 
14485
14645
  /**
14486
14646
  * Ячейка таблицы.
14487
14647
  *
14488
- * Вне фокуса значение всегда показано через `stringFormat` редактор подставляется по клику.
14648
+ * Вне фокуса значение всегда показано через `stringFormat`, редактор подставляется на фокус.
14489
14649
  * Иначе форматирование (округление, разряды, единицы) пришлось бы дублировать внутри инпута,
14490
14650
  * а вводить в отформатированное поле нельзя.
14491
14651
  */
14492
14652
  const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
14493
14653
  const { t, language } = useGlobalContext();
14494
- const { editing, editorRef, startEditing, onKeyDown } = useCellEditing();
14654
+ const { editing, buttonProps, editorProps } = useCellEditing();
14495
14655
  const { attributeName, type, isEditable, stringFormat } = attribute;
14496
14656
  const value = row.properties[attributeName];
14497
14657
  const handleChange = React.useCallback((next) => {
@@ -14520,22 +14680,53 @@ const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
14520
14680
  return jsxRuntime.jsx(CellText, { title: formatted, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) });
14521
14681
  }
14522
14682
  if (!editing) {
14523
- return (jsxRuntime.jsx(CellButton, { type: "button", onClick: startEditing, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) }));
14683
+ return (jsxRuntime.jsx(CellButton, { type: "button", ...buttonProps, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) }));
14524
14684
  }
14525
14685
  const renderEditor = () => {
14526
14686
  if (type === api.AttributeType.DateTime) {
14527
- return (jsxRuntime.jsx(uilibGl.DatePicker, { value: dateValue, locale: uilibGl.getLocale(language), withTime: withTime, withHeader: true, width: "100%", onChange: date => handleChange(date.toISOString()) }));
14687
+ return (jsxRuntime.jsx(uilibGl.DatePicker, { focus: true, value: dateValue, locale: uilibGl.getLocale(language), withTime: withTime, withHeader: true, width: "100%", onChange: date => handleChange(date.toISOString()) }));
14528
14688
  }
14529
14689
  if (NUMBER_EDITOR_TYPES.includes(type)) {
14530
14690
  return (jsxRuntime.jsx(uilibGl.NumberInput, { autoFocus: true, value: value === null || value === undefined ? null : Number(value), width: "100%", numeralDecimalScale: INTEGER_ATTRIBUTE_TYPES.includes(type) ? 0 : undefined, onChange: next => handleChange(next) }));
14531
14691
  }
14532
14692
  return (jsxRuntime.jsx(uilibGl.Input, { autoFocus: true, value: value === null || value === undefined ? "" : String(value), width: "100%", onChange: ({ target }) => handleChange(target.value) }));
14533
14693
  };
14534
- return (jsxRuntime.jsxs(CellEditor, { ref: editorRef, onKeyDown: onKeyDown, children: [jsxRuntime.jsx(CellGhost, { children: formatted || "—" }), jsxRuntime.jsx(CellField, { children: renderEditor() })] }));
14694
+ return (jsxRuntime.jsxs(CellEditor, { ...editorProps, children: [jsxRuntime.jsx(CellGhost, { children: formatted || "—" }), jsxRuntime.jsx(CellField, { children: renderEditor() })] }));
14535
14695
  });
14536
14696
 
14537
14697
  const ContainerLoading = () => (jsxRuntime.jsx(uilibGl.Flex, { alignContent: "center", justifyContent: "center", width: "100%", children: jsxRuntime.jsx(uilibGl.CircularProgress, { diameter: 1.5, mono: true }) }));
14538
14698
 
14699
+ /**
14700
+ * Отсечка тела таблицы по нижней кромке липкой шапки.
14701
+ *
14702
+ * Шапка прозрачная — фон контейнера виден сквозь неё, и красить её нечем: цвет контейнера задаётся
14703
+ * конфигом и меняется. Значит, строки не должны под неё заезжать в принципе. Тело для этого
14704
+ * отсекается (`clip-path`) ровно на то, на сколько шапка его накрыла: низ шапки минус верх тела.
14705
+ *
14706
+ * Разница считается по факту, а не по величине прокрутки: она одинаково верна и когда прокручивает
14707
+ * собственный бокс таблицы, и когда её прокручивает область представления контейнера. Пока
14708
+ * не прокручено, низ шапки совпадает с верхом тела — отсекать нечего.
14709
+ *
14710
+ * Прокрутка не всплывает, но проходит фазу перехвата, поэтому слушаем документ: какой именно
14711
+ * предок прокручивается, знать не нужно.
14712
+ */
14713
+ const useHeadOverlap = (table) => {
14714
+ React.useLayoutEffect(() => {
14715
+ if (!table) {
14716
+ return undefined;
14717
+ }
14718
+ const update = () => {
14719
+ const head = table.tHead?.getBoundingClientRect();
14720
+ const body = table.tBodies[0]?.getBoundingClientRect();
14721
+ const overlap = head && body ? Math.max(0, head.bottom - body.top) : 0;
14722
+ table.style.setProperty(HEAD_OVERLAP_VARIABLE, `${overlap}px`);
14723
+ };
14724
+ update();
14725
+ document.addEventListener("scroll", update, { capture: true, passive: true });
14726
+ return () => document.removeEventListener("scroll", update, { capture: true });
14727
+ }, [table]);
14728
+ };
14729
+
14539
14730
  const compareValues = (left, right, type) => {
14540
14731
  if (NUMERIC_ATTRIBUTE_TYPES.includes(type)) {
14541
14732
  return Number(left) - Number(right);
@@ -14580,28 +14771,66 @@ const getNextSort = (current, attributeName) => {
14580
14771
  };
14581
14772
 
14582
14773
  /**
14583
- * Вид таблицы: набор и порядок колонок из `columnNames` плюс локальная сортировка.
14774
+ * Css собственного бокса таблицы: размер из `options` представления плюс прокрутка того,
14775
+ * что в этот размер не влез.
14776
+ *
14777
+ * Размеров нет — нет и бокса: таблица занимает столько, сколько просит содержимое, а лишнее,
14778
+ * как и раньше, уходит в прокрутку области представления контейнера.
14779
+ *
14780
+ * Проценты работают только по ширине. Между областью представления и таблицей стоит блочная
14781
+ * обёртка элемента (`ElementValueWrapper`) с высотой `auto`, и `height: "100%"` в ней не
14782
+ * разрешается — вертикальный предел задают конкретные единицы (`240`, `"20rem"`, `"50vh"`),
14783
+ * а «занять всю выданную высоту» остаётся за `options.height` контейнера.
14784
+ *
14785
+ * `contain: inline-size` идёт в пару к fill-ширине: сама по себе она предком не ограничена
14786
+ * (процент от раскладки, которая меряет себя содержимым, вырождается в `auto`), и таблица
14787
+ * распирала бы ряд плиток или трек `auto` изнутри. С containment вклад бокса в ширину предка
14788
+ * обнуляется: ширину он берёт снаружи, а внутрь по ней не смотрят.
14789
+ */
14790
+ const getTableBoxStyle = (width, height) => {
14791
+ if (width == null && height == null) {
14792
+ return undefined;
14793
+ }
14794
+ return {
14795
+ ...getWrapperSizeStyle({ width, height, overflow: "auto" }),
14796
+ ...(isFillSize(width) && { contain: "inline-size" }),
14797
+ };
14798
+ };
14799
+
14800
+ /**
14801
+ * Вид таблицы: колонки плюс локальная сортировка.
14802
+ *
14803
+ * Набор и порядок колонок — это схема контейнера (`attributesDescription`) как есть: показываем
14804
+ * ровно то, что описано, и в том же порядке. Своего отбора у представления нет — иначе описанный
14805
+ * атрибут и видимая колонка расходились бы, а в фильтр всё равно уходят все атрибуты схемы.
14584
14806
  *
14585
14807
  * Сортировка живёт здесь, а не в контейнере: она ничего не меняет в данных и не должна
14586
14808
  * влиять на порядок строк, уходящих в фильтр.
14809
+ *
14810
+ * `width`/`height` — размер собственного бокса таблицы: он ограничивает её саму и прокручивает
14811
+ * то, что не влезло. Место, отведённое контейнеру, задаётся его же опциями и живёт отдельно.
14587
14812
  */
14588
14813
  const useTableView = (elementConfig) => {
14589
14814
  const context = React.useContext(StructuredDataContext);
14590
- const { columnNames, sort: sortEnabled } = elementConfig?.options || {};
14815
+ const { sort: sortEnabled, width, height } = elementConfig?.options || {};
14591
14816
  const [sort, setSort] = React.useState(null);
14592
14817
  const schema = context?.schema;
14593
14818
  const rows = context?.rows;
14594
- const columns = React.useMemo(() => {
14595
- if (!columnNames?.length) {
14596
- return schema ?? [];
14597
- }
14598
- return columnNames
14599
- .map(columnName => schema?.find(({ attributeName }) => attributeName === columnName))
14600
- .filter(Boolean);
14601
- }, [columnNames, schema]);
14819
+ const columns = React.useMemo(() => schema ?? [], [schema]);
14602
14820
  const sortedRows = React.useMemo(() => sortRows(rows ?? [], sort, schema ?? []), [rows, schema, sort]);
14821
+ const sizeCss = React.useMemo(() => getTableBoxStyle(width, height), [height, width]);
14603
14822
  const onSortToggle = React.useCallback((attributeName) => setSort(current => getNextSort(current, attributeName)), []);
14604
- return { context, columns, rows: sortedRows, sort, sortEnabled: !!sortEnabled, onSortToggle };
14823
+ return {
14824
+ context,
14825
+ columns,
14826
+ rows: sortedRows,
14827
+ sort,
14828
+ sortEnabled: !!sortEnabled,
14829
+ sizeCss,
14830
+ // Ширина задана — колонки по содержимому: лишнее уходит в прокрутку бокса, а не в многоточие.
14831
+ contentWidth: width != null,
14832
+ onSortToggle,
14833
+ };
14605
14834
  };
14606
14835
 
14607
14836
  /**
@@ -14612,7 +14841,10 @@ const useTableView = (elementConfig) => {
14612
14841
  */
14613
14842
  const ElementTable = React.memo(({ elementConfig }) => {
14614
14843
  const { t } = useGlobalContext();
14615
- const { context, columns, rows, sort, sortEnabled, onSortToggle } = useTableView(elementConfig);
14844
+ const { context, columns, rows, sort, sortEnabled, sizeCss, contentWidth, onSortToggle } = useTableView(elementConfig);
14845
+ // Узел таблицы держим состоянием, а не ref: отсечка должна встать сразу, как он появится.
14846
+ const [table, setTable] = React.useState(null);
14847
+ useHeadOverlap(table);
14616
14848
  if (!context) {
14617
14849
  return null;
14618
14850
  }
@@ -14625,7 +14857,10 @@ const ElementTable = React.memo(({ elementConfig }) => {
14625
14857
  defaultValue: "Схема данных не задана",
14626
14858
  }) }));
14627
14859
  }
14628
- return (jsxRuntime.jsxs(TableWrapper, { children: [jsxRuntime.jsx(TableHead, { children: jsxRuntime.jsxs("tr", { children: [columns.map(({ attributeName, alias, description }) => (jsxRuntime.jsx(TableHeadCell, { title: description || alias, "$sortable": sortEnabled, onClick: sortEnabled ? () => onSortToggle(attributeName) : undefined, children: jsxRuntime.jsxs(TableHeadContent, { children: [alias, sort?.attributeName === attributeName && (jsxRuntime.jsx(uilibGl.Icon, { kind: sort.direction === "asc" ? "sorting_asc" : "sorting_des" }))] }) }, attributeName))), context.canEdit && jsxRuntime.jsx(TableHeadCell, {})] }) }), jsxRuntime.jsx("tbody", { children: rows.map(row => (jsxRuntime.jsxs(TableRow, { children: [columns.map(attribute => (jsxRuntime.jsx(TableCellWrapper, { children: jsxRuntime.jsx(TableCell, { attribute: attribute, row: row, canEdit: context.canEdit, onChange: context.onCellChange }) }, attribute.attributeName))), context.canEdit && (jsxRuntime.jsx(TableActionsCell, { children: jsxRuntime.jsx(uilibGl.IconButton, { kind: "delete", title: t("actions.delete", { ns: "common", defaultValue: "Удалить" }), onClick: () => context.onRowDelete(row.key) }) }))] }, row.key))) })] }));
14860
+ return (jsxRuntime.jsx(TableBox, { "$sizeCss": sizeCss, children: jsxRuntime.jsxs(TableWrapper, { ref: setTable, "$contentWidth": contentWidth, children: [jsxRuntime.jsx(TableHead, { children: jsxRuntime.jsxs("tr", { children: [columns.map(({ attributeName, alias, description }) => {
14861
+ const sorted = sort?.attributeName === attributeName;
14862
+ return (jsxRuntime.jsx(TableHeadCell, { title: description || alias, "$sortable": sortEnabled, onClick: sortEnabled ? () => onSortToggle(attributeName) : undefined, children: jsxRuntime.jsxs(TableHeadContent, { children: [alias, sortEnabled && (jsxRuntime.jsx(TableSortSlot, { "$active": sorted, children: jsxRuntime.jsx(uilibGl.Icon, { kind: sorted && sort?.direction === "desc" ? "sorting_des" : "sorting_asc" }) }))] }) }, attributeName));
14863
+ }), context.canEdit && jsxRuntime.jsx(TableHeadCell, {})] }) }), jsxRuntime.jsx(TableBody, { children: rows.map(row => (jsxRuntime.jsxs(TableRow, { children: [columns.map(attribute => (jsxRuntime.jsx(TableCellWrapper, { children: jsxRuntime.jsx(TableCell, { attribute: attribute, row: row, canEdit: context.canEdit, onChange: context.onCellChange }) }, attribute.attributeName))), context.canEdit && (jsxRuntime.jsx(TableActionsCell, { children: jsxRuntime.jsx(uilibGl.IconButton, { kind: "delete", title: t("actions.delete", { ns: "common", defaultValue: "Удалить" }), onClick: () => context.onRowDelete(row.key) }) }))] }, row.key))) })] }) }));
14629
14864
  });
14630
14865
 
14631
14866
  const TooltipIcon = styled(uilibGl.Icon).withConfig({ displayName: "TooltipIcon", componentId: "sc-1lkxudm" }) `
@@ -17819,6 +18054,7 @@ const DEFAULT_HEATMAP_STYLE = {
17819
18054
  exports.ALIGNMENTS = ALIGNMENTS;
17820
18055
  exports.ALIGN_ITEMS = ALIGN_ITEMS;
17821
18056
  exports.ATTRIBUTE_ICON_ELEMENT_TYPES = ATTRIBUTE_ICON_ELEMENT_TYPES;
18057
+ exports.AddButtonRow = AddButtonRow;
17822
18058
  exports.AddFeatureButton = AddFeatureButton;
17823
18059
  exports.AddFeatureContainer = AddFeatureContainer;
17824
18060
  exports.AlertIconContainer = AlertIconContainer;