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