@evergis/react 4.0.161 → 4.0.162

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.
Files changed (39) hide show
  1. package/dist/components/Dashboard/componentTypes.d.ts +6 -6
  2. package/dist/components/Dashboard/constants.d.ts +25 -3
  3. package/dist/components/Dashboard/containers/FiltersContainer/utils/getFilterWidth.d.ts +2 -3
  4. package/dist/components/Dashboard/elements/ElementControl/hooks/useFirstNonEmptyValue.d.ts +6 -0
  5. package/dist/components/Dashboard/elements/ElementModal/hooks/useModalContent.d.ts +2 -2
  6. package/dist/components/Dashboard/elements/ElementTable/utils/tableBox.d.ts +2 -2
  7. package/dist/components/Dashboard/hooks/index.d.ts +2 -0
  8. package/dist/components/Dashboard/hooks/useConfigStringSources.d.ts +8 -0
  9. package/dist/components/Dashboard/hooks/useDashboardFilterStore.d.ts +9 -0
  10. package/dist/components/Dashboard/hooks/useEqualTileWidth.d.ts +1 -1
  11. package/dist/components/Dashboard/hooks/useWidgetPage.d.ts +3 -0
  12. package/dist/components/Dashboard/hooks/useWrapperSize.d.ts +3 -3
  13. package/dist/components/Dashboard/styled/containers.d.ts +5 -6
  14. package/dist/components/Dashboard/styled/index.d.ts +2 -3
  15. package/dist/components/Dashboard/types.d.ts +5 -2
  16. package/dist/components/Dashboard/utils/applyQueryFilters.d.ts +4 -3
  17. package/dist/components/Dashboard/utils/chartAxis.d.ts +1 -1
  18. package/dist/components/Dashboard/utils/configString/formatConditionString.d.ts +18 -0
  19. package/dist/components/Dashboard/utils/configString/formatters.d.ts +16 -0
  20. package/dist/components/Dashboard/utils/configString/getFilterTokenValue.d.ts +13 -0
  21. package/dist/components/Dashboard/utils/configString/index.d.ts +10 -0
  22. package/dist/components/Dashboard/utils/configString/isDashboardReference.d.ts +2 -0
  23. package/dist/components/Dashboard/utils/configString/referencesFilters.d.ts +11 -0
  24. package/dist/components/Dashboard/utils/configString/resolveConfigToken.d.ts +3 -0
  25. package/dist/components/Dashboard/utils/configString/resolveElementStrings.d.ts +8 -0
  26. package/dist/components/Dashboard/utils/configString/resolveFilterDefaults.d.ts +8 -0
  27. package/dist/components/Dashboard/utils/configString/tokenizeConfigString.d.ts +5 -0
  28. package/dist/components/Dashboard/utils/configString/types.d.ts +78 -0
  29. package/dist/components/Dashboard/utils/formatDataSourceCondition.d.ts +16 -23
  30. package/dist/components/Dashboard/utils/getChangedFilterNames.d.ts +8 -0
  31. package/dist/components/Dashboard/utils/getPageFilters.d.ts +3 -0
  32. package/dist/components/Dashboard/utils/getRenderElement.d.ts +1 -1
  33. package/dist/components/Dashboard/utils/index.d.ts +4 -0
  34. package/dist/components/Dashboard/utils/selectUpdatingDataSources.d.ts +21 -0
  35. package/dist/index.js +900 -559
  36. package/dist/index.js.map +1 -1
  37. package/dist/react.esm.js +879 -560
  38. package/dist/react.esm.js.map +1 -1
  39. package/package.json +11 -4
package/dist/index.js CHANGED
@@ -14,9 +14,9 @@ var wkt = require('wkt');
14
14
  var turf = require('@turf/turf');
15
15
  var uuid = require('uuid');
16
16
  var signalr = require('@microsoft/signalr');
17
+ var findAnd = require('find-and');
17
18
  var MapboxDraw = require('@mapbox/mapbox-gl-draw');
18
19
  var react = require('swiper/react');
19
- var findAnd = require('find-and');
20
20
  var ReactMarkdown = require('react-markdown');
21
21
  var rehypeRaw = require('rehype-raw');
22
22
  var rehypeSanitize = require('rehype-sanitize');
@@ -2104,7 +2104,7 @@ function tweenValue(transition, name, value) {
2104
2104
  };
2105
2105
  }
2106
2106
 
2107
- function interpolate(a, b) {
2107
+ function interpolate$1(a, b) {
2108
2108
  var c;
2109
2109
  return (typeof b === "number" ? interpolateNumber
2110
2110
  : b instanceof color ? interpolateRgb
@@ -2179,7 +2179,7 @@ function attrFunctionNS(fullname, interpolate, value) {
2179
2179
  }
2180
2180
 
2181
2181
  function transition_attr(name, value) {
2182
- var fullname = namespace(name), i = fullname === "transform" ? interpolateTransformSvg : interpolate;
2182
+ var fullname = namespace(name), i = fullname === "transform" ? interpolateTransformSvg : interpolate$1;
2183
2183
  return this.attrTween(name, typeof value === "function"
2184
2184
  ? (fullname.local ? attrFunctionNS : attrFunction)(fullname, i, tweenValue(this, "attr." + name, value))
2185
2185
  : value == null ? (fullname.local ? attrRemoveNS : attrRemove)(fullname)
@@ -2487,7 +2487,7 @@ function styleMaybeRemove(id, name) {
2487
2487
  }
2488
2488
 
2489
2489
  function transition_style(name, value, priority) {
2490
- var i = (name += "") === "transform" ? interpolateTransformCss : interpolate;
2490
+ var i = (name += "") === "transform" ? interpolateTransformCss : interpolate$1;
2491
2491
  return value == null ? this
2492
2492
  .styleTween(name, styleNull(name, i))
2493
2493
  .on("end.style." + name, styleRemove(name))
@@ -3428,9 +3428,42 @@ const PROJECT_PROPS = [PROJECT_ALIAS_PROP, PROJECT_NAME_PROP, ""];
3428
3428
  const PROVIDER_PREFIX = "$";
3429
3429
  exports.ProviderPrefix = void 0;
3430
3430
  (function (ProviderPrefix) {
3431
+ /** `$card:<слой>:<поле>` — атрибут объекта карточки. */
3431
3432
  ProviderPrefix["Card"] = "card";
3432
- ProviderPrefix["Left"] = "left";
3433
+ /**
3434
+ * Ближайший дашборд, в ряд к `%` своего виджета: `$dashboard:<фильтр>[.свойство]` — его фильтр,
3435
+ * `$dashboard:<датасорс>:<поле>` — поле первой строки его датасорса.
3436
+ */
3437
+ ProviderPrefix["Dashboard"] = "dashboard";
3433
3438
  })(exports.ProviderPrefix || (exports.ProviderPrefix = {}));
3439
+ /**
3440
+ * Имя слоя или датасорса в подстановке: буквы, цифры, `_`, `.` и `-`. Более широкий класс захватил бы
3441
+ * соседний токен: `$dashboard:a.min–$dashboard:a.max` читался бы как датасорс `a.min–$dashboard`.
3442
+ */
3443
+ const CONFIG_SOURCE_NAME = "[\\w.-]+";
3444
+ /**
3445
+ * Все подстановки строки конфига одной грамматикой, по группам:
3446
+ * 1–2 `$dashboard:<датасорс>:<поле>`, 3–4 `$dashboard:<фильтр>[.свойство]`, 5–6 `$card:<слой>:<поле>`,
3447
+ * 7–8 `%<фильтр>[.свойство]`, 9 `{<атрибут>}`. Форма датасорса проверяется раньше формы фильтра:
3448
+ * их различает второе двоеточие. Имя фильтра — `\w+` целиком, поэтому `%name` не цепляет начало
3449
+ * `%name2`, а точка в конце предложения («за %year.») не считается свойством.
3450
+ */
3451
+ const CONFIG_TOKEN_REGEXP = new RegExp([
3452
+ `\\${PROVIDER_PREFIX}${exports.ProviderPrefix.Dashboard}:(${CONFIG_SOURCE_NAME}):(\\w+)`,
3453
+ `\\${PROVIDER_PREFIX}${exports.ProviderPrefix.Dashboard}:(\\w+)(?:\\.(\\w+))?`,
3454
+ `\\${PROVIDER_PREFIX}${exports.ProviderPrefix.Card}:(${CONFIG_SOURCE_NAME}):(\\w+)`,
3455
+ `${FILTER_PREFIX}(\\w+)(?:\\.(\\w+))?`,
3456
+ "\\{([^{}\\s]+)\\}",
3457
+ ].join("|"), "g");
3458
+ /** Свойства границ диапазона: `%name.min` / `%name.max`. */
3459
+ const RANGE_PROPS = ["min", "max"];
3460
+ /** Разделитель элементов массива, подставленного в текст UI. */
3461
+ const CONFIG_TEXT_ARRAY_SEPARATOR = ", ";
3462
+ /**
3463
+ * Подписи в `options` элемента, которые проходят через подстановки строк конфига. Список явный:
3464
+ * служебные строки (`attributeName`, `chartId`, `relatedDataSource`, ...) подстановкам не подлежат.
3465
+ */
3466
+ const CONFIG_STRING_OPTION_KEYS = ["label", "placeholder", "title"];
3434
3467
  const FILTERED_VALUE_OPACITY = 28;
3435
3468
  const DEFAULT_ATTRIBUTE_NAME = "name";
3436
3469
  const DEFAULT_DATA_SOURCE_LIMIT = 100;
@@ -3485,8 +3518,8 @@ const BASE_CONTAINER_STYLE = {
3485
3518
  /**
3486
3519
  * Маркер тела контейнера внутри `ContainerRoot` — содержимое без заголовка.
3487
3520
  *
3488
- * Нужен, чтобы находить тело по DOM снаружи: `#<id контейнера>` теперь указывает на корень,
3489
- * то есть на пару «заголовок + тело».
3521
+ * Нужен, чтобы находить тело по DOM снаружи: `#<id контейнера>` указывает на корень, то есть на
3522
+ * пару «заголовок + тело».
3490
3523
  */
3491
3524
  const CONTAINER_BODY_ATTRIBUTE = "data-container-body";
3492
3525
  /**
@@ -3873,6 +3906,8 @@ const addDataSources = (dashboardConfiguration, pageIndex, layerNames) => {
3873
3906
  return newConfig.children;
3874
3907
  };
3875
3908
 
3909
+ const isEmptyValue = (value) => value === "" || value === null || value === undefined;
3910
+
3876
3911
  const transparentizeColor = (originalColor, percent) => {
3877
3912
  const color = new color$1.Color(originalColor);
3878
3913
  color.a = Math.round((255 / 100) * percent);
@@ -4212,7 +4247,7 @@ const formatDateValue = (stringFormat, dateValue) => {
4212
4247
  const timeOption = getValueFromOptions(timeOptions, stringFormat.format);
4213
4248
  const timeOptionValue = timeOption && (isRuLocale ? timeOption.value : `${timeOption.value} aa`);
4214
4249
  const timeFormat = (timeOptionValue && dateFns.format(value, showTimeZone ? `${timeOptionValue} zz` : timeOptionValue)) || "";
4215
- return dateFormat ? `${dateFormat} ${timeFormat}` : "";
4250
+ return dateFormat ? [dateFormat, timeFormat].filter(Boolean).join(" ") : "";
4216
4251
  };
4217
4252
  const appendUnitsLabel = (value, unitsLabel, noUnits) => (unitsLabel && !noUnits ? `${value} ${unitsLabel}` : value)?.toString() || "";
4218
4253
  const formatNumberValue = (stringFormat, value, type, noUnits = false) => {
@@ -4233,8 +4268,8 @@ const formatNumberValue = (stringFormat, value, type, noUnits = false) => {
4233
4268
  if (typeof compactValue === "string" && compactValue) {
4234
4269
  return appendUnitsLabel(compactValue, unitsLabel, noUnits);
4235
4270
  }
4236
- if (hasRounding && (!isIntValue || !isDefaultScaling)) {
4237
- currentValue = currentValue && Number(currentValue).toFixed(rounding);
4271
+ if (hasRounding && (!isIntValue || !isDefaultScaling) && !lodash.isNil(currentValue)) {
4272
+ currentValue = Number(currentValue).toFixed(rounding);
4238
4273
  }
4239
4274
  if (splitDigitGroup) {
4240
4275
  currentValue = numberWithSpaces(currentValue);
@@ -4355,6 +4390,49 @@ const metersPerPixel = (zoom, lat) => {
4355
4390
  return (156543.03 * Math.cos(lat * (Math.PI / 180))) / 2 ** zoom;
4356
4391
  };
4357
4392
 
4393
+ const checkIsDateFormat = (value) => value.startsWith("#'") && value.endsWith("'");
4394
+ const checkIfNeedQuotes = (value, attributeType) => {
4395
+ return attributeType
4396
+ ? !NUMERIC_ATTRIBUTE_TYPES.includes(attributeType)
4397
+ : typeof value === "number"
4398
+ ? false
4399
+ : typeof value === "string" && !isNumeric(value) && !checkIsDateFormat(value);
4400
+ };
4401
+ const formatConditionValue = ({ value, attributeType, defaultValue, checkQuotes = true, isSetParams = false, }) => {
4402
+ if (isEmptyValue(value))
4403
+ return "";
4404
+ const formattedValue = Array.isArray(defaultValue) && !Array.isArray(value) ? [value] : value;
4405
+ const isArray = Array.isArray(formattedValue);
4406
+ let stringValue = formattedValue.toString();
4407
+ if (isArray) {
4408
+ return formattedValue.length && !formattedValue.every(item => !item)
4409
+ ? `[${formattedValue
4410
+ .map(item => (checkQuotes && checkIfNeedQuotes(item, attributeType) ? `'${item}'` : item))
4411
+ .join(",")}]`
4412
+ : "";
4413
+ }
4414
+ if (isSetParams && checkIsDateFormat(stringValue)) {
4415
+ stringValue = stringValue.slice(2, -1);
4416
+ }
4417
+ return checkQuotes && checkIfNeedQuotes(stringValue, attributeType) ? `'${stringValue}'` : stringValue;
4418
+ };
4419
+
4420
+ /**
4421
+ * Значение системной подстановки текущего проекта. Голый `%project` и `%project.name` дают
4422
+ * системное имя, `%project.alias` — алиас с фолбэком на имя: пустой алиас не должен уводить
4423
+ * запрос в пустое значение. Чужое свойство (`%project.foo`) не наше — вернём `undefined`,
4424
+ * и плейсхолдер останется нетронутым, как у незаполненного фильтра.
4425
+ */
4426
+ const getProjectValue = ({ prop, projectName, projectAlias, }) => {
4427
+ if (prop && prop !== PROJECT_NAME_PROP && prop !== PROJECT_ALIAS_PROP) {
4428
+ return undefined;
4429
+ }
4430
+ if (prop === PROJECT_ALIAS_PROP) {
4431
+ return projectAlias || projectName || undefined;
4432
+ }
4433
+ return projectName || undefined;
4434
+ };
4435
+
4358
4436
  /** Ключ уровня в значении tree-фильтра: `l1`, `l2`, ... */
4359
4437
  const LEVEL_KEY = /^l\d+$/;
4360
4438
  /**
@@ -4404,22 +4482,6 @@ const getDataSourceFilterValue = ({ filterName, filterProp, attributeAlias, data
4404
4482
  return feature?.properties?.[filterProp];
4405
4483
  };
4406
4484
 
4407
- /**
4408
- * Значение системной подстановки текущего проекта. Голый `%project` и `%project.name` дают
4409
- * системное имя, `%project.alias` — алиас с фолбэком на имя: пустой алиас не должен уводить
4410
- * запрос в пустое значение. Чужое свойство (`%project.foo`) не наше — вернём `undefined`,
4411
- * и плейсхолдер останется нетронутым, как у незаполненного фильтра.
4412
- */
4413
- const getProjectValue = ({ prop, projectName, projectAlias, }) => {
4414
- if (prop && prop !== PROJECT_NAME_PROP && prop !== PROJECT_ALIAS_PROP) {
4415
- return undefined;
4416
- }
4417
- if (prop === PROJECT_ALIAS_PROP) {
4418
- return projectAlias || projectName || undefined;
4419
- }
4420
- return projectName || undefined;
4421
- };
4422
-
4423
4485
  const getSelectedFilterValue = (filterName, selectedFilters, defaultValue) => {
4424
4486
  return ((!lodash.isNil(selectedFilters?.[filterName]?.value) &&
4425
4487
  Array.isArray(defaultValue) &&
@@ -4428,124 +4490,370 @@ const getSelectedFilterValue = (filterName, selectedFilters, defaultValue) => {
4428
4490
  : selectedFilters?.[filterName]?.value) ?? defaultValue);
4429
4491
  };
4430
4492
 
4431
- const applyQueryFilters = ({ parameters: configParameters, filters: configFilters, selectedFilters, geometry, extent, zoomLevel, projectName, projectAlias, attributes, layerInfo, dataSources, projectDataSources, }) => {
4493
+ const known = (value, defaultValue) => ({ known: true, value, defaultValue });
4494
+ const getRangeBound = (prop, defaultValue, scalarDefault) => {
4495
+ if (Array.isArray(defaultValue)) {
4496
+ return defaultValue[prop === "min" ? 0 : 1];
4497
+ }
4498
+ return scalarDefault ? defaultValue : undefined;
4499
+ };
4500
+ /** `.prop` — свойство строки `relatedDataSource`, выбранной фильтром. */
4501
+ const getRelatedValue = ({ name, prop, store, configFilter, defaultValue }) => getDataSourceFilterValue({
4502
+ filterName: name,
4503
+ filterProp: prop,
4504
+ selectedFilters: store.selectedFilters ?? {},
4505
+ attributeAlias: configFilter?.attributeAlias,
4506
+ dataSource: getDataSource(configFilter?.relatedDataSource, store.dataSources),
4507
+ }) ?? defaultValue;
4508
+ const getTreeLevel = (value, prop) => {
4509
+ const level = value[prop];
4510
+ return lodash.isNil(level) || !level.length ? undefined : level;
4511
+ };
4512
+ /** Параметры запроса и текст UI: пустое (`null`/`undefined`) значение падает в дефолт. */
4513
+ const getValueModeToken = (context) => {
4514
+ const { name, prop, store, configFilter, selected, defaultValue } = context;
4515
+ if (RANGE_PROPS.includes(prop)) {
4516
+ return known(selected?.[prop] ?? getRangeBound(prop, defaultValue, true));
4517
+ }
4518
+ if (prop) {
4519
+ return isTreeFilterValue(selected?.value, configFilter)
4520
+ ? known(getTreeLevel(selected.value, prop))
4521
+ : known(getRelatedValue(context));
4522
+ }
4523
+ return known(getSelectedFilterValue(name, store.selectedFilters, defaultValue), defaultValue);
4524
+ };
4525
+ /** Условие слоя: выбранное пустое значение не подставляется, объектные значения — только уровнями tree. */
4526
+ const getConditionModeToken = (context) => {
4527
+ const { prop, configFilter, selected, defaultValue } = context;
4528
+ const rawValue = selected !== undefined ? selected.value : defaultValue;
4529
+ if (configFilter?.valueType === "features" || isFeaturesFilterValue(rawValue)) {
4530
+ return { known: false };
4531
+ }
4532
+ if (isTreeFilterValue(rawValue, configFilter)) {
4533
+ return prop ? known(getTreeLevel(rawValue, prop)) : { known: false };
4534
+ }
4535
+ const hasValue = selected !== undefined && !!selected.value?.toString().length;
4536
+ if (RANGE_PROPS.includes(prop)) {
4537
+ return known(hasValue ? selected[prop] : getRangeBound(prop, defaultValue, false));
4538
+ }
4539
+ if (prop) {
4540
+ return known(getRelatedValue(context));
4541
+ }
4542
+ return known(hasValue ? selected.value : selected === undefined ? defaultValue : "", defaultValue);
4543
+ };
4544
+ /**
4545
+ * Значение токена фильтра (`%name[.prop]`, `$dashboard:name[.prop]`) из переданного стора.
4546
+ * Фильтр, о котором стор ничего не знает, — `known: false`: токен остаётся в строке как есть.
4547
+ */
4548
+ const getFilterTokenValue = (params) => {
4549
+ const { name, store, mode } = params;
4550
+ const configFilter = getConfigFilter(name, store.configFilters);
4551
+ const selected = store.selectedFilters?.[name];
4552
+ if (!configFilter && (mode === "condition" || selected === undefined)) {
4553
+ return { known: false };
4554
+ }
4555
+ const context = { ...params, configFilter, selected, defaultValue: configFilter?.defaultValue };
4556
+ return mode === "condition" ? getConditionModeToken(context) : getValueModeToken(context);
4557
+ };
4558
+
4559
+ const UNKNOWN = { known: false };
4560
+ /** Стор `$dashboard:`. Без отдельного стора строка живёт на самом дашборде — это стор виджета. */
4561
+ const getDashboardStore = ({ dashboard, widget }) => dashboard ?? widget ?? {};
4562
+ const isValidGeometry = (geometry) => !!geometry && !geometry.includes("()") && geometry.endsWith(")");
4563
+ /**
4564
+ * Системные подстановки `%geometry`, `%extent`, `%zoom`, `%project[.name|.alias]` — значения из
4565
+ * `GlobalContext`, а не из фильтров. Пока значения нет, `geometry` / `extent` / `zoom` уступают
4566
+ * одноимённому фильтру; имя `project` зарезервировано целиком.
4567
+ */
4568
+ const resolveSystemToken = ({ name, prop }, sources) => {
4569
+ const { geometry, extent, zoomLevel, projectName, projectAlias } = sources;
4570
+ if (name === GEOMETRY_FILTER_NAME && !prop && isValidGeometry(geometry)) {
4571
+ return { known: true, value: geometry, system: "quoted" };
4572
+ }
4573
+ if (name === EXTENT_FILTER_NAME && !prop && extent) {
4574
+ return { known: true, value: extent, system: "quoted" };
4575
+ }
4576
+ if (name === ZOOM_FILTER_NAME && !prop && !lodash.isNil(zoomLevel)) {
4577
+ return { known: true, value: zoomLevel, system: "number" };
4578
+ }
4579
+ if (name === PROJECT_FILTER_NAME) {
4580
+ const value = getProjectValue({ prop, projectName, projectAlias });
4581
+ return lodash.isNil(value) ? UNKNOWN : { known: true, value, system: "quoted" };
4582
+ }
4583
+ return null;
4584
+ };
4585
+ const resolveFilterToken = (token, sources, mode) => {
4586
+ const { scope, name, prop } = token;
4587
+ if (scope === "widget") {
4588
+ const system = resolveSystemToken(token, sources);
4589
+ if (system)
4590
+ return system;
4591
+ }
4592
+ const store = scope === "dashboard" ? getDashboardStore(sources) : sources.widget ?? {};
4593
+ return getFilterTokenValue({ name, prop, store, mode });
4594
+ };
4595
+ /** `$card:<слой>:<поле>` — атрибут объекта карточки, если карточка открыта на этом слое. */
4596
+ const resolveCardToken = ({ layerName, fieldName }, { attributes, layerInfo }) => {
4597
+ if (!attributes || layerInfo?.name !== layerName)
4598
+ return UNKNOWN;
4599
+ const attribute = attributes.find(({ attributeName }) => attributeName === fieldName);
4600
+ return { known: true, value: attribute?.value ?? null, attributeType: attribute?.type };
4601
+ };
4602
+ /**
4603
+ * `$dashboard:<датасорс>:<поле>` — поле первой строки датасорса дашборда (по имени, затем по
4604
+ * `layerName`). Пока датасорс не загружен, значение пустое: токен не должен мелькать в тексте.
4605
+ */
4606
+ const resolveDataSourceToken = ({ dataSourceName, fieldName }, sources) => {
4607
+ const { dataSources } = getDashboardStore(sources);
4608
+ const dataSource = dataSources?.find(({ name }) => name === dataSourceName) ??
4609
+ dataSources?.find(({ layerName }) => layerName === dataSourceName);
4610
+ return { known: true, value: dataSource?.features?.[0]?.properties?.[fieldName] ?? null };
4611
+ };
4612
+ /** `{name}` — атрибут объекта; пустой атрибут даёт пустую строку, незнакомое имя остаётся как есть. */
4613
+ const resolveAttributeToken = ({ name }, { attributes }) => {
4614
+ const attribute = attributes?.find(({ attributeName }) => attributeName === name);
4615
+ return attribute ? { known: true, value: attribute.value ?? "", attributeType: attribute.type } : UNKNOWN;
4616
+ };
4617
+ /** Сырое значение токена строки конфига — без форматирования под параметр, условие или текст. */
4618
+ const resolveConfigToken = (token, sources, mode) => {
4619
+ if (token.kind === "filter")
4620
+ return resolveFilterToken(token, sources, mode);
4621
+ if (token.kind === "card")
4622
+ return resolveCardToken(token, sources);
4623
+ if (token.kind === "dataSource")
4624
+ return resolveDataSourceToken(token, sources);
4625
+ return resolveAttributeToken(token, sources);
4626
+ };
4627
+
4628
+ const toToken = ([raw, ...groups]) => {
4629
+ const [dataSourceName, dataSourceField, dashboardFilter, dashboardProp, cardLayer, cardField, filterName, filterProp, attributeName,] = groups;
4630
+ if (dataSourceName) {
4631
+ return { kind: "dataSource", dataSourceName, fieldName: dataSourceField, raw };
4632
+ }
4633
+ if (dashboardFilter) {
4634
+ return { kind: "filter", scope: "dashboard", name: dashboardFilter, prop: dashboardProp, raw };
4635
+ }
4636
+ if (cardLayer) {
4637
+ return { kind: "card", layerName: cardLayer, fieldName: cardField, raw };
4638
+ }
4639
+ if (filterName) {
4640
+ return { kind: "filter", scope: "widget", name: filterName, prop: filterProp, raw };
4641
+ }
4642
+ return { kind: "attribute", name: attributeName, raw };
4643
+ };
4644
+ /** Разбирает строку конфига на литералы и токены подстановок — в исходном порядке. */
4645
+ const tokenizeConfigString = (value) => {
4646
+ const segments = [];
4647
+ let lastIndex = 0;
4648
+ for (const match of value.matchAll(CONFIG_TOKEN_REGEXP)) {
4649
+ if (match.index > lastIndex) {
4650
+ segments.push(value.slice(lastIndex, match.index));
4651
+ }
4652
+ segments.push(toToken(match));
4653
+ lastIndex = match.index + match[0].length;
4654
+ }
4655
+ if (lastIndex < value.length) {
4656
+ segments.push(value.slice(lastIndex));
4657
+ }
4658
+ return segments;
4659
+ };
4660
+ /** Строка целиком — один токен: тогда значение уходит как есть, с типом (число, массив). */
4661
+ const getSingleToken = (segments) => segments.length === 1 && typeof segments[0] !== "string" ? segments[0] : null;
4662
+
4663
+ /** Склейка сегментов: токен заменяется своим форматом, незнакомый — остаётся как написан. */
4664
+ const joinConfigSegments = (segments, formatToken) => segments.map(segment => (typeof segment === "string" ? segment : formatToken(segment))).join("");
4665
+ const hasTokens = (segments) => segments.some(segment => typeof segment !== "string");
4666
+ const toParameterText = (value) => (lodash.isNil(value) ? "" : String(value));
4667
+ const toUiText = (value) => {
4668
+ if (lodash.isNil(value) || isObject(value) && !(value instanceof Date))
4669
+ return "";
4670
+ if (Array.isArray(value)) {
4671
+ return value
4672
+ .map(toUiText)
4673
+ .filter(item => item !== "")
4674
+ .join(CONFIG_TEXT_ARRAY_SEPARATOR);
4675
+ }
4676
+ return String(value);
4677
+ };
4678
+ const interpolate = (segments, sources, toText) => joinConfigSegments(segments, token => {
4679
+ const resolved = resolveConfigToken(token, sources, "value");
4680
+ return resolved.known ? toText(resolved.value) : token.raw;
4681
+ });
4682
+ /**
4683
+ * Значение параметра запроса. Строка-токен целиком уходит сырым значением с типом, а пустой или
4684
+ * незнакомый фильтр не уходит вовсе (`undefined`). Токены внутри строки интерполируются текстом.
4685
+ */
4686
+ const resolveQueryParameter = (value, sources) => {
4687
+ if (typeof value !== "string")
4688
+ return value;
4689
+ const segments = tokenizeConfigString(value);
4690
+ const token = getSingleToken(segments);
4691
+ if (token) {
4692
+ const resolved = resolveConfigToken(token, sources, "value");
4693
+ // Незнакомое имя в фигурных скобках — не подстановка, а литерал (JSON, шаблон сервиса).
4694
+ if (!resolved.known && token.kind === "attribute")
4695
+ return value;
4696
+ return resolved.known && !lodash.isNil(resolved.value) ? resolved.value : undefined;
4697
+ }
4698
+ return hasTokens(segments) ? interpolate(segments, sources, toParameterText) : value;
4699
+ };
4700
+ /**
4701
+ * Строка конфига для UI (значение элемента, дефолт, подпись). Строка-токен целиком отдаёт сырое
4702
+ * значение (число, массив — например, дефолт мультиселекта); пустое — пустая строка. Внутри текста
4703
+ * массив склеивается через `", "`. Незнакомый токен (опечатка, литерал `50%off`) остаётся как написан.
4704
+ */
4705
+ const resolveConfigString = (value, sources) => {
4706
+ if (typeof value !== "string" || !value)
4707
+ return value;
4708
+ const segments = tokenizeConfigString(value);
4709
+ const token = getSingleToken(segments);
4710
+ if (token) {
4711
+ const resolved = resolveConfigToken(token, sources, "value");
4712
+ if (!resolved.known)
4713
+ return value;
4714
+ return lodash.isNil(resolved.value) || isObject(resolved.value) && !(resolved.value instanceof Date) ? "" : resolved.value;
4715
+ }
4716
+ return hasTokens(segments) ? interpolate(segments, sources, toUiText) : value;
4717
+ };
4718
+
4719
+ const SET_PARAMS_REGEXP = /\$\([^)]+\)/g;
4720
+ const CONDITION_SPLITTER = " AND ";
4721
+ const formatResolvedToken = (resolved, isSetParams) => {
4722
+ if (!resolved.known)
4723
+ return "";
4724
+ const { value, system, attributeType, defaultValue } = resolved;
4725
+ if (system === "quoted")
4726
+ return `'${value}'`;
4727
+ if (system === "number")
4728
+ return String(value);
4729
+ return formatConditionValue({
4730
+ value: (attributeType ? value?.toString() : value),
4731
+ attributeType,
4732
+ defaultValue,
4733
+ isSetParams,
4734
+ });
4735
+ };
4736
+ /**
4737
+ * Eql-параметры слоя подставляются по голому имени. Имя внутри `$(имя=` прячется на время замены,
4738
+ * чтобы не задеть объявление set-параметра.
4739
+ */
4740
+ const applyEqlParameters = (section, { eqlParameters, layerParams }) => {
4741
+ if (!eqlParameters || !layerParams)
4742
+ return section;
4743
+ return Object.keys(eqlParameters).reduce((result, param) => result
4744
+ .replace(new RegExp(`\\$\\(${param}=`), "***")
4745
+ .replace(new RegExp(param), formatConditionValue({ value: layerParams[param] }))
4746
+ .replace(/\*\*\*/, `$(${param}=`), section);
4747
+ };
4748
+ /**
4749
+ * Одна секция условия: все токены в формате условия. Пустое значение фильтра оставляет токен как
4750
+ * есть; пустой атрибут объекта (`{name}`) заменяется пустой строкой.
4751
+ */
4752
+ const formatConditionSection = (section, sources, { isSetParams = false, ...eql } = {}) => {
4753
+ const formatted = joinConfigSegments(tokenizeConfigString(section), (token) => {
4754
+ const resolved = resolveConfigToken(token, sources, "condition");
4755
+ const value = formatResolvedToken(resolved, isSetParams);
4756
+ return value || (token.kind === "attribute" && resolved.known ? "" : token.raw);
4757
+ });
4758
+ return applyEqlParameters(formatted, eql);
4759
+ };
4760
+ /**
4761
+ * Условие слоя: ведущие set-параметры `$(имя=…)` форматируются отдельно (даты без `#'…'`),
4762
+ * остаток — по секциям `AND`.
4763
+ */
4764
+ const formatConditionString = (condition, sources, eql = {}) => {
4765
+ const setParams = condition.match(SET_PARAMS_REGEXP) ?? [];
4766
+ const setParamsSection = setParams.map(section => formatConditionSection(section, sources, { ...eql, isSetParams: true }));
4767
+ const conditionSection = condition
4768
+ .substring(setParams.join("").length)
4769
+ .split(CONDITION_SPLITTER)
4770
+ .map(section => formatConditionSection(section, sources, eql));
4771
+ if (!setParamsSection.length)
4772
+ return conditionSection.join(CONDITION_SPLITTER);
4773
+ return conditionSection.length
4774
+ ? [setParamsSection.join(""), conditionSection.join(CONDITION_SPLITTER)].join(" ")
4775
+ : setParamsSection.join("");
4776
+ };
4777
+
4778
+ const DASHBOARD_TOKEN_PREFIX = `${PROVIDER_PREFIX}${exports.ProviderPrefix.Dashboard}:`;
4779
+ /** Строка ссылается на фильтр дашборда (`$dashboard:`). */
4780
+ const isDashboardReference = (value) => typeof value === "string" && value.includes(DASHBOARD_TOKEN_PREFIX);
4781
+
4782
+ /**
4783
+ * Строка конфига ссылается на один из фильтров: `%name[.prop]` — на фильтр виджета,
4784
+ * `$dashboard:name[.prop]` — на фильтр дашборда. На самом дашборде (`hasDashboardStore = false`)
4785
+ * `$dashboard:` — те же фильтры виджета.
4786
+ */
4787
+ const referencesFilters = ({ value, widgetNames, dashboardNames, hasDashboardStore, }) => typeof value === "string" &&
4788
+ tokenizeConfigString(value).some(segment => {
4789
+ if (typeof segment === "string" || segment.kind !== "filter")
4790
+ return false;
4791
+ const names = segment.scope === "dashboard" && hasDashboardStore ? dashboardNames : widgetNames;
4792
+ return names.includes(segment.name);
4793
+ });
4794
+
4795
+ /**
4796
+ * Узел конфига с подставленными строками: `value`, `defaultValue` и подписи из
4797
+ * {@link CONFIG_STRING_OPTION_KEYS}. Без подстановок узел возвращается тем же объектом —
4798
+ * мемоизированные контейнеры не перерисовываются зря.
4799
+ */
4800
+ const resolveElementStrings = (element, sources) => {
4801
+ if (!element || !sources)
4802
+ return element;
4803
+ const value = resolveConfigString(element.value, sources);
4804
+ const defaultValue = resolveConfigString(element.defaultValue, sources);
4805
+ const changedOptions = CONFIG_STRING_OPTION_KEYS.reduce((result, key) => {
4806
+ const option = element.options?.[key];
4807
+ const resolved = resolveConfigString(option, sources);
4808
+ return resolved === option ? result : { ...result, [key]: resolved };
4809
+ }, {});
4810
+ const hasChangedOptions = Object.keys(changedOptions).length > 0;
4811
+ if (value === element.value && defaultValue === element.defaultValue && !hasChangedOptions) {
4812
+ return element;
4813
+ }
4814
+ return {
4815
+ ...element,
4816
+ value,
4817
+ defaultValue,
4818
+ ...(hasChangedOptions && { options: { ...element.options, ...changedOptions } }),
4819
+ };
4820
+ };
4821
+
4822
+ /**
4823
+ * Дефолты фильтров виджета из фильтров дашборда: `defaultValue: "$dashboard:year"`. Резолвится
4824
+ * только `$dashboard:` — ссылки на собственные фильтры (`%name`) дали бы цикл и остаются как есть.
4825
+ * Дефолт живой: пока фильтр виджета не выбран, он следует за дашбордом, ручной выбор его перекрывает.
4826
+ */
4827
+ const resolveFilterDefaults = (configFilters, dashboard) => {
4828
+ if (!dashboard || !configFilters?.some(({ defaultValue }) => isDashboardReference(defaultValue))) {
4829
+ return configFilters;
4830
+ }
4831
+ return configFilters.map(filter => {
4832
+ if (!isDashboardReference(filter.defaultValue))
4833
+ return filter;
4834
+ const defaultValue = resolveQueryParameter(filter.defaultValue, { widget: {}, dashboard });
4835
+ return defaultValue === filter.defaultValue ? filter : { ...filter, defaultValue: defaultValue };
4836
+ });
4837
+ };
4838
+
4839
+ const applyQueryFilters = ({ parameters: configParameters, filters: configFilters, selectedFilters, dashboard, geometry, extent, zoomLevel, projectName, projectAlias, attributes, layerInfo, dataSources, }) => {
4432
4840
  if (!configParameters) {
4433
4841
  return {};
4434
4842
  }
4843
+ const sources = {
4844
+ widget: { selectedFilters, configFilters, dataSources },
4845
+ dashboard,
4846
+ geometry,
4847
+ extent,
4848
+ zoomLevel,
4849
+ projectName,
4850
+ projectAlias,
4851
+ attributes,
4852
+ layerInfo,
4853
+ };
4435
4854
  return Object.keys(configParameters).reduce((result, key) => {
4436
- if (typeof configParameters[key] === "string" && configParameters[key].startsWith(PROVIDER_PREFIX)) {
4437
- const raw = configParameters[key].slice(PROVIDER_PREFIX.length);
4438
- const [provider, layerName, fieldName] = raw.split(":");
4439
- if (provider === exports.ProviderPrefix.Card && fieldName && layerInfo.name === layerName && attributes) {
4440
- const attribute = attributes.find(({ attributeName }) => attributeName === fieldName);
4441
- const cardValue = attribute?.value ?? null;
4442
- if (lodash.isNil(cardValue))
4443
- return result;
4444
- return { ...result, [key]: cardValue };
4445
- }
4446
- if (provider === exports.ProviderPrefix.Left && layerName && fieldName && projectDataSources?.length) {
4447
- const dataSource = projectDataSources.find(({ layerName: name }) => name === layerName);
4448
- const leftValue = dataSource?.features?.[0]?.properties?.[fieldName] ?? null;
4449
- if (lodash.isNil(leftValue))
4450
- return result;
4451
- return { ...result, [key]: leftValue };
4452
- }
4453
- return result;
4454
- }
4455
- if (typeof configParameters[key] === "string" && configParameters[key].includes("{") && attributes?.length) {
4456
- const exactAttr = attributes.find(({ attributeName }) => configParameters[key] === `{${attributeName}}`);
4457
- if (exactAttr) {
4458
- return { ...result, [key]: exactAttr.value ?? "" };
4459
- }
4460
- let interpolated = configParameters[key];
4461
- attributes.forEach(({ attributeName, value: attrValue }) => {
4462
- interpolated = interpolated.replace(new RegExp(`\\{${attributeName}\\}`, "g"), attrValue?.toString() ?? "");
4463
- });
4464
- return { ...result, [key]: interpolated };
4465
- }
4466
- if (typeof configParameters[key] !== "string" || !configParameters[key].startsWith("%")) {
4467
- return {
4468
- ...result,
4469
- [key]: configParameters[key],
4470
- };
4471
- }
4472
- const filterFullName = configParameters[key].replace("%", "");
4473
- const [filterName, filterProp] = filterFullName.includes(".") ? filterFullName.split(".") : [filterFullName, null];
4474
- const configFilter = getConfigFilter(filterName, configFilters);
4475
- const { defaultValue, relatedDataSource, attributeAlias } = configFilter || {};
4476
- if (filterName === GEOMETRY_FILTER_NAME && geometry && !geometry.includes("()") && geometry.endsWith(")")) {
4477
- return {
4478
- ...result,
4479
- [key]: geometry,
4480
- };
4481
- }
4482
- // Системные фильтры вида карты: значение приходит из `GlobalContext`, а не из `SelectedFilters`,
4483
- // поэтому резолвятся до веток `.min` / `.max` / `.property` — как `geometry` выше.
4484
- if (filterName === EXTENT_FILTER_NAME && extent) {
4485
- return {
4486
- ...result,
4487
- [key]: extent,
4488
- };
4489
- }
4490
- // Единственная нестроковая системная подстановка: уровень зума уходит числом.
4491
- if (filterName === ZOOM_FILTER_NAME && !lodash.isNil(zoomLevel)) {
4492
- return {
4493
- ...result,
4494
- [key]: zoomLevel,
4495
- };
4496
- }
4497
- // Текущий проект: значение приходит из `GlobalContext`, а не из `SelectedFilters`. Резолвим
4498
- // до веток `.min` / `.max` / `.property`, как экстент и зум, — имя `project` зарезервировано.
4499
- if (filterName === PROJECT_FILTER_NAME) {
4500
- const projectValue = getProjectValue({ prop: filterProp, projectName, projectAlias });
4501
- if (lodash.isNil(projectValue)) {
4502
- return result;
4503
- }
4504
- return {
4505
- ...result,
4506
- [key]: projectValue,
4507
- };
4508
- }
4509
- if (configParameters[key].endsWith(".max")) {
4510
- return {
4511
- ...result,
4512
- [key]: selectedFilters?.[filterName]?.max ?? (Array.isArray(defaultValue) ? defaultValue[1] : defaultValue),
4513
- };
4514
- }
4515
- if (configParameters[key].endsWith(".min")) {
4516
- return {
4517
- ...result,
4518
- [key]: selectedFilters?.[filterName]?.min ?? (Array.isArray(defaultValue) ? defaultValue[0] : defaultValue),
4519
- };
4520
- }
4521
- if (configParameters[key].includes(".")) {
4522
- const selectedValue = selectedFilters?.[filterName]?.value;
4523
- if (isTreeFilterValue(selectedValue, configFilter)) {
4524
- const levelValue = filterProp ? selectedValue[filterProp] : undefined;
4525
- if (lodash.isNil(levelValue) || !levelValue.length) {
4526
- return result;
4527
- }
4528
- return { ...result, [key]: levelValue };
4529
- }
4530
- return {
4531
- ...result,
4532
- [key]: getDataSourceFilterValue({
4533
- filterName,
4534
- filterProp,
4535
- selectedFilters,
4536
- attributeAlias,
4537
- dataSource: getDataSource(relatedDataSource, dataSources),
4538
- }) ?? defaultValue,
4539
- };
4540
- }
4541
- const value = getSelectedFilterValue(filterName, selectedFilters, defaultValue);
4542
- if (lodash.isNil(value)) {
4543
- return result;
4544
- }
4545
- return {
4546
- ...result,
4547
- [key]: value,
4548
- };
4855
+ const value = resolveQueryParameter(configParameters[key], sources);
4856
+ return lodash.isUndefined(value) ? result : { ...result, [key]: value };
4549
4857
  }, {});
4550
4858
  };
4551
4859
 
@@ -4600,35 +4908,6 @@ const createConfigPage = (props) => {
4600
4908
  };
4601
4909
  };
4602
4910
 
4603
- const isEmptyValue = (value) => value === "" || value === null || value === undefined;
4604
-
4605
- const checkIsDateFormat = (value) => value.startsWith("#'") && value.endsWith("'");
4606
- const checkIfNeedQuotes = (value, attributeType) => {
4607
- return attributeType
4608
- ? !NUMERIC_ATTRIBUTE_TYPES.includes(attributeType)
4609
- : typeof value === "number"
4610
- ? false
4611
- : typeof value === "string" && !isNumeric(value) && !checkIsDateFormat(value);
4612
- };
4613
- const formatConditionValue = ({ value, attributeType, defaultValue, checkQuotes = true, isSetParams = false, }) => {
4614
- if (isEmptyValue(value))
4615
- return "";
4616
- const formattedValue = Array.isArray(defaultValue) && !Array.isArray(value) ? [value] : value;
4617
- const isArray = Array.isArray(formattedValue);
4618
- let stringValue = formattedValue.toString();
4619
- if (isArray) {
4620
- return formattedValue.length && !formattedValue.every(item => !item)
4621
- ? `[${formattedValue
4622
- .map(item => (checkQuotes && checkIfNeedQuotes(item, attributeType) ? `'${item}'` : item))
4623
- .join(",")}]`
4624
- : "";
4625
- }
4626
- if (isSetParams && checkIsDateFormat(stringValue)) {
4627
- stringValue = stringValue.slice(2, -1);
4628
- }
4629
- return checkQuotes && checkIfNeedQuotes(stringValue, attributeType) ? `'${stringValue}'` : stringValue;
4630
- };
4631
-
4632
4911
  /**
4633
4912
  * Подставляет плейсхолдеры уровней `%name.l{N}` значениями id соответствующего уровня
4634
4913
  * (формат `[id1,id2,...]` для оператора IN). Уровни, отсутствующие в значении (или пустые),
@@ -4676,10 +4955,10 @@ const isDevEnvironment = (() => {
4676
4955
  }
4677
4956
  })();
4678
4957
  /**
4679
- * Один раз на источник сообщает, что конфиг остался на устаревшей плоской форме.
4958
+ * Один раз на источник сообщает, что конфиг задаёт ось устаревшей плоской формой.
4680
4959
  *
4681
- * Без такого маркера момент «легаси закончилось» не наступает никогда, и временный слой
4682
- * совместимости становится постоянным.
4960
+ * Предупреждение показывает, какие конфиги ещё держатся на плоской форме, — без него её поддержку
4961
+ * нельзя убрать, не проверив все конфиги вручную.
4683
4962
  */
4684
4963
  const warnLegacyAxis = ({ dataSourceName, attributeName }) => {
4685
4964
  if (!isDevEnvironment)
@@ -4719,7 +4998,7 @@ const resolveAxis = (source) => {
4719
4998
  * Источник питает ось значений.
4720
4999
  *
4721
5000
  * Отсутствие `axis.type` означает `"y"`: конфиг без явного типа — самый частый случай,
4722
- * и раньше такой источник молча выпадал из графика.
5001
+ * и такой источник не должен выпадать из графика.
4723
5002
  */
4724
5003
  const isValueAxis = (source) => (resolveAxis(source).type ?? "y") === "y";
4725
5004
  /** Сторона оси; отсутствие `axis.side` означает `"left"`. */
@@ -4784,161 +5063,40 @@ const fetchQueryDescription = (api, { ds, query, parameters }) => {
4784
5063
  return request;
4785
5064
  };
4786
5065
 
4787
- const applyFiltersToCondition = ({ condition, name, defaultValue, filters, isSingle, isSetParams, configFilter, }) => {
4788
- const rawValue = filters[name] !== undefined ? filters[name].value : defaultValue;
4789
- // `features` живёт только в `parameters`: в условии значение прошло бы через
4790
- // `formatConditionValue` и выродилось в `[object Object]`. Плейсхолдер остаётся нетронутым —
4791
- // ровно как у любого незаполненного фильтра.
4792
- if (configFilter?.valueType === "features" || isFeaturesFilterValue(rawValue)) {
4793
- return condition;
4794
- }
4795
- if (isTreeFilterValue(rawValue, configFilter)) {
4796
- return applyTreeFilterToCondition(condition, name, rawValue, isSingle);
4797
- }
4798
- const hasFilter = filters[name] !== undefined && !!filters[name].value?.toString().length;
4799
- const filterName = `${FILTER_PREFIX}${name}`;
4800
- // Пока фильтр не применён, границы `%name.min` / `%name.max` берём из range-дефолта конфига
4801
- // (`defaultValue: [from, to]`) — иначе в условие ушёл бы весь массив вместо своей границы.
4802
- // Проверка плейсхолдеров обязательна: у array-фильтра массивный дефолт подставляется целиком.
4803
- const rangeDefault = !hasFilter &&
4804
- Array.isArray(defaultValue) &&
4805
- (condition.includes(`${filterName}.min`) || condition.includes(`${filterName}.max`))
4806
- ? defaultValue
4807
- : null;
4808
- const min = formatConditionValue({
4809
- value: hasFilter ? filters[name].min : rangeDefault?.[0] ?? null,
4810
- checkQuotes: !isSingle,
4811
- isSetParams,
4812
- });
4813
- const max = formatConditionValue({
4814
- value: hasFilter ? filters[name].max : rangeDefault?.[1] ?? null,
4815
- checkQuotes: !isSingle,
4816
- isSetParams,
4817
- });
4818
- const value = formatConditionValue({
4819
- value: hasFilter ? filters[name].value : filters[name] === undefined ? defaultValue : "",
4820
- defaultValue,
4821
- checkQuotes: !isSingle,
4822
- isSetParams,
4823
- });
4824
- const hasMin = min !== "" && min !== null && min !== undefined;
4825
- const hasMax = max !== "" && max !== null && max !== undefined;
4826
- let result = condition;
4827
- if (hasMin || hasMax) {
4828
- if (hasMin) {
4829
- result = result.replace(new RegExp(`${filterName}\\.min`), min);
4830
- }
4831
- if (hasMax) {
4832
- result = result.replace(new RegExp(`${filterName}\\.max`), max);
4833
- }
4834
- }
4835
- else if (value) {
4836
- result = result.replace(new RegExp(filterName), value);
4837
- }
4838
- return isSingle && isNumeric(result) ? Number(result) : result;
4839
- };
4840
- const applyVarsToCondition = ({ section, configFilters, filters, attributes, layerParams, eqlParameters, geometry, extent, zoomLevel, projectName, projectAlias, isSetParams, }) => {
5066
+ const toSources = ({ configFilters, filters, dataSources, dashboard, attributes, geometry, extent, zoomLevel, projectName, projectAlias, }) => ({
5067
+ widget: { selectedFilters: filters, configFilters, dataSources },
5068
+ dashboard,
5069
+ attributes,
5070
+ geometry,
5071
+ extent,
5072
+ zoomLevel,
5073
+ projectName,
5074
+ projectAlias,
5075
+ });
5076
+ /** Подстановки в секции условия (строке или массиве секций) — без разбора set-параметров. */
5077
+ const applyVarsToCondition = ({ section, isSetParams, ...params }) => {
4841
5078
  if (!section?.length)
4842
5079
  return [];
4843
- const isSingle = typeof section === "string";
4844
- const result = isSingle ? [section] : [...section];
4845
- result.forEach((_, index) => {
4846
- if (geometry && !geometry.includes("()") && geometry.endsWith(")")) {
4847
- result[index] = result[index].replace(new RegExp("%geometry"), `'${geometry}'`);
4848
- }
4849
- // Системные фильтры вида карты подставляются ДО цикла по `configFilters`: одноимённый
4850
- // пользовательский фильтр не должен перехватывать `%extent` / `%zoom`. Lookahead `(?![\w.])`
4851
- // оставляет фильтрам составные формы (`%zoomLevel`, `%extent_id`, `%zoom.min`) — та же
4852
- // граница, что уже разводит `%name` и `%name2.l1` в `getUpdatingDataSources`.
4853
- if (extent) {
4854
- result[index] = result[index].replace(new RegExp(`${FILTER_PREFIX}${EXTENT_FILTER_NAME}(?![\\w.])`, "g"), `'${extent}'`);
4855
- }
4856
- // Зум — число, кавычки не ставим.
4857
- if (!lodash.isNil(zoomLevel)) {
4858
- result[index] = result[index].replace(new RegExp(`${FILTER_PREFIX}${ZOOM_FILTER_NAME}(?![\\w.])`, "g"), String(zoomLevel));
4859
- }
4860
- // Текущий проект — строковая подстановка, поэтому в кавычках, как `%geometry`. Свойства
4861
- // заменяются раньше голого плейсхолдера, а граница `(?![\w.])` оставляет фильтрам составные
4862
- // формы (`%project_id`) и не трогает чужое свойство (`%project.foo`).
4863
- PROJECT_PROPS.forEach(prop => {
4864
- const projectValue = getProjectValue({ prop, projectName, projectAlias });
4865
- if (lodash.isNil(projectValue))
4866
- return;
4867
- result[index] = result[index].replace(new RegExp(`${FILTER_PREFIX}${PROJECT_FILTER_NAME}${prop ? `\\.${prop}` : ""}(?![\\w.])`, "g"), `'${projectValue}'`);
4868
- });
4869
- if (configFilters?.length) {
4870
- configFilters.forEach(filter => {
4871
- result[index] = applyFiltersToCondition({
4872
- filters,
4873
- condition: result[index],
4874
- name: filter.name,
4875
- defaultValue: filter.defaultValue,
4876
- configFilter: filter,
4877
- isSetParams,
4878
- isSingle: isSingle && !Array.isArray(filter.defaultValue),
4879
- }).toString();
4880
- });
4881
- }
4882
- if (attributes?.length) {
4883
- attributes.forEach(({ attributeName, value, type: attributeType }) => {
4884
- result[index] = result[index].replace(new RegExp(`{${attributeName}}`), formatConditionValue({ value: value?.toString(), attributeType, checkQuotes: !isSingle }));
4885
- });
4886
- }
4887
- if (eqlParameters && layerParams) {
4888
- Object.keys(eqlParameters).forEach(param => {
4889
- result[index] = result[index].replace(new RegExp(`\\$\\(${param}=`), "***");
4890
- result[index] = result[index].replace(new RegExp(param), formatConditionValue({ value: layerParams[param], checkQuotes: !isSingle }));
4891
- result[index] = result[index].replace(new RegExp("\\*\\*\\*"), `$(${param}=`);
4892
- });
4893
- }
4894
- });
4895
- return isSingle ? result?.[0] : result;
4896
- };
4897
- const formatSingleCondition = (condition, { configFilters, filters, attributes, eqlParameters, layerParams, geometry, extent, zoomLevel, projectName, projectAlias, }) => {
4898
- const setParams = condition.match(new RegExp("\\$\\([^)]+\\)", "g"));
4899
- const setParamsSection = applyVarsToCondition({
4900
- section: setParams,
4901
- configFilters,
4902
- filters,
4903
- attributes,
4904
- eqlParameters,
4905
- layerParams,
4906
- geometry,
4907
- extent,
4908
- zoomLevel,
4909
- projectName,
4910
- projectAlias,
4911
- isSetParams: true,
4912
- });
4913
- const splitter = " AND ";
4914
- const setParamsLength = setParams?.join("").length || 0;
4915
- const conditionSection = applyVarsToCondition({
4916
- section: condition.substring(setParamsLength).split(splitter),
4917
- configFilters,
4918
- filters,
4919
- attributes,
4920
- eqlParameters,
4921
- layerParams,
4922
- geometry,
4923
- extent,
4924
- zoomLevel,
4925
- projectName,
4926
- projectAlias,
4927
- });
4928
- return setParamsSection?.length && conditionSection.length
4929
- ? [setParamsSection.join(""), conditionSection.join(splitter)].join(" ")
4930
- : setParamsSection?.length
4931
- ? setParamsSection.join("")
4932
- : conditionSection.join(splitter);
5080
+ const sources = toSources(params);
5081
+ const { eqlParameters, layerParams } = params;
5082
+ const format = (item) => formatConditionSection(item, sources, { eqlParameters, layerParams, isSetParams });
5083
+ return (typeof section === "string" ? format(section) : section.map(format));
4933
5084
  };
5085
+ /**
5086
+ * Условие слоя с подстановками строк конфига: фильтры (`%filter`, `$dashboard:filter`), атрибуты
5087
+ * объекта, системные значения карты и проекта, eql-параметры слоя. Пустое значение фильтра
5088
+ * оставляет плейсхолдер нетронутым.
5089
+ */
4934
5090
  const formatDataSourceCondition = ({ condition, ...rest }) => {
5091
+ const sources = toSources(rest);
5092
+ const eql = { eqlParameters: rest.eqlParameters, layerParams: rest.layerParams };
4935
5093
  if (Array.isArray(condition)) {
4936
- return condition.map(item => formatSingleCondition(item, rest));
5094
+ return condition.map(item => formatConditionString(item, sources, eql));
4937
5095
  }
4938
5096
  if (!condition) {
4939
5097
  return "";
4940
5098
  }
4941
- return formatSingleCondition(condition, rest);
5099
+ return formatConditionString(condition, sources, eql);
4942
5100
  };
4943
5101
 
4944
5102
  const DashboardChipsContainer = styled(uilibGl.Flex).withConfig({ displayName: "DashboardChipsContainer", componentId: "sc-pzlzu7" }) `
@@ -5109,10 +5267,9 @@ const noMarginMixin = styled.css `
5109
5267
  /**
5110
5268
  * Обёртка значения элемента.
5111
5269
  *
5112
- * Контейнеры, владеющие своим корнем (`ROOT_OWNING_TEMPLATES`), сюда больше не попадают —
5113
- * обёртка дублировала их `data-id`, `data-templatename` и `style` вторым узлом DOM.
5114
- * `$sizeTransparent` поэтому работает только на контейнерах, которые ещё не переведены
5115
- * на `useContainerRoot`.
5270
+ * Контейнеры, владеющие своим корнем (`ROOT_OWNING_TEMPLATES`), сюда не попадают — обёртка
5271
+ * продублировала бы их `data-id`, `data-templatename` и `style` вторым узлом DOM.
5272
+ * `$sizeTransparent` поэтому работает только на контейнерах без `useContainerRoot`.
5116
5273
  */
5117
5274
  const ElementValueWrapper = styled.div.withConfig({ displayName: "ElementValueWrapper", componentId: "sc-x3ra9l" }) `
5118
5275
  transition: background-color ${uilibGl.transition.toggle};
@@ -5141,8 +5298,8 @@ const ElementValueWrapper = styled.div.withConfig({ displayName: "ElementValueWr
5141
5298
  * а доля из `options.width` достаётся только телу. Поэтому `id`, `data-templatename`, авторский
5142
5299
  * `style` и `$sizeCss` живут здесь, а не на теле — см. `useContainerRoot`.
5143
5300
  *
5144
- * Всегда flex-колонка: заголовок и тело раньше были flex-элементами родительского `Container`,
5145
- * и блочная обёртка вернула бы схлопывание вертикальных отступов между ними.
5301
+ * Всегда flex-колонка: в блочной обёртке вертикальные отступы между заголовком и телом
5302
+ * схлопывались бы.
5146
5303
  */
5147
5304
  const ContainerRoot = styled(uilibGl.Flex).withConfig({ displayName: "ContainerRoot", componentId: "sc-1m2pft5" }) `
5148
5305
  flex-direction: column;
@@ -5610,6 +5767,34 @@ const getChartFilterName = (relatedDataSources) => {
5610
5767
  return axes?.[0]?.filterName;
5611
5768
  };
5612
5769
 
5770
+ const isValueChanged = (value, prevValue, configFilter) => {
5771
+ const isObjectValue = isTreeFilterValue(value, configFilter) ||
5772
+ isTreeFilterValue(prevValue, configFilter) ||
5773
+ isFeaturesFilterValue(value) ||
5774
+ isFeaturesFilterValue(prevValue);
5775
+ if (isObjectValue) {
5776
+ return JSON.stringify(value) !== JSON.stringify(prevValue);
5777
+ }
5778
+ if (Array.isArray(value)) {
5779
+ return (value.length !== prevValue?.length ||
5780
+ value.some((item, index) => item !== prevValue?.[index]));
5781
+ }
5782
+ return value !== prevValue;
5783
+ };
5784
+ /** Имена фильтров, чьё выбранное значение изменилось с прошлого снимка. */
5785
+ const getChangedFilterNames = (filters, prevFilters, configFilters) => filters
5786
+ ? Object.keys(filters).filter(key => isValueChanged(filters[key].value, prevFilters[key]?.value, getConfigFilter(key, configFilters)))
5787
+ : [];
5788
+ /**
5789
+ * Имена фильтров, чей дефолт изменился (`defaultValue: "$dashboard:year"` при смене фильтра дашборда),
5790
+ * пока пользователь не выбрал своё значение, — для потребителей это то же, что смена значения.
5791
+ */
5792
+ const getChangedDefaultNames = (configFilters, prevConfigFilters, filters) => (configFilters ?? [])
5793
+ .filter(({ name, defaultValue }) => filters?.[name] === undefined &&
5794
+ prevConfigFilters !== undefined &&
5795
+ !lodash.isEqual(defaultValue, getConfigFilter(name, prevConfigFilters)?.defaultValue))
5796
+ .map(({ name }) => name);
5797
+
5613
5798
  function getValueIndex(items, attributes) {
5614
5799
  return items?.findIndex(({ name }) => name.toString() === attributes.value?.toString());
5615
5800
  }
@@ -6254,29 +6439,227 @@ const useAutoCompleteControl = (items) => {
6254
6439
  .map(item => ({ text: String(item), value: String(item) })), [items]);
6255
6440
  const onChange = React.useCallback((newValue) => setValue(newValue), []);
6256
6441
  return {
6257
- value,
6258
- setValue,
6259
- onChange,
6260
- options: staticOptions.length ? staticOptions : options,
6261
- setOptions,
6442
+ value,
6443
+ setValue,
6444
+ onChange,
6445
+ options: staticOptions.length ? staticOptions : options,
6446
+ setOptions,
6447
+ };
6448
+ };
6449
+
6450
+ /**
6451
+ * Пропсы хоста фонового слоя для корней, которые не берут пропсы из `useWrapperSize`
6452
+ * (ставят `id`/`style` руками — `Title`, `Icon`, `Tabs`, `AddFeature`, `ExportPdf`, …).
6453
+ *
6454
+ * Контейнеры на `useContainerRoot` этот хук напрямую не зовут: `useWrapperSize` вызывает его
6455
+ * сам и кладёт результат в `root` — так признаки хоста считаются в одном месте.
6456
+ */
6457
+ const useBgImageHost = (elementConfig) => {
6458
+ const hasBgImage = React.useMemo(() => hasContainerBgImage(elementConfig), [elementConfig]);
6459
+ const innerPadding = !!elementConfig?.options?.innerPadding;
6460
+ return React.useMemo(() => ({ $hasBgImage: hasBgImage, $innerPadding: innerPadding }), [hasBgImage, innerPadding]);
6461
+ };
6462
+
6463
+ const getPagesFromConfig = (config) => config?.children?.[0]?.children;
6464
+
6465
+ /** Фильтры страницы `pageIndex` (с 1) вместе с общими фильтрами конфига; общие перекрывают одноимённые. */
6466
+ const getPageFilters = (config, pageIndex) => {
6467
+ const page = getPagesFromConfig(config)?.[pageIndex - 1];
6468
+ return [
6469
+ ...(page?.filters?.filter(({ name }) => !config?.filters?.some(item => item.name === name)) || []),
6470
+ ...(config?.filters || []),
6471
+ ];
6472
+ };
6473
+
6474
+ /**
6475
+ * Generic-параметр `T` сейчас идёт как намерение (под какой виджет). Дальнейшее сужение
6476
+ * `config` под конкретный виджет — отдельная задача.
6477
+ */
6478
+ const useWidgetConfig = (type = exports.WidgetType.Dashboard) => {
6479
+ const { config: configProp, containerIds, projectInfo, layerInfo, isEditing } = useWidgetContext(type);
6480
+ const config = React.useMemo(() => {
6481
+ if (configProp) {
6482
+ return configProp;
6483
+ }
6484
+ if (isEditing) {
6485
+ return type === exports.WidgetType.Dashboard
6486
+ ? (projectInfo?.content?.editConfiguration || {})
6487
+ : (layerInfo?.configuration?.editConfiguration || {});
6488
+ }
6489
+ const currentConfig = type === exports.WidgetType.Dashboard
6490
+ ? (projectInfo?.content?.dashboardConfiguration || {})
6491
+ : (layerInfo?.configuration?.cardConfiguration || {});
6492
+ if (containerIds?.length) {
6493
+ const elements = containerIds.map(id => findAnd.returnFound(currentConfig, { id })).filter(Boolean);
6494
+ if (!elements.length)
6495
+ return currentConfig;
6496
+ const newConfig = JSON.parse(JSON.stringify(currentConfig));
6497
+ const pages = getPagesFromConfig(newConfig);
6498
+ if (!pages?.[0]?.children)
6499
+ return currentConfig;
6500
+ pages[0].children = elements;
6501
+ return newConfig;
6502
+ }
6503
+ return currentConfig;
6504
+ }, [configProp, containerIds, layerInfo?.configuration?.cardConfiguration, projectInfo?.content?.dashboardConfiguration, type, isEditing]);
6505
+ const pages = React.useMemo(() => (getPagesFromConfig(config) || []), [config]);
6506
+ return {
6507
+ config,
6508
+ pages,
6509
+ };
6510
+ };
6511
+
6512
+ /**
6513
+ * Фильтры ближайшего дашборда для `$dashboard:` в строках конфига виджета `type`. На самом
6514
+ * дашборде стор не нужен (`undefined`): `$dashboard:name` там равен `%name`.
6515
+ *
6516
+ * Страница дашборда собирается напрямую из конфига, а не через `useWidgetPage`: тот сам зовёт этот хук.
6517
+ */
6518
+ const useDashboardFilterStore = (type) => {
6519
+ const { filters, dataSources, pageIndex } = useWidgetContext(exports.WidgetType.Dashboard);
6520
+ const { config } = useWidgetConfig(exports.WidgetType.Dashboard);
6521
+ const isDashboard = type === exports.WidgetType.Dashboard;
6522
+ const configFilters = React.useMemo(() => (isDashboard ? undefined : getPageFilters(config, pageIndex)), [config, isDashboard, pageIndex]);
6523
+ return React.useMemo(() => (isDashboard ? undefined : { selectedFilters: filters, configFilters, dataSources }), [configFilters, dataSources, filters, isDashboard]);
6524
+ };
6525
+
6526
+ /**
6527
+ * Generic-параметр `T` — намерение под виджет. Дальнейшая параметризация — отдельная задача.
6528
+ *
6529
+ * Дефолты фильтров не-дашборда (карточки) с `$dashboard:` приходят уже разрезолвленными из фильтров
6530
+ * дашборда. Страница дашборда отдаётся как есть: её `currentPage` редактор пишет обратно в конфиг.
6531
+ */
6532
+ const useWidgetPage = (type = exports.WidgetType.Dashboard) => {
6533
+ const { t } = useGlobalContext();
6534
+ const { pageIndex, changePage, projectInfo, updateProject } = useWidgetContext(type);
6535
+ const { config } = useWidgetConfig(type);
6536
+ const dashboardFilterStore = useDashboardFilterStore(type);
6537
+ const pageFilters = React.useMemo(() => getPageFilters(config, pageIndex), [config, pageIndex]);
6538
+ // Стор дашборда меняется на каждый его фильтр: без ссылок на `$dashboard:` страница от него не зависит.
6539
+ const defaultsStore = React.useMemo(() => (pageFilters.some(({ defaultValue }) => isDashboardReference(defaultValue)) ? dashboardFilterStore : undefined), [dashboardFilterStore, pageFilters]);
6540
+ const resolvedFilters = React.useMemo(() => resolveFilterDefaults(pageFilters, defaultsStore), [defaultsStore, pageFilters]);
6541
+ const filtersRef = React.useRef(resolvedFilters);
6542
+ // Стор дашборда пересобирается и на загрузку его датасорсов — страницу меняют только новые дефолты.
6543
+ if (!lodash.isEqual(filtersRef.current, resolvedFilters)) {
6544
+ filtersRef.current = resolvedFilters;
6545
+ }
6546
+ const filters = filtersRef.current;
6547
+ const currentPage = React.useMemo(() => {
6548
+ const page = getPagesFromConfig(config)?.[pageIndex - 1];
6549
+ return {
6550
+ ...(page || {}),
6551
+ header: page?.header || config.header,
6552
+ dataSources: [
6553
+ ...(page?.dataSources?.filter(({ name }) => !config.dataSources?.some(item => item.name === name)) || []),
6554
+ ...(config.dataSources || []),
6555
+ ],
6556
+ filters,
6557
+ layers: page?.layers?.map((layer) => ({
6558
+ ...layer,
6559
+ isVisible: layer.isVisible ?? true,
6560
+ })) || [],
6561
+ };
6562
+ }, [config, pageIndex, filters]);
6563
+ const updateConfigPage = React.useCallback((newConfig) => {
6564
+ const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
6565
+ const pages = getPagesFromProjectInfo(newProjectInfo);
6566
+ if (!pages?.[pageIndex - 1])
6567
+ return;
6568
+ pages[pageIndex - 1] = { ...pages[pageIndex - 1], ...newConfig };
6569
+ updateProject(newProjectInfo);
6570
+ }, [projectInfo, pageIndex, updateProject]);
6571
+ const addConfigPage = React.useCallback(() => {
6572
+ const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
6573
+ const pages = getPagesFromProjectInfo(newProjectInfo);
6574
+ if (!pages)
6575
+ return;
6576
+ pages.push(createConfigPage({ pages, defaultTitle: t("page", { ns: "dashboard", defaultValue: "Страница" }) }));
6577
+ updateProject(newProjectInfo);
6578
+ changePage(pages.length);
6579
+ }, [changePage, projectInfo, t, updateProject]);
6580
+ const deleteConfigPage = React.useCallback((index) => {
6581
+ const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
6582
+ const pages = getPagesFromProjectInfo(newProjectInfo);
6583
+ if (!pages?.[index] || pages.length === 1)
6584
+ return;
6585
+ pages.splice(index, 1);
6586
+ updateProject(newProjectInfo);
6587
+ if (!pages[pageIndex]) {
6588
+ changePage(pageIndex > 1 ? pageIndex - 1 : 1);
6589
+ }
6590
+ }, [changePage, pageIndex, projectInfo, updateProject]);
6591
+ const updateConfigLayer = React.useCallback((name, newProps) => {
6592
+ const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
6593
+ const pages = getPagesFromProjectInfo(newProjectInfo);
6594
+ const page = pages?.[pageIndex - 1];
6595
+ const index = page?.layers?.findIndex(layer => layer.name === name) ?? -1;
6596
+ if (index === -1)
6597
+ return;
6598
+ page.layers[index] = { ...page.layers[index], ...newProps };
6599
+ updateProject(newProjectInfo);
6600
+ }, [projectInfo, pageIndex, updateProject]);
6601
+ const updateConfigLayers = React.useCallback((props) => {
6602
+ const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
6603
+ const pages = getPagesFromProjectInfo(newProjectInfo);
6604
+ const page = pages?.[pageIndex - 1];
6605
+ if (!page?.layers?.length)
6606
+ return;
6607
+ page.layers = page.layers.map(layer => ({
6608
+ ...layer,
6609
+ ...(props[layer.name] || {}),
6610
+ }));
6611
+ updateProject(newProjectInfo);
6612
+ }, [projectInfo, pageIndex, updateProject]);
6613
+ return {
6614
+ pageIndex,
6615
+ currentPage,
6616
+ changePage,
6617
+ updateConfigPage,
6618
+ addConfigPage,
6619
+ deleteConfigPage,
6620
+ updateConfigLayer,
6621
+ updateConfigLayers,
6262
6622
  };
6263
6623
  };
6264
6624
 
6265
6625
  /**
6266
- * Пропсы хоста фонового слоя для корней, которые не берут пропсы из `useWrapperSize`
6267
- * (ставят `id`/`style` руками — `Title`, `Icon`, `Tabs`, `AddFeature`, `ExportPdf`, …).
6268
- *
6269
- * Контейнеры на `useContainerRoot` этот хук напрямую не зовут: `useWrapperSize` вызывает его
6270
- * сам и кладёт результат в `root` — так признаки хоста считаются в одном месте.
6626
+ * Все источники подстановок строк конфига для виджета `type`: его фильтры (`%`), фильтры и
6627
+ * датасорсы дашборда (`$dashboard:`), атрибуты объекта, вид карты и проект.
6628
+ * Атрибуты — объекта виджета; строки `DataSource` подменяют их своими при рендере.
6271
6629
  */
6272
- const useBgImageHost = (elementConfig) => {
6273
- const hasBgImage = React.useMemo(() => hasContainerBgImage(elementConfig), [elementConfig]);
6274
- const innerPadding = !!elementConfig?.options?.innerPadding;
6275
- return React.useMemo(() => ({ $hasBgImage: hasBgImage, $innerPadding: innerPadding }), [hasBgImage, innerPadding]);
6630
+ const useConfigStringSources = (type) => {
6631
+ const { ewktGeometry, ewktExtent, zoomLevel, projectName, projectAlias } = useGlobalContext();
6632
+ const { filters, dataSources, attributes, layerInfo } = useWidgetContext(type);
6633
+ const { currentPage } = useWidgetPage(type);
6634
+ const dashboard = useDashboardFilterStore(type);
6635
+ return React.useMemo(() => ({
6636
+ widget: { selectedFilters: filters, configFilters: currentPage?.filters, dataSources },
6637
+ dashboard,
6638
+ attributes,
6639
+ layerInfo,
6640
+ geometry: ewktGeometry,
6641
+ extent: ewktExtent,
6642
+ zoomLevel,
6643
+ projectName,
6644
+ projectAlias,
6645
+ }), [
6646
+ attributes,
6647
+ currentPage?.filters,
6648
+ dashboard,
6649
+ dataSources,
6650
+ ewktExtent,
6651
+ ewktGeometry,
6652
+ filters,
6653
+ layerInfo,
6654
+ projectAlias,
6655
+ projectName,
6656
+ zoomLevel,
6657
+ ]);
6276
6658
  };
6277
6659
 
6278
6660
  const useRenderContainerItem = (type, renderElement) => {
6279
6661
  const { config, layerInfo, selectedTabId, attributes } = useWidgetContext(type);
6662
+ const configStringSources = useConfigStringSources(type);
6280
6663
  return React.useCallback((elementConfig, attribute) => {
6281
6664
  const { id, options, style, children } = elementConfig || {};
6282
6665
  const { hideEmpty, innerTemplateStyle } = options || {};
@@ -6309,6 +6692,7 @@ const useRenderContainerItem = (type, renderElement) => {
6309
6692
  attributes,
6310
6693
  layerInfo,
6311
6694
  type,
6695
+ configStringSources,
6312
6696
  })
6313
6697
  : renderElement;
6314
6698
  const value = render({ id: "value" });
@@ -6321,7 +6705,7 @@ const useRenderContainerItem = (type, renderElement) => {
6321
6705
  hasUnits,
6322
6706
  render,
6323
6707
  };
6324
- }, [config, layerInfo, selectedTabId, attributes, type, renderElement]);
6708
+ }, [config, layerInfo, selectedTabId, attributes, type, renderElement, configStringSources]);
6325
6709
  };
6326
6710
 
6327
6711
  const customModes = MapboxDraw.modes;
@@ -7071,8 +7455,8 @@ const useContainerAttributes = ({ elementConfig, type, renderElement }) => {
7071
7455
  * Размеры уходят styled-пропом (класс), а не inline-стилем, поэтому перебиваются снаружи
7072
7456
  * без `!important`. Авторский `style` остаётся inline — у него приоритет по замыслу автора конфига.
7073
7457
  *
7074
- * `data-id` и `$noMargin` раньше жили на внешней обёртке `ElementValueWrapper`; она вешала их
7075
- * на второй узел поверх этого корня, дублируя стили, и для таких контейнеров больше не создаётся
7458
+ * `data-id` и `$noMargin` живут на самом корне: внешняя обёртка `ElementValueWrapper` повесила бы
7459
+ * их на второй узел поверх него, дублируя стили, поэтому для таких контейнеров она не создаётся
7076
7460
  * (см. `ROOT_OWNING_TEMPLATES`).
7077
7461
  *
7078
7462
  * Пропсы хоста фонового слоя (`$hasBgImage`, `$innerPadding`) подмешиваются здесь, а не в каждом
@@ -7485,132 +7869,6 @@ const getModalsDataSources = (config, modalIds) => lodash.uniqBy((config?.modals
7485
7869
  .filter(({ id }) => !modalIds || modalIds.includes(id))
7486
7870
  .flatMap(({ dataSources }) => dataSources ?? []), "name");
7487
7871
 
7488
- /**
7489
- * Generic-параметр `T` сейчас идёт как намерение (под какой виджет). Дальнейшее сужение
7490
- * `config` под конкретный виджет — отдельная задача.
7491
- */
7492
- const useWidgetConfig = (type = exports.WidgetType.Dashboard) => {
7493
- const { config: configProp, containerIds, projectInfo, layerInfo, isEditing } = useWidgetContext(type);
7494
- const config = React.useMemo(() => {
7495
- if (configProp) {
7496
- return configProp;
7497
- }
7498
- if (isEditing) {
7499
- return type === exports.WidgetType.Dashboard
7500
- ? (projectInfo?.content?.editConfiguration || {})
7501
- : (layerInfo?.configuration?.editConfiguration || {});
7502
- }
7503
- const currentConfig = type === exports.WidgetType.Dashboard
7504
- ? (projectInfo?.content?.dashboardConfiguration || {})
7505
- : (layerInfo?.configuration?.cardConfiguration || {});
7506
- if (containerIds?.length) {
7507
- const elements = containerIds.map(id => findAnd.returnFound(currentConfig, { id })).filter(Boolean);
7508
- if (!elements.length)
7509
- return currentConfig;
7510
- const newConfig = JSON.parse(JSON.stringify(currentConfig));
7511
- const pages = getPagesFromConfig(newConfig);
7512
- if (!pages?.[0]?.children)
7513
- return currentConfig;
7514
- pages[0].children = elements;
7515
- return newConfig;
7516
- }
7517
- return currentConfig;
7518
- }, [configProp, containerIds, layerInfo?.configuration?.cardConfiguration, projectInfo?.content?.dashboardConfiguration, type, isEditing]);
7519
- const pages = React.useMemo(() => (getPagesFromConfig(config) || []), [config]);
7520
- return {
7521
- config,
7522
- pages,
7523
- };
7524
- };
7525
-
7526
- /**
7527
- * Generic-параметр `T` — намерение под виджет. Дальнейшая параметризация — отдельная задача.
7528
- */
7529
- const useWidgetPage = (type = exports.WidgetType.Dashboard) => {
7530
- const { t } = useGlobalContext();
7531
- const { pageIndex, changePage, projectInfo, updateProject } = useWidgetContext(type);
7532
- const { config } = useWidgetConfig(type);
7533
- const currentPage = React.useMemo(() => {
7534
- const page = getPagesFromConfig(config)?.[pageIndex - 1];
7535
- return {
7536
- ...(page || {}),
7537
- header: page?.header || config.header,
7538
- dataSources: [
7539
- ...(page?.dataSources?.filter(({ name }) => !config.dataSources?.some(item => item.name === name)) || []),
7540
- ...(config.dataSources || [])
7541
- ],
7542
- filters: [
7543
- ...(page?.filters?.filter(({ name }) => !config.filters?.some(item => item.name === name)) || []),
7544
- ...(config.filters || [])
7545
- ],
7546
- layers: page?.layers?.map((layer) => ({
7547
- ...layer,
7548
- isVisible: layer.isVisible ?? true
7549
- })) || []
7550
- };
7551
- }, [config, pageIndex]);
7552
- const updateConfigPage = React.useCallback((newConfig) => {
7553
- const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
7554
- const pages = getPagesFromProjectInfo(newProjectInfo);
7555
- if (!pages?.[pageIndex - 1])
7556
- return;
7557
- pages[pageIndex - 1] = { ...pages[pageIndex - 1], ...newConfig };
7558
- updateProject(newProjectInfo);
7559
- }, [projectInfo, pageIndex, updateProject]);
7560
- const addConfigPage = React.useCallback(() => {
7561
- const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
7562
- const pages = getPagesFromProjectInfo(newProjectInfo);
7563
- if (!pages)
7564
- return;
7565
- pages.push(createConfigPage({ pages, defaultTitle: t("page", { ns: "dashboard", defaultValue: "Страница" }) }));
7566
- updateProject(newProjectInfo);
7567
- changePage(pages.length);
7568
- }, [changePage, projectInfo, t, updateProject]);
7569
- const deleteConfigPage = React.useCallback((index) => {
7570
- const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
7571
- const pages = getPagesFromProjectInfo(newProjectInfo);
7572
- if (!pages?.[index] || pages.length === 1)
7573
- return;
7574
- pages.splice(index, 1);
7575
- updateProject(newProjectInfo);
7576
- if (!pages[pageIndex]) {
7577
- changePage(pageIndex > 1 ? pageIndex - 1 : 1);
7578
- }
7579
- }, [changePage, pageIndex, projectInfo, updateProject]);
7580
- const updateConfigLayer = React.useCallback((name, newProps) => {
7581
- const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
7582
- const pages = getPagesFromProjectInfo(newProjectInfo);
7583
- const page = pages?.[pageIndex - 1];
7584
- const index = page?.layers?.findIndex(layer => layer.name === name) ?? -1;
7585
- if (index === -1)
7586
- return;
7587
- page.layers[index] = { ...page.layers[index], ...newProps };
7588
- updateProject(newProjectInfo);
7589
- }, [projectInfo, pageIndex, updateProject]);
7590
- const updateConfigLayers = React.useCallback((props) => {
7591
- const newProjectInfo = JSON.parse(JSON.stringify(projectInfo));
7592
- const pages = getPagesFromProjectInfo(newProjectInfo);
7593
- const page = pages?.[pageIndex - 1];
7594
- if (!page?.layers?.length)
7595
- return;
7596
- page.layers = page.layers.map(layer => ({
7597
- ...layer,
7598
- ...(props[layer.name] || {}),
7599
- }));
7600
- updateProject(newProjectInfo);
7601
- }, [projectInfo, pageIndex, updateProject]);
7602
- return {
7603
- pageIndex,
7604
- currentPage,
7605
- changePage,
7606
- updateConfigPage,
7607
- addConfigPage,
7608
- deleteConfigPage,
7609
- updateConfigLayer,
7610
- updateConfigLayers,
7611
- };
7612
- };
7613
-
7614
7872
  /**
7615
7873
  * Конфиги всех источников, доступных контейнерам виджета: страница с корнем и все модалки.
7616
7874
  * Одноимённый источник модалки перекрывается страничным. Только для поиска по имени — в конфиг
@@ -7711,6 +7969,7 @@ const useHeaderRender = (elementConfig, type = exports.WidgetType.Dashboard) =>
7711
7969
  const { layerInfo, attributes, expandedContainers, selectedTabId, setSelectedTabId, } = useWidgetContext(type);
7712
7970
  const { config } = useWidgetConfig(type);
7713
7971
  const { pageIndex } = useWidgetPage(type);
7972
+ const configStringSources = useConfigStringSources(type);
7714
7973
  return React.useMemo(() => getRenderElement({
7715
7974
  config,
7716
7975
  elementConfig,
@@ -7720,8 +7979,10 @@ const useHeaderRender = (elementConfig, type = exports.WidgetType.Dashboard) =>
7720
7979
  selectedTabId,
7721
7980
  setSelectedTabId,
7722
7981
  pageIndex,
7982
+ configStringSources,
7723
7983
  }), [
7724
7984
  config,
7985
+ configStringSources,
7725
7986
  elementConfig,
7726
7987
  attributes,
7727
7988
  layerInfo,
@@ -7779,9 +8040,11 @@ const useDataSourceLoading = (type) => {
7779
8040
  const useDataSources = ({ type: widgetType, config, attributes, filters, layerParams, eqlParameters, }) => {
7780
8041
  const { ewktGeometry, ewktExtent, zoomLevel, projectName, projectAlias, api } = useGlobalContext();
7781
8042
  const { dataSources, layerInfo } = useWidgetContext(widgetType);
7782
- const { dataSources: projectDataSources } = useWidgetContext(exports.WidgetType.Dashboard);
8043
+ const dashboard = useDashboardFilterStore(widgetType ?? exports.WidgetType.Dashboard);
7783
8044
  const { filters: configFilters, dataSources: configDataSources } = config || {};
7784
8045
  const prevFilters = React.useRef({});
8046
+ const prevDashboardFilters = React.useRef(dashboard?.selectedFilters ?? {});
8047
+ const prevConfigFilters = React.useRef(configFilters);
7785
8048
  const inFlightRequests = React.useRef(new Map());
7786
8049
  const getDataSourcePromises = React.useCallback(async ({ ds, query, parameters, layerName, limit, condition, url, resourceId, fileName, methodName, }, newFilters, offset = 0) => {
7787
8050
  // Дедуп in-flight по сигнатуре резолвленного запроса: если идентичный
@@ -7811,7 +8074,7 @@ const useDataSources = ({ type: widgetType, config, attributes, filters, layerPa
7811
8074
  projectAlias,
7812
8075
  layerInfo,
7813
8076
  dataSources,
7814
- projectDataSources,
8077
+ dashboard,
7815
8078
  });
7816
8079
  if (resourceId) {
7817
8080
  const newSignature = JSON.stringify(["python", resourceId, newParams, methodName, fileName]);
@@ -7882,6 +8145,8 @@ const useDataSources = ({ type: widgetType, config, attributes, filters, layerPa
7882
8145
  condition,
7883
8146
  configFilters,
7884
8147
  filters: selectedFilters,
8148
+ dataSources,
8149
+ dashboard,
7885
8150
  geometry: ewktGeometry,
7886
8151
  extent: ewktExtent,
7887
8152
  zoomLevel,
@@ -7916,50 +8181,34 @@ const useDataSources = ({ type: widgetType, config, attributes, filters, layerPa
7916
8181
  attributes,
7917
8182
  layerParams,
7918
8183
  layerInfo,
7919
- projectDataSources,
7920
8184
  eqlParameters,
8185
+ dashboard,
7921
8186
  ]);
8187
+ /**
8188
+ * Датасорсы, которые нужно перезапросить: их параметры ссылаются на изменившийся фильтр виджета
8189
+ * (`%name`, в том числе через дефолт `$dashboard:`) или дашборда (`$dashboard:name`).
8190
+ * Датасорсы слоя без `query` перезапрашиваются на любое изменение.
8191
+ */
7922
8192
  const getUpdatingDataSources = React.useCallback(() => {
7923
- const diffFilterNames = filters
7924
- ? Object.keys(filters).filter(key => {
7925
- const { value } = filters[key];
7926
- const prevValue = prevFilters.current[key]?.value;
7927
- const configFilter = getConfigFilter(key, configFilters);
7928
- const isObjectValue = isTreeFilterValue(value, configFilter) ||
7929
- isTreeFilterValue(prevValue, configFilter) ||
7930
- isFeaturesFilterValue(value) ||
7931
- isFeaturesFilterValue(prevValue);
7932
- if (isObjectValue) {
7933
- return JSON.stringify(value) !== JSON.stringify(prevValue);
7934
- }
7935
- if (Array.isArray(value)) {
7936
- return (value.length !== prevValue?.length ||
7937
- value.some((item, index) => item !== prevValue?.[index]));
7938
- }
7939
- return value !== prevValue;
7940
- })
7941
- : [];
7942
- if (!diffFilterNames.length)
7943
- return;
7944
- const filteredDataSources = configDataSources?.filter(({ parameters = {}, query }) => lodash.isNil(query) ||
7945
- diffFilterNames.some(diffFilterName => {
7946
- // Плейсхолдер параметра может ссылаться на фильтр целиком (`%name`)
7947
- // либо на его свойство — уровень tree-фильтра или границу range
7948
- // (`%name.lN`, `%name.min`, `%name.max`). Граница `.` обязательна,
7949
- // иначе `%name` ложно матчит `%name2.l1`.
7950
- const placeholder = `${FILTER_PREFIX}${diffFilterName}`;
7951
- return Object.keys(parameters).some(paramName => {
7952
- const paramValue = parameters[paramName];
7953
- return (typeof paramValue === "string" &&
7954
- (paramValue === placeholder || paramValue.startsWith(`${placeholder}.`)));
7955
- });
7956
- }));
7957
- if (!filteredDataSources?.length) {
8193
+ const filteredDataSources = selectUpdatingDataSources({
8194
+ configDataSources,
8195
+ filters,
8196
+ configFilters,
8197
+ dashboard,
8198
+ prev: {
8199
+ filters: prevFilters.current,
8200
+ configFilters: prevConfigFilters.current,
8201
+ dashboardFilters: prevDashboardFilters.current,
8202
+ },
8203
+ });
8204
+ if (!filteredDataSources) {
7958
8205
  return;
7959
8206
  }
7960
8207
  prevFilters.current = JSON.parse(JSON.stringify(filters));
8208
+ prevDashboardFilters.current = JSON.parse(JSON.stringify(dashboard?.selectedFilters ?? {}));
8209
+ prevConfigFilters.current = configFilters;
7961
8210
  return filteredDataSources;
7962
- }, [configDataSources, configFilters, filters]);
8211
+ }, [configDataSources, configFilters, dashboard, filters]);
7963
8212
  const getUpdatedDataSources = React.useCallback((responses, currentDataSources, otherDataSources) => {
7964
8213
  const newDataSources = JSON.parse(JSON.stringify([...currentDataSources, ...otherDataSources]));
7965
8214
  responses.forEach((response, index) => {
@@ -8301,8 +8550,9 @@ const useExportPdf = (id, margin = 20) => {
8301
8550
  const useSavePrototypeBuilder = () => {
8302
8551
  const { ewktGeometry } = useGlobalContext();
8303
8552
  const { layerInfo, attributes, dataSources, filters: selectedFilters, } = useWidgetContext(exports.WidgetType.FeatureCard);
8304
- const { projectInfo, dataSources: projectDataSources } = useWidgetContext(exports.WidgetType.Dashboard);
8553
+ const { projectInfo } = useWidgetContext(exports.WidgetType.Dashboard);
8305
8554
  const { currentPage } = useWidgetPage(exports.WidgetType.FeatureCard);
8555
+ const dashboard = useDashboardFilterStore(exports.WidgetType.FeatureCard);
8306
8556
  return React.useCallback((hook, input) => {
8307
8557
  const resolvedParameters = applyQueryFilters({
8308
8558
  parameters: hook.parameters ?? {},
@@ -8312,7 +8562,7 @@ const useSavePrototypeBuilder = () => {
8312
8562
  attributes,
8313
8563
  layerInfo,
8314
8564
  dataSources,
8315
- projectDataSources,
8565
+ dashboard,
8316
8566
  });
8317
8567
  const scriptParameters = {
8318
8568
  projectName: projectInfo?.name,
@@ -8345,10 +8595,10 @@ const useSavePrototypeBuilder = () => {
8345
8595
  }, [
8346
8596
  attributes,
8347
8597
  currentPage?.filters,
8598
+ dashboard,
8348
8599
  dataSources,
8349
8600
  ewktGeometry,
8350
8601
  layerInfo,
8351
- projectDataSources,
8352
8602
  projectInfo?.name,
8353
8603
  selectedFilters,
8354
8604
  ]);
@@ -8570,6 +8820,7 @@ const useRenderElement = (type = exports.WidgetType.Dashboard, elementConfig) =>
8570
8820
  const { selectedTabId, setSelectedTabId, expandedContainers, layerInfo, attributes } = useWidgetContext(type);
8571
8821
  const { config } = useWidgetConfig(type);
8572
8822
  const { pageIndex } = useWidgetPage(type);
8823
+ const configStringSources = useConfigStringSources(type);
8573
8824
  return React.useMemo(() => getRenderElement({
8574
8825
  t,
8575
8826
  type,
@@ -8581,9 +8832,11 @@ const useRenderElement = (type = exports.WidgetType.Dashboard, elementConfig) =>
8581
8832
  pageIndex,
8582
8833
  selectedTabId,
8583
8834
  setSelectedTabId,
8835
+ configStringSources,
8584
8836
  }), [
8585
8837
  attributes,
8586
8838
  config,
8839
+ configStringSources,
8587
8840
  elementConfig,
8588
8841
  expandedContainers,
8589
8842
  layerInfo,
@@ -9087,9 +9340,8 @@ const LinkDialogContent = styled.div.withConfig({ displayName: "LinkDialogConten
9087
9340
  * Заглушка недоступного изображения.
9088
9341
  *
9089
9342
  * Масштаб значка считается от размера самого превью: `font-size` контейнера приравнен к нему, а
9090
- * бокс значка и его глиф заданы одной долей от этого размера. Раньше бокс был долей превью, а
9091
- * глиф — фиксированным: в списке, где превью всего `1.5rem`, глиф выходил втрое крупнее своего
9092
- * бокса и съезжал относительно квадрата.
9343
+ * бокс значка и его глиф заданы одной долей от этого размера. Фиксированный глиф в мелком превью
9344
+ * (`1.5rem` в списке) вышел бы крупнее своего бокса и съехал относительно квадрата.
9093
9345
  */
9094
9346
  const ImagePreviewError = styled.div.withConfig({ displayName: "ImagePreviewError", componentId: "sc-7cwrhm" }) `
9095
9347
  display: flex;
@@ -9661,6 +9913,7 @@ const DataSourceInnerContainer = React.memo(({ config, elementConfig, feature, m
9661
9913
  valueElement?.attributeName,
9662
9914
  ]);
9663
9915
  const { hasAnyFilter, isFiltered, onFilter } = useWidgetFilters(type, filterName, data);
9916
+ const configStringSources = useConfigStringSources(type);
9664
9917
  const render = React.useMemo(() => getRenderElement({
9665
9918
  config,
9666
9919
  elementConfig,
@@ -9671,7 +9924,19 @@ const DataSourceInnerContainer = React.memo(({ config, elementConfig, feature, m
9671
9924
  setSelectedTabId,
9672
9925
  pageIndex,
9673
9926
  type,
9674
- }), [config, elementConfig, attributes, layerInfo, expandedContainers, selectedTabId, setSelectedTabId, pageIndex, type]);
9927
+ configStringSources,
9928
+ }), [
9929
+ config,
9930
+ elementConfig,
9931
+ attributes,
9932
+ layerInfo,
9933
+ expandedContainers,
9934
+ selectedTabId,
9935
+ setSelectedTabId,
9936
+ pageIndex,
9937
+ type,
9938
+ configStringSources,
9939
+ ]);
9675
9940
  if (!InnerContainer) {
9676
9941
  return null;
9677
9942
  }
@@ -10024,8 +10289,8 @@ const TreeFilterContainer = styled.div.withConfig({ displayName: "TreeFilterCont
10024
10289
  }
10025
10290
  `;
10026
10291
  const TextFilterContainer = styled.div.withConfig({ displayName: "TextFilterContainer", componentId: "sc-1cbkxpd" }) `
10027
- /* Ширину задаёт контейнер фильтров. Раньше здесь стояли жёсткие 15.85rem, из-за чего
10028
- текстовый фильтр вставал уже соседних. */
10292
+ /* Ширину задаёт контейнер фильтров: своя фиксированная ширина сделала бы текстовый фильтр
10293
+ уже соседних. */
10029
10294
  width: 100%;
10030
10295
 
10031
10296
  ${uilibGl.MultiSelectContainer} {
@@ -11249,6 +11514,7 @@ const PagesContainer = React.memo(({ type = exports.WidgetType.Dashboard, noBord
11249
11514
  const { config } = useWidgetConfig(type);
11250
11515
  const { pageIndex, currentPage } = useWidgetPage(type);
11251
11516
  const { selectedTabId, setSelectedTabId, expandedContainers, attributes, onContainerChange } = useWidgetContext(type);
11517
+ const configStringSources = useConfigStringSources(type);
11252
11518
  const { options } = config || {};
11253
11519
  const { column, width } = options || {};
11254
11520
  const isColumn = column === undefined || column;
@@ -11266,12 +11532,13 @@ const PagesContainer = React.memo(({ type = exports.WidgetType.Dashboard, noBord
11266
11532
  selectedTabId,
11267
11533
  setSelectedTabId,
11268
11534
  onChange: onContainerChange,
11535
+ configStringSources,
11269
11536
  }), [
11270
11537
  attributes,
11271
11538
  config,
11539
+ configStringSources,
11272
11540
  currentPage,
11273
11541
  expandedContainers,
11274
- getRenderElement,
11275
11542
  onContainerChange,
11276
11543
  pageIndex,
11277
11544
  selectedTabId,
@@ -11371,8 +11638,8 @@ const TINTED_BACKGROUND_PERCENT = 6;
11371
11638
  /** Тонированный фон из цвета-источника; без цвета — `undefined`, чтобы уступить следующему в приоритете. */
11372
11639
  const tintedBackground = (color) => color ? transparentizeColor(color, TINTED_BACKGROUND_PERCENT) : undefined;
11373
11640
  /**
11374
- * Отступ между элементами плитки: заданный в конфиге `innerGap` (px) либо исторический хардкод,
11375
- * если опция не задана — так вид уже существующих дашбордов не меняется.
11641
+ * Отступ между элементами плитки: заданный в конфиге `innerGap` (px) либо собственный отступ
11642
+ * элемента по умолчанию, если опция не задана.
11376
11643
  */
11377
11644
  const innerGapOr = (fallback) => ({ $innerGap }) => ($innerGap == null ? fallback : `${$innerGap}px`);
11378
11645
  /**
@@ -11486,7 +11753,7 @@ const RoundedBackgroundContainerWrapper = styled(uilibGl.Flex).withConfig({ disp
11486
11753
  text-align: left;
11487
11754
  }
11488
11755
 
11489
- /* Зазор наружу даёт обёртка иконки, здесь — исторический отступ внутри её бокса. */
11756
+ /* Зазор наружу даёт обёртка иконки, здесь — отступ внутри её бокса. */
11490
11757
  span[kind] {
11491
11758
  margin-right: 0.5rem;
11492
11759
  }
@@ -11506,7 +11773,7 @@ const RoundedBackgroundContainerWrapper = styled(uilibGl.Flex).withConfig({ disp
11506
11773
 
11507
11774
  ${ContainerUnits} {
11508
11775
  margin-left: ${({ $inlineUnits, $innerGap }) => ($inlineUnits ? innerGapOr("0.25rem")({ $innerGap }) : 0)};
11509
- /* Единицы под значением: зазора исторически не было, поэтому дефолт — 0. */
11776
+ /* Единицы под значением: без innerGap зазора нет. */
11510
11777
  margin-top: ${({ $inlineUnits, $innerGap }) => (!$inlineUnits && $innerGap != null ? `${$innerGap}px` : 0)};
11511
11778
  }
11512
11779
 
@@ -11549,17 +11816,17 @@ const RoundedBackgroundContainer = React.memo(({ type, elementConfig, feature, r
11549
11816
  const color = React.useMemo(() => attributes?.find(({ attributeName }) => attributeName === colorAttribute)?.value || fontColor, [attributes, colorAttribute, fontColor]);
11550
11817
  /**
11551
11818
  * Цвет иконки — только источник фона: сам глиф красит ElementIcon/ElementSvg своим `fontColor`.
11552
- * Исторический `style.color` читается фолбэком — тем же порядком, что и в редакторе.
11819
+ * `style.color` читается фолбэком — тем же порядком, что и в редакторе.
11553
11820
  */
11554
11821
  const iconColor = React.useMemo(() => iconElement?.options?.fontColor || iconElement?.style?.color, [iconElement]);
11555
11822
  /**
11556
11823
  * Отступ иконки задаётся на её обёртке `ContainerIconWrapper`: сам элемент `icon` рендерится через
11557
11824
  * ElementIcon/ElementSvg со своими styled-компонентами, до него из стилей плитки не дотянуться.
11558
11825
  *
11559
- * В `big` иконка и текст стоят в строку — зазор горизонтальный (исторический 0.5rem).
11560
- * В колоночной плитке зазор вертикальный, и исторически его не было: `mb` появляется только
11826
+ * В `big` иконка и текст стоят в строку — зазор горизонтальный (по умолчанию 0.5rem).
11827
+ * В колоночной плитке зазор вертикальный и по умолчанию нулевой: `mb` появляется только
11561
11828
  * с заданным `innerGap`. `mr` там на вертикаль не влияет, но участвует в горизонтальном
11562
- * центрировании, поэтому остаётся прежним — иначе иконка сдвинулась бы у всех существующих плиток.
11829
+ * центрировании, поэтому от `innerGap` не зависит — иначе он сдвигал бы иконку вбок.
11563
11830
  */
11564
11831
  const renderIcon = React.useMemo(() => {
11565
11832
  if (!iconElement)
@@ -11605,6 +11872,7 @@ const SlideshowContainer = React.memo(({ config, elementConfig, type }) => {
11605
11872
  elementConfig,
11606
11873
  dataSources,
11607
11874
  });
11875
+ const configStringSources = useConfigStringSources(type);
11608
11876
  const { id, options } = elementConfig || {};
11609
11877
  const { expandable, expanded } = options || {};
11610
11878
  const isVisible = isVisibleContainer(id, expandable, expanded, expandedContainers);
@@ -11619,9 +11887,11 @@ const SlideshowContainer = React.memo(({ config, elementConfig, type }) => {
11619
11887
  setSelectedTabId,
11620
11888
  pageIndex,
11621
11889
  type,
11890
+ configStringSources,
11622
11891
  }), [
11623
11892
  getRenderElement,
11624
11893
  config,
11894
+ configStringSources,
11625
11895
  elementConfig,
11626
11896
  attributes,
11627
11897
  layerInfo,
@@ -11861,7 +12131,7 @@ const useStructuredDataDraft = ({ schema, dataSource, hasDataSource, filterValue
11861
12131
  }, []);
11862
12132
  const reset = React.useCallback(() => setRows(baseline.current), []);
11863
12133
  const commitSaved = React.useCallback((next) => {
11864
- // Сохранённое состояние — наше: значение фильтра теперь пишем мы, и вернувшееся из него
12134
+ // Сохранённое состояние — наше: значение фильтра записали мы, и вернувшееся из него
11865
12135
  // обновление не должно пересоздавать ключи уже показанных строк.
11866
12136
  hydrated.current = true;
11867
12137
  applyBaseline(next);
@@ -13224,8 +13494,8 @@ const useVoteContainer = (elementConfig) => {
13224
13494
  const VoteContainer = React.memo(({ type, elementConfig, isVisible, renderElement }) => {
13225
13495
  const { root, body } = useContainerRoot({ elementConfig });
13226
13496
  const { configured, ...vote } = useVoteContainer(elementConfig);
13227
- // Без имени атрибута или хотя бы одного источника контейнеру не с чем работать: раньше он
13228
- // молча возвращал null, и опечатка в конфиге выглядела как «блока просто нет».
13497
+ // Без имени атрибута или хотя бы одного источника контейнеру не с чем работать. Ошибка вместо
13498
+ // пустоты: молчаливый `null` выдавал бы опечатку в конфиге за «блока просто нет».
13229
13499
  if (!configured) {
13230
13500
  return jsxRuntime.jsx(DataSourceError, { name: elementConfig?.templateName ?? exports.ContainerTemplate.Vote });
13231
13501
  }
@@ -14071,8 +14341,8 @@ const useTaskNotifications = ({ enabled, suppressed, status, lastMessage, openLo
14071
14341
  const TaskContainer = React.memo(({ type, elementConfig, renderElement }) => {
14072
14342
  const { t, ewktGeometry } = useGlobalContext();
14073
14343
  const { dataSources, filters: selectedFilters, attributes, layerInfo, changeFilters, } = useWidgetContext(type);
14074
- const { dataSources: projectDataSources } = useWidgetContext(exports.WidgetType.Dashboard);
14075
14344
  const { currentPage } = useWidgetPage(type);
14345
+ const dashboard = useDashboardFilterStore(type);
14076
14346
  const { taskId, runTask, stopTask, status, openLog, loading, isLogDialogOpen, closeLog, log, lastMessage, result, } = usePythonTask();
14077
14347
  const { options } = elementConfig || {};
14078
14348
  const { title, relatedResources, center, icon, statusColors, responseFilters, useNotifications, } = options || {};
@@ -14111,11 +14381,11 @@ const TaskContainer = React.memo(({ type, elementConfig, renderElement }) => {
14111
14381
  attributes,
14112
14382
  layerInfo,
14113
14383
  dataSources,
14114
- projectDataSources,
14384
+ dashboard,
14115
14385
  });
14116
14386
  return runTask({ resourceId, parameters: newParams, script, fileName, methodName });
14117
14387
  }));
14118
- }, [attributes, currentPage.filters, dataSources, ewktGeometry, layerInfo, projectDataSources, relatedResources, runTask, selectedFilters, stopTask, taskId]);
14388
+ }, [attributes, currentPage.filters, dashboard, dataSources, ewktGeometry, layerInfo, relatedResources, runTask, selectedFilters, stopTask, taskId]);
14119
14389
  return (jsxRuntime.jsxs(ContainerRoot, { ...root, children: [jsxRuntime.jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsxRuntime.jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), jsxRuntime.jsxs(TaskContainerWrapper, { ...body, justifyContent: center ? "center" : "flex-start", children: [jsxRuntime.jsx(StatusWaitingButton, { title: title || t("run", { ns: "dashboard", defaultValue: "Запуск" }), icon: icon, status: status, statusColors: statusColors, isWaiting: loading || !!taskId, isDisabled: !relatedResources?.length, onClick: onClick }), !!(log || taskId) && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(uilibGl.IconButton, { kind: "info", onClick: openLog }), jsxRuntime.jsx(LogDialog, { logs: log, status: status, statusColors: statusColors, isOpen: isLogDialogOpen, onClose: onCloseLog, onMinimize: useNotifications ? onMinimizeLog : undefined })] }))] })] }));
14120
14390
  });
14121
14391
 
@@ -14517,7 +14787,7 @@ const EditAttachmentContainer = React.memo(({ type, elementConfig, renderElement
14517
14787
  return (jsxRuntime.jsxs(AttachmentsContainer, { id: id, style: { ...BASE_CONTAINER_STYLE, ...style }, ...bgHost, children: [jsxRuntime.jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsxRuntime.jsx(AttachmentsHeader, { alias: renderElement?.({ id: "alias" }), count: items.length, viewMode: viewMode, onChangeViewMode: setViewMode }), jsxRuntime.jsx(AttachmentsContent, { children: viewMode === "grid" ? (jsxRuntime.jsx(AttachmentsGrid, { items: visibleItems, isEdit: true, onPreview: handlePreview, onDelete: handleDelete })) : (jsxRuntime.jsx(AttachmentsList, { items: visibleItems, isEdit: true, onPreview: handlePreview, onDelete: handleDelete })) }), hasMore && !showMore && (jsxRuntime.jsx(ShowMoreButton, { hiddenCount: hiddenCount, onClick: handleShowMore })), jsxRuntime.jsx(AddButton, { accept: fileExtensions, onSelectFiles: uploading ? () => undefined : handleUpload, onSelectFromCatalog: handleSelectFromCatalog, onSelectFromLink: handleOpenLinkDialog }), jsxRuntime.jsx(AttachmentLinkDialog, { isOpen: isLinkDialogOpen, onClose: handleCloseLinkDialog, onSubmit: handleAddByLink }), previewIndex !== null && (jsxRuntime.jsx(uilibGl.Preview, { images: previewImages, initialIndex: previewIndex, isOpen: previewIndex !== null, onClose: handleClosePreview, onDownload: handleDownload }, previewIndex))] }));
14518
14788
  });
14519
14789
 
14520
- // `ProgressContainer` и `RoundedBackgroundContainer` исторически принимают `InnerContainerProps`
14790
+ // `ProgressContainer` и `RoundedBackgroundContainer` принимают `InnerContainerProps`
14521
14791
  // (см. `DataSourceInnerContainer/types.ts`). `<Name>ContainerProps` ⊆ `InnerContainerProps`
14522
14792
  // по структуре — каст оправдан, расхождение только в generic-параметре `propTypes`.
14523
14793
  const ProgressContainerTyped = ProgressContainer;
@@ -15556,10 +15826,25 @@ const ElementChips = React.memo(({ type, elementConfig }) => {
15556
15826
  return (jsxRuntime.jsx(DashboardChipsContainer, { style: style, children: tags.map(tag => (jsxRuntime.jsx(DashboardChip$1, { text: tag.text, "$bgColor": tag.color || colorAttr?.value || bgColor, "$fontColor": fontColor, "$fontSize": fontSize, "$isDefault": !tag.color && !colorAttr?.value }, tag.text))) }));
15557
15827
  });
15558
15828
 
15829
+ /**
15830
+ * Первое непустое значение за жизнь компонента. Дефолт контрола с подстановкой
15831
+ * (`$dashboard:scenario`) фиксируется при открытии карточки: пока фильтры дашборда грузятся, он ещё
15832
+ * пуст, а после — смена фильтра дашборда не должна затирать выбор.
15833
+ */
15834
+ const useFirstNonEmptyValue = (value) => {
15835
+ const firstValue = React.useRef(value);
15836
+ if (isEmptyValue(firstValue.current) && !isEmptyValue(value)) {
15837
+ firstValue.current = value;
15838
+ }
15839
+ return firstValue.current;
15840
+ };
15841
+
15559
15842
  const ElementControl = ({ elementConfig }) => {
15560
15843
  const { t } = useGlobalContext();
15561
15844
  const { attributes, layerInfo, dataSources, controls, changeControls } = useWidgetContext(exports.WidgetType.FeatureCard);
15562
15845
  const { options, attributeName, defaultValue } = elementConfig || {};
15846
+ // Дефолт фиксируется при открытии карточки и в `controls` не пишется — только показ.
15847
+ const initialDefaultValue = useFirstNonEmptyValue(defaultValue);
15563
15848
  const { relatedDataSource, label, width, control, placeholder = t("selectValue", {
15564
15849
  ns: "dashboard",
15565
15850
  defaultValue: "Выберите значение",
@@ -15589,7 +15874,7 @@ const ElementControl = ({ elementConfig }) => {
15589
15874
  }, [control?.targetAttributeName, layerInfo?.configuration]);
15590
15875
  const currentValue = controls?.[control?.targetAttributeName] ??
15591
15876
  attribute?.value ??
15592
- defaultValue;
15877
+ initialDefaultValue;
15593
15878
  const handleChange = React.useCallback(([option]) => {
15594
15879
  if (control?.targetAttributeName && changeControls) {
15595
15880
  changeControls({
@@ -17325,8 +17610,8 @@ const getNextSort = (current, attributeName) => {
17325
17610
  * Css собственного бокса таблицы: размер из `options` представления плюс прокрутка того,
17326
17611
  * что в этот размер не влез.
17327
17612
  *
17328
- * Размеров нет — нет и бокса: таблица занимает столько, сколько просит содержимое, а лишнее,
17329
- * как и раньше, уходит в прокрутку области представления контейнера.
17613
+ * Размеров нет — нет и бокса: таблица занимает столько, сколько просит содержимое, а лишнее
17614
+ * уходит в прокрутку области представления контейнера.
17330
17615
  *
17331
17616
  * Проценты работают только по ширине. Между областью представления и таблицей стоит блочная
17332
17617
  * обёртка элемента (`ElementValueWrapper`) с высотой `auto`, и `height: "100%"` в ней не
@@ -17596,8 +17881,8 @@ const getDataFromRelatedFeatures = ({ t, config, filters, relatedConfig, dataSou
17596
17881
  }
17597
17882
  // Источник чарта отличается от источника фильтра наличием осей: BarChartFilter собирает
17598
17883
  // relatedConfig вручную и оси не задаёт, потому что ему нужна исходная палитра, а не растянутая
17599
- // под число элементов. Гейт смотрит и на устаревший chartAxis — иначе у старых конфигов
17600
- // молча поехали бы цвета столбцов и долей.
17884
+ // под число элементов. Гейт смотрит и на устаревший chartAxis — иначе у конфигов с плоской
17885
+ // формой оси молча поехали бы цвета столбцов и долей.
17601
17886
  const gradientArray = hasAxisConfig(relatedConfig)
17602
17887
  ? stretchPalette(colors, data.length)
17603
17888
  : colors;
@@ -17624,7 +17909,7 @@ const getDataFromRelatedFeatures = ({ t, config, filters, relatedConfig, dataSou
17624
17909
  min: filter?.attributeMin
17625
17910
  ? feature.properties[filter.attributeMin]
17626
17911
  : null,
17627
- max: filter?.attributeMin
17912
+ max: filter?.attributeMax
17628
17913
  ? feature.properties[filter.attributeMax]
17629
17914
  : null,
17630
17915
  },
@@ -17994,7 +18279,7 @@ const isContainerRef = (element) => "containerId" in element &&
17994
18279
  !("templateName" in element) &&
17995
18280
  !("type" in element) &&
17996
18281
  !("children" in element);
17997
- const getRenderElement = ({ t, config, elementConfig, attributes = [], layerInfo, selectedTabId, setSelectedTabId, pageIndex, expandedContainers, onChange, type = exports.WidgetType.FeatureCard, }) => (props) => {
18282
+ const getRenderElement = ({ t, config, elementConfig, attributes = [], layerInfo, selectedTabId, setSelectedTabId, pageIndex, expandedContainers, onChange, type = exports.WidgetType.FeatureCard, configStringSources, }) => (props) => {
17998
18283
  const { id, index, wrap = true } = props;
17999
18284
  const currentConfig = elementConfig || config;
18000
18285
  let element = id
@@ -18011,6 +18296,12 @@ const getRenderElement = ({ t, config, elementConfig, attributes = [], layerInfo
18011
18296
  container = container[0];
18012
18297
  element = container || undefined;
18013
18298
  }
18299
+ // Строки узла — от атрибутов этого рендера: у строк `DataSource` они свои, не карточки.
18300
+ element = resolveElementStrings(element, configStringSources && {
18301
+ ...configStringSources,
18302
+ attributes: attributes.length ? attributes : configStringSources.attributes,
18303
+ layerInfo: layerInfo ?? configStringSources.layerInfo,
18304
+ });
18014
18305
  const { options, children, templateName } = element || {};
18015
18306
  const { tabId, hideEmpty, expandable, expanded, innerTemplateName } = options || {};
18016
18307
  if (isNotValidSelectedTab(tabId, selectedTabId))
@@ -18027,6 +18318,7 @@ const getRenderElement = ({ t, config, elementConfig, attributes = [], layerInfo
18027
18318
  pageIndex,
18028
18319
  onChange,
18029
18320
  type,
18321
+ configStringSources,
18030
18322
  });
18031
18323
  // Ставится внутри `getDefaultContainer` — единственного места, которое знает по построению,
18032
18324
  // что значение оказалось контейнером. Коллбэк вызывается синхронно, читаем строго после.
@@ -18073,7 +18365,12 @@ const getRenderElement = ({ t, config, elementConfig, attributes = [], layerInfo
18073
18365
  const useModalContent = (type, modalId) => {
18074
18366
  const { config } = useWidgetConfig(type);
18075
18367
  const { expandedContainers, attributes } = useWidgetContext(type);
18076
- const modalConfig = React.useMemo(() => (config?.modals ?? []).find(({ id }) => id === modalId) ?? null, [config?.modals, modalId]);
18368
+ const configStringSources = useConfigStringSources(type);
18369
+ // Заголовок модалки (`options.title`) — тоже строка конфига с подстановками.
18370
+ const modalConfig = React.useMemo(() => {
18371
+ const modal = (config?.modals ?? []).find(({ id }) => id === modalId) ?? null;
18372
+ return resolveElementStrings(modal, configStringSources);
18373
+ }, [config?.modals, configStringSources, modalId]);
18077
18374
  const modalContent = React.useMemo(() => (modalConfig?.children ?? []), [modalConfig]);
18078
18375
  const modalSlots = React.useMemo(() => modalContent.filter(({ id }) => MODAL_SLOT_IDS.includes(id)), [modalContent]);
18079
18376
  const renderElement = React.useMemo(() => getRenderElement({
@@ -18082,14 +18379,16 @@ const useModalContent = (type, modalId) => {
18082
18379
  elementConfig: { children: modalContent },
18083
18380
  attributes,
18084
18381
  expandedContainers,
18085
- }), [type, config, modalContent, attributes, expandedContainers]);
18382
+ configStringSources,
18383
+ }), [type, config, modalContent, attributes, expandedContainers, configStringSources]);
18086
18384
  const renderSlot = React.useMemo(() => getRenderElement({
18087
18385
  type,
18088
18386
  config,
18089
18387
  elementConfig: { children: modalSlots },
18090
18388
  attributes,
18091
18389
  expandedContainers,
18092
- }), [type, config, modalSlots, attributes, expandedContainers]);
18390
+ configStringSources,
18391
+ }), [type, config, modalSlots, attributes, expandedContainers, configStringSources]);
18093
18392
  const hasTitleIcon = React.useMemo(() => modalSlots.some(({ id }) => id === MODAL_TITLE_ICON_SLOT_ID), [modalSlots]);
18094
18393
  const hasBgImage = React.useMemo(() => hasContainerBgImage({ children: modalSlots }), [modalSlots]);
18095
18394
  return { modalConfig, modalContent, renderElement, renderSlot, hasTitleIcon, hasBgImage };
@@ -18260,9 +18559,8 @@ const getFeatureCardHeader = (templateName) => {
18260
18559
  /**
18261
18560
  * Ширина фильтра из конфига. Без явного значения фильтр занимает всю ширину контейнера.
18262
18561
  *
18263
- * Раньше у каждого фильтра был свой пиксельный фолбэк, подогнанный под левую панель приложения
18264
- * (288px у Dropdown, 17.25rem у Text, 17rem у RangeNumber, ничего у Tree), из-за чего в одной
18265
- * панели фильтры вставали «лесенкой» — и тем заметнее, чем шире контейнер.
18562
+ * Фолбэк общий для всех типов фильтров: свои пиксельные фолбэки у разных типов выстраивали бы
18563
+ * фильтры одной панели «лесенкой» — и тем заметнее, чем шире контейнер.
18266
18564
  */
18267
18565
  const getFilterWidth = (width) => toCssSize(width) ?? "100%";
18268
18566
 
@@ -18749,7 +19047,7 @@ const RangeDateFilter = ({ type, filter }) => {
18749
19047
  const maxDate = React.useMemo(() => maxValue || maxFromData?.toISOString(), [maxFromData, maxValue]);
18750
19048
  // Диапазон по умолчанию: `defaultValue: [from, to]` из конфига фильтра — начальное значение
18751
19049
  // и точка сброса. Границы календаря (min/max) при этом не меняются; незаданная граница
18752
- // дефолта падает на них же — как было до появления `defaultValue`.
19050
+ // дефолта падает на них же — как и без `defaultValue`.
18753
19051
  const defaultRange = React.useMemo(() => {
18754
19052
  const [from, to] = (Array.isArray(configFilter?.defaultValue) ? configFilter.defaultValue : []);
18755
19053
  return [getDate(from)?.toISOString() ?? minDate, getDate(to)?.toISOString() ?? maxDate];
@@ -19395,21 +19693,23 @@ const getFilterValue = ({ selectedFilters, configFilters, filterName, newValue,
19395
19693
  /** Условие источника задаётся строкой или массивом строк — приводим к массиву для единообразного обхода. */
19396
19694
  const toConditionsArray = (value) => Array.isArray(value) ? value : value ? [value] : [];
19397
19695
 
19398
- const MAP_VIEW_PLACEHOLDERS = MAP_VIEW_FILTER_NAMES.map(name => `${FILTER_PREFIX}${name}`);
19399
- // Та же граница, что и при подстановке в `formatDataSourceCondition`: составные формы
19400
- // (`%zoomLevel`, `%extent_id`, `%zoom.min`) принадлежат пользовательским фильтрам, не карте.
19401
- const hasMapViewPlaceholder = (value) => MAP_VIEW_PLACEHOLDERS.some(placeholder => new RegExp(`${placeholder}(?![\\w.])`).test(value));
19696
+ /**
19697
+ * Строка ссылается на вид карты — `%extent` / `%zoom` целиком или внутри строки. Граница та же,
19698
+ * что у резолвера строк конфига: составные формы (`%zoomLevel`, `%extent_id`, `%zoom.min`)
19699
+ * принадлежат пользовательским фильтрам, не карте.
19700
+ */
19701
+ const hasMapViewPlaceholder = (value) => typeof value === "string" &&
19702
+ tokenizeConfigString(value).some(segment => typeof segment !== "string" &&
19703
+ segment.kind === "filter" &&
19704
+ segment.scope === "widget" &&
19705
+ !segment.prop &&
19706
+ MAP_VIEW_FILTER_NAMES.includes(segment.name));
19402
19707
  /**
19403
19708
  * Источники, чей запрос зависит от текущего вида карты — в `parameters` или `condition` у них
19404
19709
  * есть `%extent` / `%zoom`. Перезапрашивают при движении карты только их: иначе каждый пан
19405
19710
  * дёргал бы все запросы страницы.
19406
19711
  */
19407
- const getMapViewDataSources = (dataSources) => dataSources?.filter(({ parameters, condition }) => {
19408
- const hasInParameters = Object.values(parameters ?? {}).some(value => typeof value === "string" && MAP_VIEW_PLACEHOLDERS.includes(value));
19409
- if (hasInParameters)
19410
- return true;
19411
- return toConditionsArray(condition).some(hasMapViewPlaceholder);
19412
- }) ?? [];
19712
+ const getMapViewDataSources = (dataSources) => dataSources?.filter(({ parameters, condition }) => Object.values(parameters ?? {}).some(hasMapViewPlaceholder) || toConditionsArray(condition).some(hasMapViewPlaceholder)) ?? [];
19413
19713
 
19414
19714
  const getFormattedAttributes = (t, data, attributes, config) => {
19415
19715
  const showOtherItems = config?.options?.otherItems < data?.length;
@@ -19530,8 +19830,6 @@ const getLayerInfo = (layer) => {
19530
19830
 
19531
19831
  const getLayerInfoFromDataSources = (layerInfos, dataSources, relatedDataSource) => layerInfos?.find(({ name }) => name === dataSources?.find(item => item.name === relatedDataSource)?.layerName);
19532
19832
 
19533
- const getPagesFromConfig = (config) => config?.children?.[0]?.children;
19534
-
19535
19833
  const getPagesFromProjectInfo = (projectInfo) => getPagesFromConfig(projectInfo?.content?.dashboardConfiguration);
19536
19834
 
19537
19835
  const getRelatedAttribute = (layerInfo, sourceAttributeName, relatedLayerName) => {
@@ -19649,6 +19947,27 @@ const removeDataSource = (dashboardConfiguration, name, pageIndex) => {
19649
19947
  return newConfig;
19650
19948
  };
19651
19949
 
19950
+ /**
19951
+ * Датасорсы, которые нужно перезапросить после изменения фильтров: их параметры ссылаются на
19952
+ * изменившийся фильтр виджета (`%name`, в том числе через дефолт `$dashboard:`) или дашборда
19953
+ * (`$dashboard:name`). Датасорсы слоя без `query` перезапрашиваются на любое изменение.
19954
+ * Нечего перезапрашивать — `undefined`.
19955
+ */
19956
+ const selectUpdatingDataSources = ({ configDataSources, filters, configFilters, dashboard, prev, }) => {
19957
+ const widgetNames = [
19958
+ ...getChangedFilterNames(filters, prev.filters ?? {}, configFilters),
19959
+ ...getChangedDefaultNames(configFilters, prev.configFilters, filters),
19960
+ ];
19961
+ const dashboardNames = dashboard
19962
+ ? getChangedFilterNames(dashboard.selectedFilters, prev.dashboardFilters ?? {}, dashboard.configFilters)
19963
+ : [];
19964
+ if (!widgetNames.length && !dashboardNames.length)
19965
+ return undefined;
19966
+ const dataSources = configDataSources?.filter(({ parameters = {}, query }) => lodash.isNil(query) ||
19967
+ Object.values(parameters).some(value => referencesFilters({ value, widgetNames, dashboardNames, hasDashboardStore: !!dashboard })));
19968
+ return dataSources?.length ? dataSources : undefined;
19969
+ };
19970
+
19652
19971
  const updateDataSource = (dashboardConfiguration, name, pageIndex, data) => {
19653
19972
  const newConfig = JSON.parse(JSON.stringify(dashboardConfiguration));
19654
19973
  if (newConfig.dataSources?.length) {
@@ -20956,6 +21275,9 @@ exports.CHART_TYPES = CHART_TYPES;
20956
21275
  exports.COMPACT_FRACTION_DIGITS = COMPACT_FRACTION_DIGITS;
20957
21276
  exports.CONFIG_PAGES_ID = CONFIG_PAGES_ID;
20958
21277
  exports.CONFIG_PAGE_ID = CONFIG_PAGE_ID;
21278
+ exports.CONFIG_STRING_OPTION_KEYS = CONFIG_STRING_OPTION_KEYS;
21279
+ exports.CONFIG_TEXT_ARRAY_SEPARATOR = CONFIG_TEXT_ARRAY_SEPARATOR;
21280
+ exports.CONFIG_TOKEN_REGEXP = CONFIG_TOKEN_REGEXP;
20959
21281
  exports.CONTAINERS_GROUP_DEFAULTS = CONTAINERS_GROUP_DEFAULTS;
20960
21282
  exports.CONTAINER_BODY_ATTRIBUTE = CONTAINER_BODY_ATTRIBUTE;
20961
21283
  exports.CONTAINER_BODY_FILL_STYLE = CONTAINER_BODY_FILL_STYLE;
@@ -21157,6 +21479,7 @@ exports.PresentationPanelWrapper = PresentationPanelWrapper;
21157
21479
  exports.PresentationWrapper = PresentationWrapper;
21158
21480
  exports.ProgressContainer = ProgressContainer;
21159
21481
  exports.QUERY_DESCRIPTION_CACHE_TTL = QUERY_DESCRIPTION_CACHE_TTL;
21482
+ exports.RANGE_PROPS = RANGE_PROPS;
21160
21483
  exports.RESIZE_HANDLE_ATTR = RESIZE_HANDLE_ATTR;
21161
21484
  exports.ResizeHandle = ResizeHandle;
21162
21485
  exports.RoundedBackgroundContainer = RoundedBackgroundContainer;
@@ -21194,7 +21517,6 @@ exports.ZOOM_FILTER_NAME = ZOOM_FILTER_NAME;
21194
21517
  exports.addDataSource = addDataSource;
21195
21518
  exports.addDataSources = addDataSources;
21196
21519
  exports.adjustColor = adjustColor;
21197
- exports.applyFiltersToCondition = applyFiltersToCondition;
21198
21520
  exports.applyQueryFilters = applyQueryFilters;
21199
21521
  exports.applyTreeFilterToCondition = applyTreeFilterToCondition;
21200
21522
  exports.applyVarsToCondition = applyVarsToCondition;
@@ -21233,6 +21555,8 @@ exports.findCellContext = findCellContext;
21233
21555
  exports.formatArea = formatArea;
21234
21556
  exports.formatAttributeValue = formatAttributeValue;
21235
21557
  exports.formatChartRelatedValue = formatChartRelatedValue;
21558
+ exports.formatConditionSection = formatConditionSection;
21559
+ exports.formatConditionString = formatConditionString;
21236
21560
  exports.formatConditionValue = formatConditionValue;
21237
21561
  exports.formatDataSourceCondition = formatDataSourceCondition;
21238
21562
  exports.formatDate = formatDate$1;
@@ -21250,6 +21574,8 @@ exports.getAttributeValue = getAttributeValue;
21250
21574
  exports.getAttributesConfiguration = getAttributesConfiguration;
21251
21575
  exports.getAverageTrackSize = getAverageTrackSize;
21252
21576
  exports.getAxisSide = getAxisSide;
21577
+ exports.getChangedDefaultNames = getChangedDefaultNames;
21578
+ exports.getChangedFilterNames = getChangedFilterNames;
21253
21579
  exports.getChartAxes = getChartAxes;
21254
21580
  exports.getChartFilterName = getChartFilterName;
21255
21581
  exports.getChartMarkers = getChartMarkers;
@@ -21269,6 +21595,7 @@ exports.getFeatureAttributes = getFeatureAttributes;
21269
21595
  exports.getFeatureCardHeader = getFeatureCardHeader;
21270
21596
  exports.getFilterComponent = getFilterComponent;
21271
21597
  exports.getFilterSelectedItems = getFilterSelectedItems;
21598
+ exports.getFilterTokenValue = getFilterTokenValue;
21272
21599
  exports.getFilterValue = getFilterValue;
21273
21600
  exports.getFormattedAttributes = getFormattedAttributes;
21274
21601
  exports.getGradientColors = getGradientColors;
@@ -21280,6 +21607,7 @@ exports.getLayerInfoFromDataSources = getLayerInfoFromDataSources;
21280
21607
  exports.getLayoutChildren = getLayoutChildren;
21281
21608
  exports.getMapViewDataSources = getMapViewDataSources;
21282
21609
  exports.getModalsDataSources = getModalsDataSources;
21610
+ exports.getPageFilters = getPageFilters;
21283
21611
  exports.getPagesFromConfig = getPagesFromConfig;
21284
21612
  exports.getPagesFromProjectInfo = getPagesFromProjectInfo;
21285
21613
  exports.getProjectValue = getProjectValue;
@@ -21290,6 +21618,7 @@ exports.getResourceUrl = getResourceUrl;
21290
21618
  exports.getRootElementId = getRootElementId;
21291
21619
  exports.getSelectedFilterValue = getSelectedFilterValue;
21292
21620
  exports.getShownItemsLimit = getShownItemsLimit;
21621
+ exports.getSingleToken = getSingleToken;
21293
21622
  exports.getSlideshowImages = getSlideshowImages;
21294
21623
  exports.getStyleAttributes = getStyleAttributes;
21295
21624
  exports.getSvgUrl = getSvgUrl;
@@ -21305,6 +21634,7 @@ exports.hasAxisConfig = hasAxisConfig;
21305
21634
  exports.hasContainerBgImage = hasContainerBgImage;
21306
21635
  exports.hexToRgba = hexToRgba;
21307
21636
  exports.isCrossOriginUrl = isCrossOriginUrl;
21637
+ exports.isDashboardReference = isDashboardReference;
21308
21638
  exports.isEmptyElementValue = isEmptyElementValue;
21309
21639
  exports.isEmptyValue = isEmptyValue;
21310
21640
  exports.isFeaturesFilterValue = isFeaturesFilterValue;
@@ -21323,6 +21653,7 @@ exports.isRootOwningContainer = isRootOwningContainer;
21323
21653
  exports.isTreeFilterValue = isTreeFilterValue;
21324
21654
  exports.isValueAxis = isValueAxis;
21325
21655
  exports.isVisibleContainer = isVisibleContainer;
21656
+ exports.joinConfigSegments = joinConfigSegments;
21326
21657
  exports.mapNodeById = mapNodeById;
21327
21658
  exports.mergeAttributeConfigurations = mergeAttributeConfigurations;
21328
21659
  exports.metersPerPixel = metersPerPixel;
@@ -21334,13 +21665,20 @@ exports.parseIconNamesFromClientStyle = parseIconNamesFromClientStyle;
21334
21665
  exports.pieChartTooltipFromAttributes = pieChartTooltipFromAttributes;
21335
21666
  exports.pieChartTooltipFromRelatedFeatures = pieChartTooltipFromRelatedFeatures;
21336
21667
  exports.pointOptions = pointOptions;
21668
+ exports.referencesFilters = referencesFilters;
21337
21669
  exports.removeDataSource = removeDataSource;
21338
21670
  exports.removeTracks = removeTracks;
21339
21671
  exports.replaceNodesByIds = replaceNodesByIds;
21340
21672
  exports.resolveAxis = resolveAxis;
21673
+ exports.resolveConfigString = resolveConfigString;
21674
+ exports.resolveConfigToken = resolveConfigToken;
21675
+ exports.resolveElementStrings = resolveElementStrings;
21676
+ exports.resolveFilterDefaults = resolveFilterDefaults;
21677
+ exports.resolveQueryParameter = resolveQueryParameter;
21341
21678
  exports.rgbToHex = rgbToHex;
21342
21679
  exports.roundFr = roundFr;
21343
21680
  exports.roundTotalSum = roundTotalSum;
21681
+ exports.selectUpdatingDataSources = selectUpdatingDataSources;
21344
21682
  exports.setLayoutChildren = setLayoutChildren;
21345
21683
  exports.sizeCssMixin = sizeCssMixin;
21346
21684
  exports.sliceShownOtherItems = sliceShownOtherItems;
@@ -21351,6 +21689,7 @@ exports.toCssSize = toCssSize;
21351
21689
  exports.toFrSize = toFrSize;
21352
21690
  exports.toPxNumber = toPxNumber;
21353
21691
  exports.toRenderableValue = toRenderableValue;
21692
+ exports.tokenizeConfigString = tokenizeConfigString;
21354
21693
  exports.tooltipNameFromAttributes = tooltipNameFromAttributes;
21355
21694
  exports.tooltipValueFromAttributes = tooltipValueFromAttributes;
21356
21695
  exports.tooltipValueFromRelatedFeatures = tooltipValueFromRelatedFeatures;
@@ -21371,10 +21710,12 @@ exports.useChartAxisTitles = useChartAxisTitles;
21371
21710
  exports.useChartChange = useChartChange;
21372
21711
  exports.useChartData = useChartData;
21373
21712
  exports.useConfigDataSources = useConfigDataSources;
21713
+ exports.useConfigStringSources = useConfigStringSources;
21374
21714
  exports.useContainerAttributes = useContainerAttributes;
21375
21715
  exports.useContainerRoot = useContainerRoot;
21376
21716
  exports.useCurrentPageLayers = useCurrentPageLayers;
21377
21717
  exports.useCustomFeatureSelect = useCustomFeatureSelect;
21718
+ exports.useDashboardFilterStore = useDashboardFilterStore;
21378
21719
  exports.useDashboardHeader = useDashboardHeader;
21379
21720
  exports.useDataSourceLoading = useDataSourceLoading;
21380
21721
  exports.useDataSources = useDataSources;