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