@evergis/react 4.0.73 → 4.0.76
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/FiltersContainer/components/TreeFilter.d.ts +3 -0
- package/dist/components/Dashboard/containers/FiltersContainer/hooks/useTreeFilter.d.ts +21 -0
- package/dist/components/Dashboard/containers/FiltersContainer/hooks/useTreeFilterData.d.ts +11 -0
- package/dist/components/Dashboard/containers/FiltersContainer/hooks/useTreeFilterSearch.d.ts +17 -0
- package/dist/components/Dashboard/containers/FiltersContainer/hooks/useTreeFilterSelection.d.ts +9 -0
- package/dist/components/Dashboard/containers/FiltersContainer/hooks/useTreeFilterTree.d.ts +15 -0
- package/dist/components/Dashboard/containers/FiltersContainer/types.d.ts +6 -0
- package/dist/components/Dashboard/containers/FiltersContainer/utils/treeFilterUtils.d.ts +25 -0
- package/dist/components/Dashboard/headers/FeatureCardDefaultHeader/styled.d.ts +2 -0
- package/dist/components/Dashboard/headers/components/HeaderLayerIcon.d.ts +3 -1
- package/dist/components/Dashboard/headers/components/styled.d.ts +1 -0
- package/dist/components/Dashboard/types.d.ts +18 -2
- package/dist/components/Dashboard/utils/applyTreeFilterToCondition.d.ts +12 -0
- package/dist/components/Dashboard/utils/getSelectedFilterValue.d.ts +1 -1
- package/dist/components/Dashboard/utils/index.d.ts +1 -0
- package/dist/index.js +438 -36
- package/dist/index.js.map +1 -1
- package/dist/react.esm.js +438 -38
- package/dist/react.esm.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4677,33 +4677,6 @@ const createConfigPage = (props) => {
|
|
|
4677
4677
|
};
|
|
4678
4678
|
};
|
|
4679
4679
|
|
|
4680
|
-
const getAttributesConfiguration = (layer) => {
|
|
4681
|
-
const layerAttributeConfiguration = layer?.configuration?.attributesConfiguration ?? {};
|
|
4682
|
-
const { geometryAttribute, idAttribute } = layerAttributeConfiguration;
|
|
4683
|
-
const emptyLayerDefinition = {
|
|
4684
|
-
attributes: [],
|
|
4685
|
-
geometryAttribute: geometryAttribute ?? GEOMETRY_ATTRIBUTE,
|
|
4686
|
-
idAttribute: idAttribute ?? DEFAULT_ID_ATTRIBUTE_NAME,
|
|
4687
|
-
titleAttribute: "",
|
|
4688
|
-
};
|
|
4689
|
-
if (!isLayerService(layer)) {
|
|
4690
|
-
return emptyLayerDefinition;
|
|
4691
|
-
}
|
|
4692
|
-
return {
|
|
4693
|
-
...emptyLayerDefinition,
|
|
4694
|
-
...layerAttributeConfiguration,
|
|
4695
|
-
attributes: layerAttributeConfiguration?.attributes ||
|
|
4696
|
-
emptyLayerDefinition.attributes,
|
|
4697
|
-
};
|
|
4698
|
-
};
|
|
4699
|
-
|
|
4700
|
-
const formatChartRelatedValue = (t, value, layerInfo, relatedAttributes) => {
|
|
4701
|
-
const layerDefinition = getAttributesConfiguration(layerInfo);
|
|
4702
|
-
const relatedAxis = relatedAttributes.find(({ chartAxis }) => chartAxis === "y");
|
|
4703
|
-
const attribute = layerDefinition.attributes[relatedAxis.attributeName];
|
|
4704
|
-
return attribute ? formatAttributeValue({ t, type: attribute.type, value, stringFormat: attribute.stringFormat }) : value;
|
|
4705
|
-
};
|
|
4706
|
-
|
|
4707
4680
|
const isEmptyValue = (value) => value === "" || value === null || value === undefined;
|
|
4708
4681
|
|
|
4709
4682
|
const checkIsDateFormat = (value) => value.startsWith("#'") && value.endsWith("'");
|
|
@@ -4733,7 +4706,59 @@ const formatConditionValue = ({ value, attributeType, defaultValue, checkQuotes
|
|
|
4733
4706
|
return checkQuotes && checkIfNeedQuotes(stringValue, attributeType) ? `'${stringValue}'` : stringValue;
|
|
4734
4707
|
};
|
|
4735
4708
|
|
|
4709
|
+
/**
|
|
4710
|
+
* Объектное значение иерархического фильтра "tree" («уровень → массив id»),
|
|
4711
|
+
* в отличие от скалярных/массивных значений остальных фильтров.
|
|
4712
|
+
*/
|
|
4713
|
+
const isTreeFilterValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
4714
|
+
/**
|
|
4715
|
+
* Подставляет плейсхолдеры уровней `%name.l{N}` значениями id соответствующего уровня
|
|
4716
|
+
* (формат `[id1,id2,...]` для оператора IN). Уровни, отсутствующие в значении (или пустые),
|
|
4717
|
+
* не подставляются — плейсхолдер остаётся нетронутым, как и любой пустой `%`-плейсхолдер.
|
|
4718
|
+
*/
|
|
4719
|
+
const applyTreeFilterToCondition = (condition, name, value, isSingle) => {
|
|
4720
|
+
const filterName = `${FILTER_PREFIX}${name}`;
|
|
4721
|
+
return Object.keys(value).reduce((result, level) => {
|
|
4722
|
+
const formatted = formatConditionValue({ value: value[level], checkQuotes: !isSingle });
|
|
4723
|
+
if (!formatted) {
|
|
4724
|
+
return result;
|
|
4725
|
+
}
|
|
4726
|
+
return result.replace(new RegExp(`${filterName}\\.${level}\\b`, "g"), formatted);
|
|
4727
|
+
}, condition);
|
|
4728
|
+
};
|
|
4729
|
+
|
|
4730
|
+
const getAttributesConfiguration = (layer) => {
|
|
4731
|
+
const layerAttributeConfiguration = layer?.configuration?.attributesConfiguration ?? {};
|
|
4732
|
+
const { geometryAttribute, idAttribute } = layerAttributeConfiguration;
|
|
4733
|
+
const emptyLayerDefinition = {
|
|
4734
|
+
attributes: [],
|
|
4735
|
+
geometryAttribute: geometryAttribute ?? GEOMETRY_ATTRIBUTE,
|
|
4736
|
+
idAttribute: idAttribute ?? DEFAULT_ID_ATTRIBUTE_NAME,
|
|
4737
|
+
titleAttribute: "",
|
|
4738
|
+
};
|
|
4739
|
+
if (!isLayerService(layer)) {
|
|
4740
|
+
return emptyLayerDefinition;
|
|
4741
|
+
}
|
|
4742
|
+
return {
|
|
4743
|
+
...emptyLayerDefinition,
|
|
4744
|
+
...layerAttributeConfiguration,
|
|
4745
|
+
attributes: layerAttributeConfiguration?.attributes ||
|
|
4746
|
+
emptyLayerDefinition.attributes,
|
|
4747
|
+
};
|
|
4748
|
+
};
|
|
4749
|
+
|
|
4750
|
+
const formatChartRelatedValue = (t, value, layerInfo, relatedAttributes) => {
|
|
4751
|
+
const layerDefinition = getAttributesConfiguration(layerInfo);
|
|
4752
|
+
const relatedAxis = relatedAttributes.find(({ chartAxis }) => chartAxis === "y");
|
|
4753
|
+
const attribute = layerDefinition.attributes[relatedAxis.attributeName];
|
|
4754
|
+
return attribute ? formatAttributeValue({ t, type: attribute.type, value, stringFormat: attribute.stringFormat }) : value;
|
|
4755
|
+
};
|
|
4756
|
+
|
|
4736
4757
|
const applyFiltersToCondition = ({ condition, name, defaultValue, filters, isSingle, isSetParams, }) => {
|
|
4758
|
+
const rawValue = filters[name] !== undefined ? filters[name].value : defaultValue;
|
|
4759
|
+
if (isTreeFilterValue(rawValue)) {
|
|
4760
|
+
return applyTreeFilterToCondition(condition, name, rawValue, isSingle);
|
|
4761
|
+
}
|
|
4737
4762
|
const hasFilter = filters[name] !== undefined && !!filters[name].value?.toString().length;
|
|
4738
4763
|
const min = formatConditionValue({
|
|
4739
4764
|
value: hasFilter ? filters[name].min : null,
|
|
@@ -8966,6 +8991,11 @@ const OverlayHeaderMixin = (overlay) => styled.css `
|
|
|
8966
8991
|
}
|
|
8967
8992
|
}
|
|
8968
8993
|
`;
|
|
8994
|
+
const NoFeatureHeaderMixin = styled.css `
|
|
8995
|
+
&&& {
|
|
8996
|
+
min-height: 5.5rem;
|
|
8997
|
+
}
|
|
8998
|
+
`;
|
|
8969
8999
|
const Header = styled(uilibGl.Flex) `
|
|
8970
9000
|
z-index: 1;
|
|
8971
9001
|
position: relative;
|
|
@@ -8978,6 +9008,8 @@ const Header = styled(uilibGl.Flex) `
|
|
|
8978
9008
|
${({ $isRow }) => $isRow && RowHeaderMixin};
|
|
8979
9009
|
|
|
8980
9010
|
${({ $overlay }) => $overlay && OverlayHeaderMixin($overlay)};
|
|
9011
|
+
|
|
9012
|
+
${({ $noFeature }) => $noFeature && NoFeatureHeaderMixin};
|
|
8981
9013
|
`;
|
|
8982
9014
|
const DefaultHeaderWrapper = styled.div `
|
|
8983
9015
|
width: ${({ withPadding }) => (withPadding ? "100%" : "calc(100% + 1rem)")};
|
|
@@ -8986,6 +9018,12 @@ const DefaultHeaderWrapper = styled.div `
|
|
|
8986
9018
|
background: url(${img$4}) 50% -2rem no-repeat;
|
|
8987
9019
|
border-radius: ${({ theme: { borderRadius } }) => borderRadius.medium};
|
|
8988
9020
|
|
|
9021
|
+
${({ $noFeature }) => $noFeature &&
|
|
9022
|
+
styled.css `
|
|
9023
|
+
background-position: center;
|
|
9024
|
+
background-size: cover;
|
|
9025
|
+
`};
|
|
9026
|
+
|
|
8989
9027
|
${({ fontColor }) => !!fontColor && HeaderFontColorMixin};
|
|
8990
9028
|
`;
|
|
8991
9029
|
|
|
@@ -8994,7 +9032,9 @@ const HeaderTitle = ({ noFeature }) => {
|
|
|
8994
9032
|
const { attributes, layerInfo, feature } = useWidgetContext(exports.WidgetType.FeatureCard);
|
|
8995
9033
|
const zoomToFeatures = useZoomToFeatures();
|
|
8996
9034
|
const { configuration } = layerInfo || {};
|
|
8997
|
-
const resultDescription =
|
|
9035
|
+
const resultDescription = noFeature
|
|
9036
|
+
? t("allLayers", { ns: "dashboard", defaultValue: "Все слои" })
|
|
9037
|
+
: configuration?.alias || configuration?.name || "";
|
|
8998
9038
|
const resultTitle = React.useMemo(() => {
|
|
8999
9039
|
const layerDefinitionAttribute = configuration && attributes?.length
|
|
9000
9040
|
? attributes.find(item => item.attributeName === configuration?.attributesConfiguration?.titleAttribute)
|
|
@@ -9075,14 +9115,32 @@ const LayerIconClickable = styled.div `
|
|
|
9075
9115
|
align-items: center;
|
|
9076
9116
|
cursor: pointer;
|
|
9077
9117
|
`;
|
|
9118
|
+
const NoFeatureIcon = styled.div `
|
|
9119
|
+
display: flex;
|
|
9120
|
+
align-items: center;
|
|
9121
|
+
justify-content: center;
|
|
9122
|
+
min-width: 1.5rem;
|
|
9123
|
+
height: 1.5rem;
|
|
9124
|
+
margin-top: 0.25rem;
|
|
9125
|
+
margin-right: 1rem;
|
|
9126
|
+
margin-left: 0.25rem;
|
|
9127
|
+
|
|
9128
|
+
${uilibGl.Icon}::after {
|
|
9129
|
+
font-size: 1.5rem;
|
|
9130
|
+
color: ${({ theme: { palette } }) => palette.iconDisabled};
|
|
9131
|
+
}
|
|
9132
|
+
`;
|
|
9078
9133
|
|
|
9079
|
-
const HeaderLayerIcon = () => {
|
|
9134
|
+
const HeaderLayerIcon = ({ noFeature }) => {
|
|
9080
9135
|
const { t } = useGlobalContext();
|
|
9081
9136
|
const { layerInfo, feature } = useWidgetContext(exports.WidgetType.FeatureCard);
|
|
9082
9137
|
const zoomToFeatures = useZoomToFeatures();
|
|
9083
9138
|
const getMaxZoomTo = useMaxZoomTo();
|
|
9084
9139
|
const [optionsMaxZoomTo, layerMaxZoomTo] = React.useMemo(() => getMaxZoomTo(layerInfo?.name), [layerInfo?.name, getMaxZoomTo]);
|
|
9085
9140
|
const handleIconClick = React.useCallback(() => zoomToFeatures([feature], { maxZoom: layerMaxZoomTo ?? optionsMaxZoomTo }), [zoomToFeatures, feature, layerMaxZoomTo, optionsMaxZoomTo]);
|
|
9141
|
+
if (noFeature) {
|
|
9142
|
+
return (jsxRuntime.jsx(NoFeatureIcon, { children: jsxRuntime.jsx(uilibGl.Icon, { kind: "no_geom" }) }));
|
|
9143
|
+
}
|
|
9086
9144
|
return (jsxRuntime.jsx(LayerIconClickable, { onClick: handleIconClick, children: jsxRuntime.jsx(uilibGl.Tooltip, { arrow: true, placement: "top", content: t("zoomToFeature", { ns: "dashboard", defaultValue: "Приблизить к объекту" }), delay: [600, 0], children: ref => (jsxRuntime.jsx(LayerIcon, { innerRef: ref, layerInfo: layerInfo })) }) }));
|
|
9087
9145
|
};
|
|
9088
9146
|
|
|
@@ -9091,7 +9149,7 @@ const FeatureCardDefaultHeader = ({ noFeature }) => {
|
|
|
9091
9149
|
const { header } = config || {};
|
|
9092
9150
|
const { options } = header || {};
|
|
9093
9151
|
const { themeName, withPadding, column, height, overlay } = options || {};
|
|
9094
|
-
return (jsxRuntime.jsx(DefaultHeaderWrapper, { withPadding: withPadding, height: height, children: jsxRuntime.jsx(uilibGl.ThemeProvider, { theme: getThemeByName(themeName), children: jsxRuntime.jsx(Header, { "$overlay": overlay, "$isRow": !column, children: jsxRuntime.jsxs(HeaderFrontView, { isDefault: !column, children: [jsxRuntime.jsxs(HeaderContainer, { children: [jsxRuntime.jsx(HeaderLayerIcon, {}), jsxRuntime.jsx(HeaderTitle, { noFeature: noFeature })] }), jsxRuntime.jsx(FeatureCardButtons, {})] }) }) }) }));
|
|
9152
|
+
return (jsxRuntime.jsx(DefaultHeaderWrapper, { withPadding: withPadding, height: height, "$noFeature": noFeature, children: jsxRuntime.jsx(uilibGl.ThemeProvider, { theme: getThemeByName(themeName), children: jsxRuntime.jsx(Header, { "$overlay": overlay, "$isRow": !column, "$noFeature": noFeature, children: jsxRuntime.jsxs(HeaderFrontView, { isDefault: !column, children: [jsxRuntime.jsxs(HeaderContainer, { children: [jsxRuntime.jsx(HeaderLayerIcon, { noFeature: noFeature }), jsxRuntime.jsx(HeaderTitle, { noFeature: noFeature })] }), jsxRuntime.jsx(FeatureCardButtons, {})] }) }) }) }));
|
|
9095
9153
|
};
|
|
9096
9154
|
|
|
9097
9155
|
const ImageContainerButton = styled(uilibGl.FlatButton) `
|
|
@@ -11310,7 +11368,7 @@ const ChipsFilter = ({ type, filter, elementConfig, }) => {
|
|
|
11310
11368
|
icon: chipIcon,
|
|
11311
11369
|
};
|
|
11312
11370
|
});
|
|
11313
|
-
}, [configFilter, dataSources,
|
|
11371
|
+
}, [variants, configFilter, dataSources, iconAttribute, colorAttribute, theme]);
|
|
11314
11372
|
const isChipActive = React.useCallback((chipValue) => {
|
|
11315
11373
|
const currentValue = filters?.[filterName]?.value;
|
|
11316
11374
|
if (currentValue === undefined) {
|
|
@@ -11386,6 +11444,339 @@ function getIconFromAttribute(option, iconAttribute) {
|
|
|
11386
11444
|
return getResourceUrl(iconValue);
|
|
11387
11445
|
}
|
|
11388
11446
|
|
|
11447
|
+
const MAX_ANCESTOR_DEPTH = 50;
|
|
11448
|
+
const buildLevelOneCondition = (attributeLevel) => `${attributeLevel} = 1`;
|
|
11449
|
+
const buildChildrenCondition = (attributeParentValue, parentId) => `${attributeParentValue} = ${formatConditionValue({ value: parentId })}`;
|
|
11450
|
+
const buildSearchCondition = (attributeAlias, query) => `${attributeAlias} LIKE '%${query.replace(/'/g, "''")}%'`;
|
|
11451
|
+
const buildIdsCondition = (attributeValue, ids) => `${attributeValue} in ${formatConditionValue({ value: ids })}`;
|
|
11452
|
+
/** Группирует выбранные id по уровню в объектное значение фильтра `{ l{N}: [ids] }`. */
|
|
11453
|
+
const buildFilterValue = (selected) => {
|
|
11454
|
+
const result = {};
|
|
11455
|
+
selected.forEach((level, id) => {
|
|
11456
|
+
const key = `l${level}`;
|
|
11457
|
+
if (!result[key]) {
|
|
11458
|
+
result[key] = [];
|
|
11459
|
+
}
|
|
11460
|
+
result[key].push(id);
|
|
11461
|
+
});
|
|
11462
|
+
return result;
|
|
11463
|
+
};
|
|
11464
|
+
/** Разбирает объектное значение фильтра в карту `id → level`. */
|
|
11465
|
+
const parseFilterValue = (value) => {
|
|
11466
|
+
const result = new Map();
|
|
11467
|
+
if (!value) {
|
|
11468
|
+
return result;
|
|
11469
|
+
}
|
|
11470
|
+
Object.keys(value).forEach(levelKey => {
|
|
11471
|
+
const level = Number(levelKey.replace(/^l/, ""));
|
|
11472
|
+
value[levelKey]?.forEach(id => result.set(id, level));
|
|
11473
|
+
});
|
|
11474
|
+
return result;
|
|
11475
|
+
};
|
|
11476
|
+
/**
|
|
11477
|
+
* Дозапрашивает цепочку предков вверх по `parentId` уровень за уровнем (батч `id in (...)`),
|
|
11478
|
+
* пока не достигнет уровня 1. Возвращает исходные узлы вместе с предками.
|
|
11479
|
+
*/
|
|
11480
|
+
const collectAncestors = async (nodes, loadByIds) => {
|
|
11481
|
+
const byId = new Map();
|
|
11482
|
+
nodes.forEach(node => byId.set(node.id, node));
|
|
11483
|
+
let frontier = nodes;
|
|
11484
|
+
for (let depth = 0; depth < MAX_ANCESTOR_DEPTH; depth++) {
|
|
11485
|
+
const missingParents = new Set();
|
|
11486
|
+
frontier.forEach(node => {
|
|
11487
|
+
const { parentId, level } = node.data ?? {};
|
|
11488
|
+
if (parentId != null && level > 1 && !byId.has(parentId)) {
|
|
11489
|
+
missingParents.add(parentId);
|
|
11490
|
+
}
|
|
11491
|
+
});
|
|
11492
|
+
if (!missingParents.size) {
|
|
11493
|
+
break;
|
|
11494
|
+
}
|
|
11495
|
+
const parents = await loadByIds([...missingParents]);
|
|
11496
|
+
if (!parents.length) {
|
|
11497
|
+
break;
|
|
11498
|
+
}
|
|
11499
|
+
parents.forEach(parent => byId.set(parent.id, parent));
|
|
11500
|
+
frontier = parents;
|
|
11501
|
+
}
|
|
11502
|
+
return [...byId.values()];
|
|
11503
|
+
};
|
|
11504
|
+
/**
|
|
11505
|
+
* Собирает плоский набор узлов в дерево по `parentId`. Узлы с подгруженными потомками
|
|
11506
|
+
* раскрываются; листья-папки без потомков получают `children = null` (ленивая подгрузка).
|
|
11507
|
+
*/
|
|
11508
|
+
const assembleTree = (nodes) => {
|
|
11509
|
+
const nodesById = new Map();
|
|
11510
|
+
nodes.forEach(node => nodesById.set(node.id, node));
|
|
11511
|
+
const childrenMap = new Map();
|
|
11512
|
+
nodesById.forEach(node => {
|
|
11513
|
+
const parentId = node.data?.parentId;
|
|
11514
|
+
if (parentId != null && nodesById.has(parentId)) {
|
|
11515
|
+
const siblings = childrenMap.get(parentId) ?? [];
|
|
11516
|
+
siblings.push(node);
|
|
11517
|
+
childrenMap.set(parentId, siblings);
|
|
11518
|
+
}
|
|
11519
|
+
});
|
|
11520
|
+
const roots = [];
|
|
11521
|
+
const expanded = [];
|
|
11522
|
+
nodesById.forEach(node => {
|
|
11523
|
+
const kids = childrenMap.get(node.id);
|
|
11524
|
+
if (kids?.length) {
|
|
11525
|
+
node.children = kids;
|
|
11526
|
+
expanded.push(node.id);
|
|
11527
|
+
}
|
|
11528
|
+
else if (node.isLeaf) {
|
|
11529
|
+
node.children = null;
|
|
11530
|
+
}
|
|
11531
|
+
const parentId = node.data?.parentId;
|
|
11532
|
+
if (parentId == null || !nodesById.has(parentId)) {
|
|
11533
|
+
roots.push(node);
|
|
11534
|
+
}
|
|
11535
|
+
});
|
|
11536
|
+
return { roots, expanded };
|
|
11537
|
+
};
|
|
11538
|
+
|
|
11539
|
+
const useTreeFilterData = (type, filterName) => {
|
|
11540
|
+
const { api } = useGlobalContext();
|
|
11541
|
+
const { currentPage } = useWidgetPage(type);
|
|
11542
|
+
const configFilter = React.useMemo(() => getConfigFilter(filterName, currentPage?.filters), [filterName, currentPage?.filters]);
|
|
11543
|
+
const { relatedDataSource, attributeValue = DEFAULT_ATTRIBUTE_NAME, attributeAlias = DEFAULT_ATTRIBUTE_NAME, attributeParentValue, attributeLevel, attributeHasChildren, limit = DEFAULT_DATA_SOURCE_LIMIT, } = configFilter ?? {};
|
|
11544
|
+
const layerName = React.useMemo(() => currentPage?.dataSources?.find(({ name }) => name === relatedDataSource)?.layerName, [currentPage?.dataSources, relatedDataSource]);
|
|
11545
|
+
const mapNode = React.useCallback(({ properties }) => {
|
|
11546
|
+
const hasChildren = attributeHasChildren ? !!properties[attributeHasChildren] : false;
|
|
11547
|
+
const data = {
|
|
11548
|
+
parentId: attributeParentValue ? properties[attributeParentValue] ?? null : null,
|
|
11549
|
+
level: attributeLevel ? Number(properties[attributeLevel]) : 1,
|
|
11550
|
+
};
|
|
11551
|
+
return {
|
|
11552
|
+
id: properties[attributeValue],
|
|
11553
|
+
text: String(properties[attributeAlias] ?? ""),
|
|
11554
|
+
isLeaf: hasChildren,
|
|
11555
|
+
children: hasChildren ? null : undefined,
|
|
11556
|
+
data,
|
|
11557
|
+
};
|
|
11558
|
+
}, [attributeValue, attributeAlias, attributeParentValue, attributeLevel, attributeHasChildren]);
|
|
11559
|
+
const runQuery = React.useCallback(async (condition) => {
|
|
11560
|
+
if (!layerName) {
|
|
11561
|
+
return [];
|
|
11562
|
+
}
|
|
11563
|
+
const response = await api.layers.getFeatures(layerName, { withGeom: false, limit, conditions: [condition] });
|
|
11564
|
+
return response.features ?? [];
|
|
11565
|
+
}, [api, layerName, limit]);
|
|
11566
|
+
const loadLevelOne = React.useCallback(async () => {
|
|
11567
|
+
if (!attributeLevel) {
|
|
11568
|
+
return [];
|
|
11569
|
+
}
|
|
11570
|
+
const features = await runQuery(buildLevelOneCondition(attributeLevel));
|
|
11571
|
+
return features.map(mapNode);
|
|
11572
|
+
}, [attributeLevel, runQuery, mapNode]);
|
|
11573
|
+
const loadChildren = React.useCallback(async (item) => {
|
|
11574
|
+
if (!attributeParentValue) {
|
|
11575
|
+
return { children: [] };
|
|
11576
|
+
}
|
|
11577
|
+
const features = await runQuery(buildChildrenCondition(attributeParentValue, item.id));
|
|
11578
|
+
return { children: features.map(mapNode) };
|
|
11579
|
+
}, [attributeParentValue, runQuery, mapNode]);
|
|
11580
|
+
const searchNodes = React.useCallback(async (query) => {
|
|
11581
|
+
const features = await runQuery(buildSearchCondition(attributeAlias, query));
|
|
11582
|
+
return features.map(mapNode);
|
|
11583
|
+
}, [attributeAlias, runQuery, mapNode]);
|
|
11584
|
+
const loadByIds = React.useCallback(async (ids) => {
|
|
11585
|
+
if (!ids.length) {
|
|
11586
|
+
return [];
|
|
11587
|
+
}
|
|
11588
|
+
const features = await runQuery(buildIdsCondition(attributeValue, ids));
|
|
11589
|
+
return features.map(mapNode);
|
|
11590
|
+
}, [attributeValue, runQuery, mapNode]);
|
|
11591
|
+
return { configFilter, layerName, loadLevelOne, loadChildren, searchNodes, loadByIds };
|
|
11592
|
+
};
|
|
11593
|
+
|
|
11594
|
+
const SEARCH_MIN_LENGTH = 3;
|
|
11595
|
+
const SEARCH_DEBOUNCE = 300;
|
|
11596
|
+
const useTreeFilterSearch = ({ searchNodes, loadByIds }) => {
|
|
11597
|
+
const [searchValue, setSearchValue] = React.useState("");
|
|
11598
|
+
const [searchData, setSearchData] = React.useState([]);
|
|
11599
|
+
const [searchExpanded, setSearchExpanded] = React.useState([]);
|
|
11600
|
+
const [searching, setSearching] = React.useState(false);
|
|
11601
|
+
const timer = React.useRef();
|
|
11602
|
+
const requestId = React.useRef(0);
|
|
11603
|
+
const isSearchActive = searchValue.trim().length >= SEARCH_MIN_LENGTH;
|
|
11604
|
+
const runSearch = React.useCallback(async (query) => {
|
|
11605
|
+
const id = ++requestId.current;
|
|
11606
|
+
setSearching(true);
|
|
11607
|
+
try {
|
|
11608
|
+
const results = await searchNodes(query);
|
|
11609
|
+
const withAncestors = await collectAncestors(results, loadByIds);
|
|
11610
|
+
const { roots, expanded } = assembleTree(withAncestors);
|
|
11611
|
+
if (id !== requestId.current) {
|
|
11612
|
+
return;
|
|
11613
|
+
}
|
|
11614
|
+
setSearchData(roots);
|
|
11615
|
+
setSearchExpanded(expanded);
|
|
11616
|
+
}
|
|
11617
|
+
finally {
|
|
11618
|
+
if (id === requestId.current) {
|
|
11619
|
+
setSearching(false);
|
|
11620
|
+
}
|
|
11621
|
+
}
|
|
11622
|
+
}, [searchNodes, loadByIds]);
|
|
11623
|
+
const clearSearchState = React.useCallback(() => {
|
|
11624
|
+
requestId.current++;
|
|
11625
|
+
setSearchData([]);
|
|
11626
|
+
setSearchExpanded([]);
|
|
11627
|
+
setSearching(false);
|
|
11628
|
+
}, []);
|
|
11629
|
+
const onSearchChange = React.useCallback((value) => {
|
|
11630
|
+
setSearchValue(value);
|
|
11631
|
+
if (timer.current) {
|
|
11632
|
+
clearTimeout(timer.current);
|
|
11633
|
+
}
|
|
11634
|
+
const query = value.trim();
|
|
11635
|
+
if (query.length < SEARCH_MIN_LENGTH) {
|
|
11636
|
+
clearSearchState();
|
|
11637
|
+
return;
|
|
11638
|
+
}
|
|
11639
|
+
timer.current = setTimeout(() => runSearch(query), SEARCH_DEBOUNCE);
|
|
11640
|
+
}, [runSearch, clearSearchState]);
|
|
11641
|
+
const resetSearch = React.useCallback(() => {
|
|
11642
|
+
if (timer.current) {
|
|
11643
|
+
clearTimeout(timer.current);
|
|
11644
|
+
}
|
|
11645
|
+
setSearchValue("");
|
|
11646
|
+
clearSearchState();
|
|
11647
|
+
}, [clearSearchState]);
|
|
11648
|
+
return {
|
|
11649
|
+
searchValue,
|
|
11650
|
+
isSearchActive,
|
|
11651
|
+
searching,
|
|
11652
|
+
searchData,
|
|
11653
|
+
setSearchData,
|
|
11654
|
+
searchExpanded,
|
|
11655
|
+
setSearchExpanded,
|
|
11656
|
+
onSearchChange,
|
|
11657
|
+
resetSearch,
|
|
11658
|
+
};
|
|
11659
|
+
};
|
|
11660
|
+
|
|
11661
|
+
const useTreeFilterSelection = (type, filterName, configFilter) => {
|
|
11662
|
+
const { filters, changeFilters } = useWidgetContext(type);
|
|
11663
|
+
const [selected, setSelected] = React.useState(() => parseFilterValue(configFilter?.defaultValue));
|
|
11664
|
+
const [selectedLabel, setSelectedLabel] = React.useState();
|
|
11665
|
+
const selectedIds = React.useMemo(() => new Set(selected.keys()), [selected]);
|
|
11666
|
+
const commit = React.useCallback((next) => {
|
|
11667
|
+
setSelected(next);
|
|
11668
|
+
changeFilters({ [filterName]: { value: buildFilterValue(next) } }, configFilter?.resetFilters);
|
|
11669
|
+
}, [changeFilters, filterName, configFilter?.resetFilters]);
|
|
11670
|
+
const onToggleSelect = React.useCallback((item, multiSelect) => {
|
|
11671
|
+
const level = item.data?.level ?? 1;
|
|
11672
|
+
const next = new Map(selected);
|
|
11673
|
+
if (next.has(item.id)) {
|
|
11674
|
+
next.delete(item.id);
|
|
11675
|
+
if (!multiSelect) {
|
|
11676
|
+
setSelectedLabel(undefined);
|
|
11677
|
+
}
|
|
11678
|
+
}
|
|
11679
|
+
else {
|
|
11680
|
+
if (!multiSelect) {
|
|
11681
|
+
next.clear();
|
|
11682
|
+
setSelectedLabel(item.text);
|
|
11683
|
+
}
|
|
11684
|
+
next.set(item.id, level);
|
|
11685
|
+
}
|
|
11686
|
+
commit(next);
|
|
11687
|
+
}, [selected, commit]);
|
|
11688
|
+
const onReset = React.useCallback(() => {
|
|
11689
|
+
setSelectedLabel(undefined);
|
|
11690
|
+
commit(new Map());
|
|
11691
|
+
}, [commit]);
|
|
11692
|
+
const initRef = React.useRef(false);
|
|
11693
|
+
React.useEffect(() => {
|
|
11694
|
+
if (initRef.current) {
|
|
11695
|
+
return;
|
|
11696
|
+
}
|
|
11697
|
+
initRef.current = true;
|
|
11698
|
+
if (selected.size && filters[filterName] === undefined) {
|
|
11699
|
+
changeFilters({ [filterName]: { value: buildFilterValue(selected) } });
|
|
11700
|
+
}
|
|
11701
|
+
}, [filters, filterName, changeFilters, selected]);
|
|
11702
|
+
return { selected, selectedIds, selectedLabel, onToggleSelect, onReset };
|
|
11703
|
+
};
|
|
11704
|
+
|
|
11705
|
+
const useTreeFilterTree = ({ selected, loadLevelOne, loadByIds }) => {
|
|
11706
|
+
const [treeData, setTreeData] = React.useState([]);
|
|
11707
|
+
const [treeExpanded, setTreeExpanded] = React.useState([]);
|
|
11708
|
+
const [loading, setLoading] = React.useState(false);
|
|
11709
|
+
const loadedRef = React.useRef(false);
|
|
11710
|
+
const selectedRef = React.useRef(selected);
|
|
11711
|
+
selectedRef.current = selected;
|
|
11712
|
+
const onOpen = React.useCallback(async () => {
|
|
11713
|
+
if (loadedRef.current) {
|
|
11714
|
+
return;
|
|
11715
|
+
}
|
|
11716
|
+
loadedRef.current = true;
|
|
11717
|
+
setLoading(true);
|
|
11718
|
+
try {
|
|
11719
|
+
const roots = await loadLevelOne();
|
|
11720
|
+
const preselected = [...selectedRef.current.keys()];
|
|
11721
|
+
if (!preselected.length) {
|
|
11722
|
+
setTreeData(roots);
|
|
11723
|
+
return;
|
|
11724
|
+
}
|
|
11725
|
+
const selectedNodes = await loadByIds(preselected);
|
|
11726
|
+
const withAncestors = await collectAncestors([...roots, ...selectedNodes], loadByIds);
|
|
11727
|
+
const { roots: assembled, expanded } = assembleTree(withAncestors);
|
|
11728
|
+
setTreeData(assembled);
|
|
11729
|
+
setTreeExpanded(expanded);
|
|
11730
|
+
}
|
|
11731
|
+
finally {
|
|
11732
|
+
setLoading(false);
|
|
11733
|
+
}
|
|
11734
|
+
}, [loadLevelOne, loadByIds]);
|
|
11735
|
+
return { treeData, setTreeData, treeExpanded, setTreeExpanded, loading, onOpen };
|
|
11736
|
+
};
|
|
11737
|
+
|
|
11738
|
+
const useTreeFilter = (type, filter) => {
|
|
11739
|
+
const { filterName, multiSelect, placeholder, width } = filter.options ?? {};
|
|
11740
|
+
const { configFilter, loadLevelOne, loadChildren, searchNodes, loadByIds } = useTreeFilterData(type, filterName ?? "");
|
|
11741
|
+
const { selected, selectedIds, selectedLabel, onToggleSelect: toggleSelect, onReset } = useTreeFilterSelection(type, filterName ?? "", configFilter);
|
|
11742
|
+
const { treeData, setTreeData, treeExpanded, setTreeExpanded, loading, onOpen } = useTreeFilterTree({
|
|
11743
|
+
selected,
|
|
11744
|
+
loadLevelOne,
|
|
11745
|
+
loadByIds,
|
|
11746
|
+
});
|
|
11747
|
+
const search = useTreeFilterSearch({ searchNodes, loadByIds });
|
|
11748
|
+
const onToggleSelect = React.useCallback(item => toggleSelect(item, multiSelect), [toggleSelect, multiSelect]);
|
|
11749
|
+
const onClose = React.useCallback(() => search.resetSearch(), [search]);
|
|
11750
|
+
const view = React.useMemo(() => search.isSearchActive
|
|
11751
|
+
? { data: search.searchData, setData: search.setSearchData, expanded: search.searchExpanded, setExpanded: search.setSearchExpanded }
|
|
11752
|
+
: { data: treeData, setData: setTreeData, expanded: treeExpanded, setExpanded: setTreeExpanded }, [search.isSearchActive, search.searchData, search.setSearchData, search.searchExpanded, search.setSearchExpanded, treeData, setTreeData, treeExpanded, setTreeExpanded]);
|
|
11753
|
+
return {
|
|
11754
|
+
width: width ? `${width}px` : undefined,
|
|
11755
|
+
placeholder,
|
|
11756
|
+
multiSelect,
|
|
11757
|
+
loading: loading || search.searching,
|
|
11758
|
+
data: view.data,
|
|
11759
|
+
setData: view.setData,
|
|
11760
|
+
expanded: view.expanded,
|
|
11761
|
+
setExpanded: view.setExpanded,
|
|
11762
|
+
onLoadChildren: loadChildren,
|
|
11763
|
+
selectedIds,
|
|
11764
|
+
selectedCount: selectedIds.size,
|
|
11765
|
+
selectedLabel,
|
|
11766
|
+
searchValue: search.searchValue,
|
|
11767
|
+
onSearchChange: search.onSearchChange,
|
|
11768
|
+
onOpen,
|
|
11769
|
+
onClose,
|
|
11770
|
+
onToggleSelect,
|
|
11771
|
+
onReset,
|
|
11772
|
+
};
|
|
11773
|
+
};
|
|
11774
|
+
|
|
11775
|
+
const TreeFilter = ({ type, filter }) => {
|
|
11776
|
+
const { width, placeholder, multiSelect, loading, data, setData, expanded, setExpanded, onLoadChildren, selectedIds, selectedCount, selectedLabel, searchValue, onSearchChange, onOpen, onClose, onToggleSelect, onReset, } = useTreeFilter(type, filter);
|
|
11777
|
+
return (jsxRuntime.jsx(uilibGl.TreeDropdown, { width: width, placeholder: placeholder, multiSelect: multiSelect, loading: loading, data: data, setData: setData, expanded: expanded, setExpanded: setExpanded, onLoadChildren: onLoadChildren, selectedIds: selectedIds, selectedCount: selectedCount, selectedLabel: selectedLabel, searchValue: searchValue, onSearchChange: onSearchChange, onOpen: onOpen, onClose: onClose, onToggleSelect: onToggleSelect, onReset: onReset }));
|
|
11778
|
+
};
|
|
11779
|
+
|
|
11389
11780
|
const getFilterComponent = (filterType) => {
|
|
11390
11781
|
switch (filterType) {
|
|
11391
11782
|
case "checkbox":
|
|
@@ -11400,6 +11791,8 @@ const getFilterComponent = (filterType) => {
|
|
|
11400
11791
|
return TextFilter;
|
|
11401
11792
|
case "chips":
|
|
11402
11793
|
return ChipsFilter;
|
|
11794
|
+
case "tree":
|
|
11795
|
+
return TreeFilter;
|
|
11403
11796
|
case "dropdown":
|
|
11404
11797
|
default:
|
|
11405
11798
|
return DropdownFilter;
|
|
@@ -12372,11 +12765,18 @@ const useDataSources = ({ type: widgetType, config, attributes, filters, layerPa
|
|
|
12372
12765
|
]);
|
|
12373
12766
|
const getUpdatingDataSources = React.useCallback(() => {
|
|
12374
12767
|
const diffFilterNames = filters
|
|
12375
|
-
? Object.keys(filters).filter(key =>
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
|
|
12768
|
+
? Object.keys(filters).filter(key => {
|
|
12769
|
+
const { value } = filters[key];
|
|
12770
|
+
const prevValue = prevFilters.current[key]?.value;
|
|
12771
|
+
if (isTreeFilterValue(value) || isTreeFilterValue(prevValue)) {
|
|
12772
|
+
return JSON.stringify(value) !== JSON.stringify(prevValue);
|
|
12773
|
+
}
|
|
12774
|
+
if (Array.isArray(value)) {
|
|
12775
|
+
return (value.length !== prevValue?.length ||
|
|
12776
|
+
value.some((item, index) => item !== prevValue?.[index]));
|
|
12777
|
+
}
|
|
12778
|
+
return value !== prevValue;
|
|
12779
|
+
})
|
|
12380
12780
|
: [];
|
|
12381
12781
|
if (!diffFilterNames.length)
|
|
12382
12782
|
return;
|
|
@@ -14076,6 +14476,7 @@ exports.addDataSources = addDataSources;
|
|
|
14076
14476
|
exports.adjustColor = adjustColor;
|
|
14077
14477
|
exports.applyFiltersToCondition = applyFiltersToCondition;
|
|
14078
14478
|
exports.applyQueryFilters = applyQueryFilters;
|
|
14479
|
+
exports.applyTreeFilterToCondition = applyTreeFilterToCondition;
|
|
14079
14480
|
exports.applyVarsToCondition = applyVarsToCondition;
|
|
14080
14481
|
exports.asAttributeName = asAttributeName;
|
|
14081
14482
|
exports.asChartId = asChartId;
|
|
@@ -14160,6 +14561,7 @@ exports.isNotValidSelectedTab = isNotValidSelectedTab;
|
|
|
14160
14561
|
exports.isNumeric = isNumeric;
|
|
14161
14562
|
exports.isObject = isObject;
|
|
14162
14563
|
exports.isProxyService = isProxyService;
|
|
14564
|
+
exports.isTreeFilterValue = isTreeFilterValue;
|
|
14163
14565
|
exports.isVisibleContainer = isVisibleContainer;
|
|
14164
14566
|
exports.metersPerPixel = metersPerPixel;
|
|
14165
14567
|
exports.numberOptions = numberOptions;
|