@evergis/react 4.0.143 → 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/components/Dashboard/containers/StructuredDataContainer/types.d.ts +12 -1
- package/dist/components/Dashboard/elements/ElementTable/components/AttachmentsCell.d.ts +9 -0
- package/dist/components/Dashboard/elements/ElementTable/components/TableHeadRow.d.ts +9 -0
- package/dist/components/Dashboard/elements/ElementTable/constants.d.ts +2 -0
- package/dist/components/Dashboard/elements/ElementTable/hooks/useAttachmentsCell.d.ts +26 -0
- package/dist/components/Dashboard/elements/ElementTable/hooks/useColumnResize.d.ts +20 -0
- package/dist/components/Dashboard/elements/ElementTable/hooks/useTableView.d.ts +7 -0
- package/dist/components/Dashboard/elements/ElementTable/styled.d.ts +38 -3
- package/dist/components/Dashboard/elements/ElementTable/types.d.ts +14 -0
- package/dist/components/Dashboard/elements/ElementTable/utils/columnStyle.d.ts +11 -0
- package/dist/components/Dashboard/types.d.ts +19 -0
- package/dist/index.js +254 -29
- package/dist/index.js.map +1 -1
- package/dist/react.esm.js +254 -29
- package/dist/react.esm.js.map +1 -1
- package/package.json +2 -2
package/dist/react.esm.js
CHANGED
|
@@ -11405,10 +11405,17 @@ const toSchemaAttribute = (params) => {
|
|
|
11405
11405
|
attributeName,
|
|
11406
11406
|
// Тип принадлежит источнику: конфиг задаёт его только там, где источника нет.
|
|
11407
11407
|
type: source?.type ?? description?.type ?? DEFAULT_ATTRIBUTE_TYPE,
|
|
11408
|
+
// Уточнение типа — целиком за конфигом: у атрибута источника поля `subType` нет.
|
|
11409
|
+
subType: description?.subType,
|
|
11408
11410
|
alias: description?.alias ?? source?.alias ?? attributeName,
|
|
11409
11411
|
description: description?.description ?? source?.description,
|
|
11410
11412
|
isEditable: resolveIsEditable(params),
|
|
11411
11413
|
stringFormat: resolveStringFormat(description, source),
|
|
11414
|
+
// Раскладка колонки живёт только в конфиге контейнера: источник о ширине таблицы не знает.
|
|
11415
|
+
width: description?.width,
|
|
11416
|
+
resizable: description?.resizable,
|
|
11417
|
+
multiline: description?.multiline,
|
|
11418
|
+
style: description?.style,
|
|
11412
11419
|
};
|
|
11413
11420
|
};
|
|
11414
11421
|
/**
|
|
@@ -15439,6 +15446,8 @@ const TIME_FORMAT = /hh:mm/;
|
|
|
15439
15446
|
* закрытие редактора по клику снаружи обязано этот контейнер пропускать.
|
|
15440
15447
|
*/
|
|
15441
15448
|
const PORTAL_ROOT_SELECTOR = "#portal-root";
|
|
15449
|
+
/** Ниже этой ширины колонку не ужать мышью: уже неё в ячейке не остаётся места под значение. */
|
|
15450
|
+
const MIN_COLUMN_WIDTH = 48;
|
|
15442
15451
|
|
|
15443
15452
|
/**
|
|
15444
15453
|
* Собственный бокс таблицы. Без размеров в `options` представления узел ничего не меняет:
|
|
@@ -15495,10 +15504,13 @@ const TableHead = styled.thead.withConfig({ displayName: "TableHead", componentI
|
|
|
15495
15504
|
z-index: 1;
|
|
15496
15505
|
`;
|
|
15497
15506
|
const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", componentId: "sc-1m6xil2" }) `
|
|
15507
|
+
/* Относительное позиционирование держит захват границы колонки (TableResizeHandle). */
|
|
15508
|
+
position: relative;
|
|
15498
15509
|
padding: 0.375rem 0.5rem;
|
|
15499
15510
|
text-align: left;
|
|
15500
15511
|
font-weight: 600;
|
|
15501
15512
|
white-space: nowrap;
|
|
15513
|
+
overflow: hidden;
|
|
15502
15514
|
color: ${({ theme }) => theme.palette.textSecondary};
|
|
15503
15515
|
/* Рамка принадлежит ячейке (рамки раздельные), поэтому едет вместе с липкой шапкой. */
|
|
15504
15516
|
border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
|
|
@@ -15509,10 +15521,37 @@ const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", compo
|
|
|
15509
15521
|
user-select: none;
|
|
15510
15522
|
`}
|
|
15511
15523
|
`;
|
|
15524
|
+
/**
|
|
15525
|
+
* Содержимое заголовка. Псевдоним обрезается многоточием, значок сортировки не сжимается:
|
|
15526
|
+
* у колонки с заданной шириной длинный псевдоним иначе выдавил бы значок за край ячейки.
|
|
15527
|
+
*/
|
|
15512
15528
|
const TableHeadContent = styled.span.withConfig({ displayName: "TableHeadContent", componentId: "sc-8hzcop" }) `
|
|
15513
|
-
display:
|
|
15529
|
+
display: flex;
|
|
15514
15530
|
align-items: center;
|
|
15515
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
|
+
}
|
|
15516
15555
|
`;
|
|
15517
15556
|
/**
|
|
15518
15557
|
* Место под значок сортировки. Занято всегда, пока сортировка включена, а сам значок только
|
|
@@ -15520,7 +15559,7 @@ const TableHeadContent = styled.span.withConfig({ displayName: "TableHeadContent
|
|
|
15520
15559
|
* клик по заголовку, вместе с телом. Оба направления рисуются значком в 1rem, поэтому
|
|
15521
15560
|
* переключение asc/des ширину тоже не трогает.
|
|
15522
15561
|
*/
|
|
15523
|
-
const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", componentId: "sc-
|
|
15562
|
+
const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", componentId: "sc-1ceykgw" }) `
|
|
15524
15563
|
display: inline-flex;
|
|
15525
15564
|
visibility: ${({ $active }) => ($active ? "visible" : "hidden")};
|
|
15526
15565
|
`;
|
|
@@ -15529,36 +15568,54 @@ const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", com
|
|
|
15529
15568
|
* её нижнюю кромку и сквозь прозрачную шапку не проступают. Величину пишет {@link useHeadOverlap},
|
|
15530
15569
|
* без неё отсечка нулевая и разметка не меняется.
|
|
15531
15570
|
*/
|
|
15532
|
-
const TableBody = styled.tbody.withConfig({ displayName: "TableBody", componentId: "sc-
|
|
15571
|
+
const TableBody = styled.tbody.withConfig({ displayName: "TableBody", componentId: "sc-c9drzt" }) `
|
|
15533
15572
|
clip-path: inset(var(${HEAD_OVERLAP_VARIABLE}, 0px) 0 0 0);
|
|
15534
15573
|
`;
|
|
15535
15574
|
/**
|
|
15536
15575
|
* Строка. Разделитель рисуют её ячейки: собственная рамка строки при раздельных рамках не
|
|
15537
15576
|
* рисуется вовсе, а при схлопнутых её рисовала бы таблица — и отсечка тела её бы не резала.
|
|
15538
15577
|
*/
|
|
15539
|
-
const TableRow = styled.tr.withConfig({ displayName: "TableRow", componentId: "sc-
|
|
15578
|
+
const TableRow = styled.tr.withConfig({ displayName: "TableRow", componentId: "sc-155nkv8" }) `
|
|
15540
15579
|
td {
|
|
15541
15580
|
border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
|
|
15542
15581
|
}
|
|
15543
15582
|
`;
|
|
15544
|
-
const TableCellWrapper = styled.td.withConfig({ displayName: "TableCellWrapper", componentId: "sc-
|
|
15583
|
+
const TableCellWrapper = styled.td.withConfig({ displayName: "TableCellWrapper", componentId: "sc-7ueth1" }) `
|
|
15545
15584
|
padding: 0.125rem 0.25rem;
|
|
15546
15585
|
vertical-align: middle;
|
|
15547
15586
|
/* Предел снимает заданная ширина: с колонками по содержимому обрезать значение незачем. */
|
|
15548
15587
|
max-width: var(--table-cell-max-width, 20rem);
|
|
15549
15588
|
`;
|
|
15550
15589
|
/** Колонка действий: узкая, не растягивается содержимым. */
|
|
15551
|
-
const TableActionsCell = styled(TableCellWrapper).withConfig({ displayName: "TableActionsCell", componentId: "sc-
|
|
15590
|
+
const TableActionsCell = styled(TableCellWrapper).withConfig({ displayName: "TableActionsCell", componentId: "sc-1m7p53b" }) `
|
|
15552
15591
|
width: 2rem;
|
|
15553
15592
|
text-align: right;
|
|
15554
15593
|
`;
|
|
15555
|
-
|
|
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" }) `
|
|
15556
15614
|
padding: 0.375rem 0.25rem;
|
|
15557
15615
|
/* Та же высота строки, что у редактора — иначе строка подпрыгивала бы по вертикали. */
|
|
15558
15616
|
line-height: 1.25rem;
|
|
15559
|
-
|
|
15560
|
-
|
|
15561
|
-
white-space: nowrap;
|
|
15617
|
+
|
|
15618
|
+
${cellWrapMixin};
|
|
15562
15619
|
`;
|
|
15563
15620
|
/**
|
|
15564
15621
|
* Редактируемая ячейка вне фокуса. Показывает значение по `stringFormat`, а редактор
|
|
@@ -15567,7 +15624,7 @@ const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "
|
|
|
15567
15624
|
* Кнопка, а не просто текст, именно ради фокуса: она — таб-стоп ячейки, и Tab по таблице
|
|
15568
15625
|
* открывает редактор с курсором внутри.
|
|
15569
15626
|
*/
|
|
15570
|
-
const CellButton = styled.button.withConfig({ displayName: "CellButton", componentId: "sc-
|
|
15627
|
+
const CellButton = styled.button.withConfig({ displayName: "CellButton", componentId: "sc-16lgve4" }) `
|
|
15571
15628
|
width: 100%;
|
|
15572
15629
|
padding: 0.375rem 0.25rem;
|
|
15573
15630
|
line-height: 1.25rem;
|
|
@@ -15578,15 +15635,14 @@ const CellButton = styled.button.withConfig({ displayName: "CellButton", compone
|
|
|
15578
15635
|
color: inherit;
|
|
15579
15636
|
text-align: left;
|
|
15580
15637
|
cursor: text;
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
white-space: nowrap;
|
|
15638
|
+
|
|
15639
|
+
${cellWrapMixin};
|
|
15584
15640
|
|
|
15585
15641
|
:hover {
|
|
15586
15642
|
border-color: ${({ theme }) => theme.palette.elementDeep};
|
|
15587
15643
|
}
|
|
15588
15644
|
`;
|
|
15589
|
-
const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder", componentId: "sc-
|
|
15645
|
+
const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder", componentId: "sc-1jnsjad" }) `
|
|
15590
15646
|
color: ${({ theme }) => theme.palette.textSecondary};
|
|
15591
15647
|
`;
|
|
15592
15648
|
/**
|
|
@@ -15595,20 +15651,21 @@ const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder",
|
|
|
15595
15651
|
* Копия остаётся в потоке и держит ширину колонки ровно такой, какой она была до клика, —
|
|
15596
15652
|
* иначе таблица дёргалась бы на каждый вход в правку и выход из неё.
|
|
15597
15653
|
*/
|
|
15598
|
-
const CellEditor = styled.div.withConfig({ displayName: "CellEditor", componentId: "sc-
|
|
15654
|
+
const CellEditor = styled.div.withConfig({ displayName: "CellEditor", componentId: "sc-ee08kh" }) `
|
|
15599
15655
|
position: relative;
|
|
15600
15656
|
min-height: 2rem;
|
|
15601
15657
|
`;
|
|
15602
|
-
const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId: "sc-
|
|
15658
|
+
const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId: "sc-1p10qm6" }) `
|
|
15603
15659
|
display: block;
|
|
15604
15660
|
padding: 0.375rem 0.25rem;
|
|
15605
15661
|
/* Рамка повторяет CellButton — с ней высота ячейки в правке совпадает с высотой на чтении. */
|
|
15606
15662
|
border: 1px solid transparent;
|
|
15607
15663
|
line-height: 1.25rem;
|
|
15608
|
-
white-space: nowrap;
|
|
15609
15664
|
visibility: hidden;
|
|
15665
|
+
|
|
15666
|
+
${cellWrapMixin};
|
|
15610
15667
|
`;
|
|
15611
|
-
const CellField = styled.div.withConfig({ displayName: "CellField", componentId: "sc-
|
|
15668
|
+
const CellField = styled.div.withConfig({ displayName: "CellField", componentId: "sc-cnkcp6" }) `
|
|
15612
15669
|
position: absolute;
|
|
15613
15670
|
inset: 0;
|
|
15614
15671
|
display: flex;
|
|
@@ -15623,11 +15680,97 @@ const CellField = styled.div.withConfig({ displayName: "CellField", componentId:
|
|
|
15623
15680
|
min-width: 0;
|
|
15624
15681
|
}
|
|
15625
15682
|
`;
|
|
15626
|
-
|
|
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" }) `
|
|
15627
15698
|
padding: 0.75rem 0.25rem;
|
|
15628
15699
|
color: ${({ theme }) => theme.palette.textSecondary};
|
|
15629
15700
|
`;
|
|
15630
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
|
+
|
|
15631
15774
|
/**
|
|
15632
15775
|
* Значение ячейки не изменилось.
|
|
15633
15776
|
*
|
|
@@ -15726,7 +15869,7 @@ const useCellEditing = () => {
|
|
|
15726
15869
|
const TableCell = memo(({ attribute, row, canEdit, onChange }) => {
|
|
15727
15870
|
const { t, language } = useGlobalContext();
|
|
15728
15871
|
const { editing, buttonProps, editorProps } = useCellEditing();
|
|
15729
|
-
const { attributeName, type, isEditable, stringFormat } = attribute;
|
|
15872
|
+
const { attributeName, type, subType, isEditable, stringFormat, multiline } = attribute;
|
|
15730
15873
|
const value = row.properties[attributeName];
|
|
15731
15874
|
const handleChange = useCallback((next) => {
|
|
15732
15875
|
// Пустые правки отбрасываем: иначе строка становилась бы «изменённой» от простого
|
|
@@ -15745,16 +15888,21 @@ const TableCell = memo(({ attribute, row, canEdit, onChange }) => {
|
|
|
15745
15888
|
const withTime = useMemo(() => TIME_FORMAT.test(stringFormat?.format ?? ""), [stringFormat?.format]);
|
|
15746
15889
|
// `canEdit` — режим таблицы, `isEditable` — разрешение конкретного атрибута: правка требует обоих.
|
|
15747
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
|
+
}
|
|
15748
15896
|
// Логическое значение и на чтение показываем галкой, а не словами «true»/«false» —
|
|
15749
15897
|
// так же, как остальной дашборд рисует булевы атрибуты.
|
|
15750
15898
|
if (type === AttributeType.Boolean) {
|
|
15751
15899
|
return jsx(Checkbox, { checked: !!value, disabled: !editable, onChange: () => handleChange(!value) });
|
|
15752
15900
|
}
|
|
15753
15901
|
if (!editable) {
|
|
15754
|
-
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" }) }));
|
|
15755
15903
|
}
|
|
15756
15904
|
if (!editing) {
|
|
15757
|
-
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" }) }));
|
|
15758
15906
|
}
|
|
15759
15907
|
const renderEditor = () => {
|
|
15760
15908
|
if (type === AttributeType.DateTime) {
|
|
@@ -15765,9 +15913,31 @@ const TableCell = memo(({ attribute, row, canEdit, onChange }) => {
|
|
|
15765
15913
|
}
|
|
15766
15914
|
return (jsx(Input, { autoFocus: true, value: value === null || value === undefined ? "" : String(value), width: "100%", onChange: ({ target }) => handleChange(target.value) }));
|
|
15767
15915
|
};
|
|
15768
|
-
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() })] }));
|
|
15769
15917
|
});
|
|
15770
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
|
+
|
|
15771
15941
|
const ContainerLoading = () => (jsx(Flex, { alignContent: "center", justifyContent: "center", width: "100%", children: jsx(CircularProgress, { diameter: 1.5, mono: true }) }));
|
|
15772
15942
|
|
|
15773
15943
|
/**
|
|
@@ -15871,6 +16041,53 @@ const getTableBoxStyle = (width, height) => {
|
|
|
15871
16041
|
};
|
|
15872
16042
|
};
|
|
15873
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
|
+
|
|
15874
16091
|
/**
|
|
15875
16092
|
* Вид таблицы: колонки плюс локальная сортировка.
|
|
15876
16093
|
*
|
|
@@ -15883,6 +16100,9 @@ const getTableBoxStyle = (width, height) => {
|
|
|
15883
16100
|
*
|
|
15884
16101
|
* `width`/`height` — размер собственного бокса таблицы: он ограничивает её саму и прокручивает
|
|
15885
16102
|
* то, что не влезло. Место, отведённое контейнеру, задаётся его же опциями и живёт отдельно.
|
|
16103
|
+
*
|
|
16104
|
+
* Ширины колонок — тоже вид: конфиг задаёт стартовую, растянутая мышью живёт в
|
|
16105
|
+
* {@link useColumnResize} и в конфиг не возвращается.
|
|
15886
16106
|
*/
|
|
15887
16107
|
const useTableView = (elementConfig) => {
|
|
15888
16108
|
const context = useContext(StructuredDataContext);
|
|
@@ -15893,6 +16113,7 @@ const useTableView = (elementConfig) => {
|
|
|
15893
16113
|
const columns = useMemo(() => schema ?? [], [schema]);
|
|
15894
16114
|
const sortedRows = useMemo(() => sortRows(rows ?? [], sort, schema ?? []), [rows, schema, sort]);
|
|
15895
16115
|
const sizeCss = useMemo(() => getTableBoxStyle(width, height), [height, width]);
|
|
16116
|
+
const { columnWidths, onResizeStart, onResizeMove, onResizeEnd } = useColumnResize(columns);
|
|
15896
16117
|
const onSortToggle = useCallback((attributeName) => setSort(current => getNextSort(current, attributeName)), []);
|
|
15897
16118
|
return {
|
|
15898
16119
|
context,
|
|
@@ -15903,7 +16124,11 @@ const useTableView = (elementConfig) => {
|
|
|
15903
16124
|
sizeCss,
|
|
15904
16125
|
// Ширина задана — колонки по содержимому: лишнее уходит в прокрутку бокса, а не в многоточие.
|
|
15905
16126
|
contentWidth: width != null,
|
|
16127
|
+
columnWidths,
|
|
15906
16128
|
onSortToggle,
|
|
16129
|
+
onResizeStart,
|
|
16130
|
+
onResizeMove,
|
|
16131
|
+
onResizeEnd,
|
|
15907
16132
|
};
|
|
15908
16133
|
};
|
|
15909
16134
|
|
|
@@ -15915,7 +16140,7 @@ const useTableView = (elementConfig) => {
|
|
|
15915
16140
|
*/
|
|
15916
16141
|
const ElementTable = memo(({ elementConfig }) => {
|
|
15917
16142
|
const { t } = useGlobalContext();
|
|
15918
|
-
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);
|
|
15919
16144
|
// Узел таблицы держим состоянием, а не ref: отсечка должна встать сразу, как он появится.
|
|
15920
16145
|
const [table, setTable] = useState(null);
|
|
15921
16146
|
useHeadOverlap(table);
|
|
@@ -15931,10 +16156,10 @@ const ElementTable = memo(({ elementConfig }) => {
|
|
|
15931
16156
|
defaultValue: "Схема данных не задана",
|
|
15932
16157
|
}) }));
|
|
15933
16158
|
}
|
|
15934
|
-
return (jsx(TableBox, { "$sizeCss": sizeCss, children: jsxs(TableWrapper, { ref: setTable, "$contentWidth": contentWidth, children: [jsx(TableHead, { children:
|
|
15935
|
-
|
|
15936
|
-
|
|
15937
|
-
|
|
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))) })] }) }));
|
|
15938
16163
|
});
|
|
15939
16164
|
|
|
15940
16165
|
const TooltipIcon = styled(Icon).withConfig({ displayName: "TooltipIcon", componentId: "sc-1lkxudm" }) `
|