@evergis/react 4.0.142 → 4.0.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7689,33 +7689,10 @@ const useDataSources = ({ type: widgetType, config, attributes, filters, layerPa
7689
7689
  });
7690
7690
  return newDataSources;
7691
7691
  }, []);
7692
- const zoomToLayersExtent = React.useCallback(async (layers) => {
7693
- const newExtent = await api.layers.getBulkExtents({ srId: 4326 }, layers?.map(({ layerName, conditions, parameters }) => ({
7694
- layerName,
7695
- conditions,
7696
- parameters,
7697
- })));
7698
- const { coordinates } = newExtent.overall || {};
7699
- if (!coordinates) {
7700
- return;
7701
- }
7702
- const hasEmptyCoordinate = coordinates.some(([x, y]) => !x && !y);
7703
- if (hasEmptyCoordinate) {
7704
- return;
7705
- }
7706
- /* const bbox = new Bbox(coordinates[0], coordinates[1], map.crs);
7707
- const resolution = Math.max(bbox.width / painter.width, bbox.height / painter.height);
7708
-
7709
- animateTo({
7710
- position: bbox.center,
7711
- resolution: map.getAdjustedResolution(resolution),
7712
- });*/
7713
- }, [api.layers]);
7714
7692
  return {
7715
7693
  getDataSourcePromises,
7716
7694
  getUpdatingDataSources,
7717
7695
  getUpdatedDataSources,
7718
- zoomToLayersExtent,
7719
7696
  };
7720
7697
  };
7721
7698
 
@@ -11430,10 +11407,17 @@ const toSchemaAttribute = (params) => {
11430
11407
  attributeName,
11431
11408
  // Тип принадлежит источнику: конфиг задаёт его только там, где источника нет.
11432
11409
  type: source?.type ?? description?.type ?? DEFAULT_ATTRIBUTE_TYPE,
11410
+ // Уточнение типа — целиком за конфигом: у атрибута источника поля `subType` нет.
11411
+ subType: description?.subType,
11433
11412
  alias: description?.alias ?? source?.alias ?? attributeName,
11434
11413
  description: description?.description ?? source?.description,
11435
11414
  isEditable: resolveIsEditable(params),
11436
11415
  stringFormat: resolveStringFormat(description, source),
11416
+ // Раскладка колонки живёт только в конфиге контейнера: источник о ширине таблицы не знает.
11417
+ width: description?.width,
11418
+ resizable: description?.resizable,
11419
+ multiline: description?.multiline,
11420
+ style: description?.style,
11437
11421
  };
11438
11422
  };
11439
11423
  /**
@@ -15464,6 +15448,8 @@ const TIME_FORMAT = /hh:mm/;
15464
15448
  * закрытие редактора по клику снаружи обязано этот контейнер пропускать.
15465
15449
  */
15466
15450
  const PORTAL_ROOT_SELECTOR = "#portal-root";
15451
+ /** Ниже этой ширины колонку не ужать мышью: уже неё в ячейке не остаётся места под значение. */
15452
+ const MIN_COLUMN_WIDTH = 48;
15467
15453
 
15468
15454
  /**
15469
15455
  * Собственный бокс таблицы. Без размеров в `options` представления узел ничего не меняет:
@@ -15520,10 +15506,13 @@ const TableHead = styled.thead.withConfig({ displayName: "TableHead", componentI
15520
15506
  z-index: 1;
15521
15507
  `;
15522
15508
  const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", componentId: "sc-1m6xil2" }) `
15509
+ /* Относительное позиционирование держит захват границы колонки (TableResizeHandle). */
15510
+ position: relative;
15523
15511
  padding: 0.375rem 0.5rem;
15524
15512
  text-align: left;
15525
15513
  font-weight: 600;
15526
15514
  white-space: nowrap;
15515
+ overflow: hidden;
15527
15516
  color: ${({ theme }) => theme.palette.textSecondary};
15528
15517
  /* Рамка принадлежит ячейке (рамки раздельные), поэтому едет вместе с липкой шапкой. */
15529
15518
  border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
@@ -15534,10 +15523,37 @@ const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", compo
15534
15523
  user-select: none;
15535
15524
  `}
15536
15525
  `;
15526
+ /**
15527
+ * Содержимое заголовка. Псевдоним обрезается многоточием, значок сортировки не сжимается:
15528
+ * у колонки с заданной шириной длинный псевдоним иначе выдавил бы значок за край ячейки.
15529
+ */
15537
15530
  const TableHeadContent = styled.span.withConfig({ displayName: "TableHeadContent", componentId: "sc-8hzcop" }) `
15538
- display: inline-flex;
15531
+ display: flex;
15539
15532
  align-items: center;
15540
15533
  gap: 0.25rem;
15534
+ min-width: 0;
15535
+ overflow: hidden;
15536
+ text-overflow: ellipsis;
15537
+ `;
15538
+ /**
15539
+ * Захват правой границы колонки. Лежит поверх края ячейки и ловит указатель сам, поэтому
15540
+ * перетаскивание не доходит до заголовка и не переключает сортировку.
15541
+ *
15542
+ * Ширина в пикселах, а не в rem: это зона попадания курсора, и от размера шрифта она не зависит.
15543
+ */
15544
+ const TableResizeHandle = styled.span.withConfig({ displayName: "TableResizeHandle", componentId: "sc-1g0q105" }) `
15545
+ position: absolute;
15546
+ top: 0;
15547
+ right: 0;
15548
+ bottom: 0;
15549
+ width: 8px;
15550
+ cursor: col-resize;
15551
+ touch-action: none;
15552
+ user-select: none;
15553
+
15554
+ :hover {
15555
+ background: ${({ theme }) => theme.palette.elementDeep};
15556
+ }
15541
15557
  `;
15542
15558
  /**
15543
15559
  * Место под значок сортировки. Занято всегда, пока сортировка включена, а сам значок только
@@ -15545,7 +15561,7 @@ const TableHeadContent = styled.span.withConfig({ displayName: "TableHeadContent
15545
15561
  * клик по заголовку, вместе с телом. Оба направления рисуются значком в 1rem, поэтому
15546
15562
  * переключение asc/des ширину тоже не трогает.
15547
15563
  */
15548
- const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", componentId: "sc-z378vf" }) `
15564
+ const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", componentId: "sc-1ceykgw" }) `
15549
15565
  display: inline-flex;
15550
15566
  visibility: ${({ $active }) => ($active ? "visible" : "hidden")};
15551
15567
  `;
@@ -15554,36 +15570,54 @@ const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", com
15554
15570
  * её нижнюю кромку и сквозь прозрачную шапку не проступают. Величину пишет {@link useHeadOverlap},
15555
15571
  * без неё отсечка нулевая и разметка не меняется.
15556
15572
  */
15557
- const TableBody = styled.tbody.withConfig({ displayName: "TableBody", componentId: "sc-1pxpl5v" }) `
15573
+ const TableBody = styled.tbody.withConfig({ displayName: "TableBody", componentId: "sc-c9drzt" }) `
15558
15574
  clip-path: inset(var(${HEAD_OVERLAP_VARIABLE}, 0px) 0 0 0);
15559
15575
  `;
15560
15576
  /**
15561
15577
  * Строка. Разделитель рисуют её ячейки: собственная рамка строки при раздельных рамках не
15562
15578
  * рисуется вовсе, а при схлопнутых её рисовала бы таблица — и отсечка тела её бы не резала.
15563
15579
  */
15564
- const TableRow = styled.tr.withConfig({ displayName: "TableRow", componentId: "sc-j62zvd" }) `
15580
+ const TableRow = styled.tr.withConfig({ displayName: "TableRow", componentId: "sc-155nkv8" }) `
15565
15581
  td {
15566
15582
  border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
15567
15583
  }
15568
15584
  `;
15569
- const TableCellWrapper = styled.td.withConfig({ displayName: "TableCellWrapper", componentId: "sc-ziu5rk" }) `
15585
+ const TableCellWrapper = styled.td.withConfig({ displayName: "TableCellWrapper", componentId: "sc-7ueth1" }) `
15570
15586
  padding: 0.125rem 0.25rem;
15571
15587
  vertical-align: middle;
15572
15588
  /* Предел снимает заданная ширина: с колонками по содержимому обрезать значение незачем. */
15573
15589
  max-width: var(--table-cell-max-width, 20rem);
15574
15590
  `;
15575
15591
  /** Колонка действий: узкая, не растягивается содержимым. */
15576
- const TableActionsCell = styled(TableCellWrapper).withConfig({ displayName: "TableActionsCell", componentId: "sc-18w13t3" }) `
15592
+ const TableActionsCell = styled(TableCellWrapper).withConfig({ displayName: "TableActionsCell", componentId: "sc-1m7p53b" }) `
15577
15593
  width: 2rem;
15578
15594
  text-align: right;
15579
15595
  `;
15580
- const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "sc-ok630s" }) `
15596
+ /**
15597
+ * Как значение ведёт себя, когда не помещается в колонку: режется многоточием в одну строку
15598
+ * или переносится по словам, растягивая строку таблицы вниз (`multiline` атрибута).
15599
+ *
15600
+ * Перенос разрешаем и посреди слова: колонка бывает уже одного длинного слова (ссылка, артикул),
15601
+ * и без этого оно вылезло бы за заданную ширину, сделав её бессмысленной.
15602
+ */
15603
+ const cellWrapMixin = styled.css `
15604
+ ${({ $multiline }) => $multiline
15605
+ ? styled.css `
15606
+ white-space: normal;
15607
+ overflow-wrap: anywhere;
15608
+ `
15609
+ : styled.css `
15610
+ overflow: hidden;
15611
+ text-overflow: ellipsis;
15612
+ white-space: nowrap;
15613
+ `}
15614
+ `;
15615
+ const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "sc-19uakee" }) `
15581
15616
  padding: 0.375rem 0.25rem;
15582
15617
  /* Та же высота строки, что у редактора — иначе строка подпрыгивала бы по вертикали. */
15583
15618
  line-height: 1.25rem;
15584
- overflow: hidden;
15585
- text-overflow: ellipsis;
15586
- white-space: nowrap;
15619
+
15620
+ ${cellWrapMixin};
15587
15621
  `;
15588
15622
  /**
15589
15623
  * Редактируемая ячейка вне фокуса. Показывает значение по `stringFormat`, а редактор
@@ -15592,7 +15626,7 @@ const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "
15592
15626
  * Кнопка, а не просто текст, именно ради фокуса: она — таб-стоп ячейки, и Tab по таблице
15593
15627
  * открывает редактор с курсором внутри.
15594
15628
  */
15595
- const CellButton = styled.button.withConfig({ displayName: "CellButton", componentId: "sc-98k9cz" }) `
15629
+ const CellButton = styled.button.withConfig({ displayName: "CellButton", componentId: "sc-16lgve4" }) `
15596
15630
  width: 100%;
15597
15631
  padding: 0.375rem 0.25rem;
15598
15632
  line-height: 1.25rem;
@@ -15603,15 +15637,14 @@ const CellButton = styled.button.withConfig({ displayName: "CellButton", compone
15603
15637
  color: inherit;
15604
15638
  text-align: left;
15605
15639
  cursor: text;
15606
- overflow: hidden;
15607
- text-overflow: ellipsis;
15608
- white-space: nowrap;
15640
+
15641
+ ${cellWrapMixin};
15609
15642
 
15610
15643
  :hover {
15611
15644
  border-color: ${({ theme }) => theme.palette.elementDeep};
15612
15645
  }
15613
15646
  `;
15614
- const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder", componentId: "sc-7tff41" }) `
15647
+ const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder", componentId: "sc-1jnsjad" }) `
15615
15648
  color: ${({ theme }) => theme.palette.textSecondary};
15616
15649
  `;
15617
15650
  /**
@@ -15620,20 +15653,21 @@ const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder",
15620
15653
  * Копия остаётся в потоке и держит ширину колонки ровно такой, какой она была до клика, —
15621
15654
  * иначе таблица дёргалась бы на каждый вход в правку и выход из неё.
15622
15655
  */
15623
- const CellEditor = styled.div.withConfig({ displayName: "CellEditor", componentId: "sc-qa22du" }) `
15656
+ const CellEditor = styled.div.withConfig({ displayName: "CellEditor", componentId: "sc-ee08kh" }) `
15624
15657
  position: relative;
15625
15658
  min-height: 2rem;
15626
15659
  `;
15627
- const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId: "sc-1i1yvb2" }) `
15660
+ const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId: "sc-1p10qm6" }) `
15628
15661
  display: block;
15629
15662
  padding: 0.375rem 0.25rem;
15630
15663
  /* Рамка повторяет CellButton — с ней высота ячейки в правке совпадает с высотой на чтении. */
15631
15664
  border: 1px solid transparent;
15632
15665
  line-height: 1.25rem;
15633
- white-space: nowrap;
15634
15666
  visibility: hidden;
15667
+
15668
+ ${cellWrapMixin};
15635
15669
  `;
15636
- const CellField = styled.div.withConfig({ displayName: "CellField", componentId: "sc-1ww3875" }) `
15670
+ const CellField = styled.div.withConfig({ displayName: "CellField", componentId: "sc-cnkcp6" }) `
15637
15671
  position: absolute;
15638
15672
  inset: 0;
15639
15673
  display: flex;
@@ -15648,11 +15682,97 @@ const CellField = styled.div.withConfig({ displayName: "CellField", componentId:
15648
15682
  min-width: 0;
15649
15683
  }
15650
15684
  `;
15651
- const TableEmpty = styled.div.withConfig({ displayName: "TableEmpty", componentId: "sc-1vamijg" }) `
15685
+ /**
15686
+ * Ячейка со вложениями. Список идёт колонкой, кнопка добавления — под ним, поэтому строка растёт
15687
+ * ровно на столько, сколько файлов в ней лежит.
15688
+ *
15689
+ * Отступы те же, что у текстовой ячейки (`CellText`): в одном ряду с обычными колонками значения
15690
+ * обязаны стоять на одной линии.
15691
+ */
15692
+ const AttachmentsCellBox = styled.div.withConfig({ displayName: "AttachmentsCellBox", componentId: "sc-9n5afp" }) `
15693
+ display: flex;
15694
+ flex-direction: column;
15695
+ align-items: flex-start;
15696
+ gap: 0.25rem;
15697
+ padding: 0.375rem 0.25rem;
15698
+ `;
15699
+ const TableEmpty = styled.div.withConfig({ displayName: "TableEmpty", componentId: "sc-1c196zb" }) `
15652
15700
  padding: 0.75rem 0.25rem;
15653
15701
  color: ${({ theme }) => theme.palette.textSecondary};
15654
15702
  `;
15655
15703
 
15704
+ /**
15705
+ * Ячейка с вложениями: разбор значения, просмотр, скачивание и правка списка.
15706
+ *
15707
+ * Значение атрибута — строка: файлы приезжают в ней списком JSON, поэтому и обратно в черновик
15708
+ * кладём строку, а не массив — иначе тип ячейки менялся бы от самой правки.
15709
+ *
15710
+ * Добавление здесь только по ссылке. Загрузка с диска требует ресурса-родителя и файлового
15711
+ * api контейнера вложений, а у колонки таблицы ни того, ни другого нет.
15712
+ */
15713
+ const useAttachmentsCell = ({ attribute, row, canEdit, onChange }) => {
15714
+ const [previewIndex, setPreviewIndex] = React.useState(null);
15715
+ const [isLinkDialogOpen, setLinkDialogOpen] = React.useState(false);
15716
+ const { attributeName, isEditable } = attribute;
15717
+ const value = row.properties[attributeName];
15718
+ const items = React.useMemo(() => parseAttachments(value), [value]);
15719
+ const previewImages = useAttachmentPreviewImages({ items, active: previewIndex !== null });
15720
+ const downloadByIndex = useAttachmentDownload(items);
15721
+ // Те же два условия, что и у обычной ячейки: режим таблицы и разрешение самого атрибута.
15722
+ const editable = canEdit && isEditable;
15723
+ const persist = React.useCallback((next) => onChange(row.key, attributeName, JSON.stringify(next)), [attributeName, onChange, row.key]);
15724
+ const onPreview = React.useCallback((link) => {
15725
+ const index = items.findIndex(item => item.link === link);
15726
+ if (index >= 0) {
15727
+ setPreviewIndex(index);
15728
+ }
15729
+ }, [items]);
15730
+ const onClosePreview = React.useCallback(() => setPreviewIndex(null), []);
15731
+ const onDownload = React.useCallback((_image, index) => downloadByIndex(index), [downloadByIndex]);
15732
+ const onDelete = React.useCallback((link) => persist(items.filter(item => item.link !== link)), [items, persist]);
15733
+ const onOpenLinkDialog = React.useCallback(() => setLinkDialogOpen(true), []);
15734
+ const onCloseLinkDialog = React.useCallback(() => setLinkDialogOpen(false), []);
15735
+ const onAddByLink = React.useCallback((url) => persist([
15736
+ ...items,
15737
+ {
15738
+ link: url,
15739
+ name: getFileNameFromUrl(url),
15740
+ mimeType: getMimeTypeFromUrl(url),
15741
+ date: new Date().toISOString(),
15742
+ isExternal: true,
15743
+ },
15744
+ ]), [items, persist]);
15745
+ return {
15746
+ items,
15747
+ editable,
15748
+ previewIndex,
15749
+ previewImages,
15750
+ isLinkDialogOpen,
15751
+ onPreview,
15752
+ onClosePreview,
15753
+ onDownload,
15754
+ onDelete,
15755
+ onOpenLinkDialog,
15756
+ onCloseLinkDialog,
15757
+ onAddByLink,
15758
+ };
15759
+ };
15760
+
15761
+ /**
15762
+ * Колонка со вложениями: список файлов прямо в ячейке.
15763
+ *
15764
+ * Тип у атрибута строковый, вложениями его делает `subType: "Attachments"` в схеме контейнера —
15765
+ * поэтому ветка стоит раньше разбора типа и обычных редакторов ячейки.
15766
+ */
15767
+ const AttachmentsCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15768
+ const { t } = useGlobalContext();
15769
+ const { items, editable, previewIndex, previewImages, isLinkDialogOpen, onPreview, onClosePreview, onDownload, onDelete, onOpenLinkDialog, onCloseLinkDialog, onAddByLink, } = useAttachmentsCell({ attribute, row, canEdit, onChange });
15770
+ return (jsxRuntime.jsxs(AttachmentsCellBox, { children: [!items.length && !editable && jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }), jsxRuntime.jsx(AttachmentsList, { items: items, isEdit: editable, onPreview: onPreview, onDelete: onDelete }), editable && (jsxRuntime.jsx(uilibGl.IconButton, { kind: "link", tabIndex: 0, title: t("attachments.fromLink", { ns: "common", defaultValue: "По ссылке" }), onClick: onOpenLinkDialog })), jsxRuntime.jsx(AttachmentLinkDialog, { isOpen: isLinkDialogOpen, onClose: onCloseLinkDialog, onSubmit: onAddByLink }), previewIndex !== null && (jsxRuntime.jsx(uilibGl.Preview, { images: previewImages, initialIndex: previewIndex, isOpen: true, onClose: onClosePreview, onDownload: onDownload, errorTitleText: t("attachments.resourceUnavailable", {
15771
+ ns: "common",
15772
+ defaultValue: "Ресурс недоступен",
15773
+ }) }, previewIndex))] }));
15774
+ });
15775
+
15656
15776
  /**
15657
15777
  * Значение ячейки не изменилось.
15658
15778
  *
@@ -15751,7 +15871,7 @@ const useCellEditing = () => {
15751
15871
  const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15752
15872
  const { t, language } = useGlobalContext();
15753
15873
  const { editing, buttonProps, editorProps } = useCellEditing();
15754
- const { attributeName, type, isEditable, stringFormat } = attribute;
15874
+ const { attributeName, type, subType, isEditable, stringFormat, multiline } = attribute;
15755
15875
  const value = row.properties[attributeName];
15756
15876
  const handleChange = React.useCallback((next) => {
15757
15877
  // Пустые правки отбрасываем: иначе строка становилась бы «изменённой» от простого
@@ -15770,16 +15890,21 @@ const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15770
15890
  const withTime = React.useMemo(() => TIME_FORMAT.test(stringFormat?.format ?? ""), [stringFormat?.format]);
15771
15891
  // `canEdit` — режим таблицы, `isEditable` — разрешение конкретного атрибута: правка требует обоих.
15772
15892
  const editable = canEdit && isEditable && EDITABLE_ATTRIBUTE_TYPES.includes(type);
15893
+ // Вложения — это строка с JSON, а не текст: разбирать и рисовать её нужно раньше любых
15894
+ // редакторов по типу, иначе в ячейке оказался бы сырой список файлов.
15895
+ if (subType === api.StringSubType.Attachments) {
15896
+ return jsxRuntime.jsx(AttachmentsCell, { attribute: attribute, row: row, canEdit: canEdit, onChange: onChange });
15897
+ }
15773
15898
  // Логическое значение и на чтение показываем галкой, а не словами «true»/«false» —
15774
15899
  // так же, как остальной дашборд рисует булевы атрибуты.
15775
15900
  if (type === api.AttributeType.Boolean) {
15776
15901
  return jsxRuntime.jsx(uilibGl.Checkbox, { checked: !!value, disabled: !editable, onChange: () => handleChange(!value) });
15777
15902
  }
15778
15903
  if (!editable) {
15779
- return jsxRuntime.jsx(CellText, { title: formatted, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) });
15904
+ return (jsxRuntime.jsx(CellText, { "$multiline": multiline, title: formatted, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) }));
15780
15905
  }
15781
15906
  if (!editing) {
15782
- return (jsxRuntime.jsx(CellButton, { type: "button", ...buttonProps, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) }));
15907
+ return (jsxRuntime.jsx(CellButton, { type: "button", "$multiline": multiline, ...buttonProps, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) }));
15783
15908
  }
15784
15909
  const renderEditor = () => {
15785
15910
  if (type === api.AttributeType.DateTime) {
@@ -15790,9 +15915,31 @@ const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15790
15915
  }
15791
15916
  return (jsxRuntime.jsx(uilibGl.Input, { autoFocus: true, value: value === null || value === undefined ? "" : String(value), width: "100%", onChange: ({ target }) => handleChange(target.value) }));
15792
15917
  };
15793
- return (jsxRuntime.jsxs(CellEditor, { ...editorProps, children: [jsxRuntime.jsx(CellGhost, { children: formatted || "—" }), jsxRuntime.jsx(CellField, { children: renderEditor() })] }));
15918
+ return (jsxRuntime.jsxs(CellEditor, { ...editorProps, children: [jsxRuntime.jsx(CellGhost, { "$multiline": multiline, children: formatted || "—" }), jsxRuntime.jsx(CellField, { children: renderEditor() })] }));
15794
15919
  });
15795
15920
 
15921
+ /**
15922
+ * Инлайн-размер ячейки колонки с заданной шириной.
15923
+ *
15924
+ * Раскладка таблицы автоматическая (ширину колонок выбирает содержимое), и одной `width` ей мало —
15925
+ * она для неё лишь пожелание. Жёстко колонку держат все три предела сразу, поэтому и ставим их
15926
+ * втроём, на каждую ячейку колонки.
15927
+ *
15928
+ * Ширины нет — стиля нет: колонка остаётся на попечении раскладки, как и была.
15929
+ */
15930
+ const getColumnStyle = (width) => width == null ? undefined : { width, minWidth: width, maxWidth: width };
15931
+
15932
+ /**
15933
+ * Шапка таблицы: заголовки колонок, сортировка кликом и захват границы для ручной ширины.
15934
+ *
15935
+ * Захват рисуется по разрешению атрибута (`resizable`) и не смотрит на режим правки контейнера:
15936
+ * ширина колонки — это вид, а не данные.
15937
+ */
15938
+ const TableHeadRow = ({ columns, sort, sortEnabled, columnWidths, withActionsColumn, onSortToggle, onResizeStart, onResizeMove, onResizeEnd, }) => (jsxRuntime.jsxs("tr", { children: [columns.map(({ attributeName, alias, description, resizable }) => {
15939
+ const sorted = sort?.attributeName === attributeName;
15940
+ return (jsxRuntime.jsxs(TableHeadCell, { title: description || alias, style: getColumnStyle(columnWidths[attributeName]), "$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" }) }))] }), resizable && (jsxRuntime.jsx(TableResizeHandle, { onPointerDown: event => onResizeStart(attributeName, event), onPointerMove: onResizeMove, onPointerUp: onResizeEnd, onPointerCancel: onResizeEnd }))] }, attributeName));
15941
+ }), withActionsColumn && jsxRuntime.jsx(TableHeadCell, {})] }));
15942
+
15796
15943
  const ContainerLoading = () => (jsxRuntime.jsx(uilibGl.Flex, { alignContent: "center", justifyContent: "center", width: "100%", children: jsxRuntime.jsx(uilibGl.CircularProgress, { diameter: 1.5, mono: true }) }));
15797
15944
 
15798
15945
  /**
@@ -15896,6 +16043,53 @@ const getTableBoxStyle = (width, height) => {
15896
16043
  };
15897
16044
  };
15898
16045
 
16046
+ /**
16047
+ * Ручное изменение ширины колонок.
16048
+ *
16049
+ * Растянутые ширины живут только здесь: тянут их в рантайме, а `width` атрибута в конфиге задаёт
16050
+ * лишь стартовое значение — записывать растянутое обратно в конфиг незачем.
16051
+ *
16052
+ * Разрешение (`resizable`) от `options.editMode` не зависит: ширина колонки — это вид таблицы,
16053
+ * а не правка данных, и тянуть её должно быть можно и на таблице только для чтения.
16054
+ *
16055
+ * Стартовую ширину берём с самой ячейки, а не из конфига: у колонки без `width` её выбрала
16056
+ * раскладка таблицы, и тянуть надо ровно от того, что видно.
16057
+ */
16058
+ const useColumnResize = (columns) => {
16059
+ const [widths, setWidths] = React.useState({});
16060
+ const dragRef = React.useRef(null);
16061
+ const onResizeStart = React.useCallback((attributeName, event) => {
16062
+ const handle = event.currentTarget;
16063
+ // Захват границы — не клик по заголовку: иначе каждое перетаскивание переключало бы сортировку.
16064
+ event.preventDefault();
16065
+ event.stopPropagation();
16066
+ dragRef.current = {
16067
+ attributeName,
16068
+ startX: event.clientX,
16069
+ startWidth: handle.closest("th")?.getBoundingClientRect().width ?? MIN_COLUMN_WIDTH,
16070
+ };
16071
+ handle.setPointerCapture(event.pointerId);
16072
+ }, []);
16073
+ const onResizeMove = React.useCallback((event) => {
16074
+ const drag = dragRef.current;
16075
+ if (!drag) {
16076
+ return;
16077
+ }
16078
+ const next = Math.max(MIN_COLUMN_WIDTH, Math.round(drag.startWidth + event.clientX - drag.startX));
16079
+ setWidths(current => current[drag.attributeName] === next ? current : { ...current, [drag.attributeName]: next });
16080
+ }, []);
16081
+ const onResizeEnd = React.useCallback((event) => {
16082
+ const handle = event.currentTarget;
16083
+ dragRef.current = null;
16084
+ if (handle.hasPointerCapture(event.pointerId)) {
16085
+ handle.releasePointerCapture(event.pointerId);
16086
+ }
16087
+ }, []);
16088
+ // Растянутая ширина перебивает конфигурационную: последнее слово за тем, кто тянул.
16089
+ const columnWidths = React.useMemo(() => columns.reduce((acc, { attributeName, width }) => ({ ...acc, [attributeName]: widths[attributeName] ?? width }), {}), [columns, widths]);
16090
+ return { columnWidths, onResizeStart, onResizeMove, onResizeEnd };
16091
+ };
16092
+
15899
16093
  /**
15900
16094
  * Вид таблицы: колонки плюс локальная сортировка.
15901
16095
  *
@@ -15908,6 +16102,9 @@ const getTableBoxStyle = (width, height) => {
15908
16102
  *
15909
16103
  * `width`/`height` — размер собственного бокса таблицы: он ограничивает её саму и прокручивает
15910
16104
  * то, что не влезло. Место, отведённое контейнеру, задаётся его же опциями и живёт отдельно.
16105
+ *
16106
+ * Ширины колонок — тоже вид: конфиг задаёт стартовую, растянутая мышью живёт в
16107
+ * {@link useColumnResize} и в конфиг не возвращается.
15911
16108
  */
15912
16109
  const useTableView = (elementConfig) => {
15913
16110
  const context = React.useContext(StructuredDataContext);
@@ -15918,6 +16115,7 @@ const useTableView = (elementConfig) => {
15918
16115
  const columns = React.useMemo(() => schema ?? [], [schema]);
15919
16116
  const sortedRows = React.useMemo(() => sortRows(rows ?? [], sort, schema ?? []), [rows, schema, sort]);
15920
16117
  const sizeCss = React.useMemo(() => getTableBoxStyle(width, height), [height, width]);
16118
+ const { columnWidths, onResizeStart, onResizeMove, onResizeEnd } = useColumnResize(columns);
15921
16119
  const onSortToggle = React.useCallback((attributeName) => setSort(current => getNextSort(current, attributeName)), []);
15922
16120
  return {
15923
16121
  context,
@@ -15928,7 +16126,11 @@ const useTableView = (elementConfig) => {
15928
16126
  sizeCss,
15929
16127
  // Ширина задана — колонки по содержимому: лишнее уходит в прокрутку бокса, а не в многоточие.
15930
16128
  contentWidth: width != null,
16129
+ columnWidths,
15931
16130
  onSortToggle,
16131
+ onResizeStart,
16132
+ onResizeMove,
16133
+ onResizeEnd,
15932
16134
  };
15933
16135
  };
15934
16136
 
@@ -15940,7 +16142,7 @@ const useTableView = (elementConfig) => {
15940
16142
  */
15941
16143
  const ElementTable = React.memo(({ elementConfig }) => {
15942
16144
  const { t } = useGlobalContext();
15943
- const { context, columns, rows, sort, sortEnabled, sizeCss, contentWidth, onSortToggle } = useTableView(elementConfig);
16145
+ const { context, columns, rows, sort, sortEnabled, sizeCss, contentWidth, columnWidths, onSortToggle, onResizeStart, onResizeMove, onResizeEnd, } = useTableView(elementConfig);
15944
16146
  // Узел таблицы держим состоянием, а не ref: отсечка должна встать сразу, как он появится.
15945
16147
  const [table, setTable] = React.useState(null);
15946
16148
  useHeadOverlap(table);
@@ -15956,10 +16158,10 @@ const ElementTable = React.memo(({ elementConfig }) => {
15956
16158
  defaultValue: "Схема данных не задана",
15957
16159
  }) }));
15958
16160
  }
15959
- 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 }) => {
15960
- const sorted = sort?.attributeName === attributeName;
15961
- 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));
15962
- }), context.canDelete && 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.canDelete && (jsxRuntime.jsx(TableActionsCell, { children: jsxRuntime.jsx(uilibGl.IconButton, { kind: "delete", title: t("actions.delete", { ns: "common", defaultValue: "Удалить" }), onClick: () => context.onRowDelete(row.key) }) }))] }, row.key))) })] }) }));
16161
+ return (jsxRuntime.jsx(TableBox, { "$sizeCss": sizeCss, children: jsxRuntime.jsxs(TableWrapper, { ref: setTable, "$contentWidth": contentWidth, children: [jsxRuntime.jsx(TableHead, { children: jsxRuntime.jsx(TableHeadRow, { columns: columns, sort: sort, sortEnabled: sortEnabled, columnWidths: columnWidths, withActionsColumn: context.canDelete, onSortToggle: onSortToggle, onResizeStart: onResizeStart, onResizeMove: onResizeMove, onResizeEnd: onResizeEnd }) }), jsxRuntime.jsx(TableBody, { children: rows.map(row => (jsxRuntime.jsxs(TableRow, { children: [columns.map(attribute => (jsxRuntime.jsx(TableCellWrapper, {
16162
+ // Стиль колонки из конфига идёт последним: шрифт задан для значений, и ширину
16163
+ // он не трогает своих размеров у него нет.
16164
+ style: { ...getColumnStyle(columnWidths[attribute.attributeName]), ...attribute.style }, children: jsxRuntime.jsx(TableCell, { attribute: attribute, row: row, canEdit: context.canEdit, onChange: context.onCellChange }) }, attribute.attributeName))), context.canDelete && (jsxRuntime.jsx(TableActionsCell, { children: jsxRuntime.jsx(uilibGl.IconButton, { kind: "delete", title: t("actions.delete", { ns: "common", defaultValue: "Удалить" }), onClick: () => context.onRowDelete(row.key) }) }))] }, row.key))) })] }) }));
15963
16165
  });
15964
16166
 
15965
16167
  const TooltipIcon = styled(uilibGl.Icon).withConfig({ displayName: "TooltipIcon", componentId: "sc-1lkxudm" }) `