@evergis/react 4.0.136 → 4.0.138

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 (27) hide show
  1. package/dist/components/Dashboard/componentTypes.d.ts +10 -1
  2. package/dist/components/Dashboard/containers/AttachmentContainer/constants.d.ts +1 -0
  3. package/dist/components/Dashboard/containers/AttachmentContainer/utils/saveBlobAsFile.d.ts +7 -0
  4. package/dist/components/Dashboard/containers/VoteContainer/components/VoteCreateForm.d.ts +3 -0
  5. package/dist/components/Dashboard/containers/VoteContainer/components/VoteResults.d.ts +3 -0
  6. package/dist/components/Dashboard/containers/VoteContainer/components/VoteScreen.d.ts +9 -0
  7. package/dist/components/Dashboard/containers/VoteContainer/components/VoteShare.d.ts +3 -0
  8. package/dist/components/Dashboard/containers/VoteContainer/constants.d.ts +46 -0
  9. package/dist/components/Dashboard/containers/VoteContainer/hooks/useVoteActions.d.ts +26 -0
  10. package/dist/components/Dashboard/containers/VoteContainer/hooks/useVoteContainer.d.ts +25 -0
  11. package/dist/components/Dashboard/containers/VoteContainer/hooks/useVoteData.d.ts +22 -0
  12. package/dist/components/Dashboard/containers/VoteContainer/hooks/useVoteForm.d.ts +17 -0
  13. package/dist/components/Dashboard/containers/VoteContainer/hooks/useVoteUser.d.ts +14 -0
  14. package/dist/components/Dashboard/containers/VoteContainer/index.d.ts +11 -0
  15. package/dist/components/Dashboard/containers/VoteContainer/styled.d.ts +31 -0
  16. package/dist/components/Dashboard/containers/VoteContainer/types.d.ts +88 -0
  17. package/dist/components/Dashboard/containers/VoteContainer/utils.d.ts +38 -0
  18. package/dist/components/Dashboard/containers/index.d.ts +1 -0
  19. package/dist/components/Dashboard/containers/registry.d.ts +1 -0
  20. package/dist/components/Dashboard/hooks/index.d.ts +1 -0
  21. package/dist/components/Dashboard/hooks/useAttachmentDownload.d.ts +9 -0
  22. package/dist/components/Dashboard/types.d.ts +16 -2
  23. package/dist/index.js +1205 -406
  24. package/dist/index.js.map +1 -1
  25. package/dist/react.esm.js +1206 -409
  26. package/dist/react.esm.js.map +1 -1
  27. package/package.json +3 -3
package/dist/react.esm.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import styled, { createGlobalStyle, css, useTheme } from 'styled-components';
2
- import { Icon, IconButtonInnerChild, IconButton, Flex, transition, Chip, shadows, Description, Divider, Popup, Menu, IconToggleButton, LinearProgress, Tooltip as Tooltip$1, IconToggle, Preview, LegendToggler, DropdownField, MultiSelectContainer, IconButtonButton, useDragAndDropEffect, FlatButton, DraggableTreeContainer, DraggableTree, FlexSpan, Dialog, DialogTitle, ActionsGroup, DialogContent, DialogActions, RaisedButton, UploaderItemArea, UploaderTitleWrapper, ThemeProvider, darkTheme, CircularProgress, Switch, AutoComplete, Input, Slider, Dropdown, Checkbox, DatePicker, getLocale, H2, Blank, Popover, NumberInput, Uploader, NumberRangeSlider, useAsyncAutocomplete, RangeNumberInput, TreeDropdown, defaultTheme, dateFormat } from '@evergis/uilib-gl';
2
+ import { Icon, IconButtonInnerChild, IconButton, Flex, transition, Chip, shadows, Description, Divider, Popup, Menu, IconToggleButton, LinearProgress, Tooltip as Tooltip$1, IconToggle, Preview, LegendToggler, DropdownField, MultiSelectContainer, IconButtonButton, useDragAndDropEffect, FlatButton, DraggableTreeContainer, DraggableTree, FlexSpan, Dialog, DialogTitle, ActionsGroup, DialogContent, DialogActions, RaisedButton, UploaderItemArea, UploaderTitleWrapper, RadioGroup, Dropdown, Input, Switch, Checkbox, Radio, ThemeProvider, darkTheme, CircularProgress, AutoComplete, Slider, DatePicker, getLocale, H2, Blank, Popover, NumberInput, Uploader, NumberRangeSlider, useAsyncAutocomplete, RangeNumberInput, TreeDropdown, defaultTheme, dateFormat } from '@evergis/uilib-gl';
3
3
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
4
4
  import { isValidElement, Fragment, createContext, memo, useRef, useState, useCallback, useEffect, useContext, useMemo, createElement, useLayoutEffect, forwardRef } from 'react';
5
5
  import { barChartClassNames, lineChartClassNames, BarChart as BarChart$1, LineChart, PieChart } from '@evergis/charts';
6
6
  import { AttributeType, AttributeIconType, generateId, STORAGE_TOKEN_KEY, parseJwt, STORAGE_REFRESH_TOKEN_KEY, RemoteTaskStatus, AttributeConfigurationType, LayerServiceType, OgcGeometryType, StringSubType } from '@evergis/api';
7
7
  import { isNil, isEqual, isEmpty, uniqueId, unescape } from 'lodash';
8
- import { Color as Color$1, ColorScale } from '@evergis/color';
8
+ import { ColorScale, Color as Color$1 } from '@evergis/color';
9
9
  import { isValid, format, parseJSON, parseISO, toDate } from 'date-fns';
10
10
  import { ru, enUS } from 'date-fns/locale';
11
11
  import { stringify } from 'wkt';
12
12
  import { toMercator, bbox } from '@turf/turf';
13
+ import { v4 } from 'uuid';
13
14
  import { HubConnectionBuilder, HttpTransportType, LogLevel } from '@microsoft/signalr';
14
15
  import MapboxDraw from '@mapbox/mapbox-gl-draw';
15
16
  import { Swiper, SwiperSlide } from 'swiper/react';
@@ -19,7 +20,6 @@ import rehypeRaw from 'rehype-raw';
19
20
  import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
20
21
  import remarkGfm from 'remark-gfm';
21
22
  import MapGL, { Source, Layer as Layer$1 } from 'react-map-gl/maplibre';
22
- import { v4 } from 'uuid';
23
23
  import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
24
24
  import 'mapbox-gl/dist/mapbox-gl.css';
25
25
  import { jsPDF } from 'jspdf';
@@ -3295,6 +3295,7 @@ var ContainerTemplate;
3295
3295
  ContainerTemplate["Attachment"] = "Attachment";
3296
3296
  ContainerTemplate["Divider"] = "Divider";
3297
3297
  ContainerTemplate["StructuredData"] = "StructuredData";
3298
+ ContainerTemplate["Vote"] = "Vote";
3298
3299
  })(ContainerTemplate || (ContainerTemplate = {}));
3299
3300
  var HeaderTemplate;
3300
3301
  (function (HeaderTemplate) {
@@ -5377,169 +5378,6 @@ const getChartMarkers = (items, markers, dataSources) => {
5377
5378
  })) || []);
5378
5379
  };
5379
5380
 
5380
- const ChartLegendContainer = styled(Flex).withConfig({ displayName: "ChartLegendContainer", componentId: "sc-1di1zl9" }) `
5381
- flex-direction: column;
5382
- flex-wrap: wrap;
5383
- justify-content: ${({ twoColumns }) => (twoColumns ? "flex-start" : "center")};
5384
- `;
5385
- const ChartLegendItem = styled(Flex).withConfig({ displayName: "ChartLegendItem", componentId: "sc-16m9piw" }) `
5386
- cursor: ${({ onClick }) => (onClick ? "pointer" : "default")};
5387
- align-items: center;
5388
- flex-wrap: nowrap;
5389
- width: auto;
5390
- margin-right: 0.375rem;
5391
- margin-bottom: 0.25rem;
5392
- line-height: 0.75rem;
5393
- opacity: ${({ isFiltered, hasAnyFilter }) => (isFiltered ? 1 : hasAnyFilter ? FILTERED_VALUE_OPACITY / 100 : 1)};
5394
- `;
5395
- const ChartLegendColor = styled.div.withConfig({ displayName: "ChartLegendColor", componentId: "sc-zxnnio" }) `
5396
- width: 0.5rem;
5397
- height: 0.5rem;
5398
- margin-right: 0.375rem;
5399
- background-color: ${({ color }) => color};
5400
- border-radius: ${({ theme: { borderRadius } }) => borderRadius.tiny};
5401
- `;
5402
- const ChartLegendName = styled.div.withConfig({ displayName: "ChartLegendName", componentId: "sc-isz576" }) `
5403
- flex: 1;
5404
- font-size: ${({ $fontSize }) => $fontSize || "0.625rem"};
5405
- color: ${({ theme: { palette }, $fontColor }) => $fontColor || palette.textPrimary};
5406
- `;
5407
-
5408
- const ContainerAlias = styled(Flex).withConfig({ displayName: "ContainerAlias", componentId: "sc-1ftaxyu" }) `
5409
- align-items: center;
5410
- flex-wrap: nowrap;
5411
- font-size: 0.75rem;
5412
- color: ${({ theme: { palette } }) => palette.textSecondary};
5413
-
5414
- &&& {
5415
- margin-bottom: ${({ hasBottomMargin }) => (hasBottomMargin ? 0.25 : 0)}rem;
5416
- }
5417
-
5418
- span[kind] {
5419
- margin-right: 0.5rem;
5420
-
5421
- :after {
5422
- color: ${({ theme: { palette } }) => palette.primary};
5423
- }
5424
- }
5425
- `;
5426
- const ContainerAliasIcon = styled.div.withConfig({ displayName: "ContainerAliasIcon", componentId: "sc-elezmi" }) `
5427
- margin-right: 0.5rem;
5428
- `;
5429
- const ContainerChart = styled(Flex).withConfig({ displayName: "ContainerChart", componentId: "sc-16sh3wn" }) `
5430
- justify-content: flex-start;
5431
-
5432
- > * {
5433
- display: flex;
5434
- justify-content: center;
5435
- width: 100%;
5436
- }
5437
-
5438
- /* fill: слот забирает остаток высоты ячейки (под слотами title/alias/legend) и центрирует тело графика. */
5439
- ${({ $fill }) => $fill &&
5440
- css `
5441
- flex: 1 1 auto;
5442
- min-height: 0;
5443
- align-items: center;
5444
- justify-content: center;
5445
-
5446
- > * {
5447
- height: 100%;
5448
- }
5449
- `}
5450
- `;
5451
- const ContainerLegend = styled(Flex).withConfig({ displayName: "ContainerLegend", componentId: "sc-z1n1s8" }) ``;
5452
- const ContainerUnits = styled.div.withConfig({ displayName: "ContainerUnits", componentId: "sc-1vurgy9" }) `
5453
- margin-left: 0.5rem;
5454
- white-space: nowrap;
5455
- font-size: 0.75rem;
5456
- `;
5457
- const ContainerValue = styled(Flex).withConfig({ displayName: "ContainerValue", componentId: "sc-aj1rs9" }) `
5458
- justify-content: flex-end;
5459
- align-items: center;
5460
- flex-wrap: nowrap;
5461
- width: 100%;
5462
- font-size: ${({ big }) => (big ? 1.5 : 1)}rem;
5463
- color: ${({ fontColor, theme: { palette } }) => fontColor || palette.textPrimary};
5464
-
5465
- /* fill: строка со слотом chart+legend забирает остаток высоты Container'а (гейт — только у Chart-контейнера). */
5466
- ${({ $fill }) => $fill &&
5467
- css `
5468
- flex: 1 1 auto;
5469
- min-height: 0;
5470
- `}
5471
-
5472
- > * {
5473
- width: ${({ column }) => (column ? "100%" : "auto")};
5474
- }
5475
-
5476
- ${ContainerChart}, ${ContainerLegend} {
5477
- width: ${({ column }) => (column ? "100%" : "50%")};
5478
- }
5479
-
5480
- ${ContainerLegend} {
5481
- margin-left: ${({ column }) => (column ? 0 : "1rem")};
5482
- }
5483
-
5484
- ${ChartLegendContainer} {
5485
- flex-direction: ${({ column }) => (column ? "row" : "column")};
5486
- margin-top: ${({ column }) => (column ? "1rem" : 0)};
5487
- }
5488
- `;
5489
- const ColorIconMixin = css `
5490
- :after {
5491
- color: ${({ $fontColor }) => $fontColor} !important;
5492
- }
5493
- `;
5494
- const SizeIconMixin = css `
5495
- :after {
5496
- font-size: ${({ $fontSize }) => $fontSize}px !important;
5497
- }
5498
- `;
5499
- styled(Icon).withConfig({ displayName: "ContainerIcon", componentId: "sc-1w9w0bq" }) `
5500
- width: auto;
5501
- height: auto;
5502
- margin-bottom: 0.5rem;
5503
- ${({ $fontColor }) => !!$fontColor && ColorIconMixin};
5504
- ${({ $fontSize }) => !!$fontSize && SizeIconMixin};
5505
- `;
5506
- const SvgContainerColorMixin$1 = css `
5507
- path,
5508
- line,
5509
- circle {
5510
- fill: ${({ $fontColor }) => $fontColor};
5511
- }
5512
- `;
5513
- const SvgContainer$1 = styled.div.withConfig({ displayName: "SvgContainer", componentId: "sc-1qs5gmx" }) `
5514
- &&& {
5515
- min-width: ${({ $width }) => ($width ? `${$width}px` : "1rem")};
5516
- max-width: ${({ $width }) => ($width ? `${$width}px` : "1rem")};
5517
-
5518
- ${({ $fontColor }) => !!$fontColor && SvgContainerColorMixin$1};
5519
-
5520
- > * {
5521
- min-width: inherit;
5522
- }
5523
- }
5524
- `;
5525
- const TwoColumnContainerWrapper = styled(Container).withConfig({ displayName: "TwoColumnContainerWrapper", componentId: "sc-1ogv4d3" }) `
5526
- flex-direction: row;
5527
- flex-wrap: nowrap;
5528
- align-items: center;
5529
-
5530
- > * {
5531
- flex: 1;
5532
- }
5533
-
5534
- > ${ContainerValue} {
5535
- justify-content: flex-end;
5536
-
5537
- > * {
5538
- text-align: right;
5539
- }
5540
- }
5541
- `;
5542
-
5543
5381
  const DashboardContext = createContext({});
5544
5382
  const DashboardProvider = memo(({ children, ...props }) => {
5545
5383
  return jsx(DashboardContext.Provider, { value: props, children: children });
@@ -5692,6 +5530,127 @@ const ServerNotificationsProvider = ({ url, initialized, apiClient, children })
5692
5530
  }, children: children }));
5693
5531
  };
5694
5532
 
5533
+ const useGlobalContext = () => {
5534
+ const { t, language, themeName, api, ewktGeometry, ewktExtent, zoomLevel, notification } = useContext(GlobalContext) || {};
5535
+ const translate = useCallback((value, options) => {
5536
+ if (t)
5537
+ return t(value, options);
5538
+ return options?.defaultValue ?? value;
5539
+ }, [t]);
5540
+ return useMemo(() => ({
5541
+ t: translate,
5542
+ language,
5543
+ themeName,
5544
+ api,
5545
+ ewktGeometry,
5546
+ ewktExtent,
5547
+ zoomLevel,
5548
+ notification,
5549
+ }), [language, translate, api, ewktGeometry, ewktExtent, zoomLevel, themeName, notification]);
5550
+ };
5551
+
5552
+ const GRID_TILE_SIZE = "4.5rem";
5553
+ const LIST_ICON_SIZE = "1.5rem";
5554
+ const JPG_MIME_TYPE = "image/jpeg";
5555
+ const PNG_MIME_TYPE = "image/png";
5556
+ const IMAGE_MIME_TYPES = [
5557
+ "image/apng",
5558
+ "image/avif",
5559
+ "image/gif",
5560
+ "image/jpeg",
5561
+ "image/png",
5562
+ "image/svg+xml",
5563
+ "image/webp",
5564
+ ];
5565
+ const XLSX_MIME_TYPES = [
5566
+ "application/vnd.ms-excel",
5567
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
5568
+ ];
5569
+ const PDF_MIME_TYPE = "application/pdf";
5570
+ const DOCX_MIME_TYPES = [
5571
+ "application/msword",
5572
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
5573
+ ];
5574
+ const CSV_MIME_TYPE = "text/csv";
5575
+ const JSON_MIME_TYPE = "application/json";
5576
+ const TXT_MIME_TYPE = "text/plain";
5577
+ const PPTX_MIME_TYPES = [
5578
+ "application/vnd.ms-powerpoint",
5579
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
5580
+ ];
5581
+ const SHP_MIME_TYPE = "application/octet-stream";
5582
+ const KML_MIME_TYPE = "application/octet-stream";
5583
+ const ZIP_MIME_TYPE = "application/zip";
5584
+ const PYTHON_MIME_TYPES = ["application/x-python-code", "text/x-python"];
5585
+ const DOWNLOAD_ERROR_DURATION = 5000;
5586
+ var AddAttachmentSource;
5587
+ (function (AddAttachmentSource) {
5588
+ AddAttachmentSource["Pc"] = "pc";
5589
+ AddAttachmentSource["Catalog"] = "catalog";
5590
+ AddAttachmentSource["Link"] = "link";
5591
+ })(AddAttachmentSource || (AddAttachmentSource = {}));
5592
+
5593
+ const REVOKE_DELAY = 1000;
5594
+ /**
5595
+ * Отдаёт браузеру уже загруженный файл под именем `fileName`.
5596
+ *
5597
+ * Адрес отзывается не сразу: часть браузеров дочитывает поток уже после клика, и мгновенный
5598
+ * `revokeObjectURL` обрывает сохранение.
5599
+ */
5600
+ const saveBlobAsFile = (blob, fileName) => {
5601
+ const url = URL.createObjectURL(blob);
5602
+ const link = document.createElement("a");
5603
+ link.href = url;
5604
+ link.download = fileName;
5605
+ link.style.display = "none";
5606
+ document.body.appendChild(link);
5607
+ link.click();
5608
+ document.body.removeChild(link);
5609
+ window.setTimeout(() => URL.revokeObjectURL(url), REVOKE_DELAY);
5610
+ };
5611
+
5612
+ /**
5613
+ * Скачивание вложения по требованию — файл запрашивается в момент клика, а не заранее.
5614
+ *
5615
+ * Свой файл лежит за авторизацией (`Authorization: Bearer`), поэтому ссылкой его не отдать:
5616
+ * он тянется через api и сохраняется из памяти. Чужой открывается ссылкой — кросс-доменный
5617
+ * `download` браузер всё равно игнорирует.
5618
+ */
5619
+ const useAttachmentDownload = (items) => {
5620
+ const { api, notification, t } = useGlobalContext();
5621
+ const inFlightRef = useRef(new Set());
5622
+ return useCallback((index) => {
5623
+ const item = items[index];
5624
+ if (!item)
5625
+ return;
5626
+ if (item.isExternal) {
5627
+ window.open(item.link, "_blank", "noopener,noreferrer");
5628
+ return;
5629
+ }
5630
+ if (!api?.catalog?.getFile || inFlightRef.current.has(item.link))
5631
+ return;
5632
+ inFlightRef.current.add(item.link);
5633
+ api.catalog
5634
+ .getFile(item.link)
5635
+ .then(blob => saveBlobAsFile(blob, item.name))
5636
+ .catch(error => {
5637
+ notification?.add({
5638
+ id: v4(),
5639
+ title: t("attachments.downloadError", {
5640
+ ns: "common",
5641
+ defaultValue: "Не удалось скачать вложение",
5642
+ }),
5643
+ description: error instanceof Error ? error.message : item.name,
5644
+ error: true,
5645
+ duration: DOWNLOAD_ERROR_DURATION,
5646
+ });
5647
+ })
5648
+ .finally(() => {
5649
+ inFlightRef.current.delete(item.link);
5650
+ });
5651
+ }, [items, api, notification, t]);
5652
+ };
5653
+
5695
5654
  /**
5696
5655
  * Контекст виджет-фрейма. Возвращаемый объект включает поля и {@link DashboardContext},
5697
5656
  * и {@link FeatureCardContext}, а гибридные (`config`, `isEditing`, `isLoading`, `pageIndex`,
@@ -5808,65 +5767,6 @@ const useAttachmentItems = ({ type, elementConfig, valueOverride, }) => {
5808
5767
  };
5809
5768
  };
5810
5769
 
5811
- const useGlobalContext = () => {
5812
- const { t, language, themeName, api, ewktGeometry, ewktExtent, zoomLevel, notification } = useContext(GlobalContext) || {};
5813
- const translate = useCallback((value, options) => {
5814
- if (t)
5815
- return t(value, options);
5816
- return options?.defaultValue ?? value;
5817
- }, [t]);
5818
- return useMemo(() => ({
5819
- t: translate,
5820
- language,
5821
- themeName,
5822
- api,
5823
- ewktGeometry,
5824
- ewktExtent,
5825
- zoomLevel,
5826
- notification,
5827
- }), [language, translate, api, ewktGeometry, ewktExtent, zoomLevel, themeName, notification]);
5828
- };
5829
-
5830
- const GRID_TILE_SIZE = "4.5rem";
5831
- const LIST_ICON_SIZE = "1.5rem";
5832
- const JPG_MIME_TYPE = "image/jpeg";
5833
- const PNG_MIME_TYPE = "image/png";
5834
- const IMAGE_MIME_TYPES = [
5835
- "image/apng",
5836
- "image/avif",
5837
- "image/gif",
5838
- "image/jpeg",
5839
- "image/png",
5840
- "image/svg+xml",
5841
- "image/webp",
5842
- ];
5843
- const XLSX_MIME_TYPES = [
5844
- "application/vnd.ms-excel",
5845
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
5846
- ];
5847
- const PDF_MIME_TYPE = "application/pdf";
5848
- const DOCX_MIME_TYPES = [
5849
- "application/msword",
5850
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
5851
- ];
5852
- const CSV_MIME_TYPE = "text/csv";
5853
- const JSON_MIME_TYPE = "application/json";
5854
- const TXT_MIME_TYPE = "text/plain";
5855
- const PPTX_MIME_TYPES = [
5856
- "application/vnd.ms-powerpoint",
5857
- "application/vnd.openxmlformats-officedocument.presentationml.presentation",
5858
- ];
5859
- const SHP_MIME_TYPE = "application/octet-stream";
5860
- const KML_MIME_TYPE = "application/octet-stream";
5861
- const ZIP_MIME_TYPE = "application/zip";
5862
- const PYTHON_MIME_TYPES = ["application/x-python-code", "text/x-python"];
5863
- var AddAttachmentSource;
5864
- (function (AddAttachmentSource) {
5865
- AddAttachmentSource["Pc"] = "pc";
5866
- AddAttachmentSource["Catalog"] = "catalog";
5867
- AddAttachmentSource["Link"] = "link";
5868
- })(AddAttachmentSource || (AddAttachmentSource = {}));
5869
-
5870
5770
  var FileType;
5871
5771
  (function (FileType) {
5872
5772
  FileType[FileType["UNKNOWN"] = 0] = "UNKNOWN";
@@ -8603,61 +8503,6 @@ const useUpdateDataSource = ({ dataSource, config, filters, attributes, layerPar
8603
8503
  }, [dataSource, getDataSourcePromises, getUpdatedDataSources, dataSources]);
8604
8504
  };
8605
8505
 
8606
- const useRenderContainer = ({ elementConfig, type, renderElement, renderBody, }) => {
8607
- const { attributes } = useWidgetContext(type);
8608
- const { getRenderContainerItem, attributesToRender } = useContainerAttributes({
8609
- elementConfig,
8610
- type,
8611
- renderElement,
8612
- });
8613
- const renderContainer = useCallback((attribute) => {
8614
- const item = getRenderContainerItem(elementConfig, attribute);
8615
- const itemAttribute = attribute
8616
- ? attributes?.find(({ attributeName }) => attributeName === attribute)
8617
- : undefined;
8618
- const overrideTemplateName = getDisplayTemplateNameFromAttribute(itemAttribute);
8619
- if (overrideTemplateName) {
8620
- const OverrideContainer = getContainerComponent(overrideTemplateName);
8621
- if (OverrideContainer) {
8622
- const itemConfig = {
8623
- ...elementConfig,
8624
- children: (elementConfig?.children ?? []).map(child => ({
8625
- ...child,
8626
- attributeName: attribute,
8627
- })),
8628
- templateName: overrideTemplateName,
8629
- };
8630
- return (jsx(OverrideContainer, { type: type, elementConfig: itemConfig, renderElement: item.render }, attribute));
8631
- }
8632
- }
8633
- if (isEmptyElementValue(item.value) && item.hideEmpty)
8634
- return null;
8635
- return renderBody(item, attribute);
8636
- }, [getRenderContainerItem, elementConfig, attributes, type, renderBody]);
8637
- return { renderContainer, attributesToRender };
8638
- };
8639
-
8640
- const OneColumnContainer = memo(({ type, elementConfig, renderElement }) => {
8641
- const { width, height, overflow } = elementConfig?.options || {};
8642
- const templateName = elementConfig?.templateName;
8643
- const bgHost = useBgImageHost(elementConfig);
8644
- const renderBody = useCallback(({ id, value, style, hasUnits, render }) => (jsxs(Container, { id: id, isColumn: true, "data-templatename": templateName, style: style, "$sizeCss": getWrapperSizeStyle({
8645
- style,
8646
- width,
8647
- height,
8648
- overflow,
8649
- defaults: BASE_CONTAINER_STYLE,
8650
- defaultWidth: FILL_SIZE,
8651
- }), ...bgHost, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: render }), jsxs(ContainerAlias, { hasBottomMargin: true, children: [render({ id: "alias" }), render({ id: "tooltip" }), render({ id: "modal" })] }), jsxs(ContainerValue, { children: [value, hasUnits && (jsx(ContainerUnits, { children: render({ id: "units" }) }))] })] })), [width, height, overflow, templateName, bgHost, elementConfig]);
8652
- const { renderContainer, attributesToRender } = useRenderContainer({
8653
- type,
8654
- elementConfig,
8655
- renderElement,
8656
- renderBody,
8657
- });
8658
- return attributesToRender ? (jsx(Fragment$1, { children: attributesToRender.map(renderContainer) })) : (renderContainer());
8659
- });
8660
-
8661
8506
  /** Обычная (не сеточная) раскладка группы: дети в колонку или в строку. */
8662
8507
  const FlowGroup = memo(({ elementConfig, type, renderElement }) => {
8663
8508
  const { expandedContainers } = useWidgetContext(type);
@@ -9200,145 +9045,310 @@ const AttachmentContainer = memo(({ type, elementConfig, renderElement }) => {
9200
9045
  setPreviewIndex(idx);
9201
9046
  }, [items]);
9202
9047
  const handleClosePreview = useCallback(() => setPreviewIndex(null), []);
9048
+ const downloadByIndex = useAttachmentDownload(items);
9049
+ const handleDownload = useCallback((_image, index) => downloadByIndex(index), [downloadByIndex]);
9203
9050
  const handleShowMore = useCallback(() => setShowMore(true), [setShowMore]);
9204
9051
  const isVisible = isVisibleContainer(id, expandable, expanded, expandedContainers);
9205
- return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsx(Container, { ...body, children: jsxs(Flex, { column: true, children: [jsx(AttachmentsHeader, { alias: renderElement?.({ id: "alias" }), count: items.length, viewMode: viewMode, onChangeViewMode: setViewMode }), jsx(AttachmentsContent, { children: viewMode === "grid" ? (jsx(AttachmentsGrid, { items: visibleItems, isEdit: false, onPreview: handlePreview })) : (jsx(AttachmentsList, { items: visibleItems, isEdit: false, onPreview: handlePreview })) }), hasMore && !showMore && (jsx(ShowMoreButton, { hiddenCount: hiddenCount, onClick: handleShowMore })), previewIndex !== null && (jsx(Preview, { images: previewImages, initialIndex: previewIndex, isOpen: previewIndex !== null, onClose: handleClosePreview, errorTitleText: t("attachments.resourceUnavailable", {
9052
+ return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsx(Container, { ...body, children: jsxs(Flex, { column: true, children: [jsx(AttachmentsHeader, { alias: renderElement?.({ id: "alias" }), count: items.length, viewMode: viewMode, onChangeViewMode: setViewMode }), jsx(AttachmentsContent, { children: viewMode === "grid" ? (jsx(AttachmentsGrid, { items: visibleItems, isEdit: false, onPreview: handlePreview })) : (jsx(AttachmentsList, { items: visibleItems, isEdit: false, onPreview: handlePreview })) }), hasMore && !showMore && (jsx(ShowMoreButton, { hiddenCount: hiddenCount, onClick: handleShowMore })), previewIndex !== null && (jsx(Preview, { images: previewImages, initialIndex: previewIndex, isOpen: previewIndex !== null, onClose: handleClosePreview, onDownload: handleDownload, errorTitleText: t("attachments.resourceUnavailable", {
9206
9053
  ns: "common",
9207
9054
  defaultValue: "Ресурс недоступен",
9208
9055
  }) }, previewIndex))] }) }))] }));
9209
9056
  });
9210
9057
 
9211
- const CameraContainer = memo(({ elementConfig, type, renderElement }) => {
9212
- const { expandedContainers } = useWidgetContext(type);
9213
- const { id, options } = elementConfig || {};
9214
- const { expandable, expanded } = options || {};
9215
- const isVisible = isVisibleContainer(id, expandable, expanded, expandedContainers);
9216
- const { root, body } = useContainerRoot({ elementConfig });
9217
- return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxs(Container, { ...body, children: [jsx(ContainerAlias, { hasBottomMargin: true, children: renderElement({ id: "alias" }) }), jsx(ContainerValue, { children: renderElement({ id: "value", wrap: false }) })] }))] }));
9218
- });
9219
-
9220
- /**
9221
- * Контекст вписывания графика (`options.fill` контейнера Chart).
9222
- *
9223
- * Компонент `Chart` рендерится через `renderElement({ id: "chart" })` и получает только дочерний
9224
- * узел `{ id: "chart" }` — опции контейнера до него не доходят. `ChartContainer` оборачивает
9225
- * график в провайдер этого контекста, а `Chart` читает значение через `useContext`.
9226
- */
9227
- const FillContext = createContext({ fill: false, fitHeight: false });
9228
-
9229
- const ChartContainer = memo(({ elementConfig, isVisible, type, renderElement }) => {
9230
- const { options, children } = elementConfig || {};
9231
- const { twoColumns, hideEmpty, height } = options || {};
9232
- const fill = options?.fill;
9233
- const fillContextValue = useMemo(() => ({ fill: !!fill, fitHeight: !!fill && height != null }), [fill, height]);
9234
- const aliasElement = children.find(child => child.id === "alias");
9235
- const chartElement = children.find(child => child.id === "chart");
9236
- const legendElement = children.find(child => child.id === "legend");
9237
- const { root, body } = useContainerRoot({ elementConfig });
9238
- const { data, loading } = useChartData({
9239
- element: chartElement,
9240
- type,
9241
- });
9242
- if (data[0] && !data[0].items) {
9243
- return jsx(DataSourceError, { name: elementConfig.templateName });
9244
- }
9245
- const hasItems = !!data[0]?.items?.length;
9246
- if (!loading && !hasItems && hideEmpty)
9247
- return null;
9248
- return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxs(Container, { ...body, isColumn: true, children: [aliasElement && jsx(ContainerAlias, { hasBottomMargin: true, children: renderElement({ id: "alias" }) }), jsx(ContainerValue, { column: !twoColumns, alignItems: twoColumns && fill ? "stretch" : "center", "$fill": !!fill, children: hasItems ? (jsxs(Fragment$1, { children: [jsx(ContainerChart, { "$fill": !!fill, children: jsx(FillContext.Provider, { value: fillContextValue, children: renderElement({ id: "chart" }) }) }), jsx(ContainerLegend, { justifyContent: legendElement?.options?.center ? "center" : "flex-start", children: renderElement({ id: "legend" }) })] })) : (jsx(Fragment$1, { children: "\u2014" })) })] }))] }));
9249
- });
9250
-
9251
- const InnerContainerWrapper = styled.div.withConfig({ displayName: "InnerContainerWrapper", componentId: "sc-1a1fl7q" }) `
9058
+ const ChartLegendContainer = styled(Flex).withConfig({ displayName: "ChartLegendContainer", componentId: "sc-1di1zl9" }) `
9059
+ flex-direction: column;
9060
+ flex-wrap: wrap;
9061
+ justify-content: ${({ twoColumns }) => (twoColumns ? "flex-start" : "center")};
9062
+ `;
9063
+ const ChartLegendItem = styled(Flex).withConfig({ displayName: "ChartLegendItem", componentId: "sc-16m9piw" }) `
9252
9064
  cursor: ${({ onClick }) => (onClick ? "pointer" : "default")};
9253
- width: ${({ column }) => (column ? "100%" : "auto")};
9254
- opacity: ${({ hasAnyFilter, isFiltered }) => (isFiltered ? 1 : hasAnyFilter ? FILTERED_VALUE_OPACITY / 100 : 1)};
9255
-
9256
- /*
9257
- В ряду обёртка не сжимается. Родительский Container снимает порог по контенту
9258
- (min-width: 0 у детей), из-за чего обёртка ужималась до доли ширины ряда, а внутренний
9259
- шаблон например, плашка RoundedBackground с жёсткими 9rem блочный и не сжимается
9260
- вместе с ней. Записи вылезали за свои обёртки и наезжали друг на друга.
9261
- */
9262
- ${({ column }) => !column && "flex-shrink: 0;"}
9065
+ align-items: center;
9066
+ flex-wrap: nowrap;
9067
+ width: auto;
9068
+ margin-right: 0.375rem;
9069
+ margin-bottom: 0.25rem;
9070
+ line-height: 0.75rem;
9071
+ opacity: ${({ isFiltered, hasAnyFilter }) => (isFiltered ? 1 : hasAnyFilter ? FILTERED_VALUE_OPACITY / 100 : 1)};
9263
9072
  `;
9264
-
9265
- const DataSourceInnerContainer = memo(({ config, elementConfig, feature, maxValue, type, index, innerComponent }) => {
9266
- const { t } = useGlobalContext();
9267
- const { expandedContainers, selectedTabId, setSelectedTabId, dataSources } = useWidgetContext(type);
9268
- const { pageIndex, currentPage } = useWidgetPage(type);
9269
- const { attributes, layerInfo } = useRelatedDataSourceAttributes({
9270
- type,
9271
- elementConfig,
9272
- dataSources,
9273
- feature,
9274
- });
9275
- const { options, children } = elementConfig || {};
9276
- const { relatedDataSource, filterName, column } = options || {};
9277
- /**
9278
- * Конфиг записи — тот же узел хоста, но без слота фона.
9279
- *
9280
- * У `DataSource`/`DataSourceProgress` дети — слоты ВНУТРЕННЕГО шаблона, поэтому `bgImage`
9281
- * достался бы и хосту, и каждой записи: одна картинка нарисовалась бы дважды. Фон остаётся
9282
- * за хостом (он и есть контейнер), а записи получают конфиг без слота.
9283
- */
9284
- const innerConfig = useMemo(() => elementConfig && children?.some(({ id }) => id === BG_IMAGE_SLOT_ID)
9285
- ? { ...elementConfig, children: children.filter(({ id }) => id !== BG_IMAGE_SLOT_ID) }
9286
- : elementConfig, [elementConfig, children]);
9287
- const aliasElement = useMemo(() => children?.find(({ id }) => id === "alias"), [children]);
9288
- const valueElement = useMemo(() => children?.find(({ id }) => id === "value"), [children]);
9289
- const aliasAttribute = useMemo(() => (aliasElement ? attributes?.find(({ attributeName }) => attributeName === aliasElement.attributeName) : null), [aliasElement, attributes]);
9290
- const value = aliasAttribute?.value;
9291
- const InnerContainer = innerComponent;
9292
- const data = useMemo(() => getDataFromRelatedFeatures({
9293
- t,
9294
- config: elementConfig,
9295
- filters: currentPage?.filters,
9296
- dataSource: dataSources?.find(({ name }) => name === relatedDataSource),
9297
- layerInfo,
9298
- relatedConfig: {
9299
- attributeName: valueElement?.attributeName,
9300
- attributeTitle: aliasElement?.attributeName,
9301
- filterName,
9302
- },
9303
- }), [
9304
- aliasElement?.attributeName,
9305
- currentPage?.filters,
9306
- dataSources,
9307
- elementConfig,
9308
- filterName,
9309
- layerInfo,
9310
- relatedDataSource,
9311
- t,
9312
- valueElement?.attributeName,
9313
- ]);
9314
- const { hasAnyFilter, isFiltered, onFilter } = useWidgetFilters(type, filterName, data);
9315
- const render = useMemo(() => getRenderElement({
9316
- config,
9317
- elementConfig,
9318
- attributes,
9319
- layerInfo,
9320
- expandedContainers,
9321
- selectedTabId,
9322
- setSelectedTabId,
9323
- pageIndex,
9324
- type,
9325
- }), [config, elementConfig, attributes, layerInfo, expandedContainers, selectedTabId, setSelectedTabId, pageIndex, type]);
9326
- if (!InnerContainer) {
9327
- return null;
9328
- }
9329
- return (jsx(InnerContainerWrapper, { hasAnyFilter: hasAnyFilter, isFiltered: isFiltered(value), column: column === undefined || column, onClick: filterName ? () => onFilter(value) : undefined, children: jsx(InnerContainer, { config: config, elementConfig: innerConfig, feature: feature, maxValue: maxValue, type: type, index: index, renderElement: render }) }));
9330
- });
9331
-
9332
- const DataSourceProgressContainerWrapper = styled.div.withConfig({ displayName: "DataSourceProgressContainerWrapper", componentId: "sc-d525nv" }) `
9333
- width: 100%;
9334
-
9335
- ${sizeCssMixin};
9073
+ const ChartLegendColor = styled.div.withConfig({ displayName: "ChartLegendColor", componentId: "sc-zxnnio" }) `
9074
+ width: 0.5rem;
9075
+ height: 0.5rem;
9076
+ margin-right: 0.375rem;
9077
+ background-color: ${({ color }) => color};
9078
+ border-radius: ${({ theme: { borderRadius } }) => borderRadius.tiny};
9336
9079
  `;
9337
- const ContainerToggler = styled(LegendToggler).withConfig({ displayName: "ContainerToggler", componentId: "sc-pgbop" }) `
9338
- width: auto;
9080
+ const ChartLegendName = styled.div.withConfig({ displayName: "ChartLegendName", componentId: "sc-isz576" }) `
9081
+ flex: 1;
9082
+ font-size: ${({ $fontSize }) => $fontSize || "0.625rem"};
9083
+ color: ${({ theme: { palette }, $fontColor }) => $fontColor || palette.textPrimary};
9339
9084
  `;
9340
9085
 
9341
- /** Дефолтный внутренний отступ между плитками, px (ТЗ §11). */
9086
+ const ContainerAlias = styled(Flex).withConfig({ displayName: "ContainerAlias", componentId: "sc-1ftaxyu" }) `
9087
+ align-items: center;
9088
+ flex-wrap: nowrap;
9089
+ font-size: 0.75rem;
9090
+ color: ${({ theme: { palette } }) => palette.textSecondary};
9091
+
9092
+ &&& {
9093
+ margin-bottom: ${({ hasBottomMargin }) => (hasBottomMargin ? 0.25 : 0)}rem;
9094
+ }
9095
+
9096
+ span[kind] {
9097
+ margin-right: 0.5rem;
9098
+
9099
+ :after {
9100
+ color: ${({ theme: { palette } }) => palette.primary};
9101
+ }
9102
+ }
9103
+ `;
9104
+ const ContainerAliasIcon = styled.div.withConfig({ displayName: "ContainerAliasIcon", componentId: "sc-elezmi" }) `
9105
+ margin-right: 0.5rem;
9106
+ `;
9107
+ const ContainerChart = styled(Flex).withConfig({ displayName: "ContainerChart", componentId: "sc-16sh3wn" }) `
9108
+ justify-content: flex-start;
9109
+
9110
+ > * {
9111
+ display: flex;
9112
+ justify-content: center;
9113
+ width: 100%;
9114
+ }
9115
+
9116
+ /* fill: слот забирает остаток высоты ячейки (под слотами title/alias/legend) и центрирует тело графика. */
9117
+ ${({ $fill }) => $fill &&
9118
+ css `
9119
+ flex: 1 1 auto;
9120
+ min-height: 0;
9121
+ align-items: center;
9122
+ justify-content: center;
9123
+
9124
+ > * {
9125
+ height: 100%;
9126
+ }
9127
+ `}
9128
+ `;
9129
+ const ContainerLegend = styled(Flex).withConfig({ displayName: "ContainerLegend", componentId: "sc-z1n1s8" }) ``;
9130
+ const ContainerUnits = styled.div.withConfig({ displayName: "ContainerUnits", componentId: "sc-1vurgy9" }) `
9131
+ margin-left: 0.5rem;
9132
+ white-space: nowrap;
9133
+ font-size: 0.75rem;
9134
+ `;
9135
+ const ContainerValue = styled(Flex).withConfig({ displayName: "ContainerValue", componentId: "sc-aj1rs9" }) `
9136
+ justify-content: flex-end;
9137
+ align-items: center;
9138
+ flex-wrap: nowrap;
9139
+ width: 100%;
9140
+ font-size: ${({ big }) => (big ? 1.5 : 1)}rem;
9141
+ color: ${({ fontColor, theme: { palette } }) => fontColor || palette.textPrimary};
9142
+
9143
+ /* fill: строка со слотом chart+legend забирает остаток высоты Container'а (гейт — только у Chart-контейнера). */
9144
+ ${({ $fill }) => $fill &&
9145
+ css `
9146
+ flex: 1 1 auto;
9147
+ min-height: 0;
9148
+ `}
9149
+
9150
+ > * {
9151
+ width: ${({ column }) => (column ? "100%" : "auto")};
9152
+ }
9153
+
9154
+ ${ContainerChart}, ${ContainerLegend} {
9155
+ width: ${({ column }) => (column ? "100%" : "50%")};
9156
+ }
9157
+
9158
+ ${ContainerLegend} {
9159
+ margin-left: ${({ column }) => (column ? 0 : "1rem")};
9160
+ }
9161
+
9162
+ ${ChartLegendContainer} {
9163
+ flex-direction: ${({ column }) => (column ? "row" : "column")};
9164
+ margin-top: ${({ column }) => (column ? "1rem" : 0)};
9165
+ }
9166
+ `;
9167
+ const ColorIconMixin = css `
9168
+ :after {
9169
+ color: ${({ $fontColor }) => $fontColor} !important;
9170
+ }
9171
+ `;
9172
+ const SizeIconMixin = css `
9173
+ :after {
9174
+ font-size: ${({ $fontSize }) => $fontSize}px !important;
9175
+ }
9176
+ `;
9177
+ styled(Icon).withConfig({ displayName: "ContainerIcon", componentId: "sc-1w9w0bq" }) `
9178
+ width: auto;
9179
+ height: auto;
9180
+ margin-bottom: 0.5rem;
9181
+ ${({ $fontColor }) => !!$fontColor && ColorIconMixin};
9182
+ ${({ $fontSize }) => !!$fontSize && SizeIconMixin};
9183
+ `;
9184
+ const SvgContainerColorMixin$1 = css `
9185
+ path,
9186
+ line,
9187
+ circle {
9188
+ fill: ${({ $fontColor }) => $fontColor};
9189
+ }
9190
+ `;
9191
+ const SvgContainer$1 = styled.div.withConfig({ displayName: "SvgContainer", componentId: "sc-1qs5gmx" }) `
9192
+ &&& {
9193
+ min-width: ${({ $width }) => ($width ? `${$width}px` : "1rem")};
9194
+ max-width: ${({ $width }) => ($width ? `${$width}px` : "1rem")};
9195
+
9196
+ ${({ $fontColor }) => !!$fontColor && SvgContainerColorMixin$1};
9197
+
9198
+ > * {
9199
+ min-width: inherit;
9200
+ }
9201
+ }
9202
+ `;
9203
+ const TwoColumnContainerWrapper = styled(Container).withConfig({ displayName: "TwoColumnContainerWrapper", componentId: "sc-1ogv4d3" }) `
9204
+ flex-direction: row;
9205
+ flex-wrap: nowrap;
9206
+ align-items: center;
9207
+
9208
+ > * {
9209
+ flex: 1;
9210
+ }
9211
+
9212
+ > ${ContainerValue} {
9213
+ justify-content: flex-end;
9214
+
9215
+ > * {
9216
+ text-align: right;
9217
+ }
9218
+ }
9219
+ `;
9220
+
9221
+ const CameraContainer = memo(({ elementConfig, type, renderElement }) => {
9222
+ const { expandedContainers } = useWidgetContext(type);
9223
+ const { id, options } = elementConfig || {};
9224
+ const { expandable, expanded } = options || {};
9225
+ const isVisible = isVisibleContainer(id, expandable, expanded, expandedContainers);
9226
+ const { root, body } = useContainerRoot({ elementConfig });
9227
+ return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxs(Container, { ...body, children: [jsx(ContainerAlias, { hasBottomMargin: true, children: renderElement({ id: "alias" }) }), jsx(ContainerValue, { children: renderElement({ id: "value", wrap: false }) })] }))] }));
9228
+ });
9229
+
9230
+ /**
9231
+ * Контекст вписывания графика (`options.fill` контейнера Chart).
9232
+ *
9233
+ * Компонент `Chart` рендерится через `renderElement({ id: "chart" })` и получает только дочерний
9234
+ * узел `{ id: "chart" }` — опции контейнера до него не доходят. `ChartContainer` оборачивает
9235
+ * график в провайдер этого контекста, а `Chart` читает значение через `useContext`.
9236
+ */
9237
+ const FillContext = createContext({ fill: false, fitHeight: false });
9238
+
9239
+ const ChartContainer = memo(({ elementConfig, isVisible, type, renderElement }) => {
9240
+ const { options, children } = elementConfig || {};
9241
+ const { twoColumns, hideEmpty, height } = options || {};
9242
+ const fill = options?.fill;
9243
+ const fillContextValue = useMemo(() => ({ fill: !!fill, fitHeight: !!fill && height != null }), [fill, height]);
9244
+ const aliasElement = children.find(child => child.id === "alias");
9245
+ const chartElement = children.find(child => child.id === "chart");
9246
+ const legendElement = children.find(child => child.id === "legend");
9247
+ const { root, body } = useContainerRoot({ elementConfig });
9248
+ const { data, loading } = useChartData({
9249
+ element: chartElement,
9250
+ type,
9251
+ });
9252
+ if (data[0] && !data[0].items) {
9253
+ return jsx(DataSourceError, { name: elementConfig.templateName });
9254
+ }
9255
+ const hasItems = !!data[0]?.items?.length;
9256
+ if (!loading && !hasItems && hideEmpty)
9257
+ return null;
9258
+ return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxs(Container, { ...body, isColumn: true, children: [aliasElement && jsx(ContainerAlias, { hasBottomMargin: true, children: renderElement({ id: "alias" }) }), jsx(ContainerValue, { column: !twoColumns, alignItems: twoColumns && fill ? "stretch" : "center", "$fill": !!fill, children: hasItems ? (jsxs(Fragment$1, { children: [jsx(ContainerChart, { "$fill": !!fill, children: jsx(FillContext.Provider, { value: fillContextValue, children: renderElement({ id: "chart" }) }) }), jsx(ContainerLegend, { justifyContent: legendElement?.options?.center ? "center" : "flex-start", children: renderElement({ id: "legend" }) })] })) : (jsx(Fragment$1, { children: "\u2014" })) })] }))] }));
9259
+ });
9260
+
9261
+ const InnerContainerWrapper = styled.div.withConfig({ displayName: "InnerContainerWrapper", componentId: "sc-1a1fl7q" }) `
9262
+ cursor: ${({ onClick }) => (onClick ? "pointer" : "default")};
9263
+ width: ${({ column }) => (column ? "100%" : "auto")};
9264
+ opacity: ${({ hasAnyFilter, isFiltered }) => (isFiltered ? 1 : hasAnyFilter ? FILTERED_VALUE_OPACITY / 100 : 1)};
9265
+
9266
+ /*
9267
+ В ряду обёртка не сжимается. Родительский Container снимает порог по контенту
9268
+ (min-width: 0 у детей), из-за чего обёртка ужималась до доли ширины ряда, а внутренний
9269
+ шаблон — например, плашка RoundedBackground с жёсткими 9rem — блочный и не сжимается
9270
+ вместе с ней. Записи вылезали за свои обёртки и наезжали друг на друга.
9271
+ */
9272
+ ${({ column }) => !column && "flex-shrink: 0;"}
9273
+ `;
9274
+
9275
+ const DataSourceInnerContainer = memo(({ config, elementConfig, feature, maxValue, type, index, innerComponent }) => {
9276
+ const { t } = useGlobalContext();
9277
+ const { expandedContainers, selectedTabId, setSelectedTabId, dataSources } = useWidgetContext(type);
9278
+ const { pageIndex, currentPage } = useWidgetPage(type);
9279
+ const { attributes, layerInfo } = useRelatedDataSourceAttributes({
9280
+ type,
9281
+ elementConfig,
9282
+ dataSources,
9283
+ feature,
9284
+ });
9285
+ const { options, children } = elementConfig || {};
9286
+ const { relatedDataSource, filterName, column } = options || {};
9287
+ /**
9288
+ * Конфиг записи — тот же узел хоста, но без слота фона.
9289
+ *
9290
+ * У `DataSource`/`DataSourceProgress` дети — слоты ВНУТРЕННЕГО шаблона, поэтому `bgImage`
9291
+ * достался бы и хосту, и каждой записи: одна картинка нарисовалась бы дважды. Фон остаётся
9292
+ * за хостом (он и есть контейнер), а записи получают конфиг без слота.
9293
+ */
9294
+ const innerConfig = useMemo(() => elementConfig && children?.some(({ id }) => id === BG_IMAGE_SLOT_ID)
9295
+ ? { ...elementConfig, children: children.filter(({ id }) => id !== BG_IMAGE_SLOT_ID) }
9296
+ : elementConfig, [elementConfig, children]);
9297
+ const aliasElement = useMemo(() => children?.find(({ id }) => id === "alias"), [children]);
9298
+ const valueElement = useMemo(() => children?.find(({ id }) => id === "value"), [children]);
9299
+ const aliasAttribute = useMemo(() => (aliasElement ? attributes?.find(({ attributeName }) => attributeName === aliasElement.attributeName) : null), [aliasElement, attributes]);
9300
+ const value = aliasAttribute?.value;
9301
+ const InnerContainer = innerComponent;
9302
+ const data = useMemo(() => getDataFromRelatedFeatures({
9303
+ t,
9304
+ config: elementConfig,
9305
+ filters: currentPage?.filters,
9306
+ dataSource: dataSources?.find(({ name }) => name === relatedDataSource),
9307
+ layerInfo,
9308
+ relatedConfig: {
9309
+ attributeName: valueElement?.attributeName,
9310
+ attributeTitle: aliasElement?.attributeName,
9311
+ filterName,
9312
+ },
9313
+ }), [
9314
+ aliasElement?.attributeName,
9315
+ currentPage?.filters,
9316
+ dataSources,
9317
+ elementConfig,
9318
+ filterName,
9319
+ layerInfo,
9320
+ relatedDataSource,
9321
+ t,
9322
+ valueElement?.attributeName,
9323
+ ]);
9324
+ const { hasAnyFilter, isFiltered, onFilter } = useWidgetFilters(type, filterName, data);
9325
+ const render = useMemo(() => getRenderElement({
9326
+ config,
9327
+ elementConfig,
9328
+ attributes,
9329
+ layerInfo,
9330
+ expandedContainers,
9331
+ selectedTabId,
9332
+ setSelectedTabId,
9333
+ pageIndex,
9334
+ type,
9335
+ }), [config, elementConfig, attributes, layerInfo, expandedContainers, selectedTabId, setSelectedTabId, pageIndex, type]);
9336
+ if (!InnerContainer) {
9337
+ return null;
9338
+ }
9339
+ return (jsx(InnerContainerWrapper, { hasAnyFilter: hasAnyFilter, isFiltered: isFiltered(value), column: column === undefined || column, onClick: filterName ? () => onFilter(value) : undefined, children: jsx(InnerContainer, { config: config, elementConfig: innerConfig, feature: feature, maxValue: maxValue, type: type, index: index, renderElement: render }) }));
9340
+ });
9341
+
9342
+ const DataSourceProgressContainerWrapper = styled.div.withConfig({ displayName: "DataSourceProgressContainerWrapper", componentId: "sc-d525nv" }) `
9343
+ width: 100%;
9344
+
9345
+ ${sizeCssMixin};
9346
+ `;
9347
+ const ContainerToggler = styled(LegendToggler).withConfig({ displayName: "ContainerToggler", componentId: "sc-pgbop" }) `
9348
+ width: auto;
9349
+ `;
9350
+
9351
+ /** Дефолтный внутренний отступ между плитками, px (ТЗ §11). */
9342
9352
  const DEFAULT_TILE_GAP = 8;
9343
9353
  /** Раскладка ряда плиток (grid `justify-content`) для не-`stretch` выравнивания. */
9344
9354
  const ROW_JUSTIFY = {
@@ -10855,6 +10865,61 @@ const LayersContainer = memo(({ type, elementConfig, renderElement }) => {
10855
10865
  return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsx(LayersContainerWrapper, { ...body, children: jsx(LayerTree, { layers: layers, onlyMainTools: true }) }))] }));
10856
10866
  });
10857
10867
 
10868
+ const useRenderContainer = ({ elementConfig, type, renderElement, renderBody, }) => {
10869
+ const { attributes } = useWidgetContext(type);
10870
+ const { getRenderContainerItem, attributesToRender } = useContainerAttributes({
10871
+ elementConfig,
10872
+ type,
10873
+ renderElement,
10874
+ });
10875
+ const renderContainer = useCallback((attribute) => {
10876
+ const item = getRenderContainerItem(elementConfig, attribute);
10877
+ const itemAttribute = attribute
10878
+ ? attributes?.find(({ attributeName }) => attributeName === attribute)
10879
+ : undefined;
10880
+ const overrideTemplateName = getDisplayTemplateNameFromAttribute(itemAttribute);
10881
+ if (overrideTemplateName) {
10882
+ const OverrideContainer = getContainerComponent(overrideTemplateName);
10883
+ if (OverrideContainer) {
10884
+ const itemConfig = {
10885
+ ...elementConfig,
10886
+ children: (elementConfig?.children ?? []).map(child => ({
10887
+ ...child,
10888
+ attributeName: attribute,
10889
+ })),
10890
+ templateName: overrideTemplateName,
10891
+ };
10892
+ return (jsx(OverrideContainer, { type: type, elementConfig: itemConfig, renderElement: item.render }, attribute));
10893
+ }
10894
+ }
10895
+ if (isEmptyElementValue(item.value) && item.hideEmpty)
10896
+ return null;
10897
+ return renderBody(item, attribute);
10898
+ }, [getRenderContainerItem, elementConfig, attributes, type, renderBody]);
10899
+ return { renderContainer, attributesToRender };
10900
+ };
10901
+
10902
+ const OneColumnContainer = memo(({ type, elementConfig, renderElement }) => {
10903
+ const { width, height, overflow } = elementConfig?.options || {};
10904
+ const templateName = elementConfig?.templateName;
10905
+ const bgHost = useBgImageHost(elementConfig);
10906
+ const renderBody = useCallback(({ id, value, style, hasUnits, render }) => (jsxs(Container, { id: id, isColumn: true, "data-templatename": templateName, style: style, "$sizeCss": getWrapperSizeStyle({
10907
+ style,
10908
+ width,
10909
+ height,
10910
+ overflow,
10911
+ defaults: BASE_CONTAINER_STYLE,
10912
+ defaultWidth: FILL_SIZE,
10913
+ }), ...bgHost, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: render }), jsxs(ContainerAlias, { hasBottomMargin: true, children: [render({ id: "alias" }), render({ id: "tooltip" }), render({ id: "modal" })] }), jsxs(ContainerValue, { children: [value, hasUnits && (jsx(ContainerUnits, { children: render({ id: "units" }) }))] })] })), [width, height, overflow, templateName, bgHost, elementConfig]);
10914
+ const { renderContainer, attributesToRender } = useRenderContainer({
10915
+ type,
10916
+ elementConfig,
10917
+ renderElement,
10918
+ renderBody,
10919
+ });
10920
+ return attributesToRender ? (jsx(Fragment$1, { children: attributesToRender.map(renderContainer) })) : (renderContainer());
10921
+ });
10922
+
10858
10923
  const PagesContainer = memo(({ type = WidgetType.Dashboard, noBorders }) => {
10859
10924
  const { config } = useWidgetConfig(type);
10860
10925
  const { pageIndex, currentPage } = useWidgetPage(type);
@@ -12023,6 +12088,734 @@ const UploadContainer = memo(({ type, elementConfig, renderElement }) => {
12023
12088
  return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsx(UploadContainerWrapper, { ...body, children: renderElement({ id: "uploader", wrap: false }) }))] }));
12024
12089
  });
12025
12090
 
12091
+ /** Зелёный для плашки «Ваш ответ принят». */
12092
+ const ACCEPTED_GREEN = "#2fa84f";
12093
+ const VoteWrapper = styled(Flex).withConfig({ displayName: "VoteWrapper", componentId: "sc-1cvc2lm" }) `
12094
+ flex-direction: column;
12095
+ gap: 0.75rem;
12096
+ width: 100%;
12097
+ color: ${({ theme: { palette } }) => palette.textPrimary};
12098
+ `;
12099
+ const VoteHeading = styled.div.withConfig({ displayName: "VoteHeading", componentId: "sc-rzdt4p" }) `
12100
+ font-size: 1rem;
12101
+ font-weight: 600;
12102
+ color: ${({ theme: { palette } }) => palette.textPrimary};
12103
+ `;
12104
+ const VoteQuestionText = styled.div.withConfig({ displayName: "VoteQuestionText", componentId: "sc-1t4w892" }) `
12105
+ font-size: 0.9375rem;
12106
+ font-weight: 600;
12107
+ color: ${({ theme: { palette } }) => palette.textPrimary};
12108
+ `;
12109
+ const VoteCategoryLabel = styled.div.withConfig({ displayName: "VoteCategoryLabel", componentId: "sc-z1fkgy" }) `
12110
+ font-size: 0.75rem;
12111
+ color: ${({ theme: { palette } }) => palette.textSecondary};
12112
+ `;
12113
+ const VoteSectionLabel = styled.div.withConfig({ displayName: "VoteSectionLabel", componentId: "sc-1xxrikh" }) `
12114
+ font-size: 0.75rem;
12115
+ color: ${({ theme: { palette } }) => palette.textSecondary};
12116
+ `;
12117
+ const VoteChipList = styled(Flex).withConfig({ displayName: "VoteChipList", componentId: "sc-1ahupix" }) `
12118
+ flex-wrap: wrap;
12119
+ gap: 0.375rem;
12120
+ `;
12121
+ const VoteChip = styled(Flex).withConfig({ displayName: "VoteChip", componentId: "sc-x1icg3" }) `
12122
+ align-items: center;
12123
+ gap: 0.25rem;
12124
+ padding: 0.25rem 0.5rem;
12125
+ background-color: ${({ theme: { palette } }) => palette.elementDark};
12126
+ border-radius: 0.25rem;
12127
+ font-size: 0.8125rem;
12128
+
12129
+ :hover {
12130
+ background-color: ${({ theme: { palette } }) => palette.elementDeep};
12131
+ }
12132
+ `;
12133
+ /**
12134
+ * Список вариантов ответа.
12135
+ *
12136
+ * Обёртка — именно `RadioGroup`: `Radio` из uilib читает его legacy-контекст и без него бросает
12137
+ * `Radio must be controlled with RadioGroup context.` — даже когда `checked` и `onChange` заданы
12138
+ * на самом контроле. Группа стоит всегда, а не только в режиме одиночного выбора: лишнего она не
12139
+ * рисует, зато контекст не может пропасть при смене экрана.
12140
+ *
12141
+ * Подпись у `Radio` своя, но текст варианта рисует строка — он делит место со счётчиком и
12142
+ * прогресс-баром. Поэтому пустой узел подписи и отступ кружка до него снимаются.
12143
+ */
12144
+ const VoteVariantList = styled(RadioGroup).withConfig({ displayName: "VoteVariantList", componentId: "sc-kie7qv" }) `
12145
+ width: 100%;
12146
+
12147
+ label > div {
12148
+ display: none;
12149
+ }
12150
+
12151
+ label > span {
12152
+ margin-right: 0;
12153
+ }
12154
+ `;
12155
+ const VoteVariantRow = styled.div.withConfig({ displayName: "VoteVariantRow", componentId: "sc-hlij7m" }) `
12156
+ display: flex;
12157
+ flex-direction: column;
12158
+ gap: 0.25rem;
12159
+ padding: 0.375rem 0;
12160
+ `;
12161
+ const VoteVariantHead = styled(Flex).withConfig({ displayName: "VoteVariantHead", componentId: "sc-1kbw26l" }) `
12162
+ align-items: center;
12163
+ justify-content: space-between;
12164
+ gap: 0.5rem;
12165
+ font-size: 0.875rem;
12166
+ `;
12167
+ const VoteVariantText = styled.span.withConfig({ displayName: "VoteVariantText", componentId: "sc-1el70dv" }) `
12168
+ flex: 1;
12169
+ min-width: 0;
12170
+ `;
12171
+ const VoteVariantCount = styled.span.withConfig({ displayName: "VoteVariantCount", componentId: "sc-jle8ey" }) `
12172
+ flex-shrink: 0;
12173
+ font-size: 0.8125rem;
12174
+ color: ${({ theme: { palette } }) => palette.textSecondary};
12175
+ `;
12176
+ const VoteAcceptedBadge = styled(Flex).withConfig({ displayName: "VoteAcceptedBadge", componentId: "sc-1utdte8" }) `
12177
+ align-items: center;
12178
+ justify-content: center;
12179
+ gap: 0.5rem;
12180
+ padding: 0.5rem 0.75rem;
12181
+ border-radius: 0.375rem;
12182
+ background-color: ${ACCEPTED_GREEN};
12183
+ color: #ffffff;
12184
+ font-size: 0.875rem;
12185
+ `;
12186
+ const VoteAuthHint = styled.div.withConfig({ displayName: "VoteAuthHint", componentId: "sc-l6fdf1" }) `
12187
+ font-size: 0.8125rem;
12188
+ color: ${({ theme: { palette } }) => palette.textSecondary};
12189
+ `;
12190
+ const VoteEditLink = styled.button.withConfig({ displayName: "VoteEditLink", componentId: "sc-1iedcup" }) `
12191
+ align-self: flex-start;
12192
+ padding: 0;
12193
+ border: none;
12194
+ background: none;
12195
+ cursor: pointer;
12196
+ font-size: 0.8125rem;
12197
+ color: ${({ theme: { palette } }) => palette.primary};
12198
+
12199
+ :hover {
12200
+ text-decoration: underline;
12201
+ }
12202
+ `;
12203
+ const VoteShareBlock = styled(Flex).withConfig({ displayName: "VoteShareBlock", componentId: "sc-a8sttg" }) `
12204
+ flex-direction: column;
12205
+ gap: 0.375rem;
12206
+ margin-top: 0.25rem;
12207
+ `;
12208
+ const VoteShareChips = styled(Flex).withConfig({ displayName: "VoteShareChips", componentId: "sc-1xnysrh" }) `
12209
+ flex-wrap: wrap;
12210
+ gap: 0.375rem;
12211
+ `;
12212
+ const VoteShareChip = styled.div.withConfig({ displayName: "VoteShareChip", componentId: "sc-mrpbcg" }) `
12213
+ padding: 0.25rem 0.625rem;
12214
+ border-radius: 1rem;
12215
+ background-color: ${({ theme: { palette } }) => palette.element};
12216
+ color: ${({ theme: { palette } }) => palette.textSecondary};
12217
+ font-size: 0.75rem;
12218
+ `;
12219
+ styled.div.withConfig({ displayName: "VoteError", componentId: "sc-1gfzwqo" }) `
12220
+ font-size: 0.8125rem;
12221
+ color: ${({ theme: { palette } }) => palette.error};
12222
+ `;
12223
+
12224
+ /** Имена полей таблиц голосования. */
12225
+ const VOTE_FIELDS = {
12226
+ category: {
12227
+ id: "id",
12228
+ name: "name",
12229
+ },
12230
+ question: {
12231
+ id: "id",
12232
+ text: "text",
12233
+ categoryId: "category_id",
12234
+ userName: "user_name",
12235
+ multiSelect: "multi_select",
12236
+ },
12237
+ variant: {
12238
+ id: "id",
12239
+ questionId: "question_id",
12240
+ text: "text",
12241
+ },
12242
+ answer: {
12243
+ id: "id",
12244
+ questionId: "question_id",
12245
+ userName: "user_name",
12246
+ variantId: "variant_id",
12247
+ },
12248
+ };
12249
+ /** Минимальное число вариантов ответа в голосовании. */
12250
+ const MIN_VARIANTS = 2;
12251
+ /** Лимит при чтении списков (вариантов/ответов/категорий). */
12252
+ const VOTE_FETCH_LIMIT = 10000;
12253
+ /** База для перевода доли голосов в проценты. */
12254
+ const PERCENT_BASE = 100;
12255
+ /** Namespace переводов потребляющего приложения (подписи берутся через `t` с defaultValue). */
12256
+ const VOTE_NS = "dashboard";
12257
+ /**
12258
+ * Имя публичного (анонимного) аккаунта портала.
12259
+ *
12260
+ * Под ним сервер отдаёт `getUserInfo()` любому неавторизованному посетителю шаренного проекта,
12261
+ * поэтому «кто уже голосовал» по нему не считается: первый же голос закрыл бы голосование всем
12262
+ * сразу. Такой посетитель для контейнера — неавторизованный, ему доступны только результаты.
12263
+ *
12264
+ * Признака анонима в API нет (`UserInfoDc` — это `username` + `roles`), поэтому имя приходится
12265
+ * знать по конвенции портала: оно же стоит ролью в ACL публичных ресурсов.
12266
+ */
12267
+ const PUBLIC_USER_NAME = "public_user";
12268
+ /** Социальные сети для блока «Поделиться». */
12269
+ const VOTE_SOCIALS = ["Telegram", "VK"];
12270
+
12271
+ /** Локальное состояние формы создания/редактирования голосования (категория, вопрос, чипсы-варианты). */
12272
+ const useVoteForm = (initialValues) => {
12273
+ const [categoryId, setCategoryId] = useState(initialValues?.categoryId ?? "");
12274
+ const [text, setText] = useState(initialValues?.text ?? "");
12275
+ const [multiSelect, setMultiSelect] = useState(initialValues?.multiSelect ?? false);
12276
+ const [variants, setVariants] = useState(initialValues?.variants ?? []);
12277
+ const [draft, setDraft] = useState("");
12278
+ const addVariant = useCallback(() => {
12279
+ const value = draft.trim();
12280
+ if (!value)
12281
+ return;
12282
+ setVariants(prev => [...prev, { text: value }]);
12283
+ setDraft("");
12284
+ }, [draft]);
12285
+ const removeVariant = useCallback((index) => {
12286
+ setVariants(prev => prev.filter((_, itemIndex) => itemIndex !== index));
12287
+ }, []);
12288
+ const isValid = useMemo(() => Boolean(categoryId && text.trim() && variants.length >= MIN_VARIANTS), [categoryId, text, variants.length]);
12289
+ const values = useMemo(() => ({ categoryId, text: text.trim(), multiSelect, variants }), [categoryId, text, multiSelect, variants]);
12290
+ return {
12291
+ categoryId,
12292
+ setCategoryId,
12293
+ text,
12294
+ setText,
12295
+ multiSelect,
12296
+ setMultiSelect,
12297
+ variants,
12298
+ addVariant,
12299
+ removeVariant,
12300
+ draft,
12301
+ setDraft,
12302
+ isValid,
12303
+ values,
12304
+ };
12305
+ };
12306
+
12307
+ const VoteCreateForm = ({ categories, initialValues, isEditing, canDelete, submitting, onSubmit, onDelete, onCancel, }) => {
12308
+ const { t } = useGlobalContext();
12309
+ const form = useVoteForm(initialValues);
12310
+ const { categoryId, setCategoryId, text, setText, multiSelect, setMultiSelect, variants, addVariant, removeVariant, draft, setDraft, isValid, values, } = form;
12311
+ const categoryOptions = useMemo(() => categories.map(({ id, name }) => ({ text: name, value: id })), [categories]);
12312
+ return (jsxs(VoteWrapper, { children: [!isEditing && jsx(VoteHeading, { children: t("vote.newVote", { ns: VOTE_NS, defaultValue: "Новое голосование" }) }), jsx(Dropdown, { zIndex: 110000, label: t("vote.category", { ns: VOTE_NS, defaultValue: "Категория" }), width: "100%", placeholder: t("vote.categoryPlaceholder", { ns: VOTE_NS, defaultValue: "Выберите категорию" }), options: categoryOptions, value: categoryId, onChange: items => setCategoryId(String(items[0]?.value ?? "")) }), jsx(Input, { label: t("vote.question", { ns: VOTE_NS, defaultValue: "Вопрос" }), width: "100%", placeholder: t("vote.questionPlaceholder", { ns: VOTE_NS, defaultValue: "Текст вопроса" }), value: text, onChange: ({ target }) => setText(target.value) }), jsx(VoteSectionLabel, { children: t("vote.variants", { ns: VOTE_NS, defaultValue: "Варианты ответов" }) }), !!variants.length && (jsx(VoteChipList, { children: variants.map(({ text: variantText }, index) => (jsxs(VoteChip, { children: [jsx(VoteVariantText, { children: variantText }), jsx(IconButton, { kind: "close", onClick: () => removeVariant(index) })] }, `${variantText}:${index}`))) })), jsx(Input, { width: "100%", placeholder: t("vote.variantPlaceholder", { ns: VOTE_NS, defaultValue: "Введите вариант и нажмите Enter" }), value: draft, onChange: ({ target }) => setDraft(target.value), onKeyDown: event => {
12313
+ if (event.key === "Enter") {
12314
+ event.preventDefault();
12315
+ addVariant();
12316
+ }
12317
+ } }), jsxs(Flex, { alignItems: "center", justifyContent: "space-between", children: [jsx(VoteSectionLabel, { children: t("vote.multiSelect", { ns: VOTE_NS, defaultValue: "Множественный выбор" }) }), jsx(Switch, { checked: multiSelect, onChange: () => setMultiSelect(prev => !prev) })] }), jsx(RaisedButton, { primary: true, disabled: !isValid || submitting, onClick: () => onSubmit(values), children: isEditing
12318
+ ? t("vote.save", { ns: VOTE_NS, defaultValue: "Сохранить" })
12319
+ : t("vote.create", { ns: VOTE_NS, defaultValue: "Создать голосование" }) }), isEditing && (jsxs(Flex, { alignItems: "center", justifyContent: "space-between", children: [onCancel && (jsx(VoteEditLink, { onClick: onCancel, children: t("vote.cancel", { ns: VOTE_NS, defaultValue: "Отмена" }) })), canDelete && onDelete && (jsx(RaisedButton, { error: true, disabled: submitting, onClick: onDelete, children: t("vote.delete", { ns: VOTE_NS, defaultValue: "Удалить голосование" }) }))] }))] }));
12320
+ };
12321
+
12322
+ /** Блок «Поделиться в соцсетях» — пока только вёрстка, без рабочих ссылок. */
12323
+ const VoteShare = () => {
12324
+ const { t } = useGlobalContext();
12325
+ return (jsxs(VoteShareBlock, { children: [jsx(VoteSectionLabel, { children: t("vote.share", { ns: VOTE_NS, defaultValue: "Поделиться в соцсетях" }) }), jsx(VoteShareChips, { children: VOTE_SOCIALS.map(name => (jsx(VoteShareChip, { children: name }, name))) })] }));
12326
+ };
12327
+
12328
+ const VoteResults = ({ question, category, results, screen, isOwner, hasAnswers, submitting, onSubmit, onEdit, }) => {
12329
+ const { t } = useGlobalContext();
12330
+ const [selected, setSelected] = useState([]);
12331
+ const showControls = screen === "voting";
12332
+ const showShare = screen === "voting" || screen === "voted";
12333
+ const canEdit = screen === "voting" && isOwner && !hasAnswers;
12334
+ const toggle = useCallback((variantId) => {
12335
+ setSelected(prev => {
12336
+ if (!question.multiSelect)
12337
+ return [variantId];
12338
+ return prev.includes(variantId) ? prev.filter(id => id !== variantId) : [...prev, variantId];
12339
+ });
12340
+ }, [question.multiSelect]);
12341
+ const canSubmit = useMemo(() => selected.length > 0 && !submitting, [selected.length, submitting]);
12342
+ return (jsxs(VoteWrapper, { children: [category && jsx(VoteCategoryLabel, { children: category.name }), jsx(VoteQuestionText, { children: question.text }), jsx(VoteVariantList, { column: true, value: selected[0] ?? "", children: results.map(({ id, text, count, percent }) => (jsxs(VoteVariantRow, { children: [jsxs(VoteVariantHead, { children: [showControls &&
12343
+ (question.multiSelect ? (jsx(Checkbox, { checked: selected.includes(id), onChange: () => toggle(id) })) : (jsx(Radio, { value: id, checked: selected.includes(id), onChange: () => toggle(id) }))), jsx(VoteVariantText, { children: text }), jsx(VoteVariantCount, { children: count })] }), jsx(LinearProgress, { done: percent })] }, id))) }), screen === "unauthenticated" && (jsx(VoteAuthHint, { children: t("vote.authHint", { ns: VOTE_NS, defaultValue: "Чтобы проголосовать, пожалуйста, авторизуйтесь" }) })), screen === "voted" && (jsxs(VoteAcceptedBadge, { children: [jsx(Icon, { kind: "success" }), t("vote.accepted", { ns: VOTE_NS, defaultValue: "Ваш ответ принят" })] })), showControls && (jsx(RaisedButton, { primary: true, disabled: !canSubmit, onClick: () => onSubmit(selected), children: t("vote.submit", { ns: VOTE_NS, defaultValue: "Отправить ответ" }) })), canEdit && onEdit && (jsx(Flex, { children: jsx(VoteEditLink, { onClick: onEdit, children: t("vote.edit", { ns: VOTE_NS, defaultValue: "Редактировать" }) }) })), showShare && jsx(VoteShare, {})] }));
12344
+ };
12345
+
12346
+ /**
12347
+ * Тело контейнера: выбирает экран по вью-модели голосования.
12348
+ *
12349
+ * Живёт отдельно от `VoteContainer`, потому что тот отвечает за корень контейнера — размеры,
12350
+ * фон и заголовок — и не должен раздуваться ветвлением по состояниям.
12351
+ */
12352
+ const VoteScreen = ({ screen, categories, question, category, results, totalVotes, isOwner, hasAnswers, isEditing, setIsEditing, editInitialValues, submitting, createVote, submitAnswer, editVote, deleteVote, }) => {
12353
+ const handleCreate = useCallback((values) => void createVote(values), [createVote]);
12354
+ const handleEdit = useCallback((values) => void editVote(values).then(() => setIsEditing(false)), [editVote, setIsEditing]);
12355
+ const handleDelete = useCallback(() => void deleteVote().then(() => setIsEditing(false)), [deleteVote, setIsEditing]);
12356
+ const handleCancel = useCallback(() => setIsEditing(false), [setIsEditing]);
12357
+ const handleEditOpen = useCallback(() => setIsEditing(true), [setIsEditing]);
12358
+ const handleSubmitAnswer = useCallback((variantIds) => void submitAnswer(variantIds), [submitAnswer]);
12359
+ if (screen === "loading")
12360
+ return jsx(ContainerLoading, {});
12361
+ if (isEditing && question) {
12362
+ return (jsx(VoteCreateForm, { categories: categories, initialValues: editInitialValues, isEditing: true, canDelete: !hasAnswers, submitting: submitting, onSubmit: handleEdit, onDelete: handleDelete, onCancel: handleCancel }));
12363
+ }
12364
+ if (screen === "create") {
12365
+ return (jsx(VoteCreateForm, { categories: categories, isEditing: false, canDelete: false, submitting: submitting, onSubmit: handleCreate }));
12366
+ }
12367
+ if (!question)
12368
+ return null;
12369
+ return (jsx(VoteResults, { question: question, category: category, results: results, totalVotes: totalVotes, screen: screen, isOwner: isOwner, hasAnswers: hasAnswers, submitting: submitting, onSubmit: handleSubmitAnswer, onEdit: handleEditOpen }));
12370
+ };
12371
+
12372
+ /** Приводит значение атрибута к строке-идентификатору (id приходят числами/строками). */
12373
+ const toId = (value) => (value === null || value === undefined ? "" : String(value));
12374
+ /**
12375
+ * Условие равенства по числовому полю для `conditions` слоя: `field == value`.
12376
+ *
12377
+ * Двойное равенство — форма, которой написаны условия во всех конфигах проекта; лексер языка
12378
+ * условий принимает и одиночное `=`, но расходиться с авторскими конфигами незачем.
12379
+ */
12380
+ const eqCondition = (field, value) => `${field} == ${value}`;
12381
+ const props = (feature) => feature.properties ?? {};
12382
+ const mapCategories = (features) => features.map(feature => ({
12383
+ id: toId(props(feature)[VOTE_FIELDS.category.id] ?? feature.id),
12384
+ name: toId(props(feature)[VOTE_FIELDS.category.name]),
12385
+ }));
12386
+ const mapQuestion = (feature) => {
12387
+ const featureProps = props(feature);
12388
+ return {
12389
+ id: toId(featureProps[VOTE_FIELDS.question.id] ?? feature.id),
12390
+ text: toId(featureProps[VOTE_FIELDS.question.text]),
12391
+ categoryId: toId(featureProps[VOTE_FIELDS.question.categoryId]),
12392
+ userName: toId(featureProps[VOTE_FIELDS.question.userName]),
12393
+ multiSelect: Boolean(featureProps[VOTE_FIELDS.question.multiSelect]),
12394
+ };
12395
+ };
12396
+ const mapVariants = (features) => features.map(feature => ({
12397
+ id: toId(props(feature)[VOTE_FIELDS.variant.id] ?? feature.id),
12398
+ text: toId(props(feature)[VOTE_FIELDS.variant.text]),
12399
+ }));
12400
+ /** Список `variant_id` из ответов конкретного пользователя. */
12401
+ const getUserVariantIds = (answers, userName) => answers
12402
+ .filter(answer => toId(props(answer)[VOTE_FIELDS.answer.userName]) === userName)
12403
+ .map(answer => toId(props(answer)[VOTE_FIELDS.answer.variantId]));
12404
+ /** Id записей-ответов конкретного пользователя (для замены ответа при переголосовании). */
12405
+ const getUserAnswerIds = (answers, userName) => answers
12406
+ .filter(answer => toId(props(answer)[VOTE_FIELDS.answer.userName]) === userName)
12407
+ .map(answer => toId(answer.id))
12408
+ .filter(Boolean);
12409
+ /**
12410
+ * Считает голоса по вариантам. `percent` — доля голосов варианта от общего числа голосов
12411
+ * по голосованию.
12412
+ */
12413
+ const computeResults = (variants, answers) => {
12414
+ const counts = new Map();
12415
+ answers.forEach(answer => {
12416
+ const variantId = toId(props(answer)[VOTE_FIELDS.answer.variantId]);
12417
+ counts.set(variantId, (counts.get(variantId) ?? 0) + 1);
12418
+ });
12419
+ const totalVotes = answers.length;
12420
+ const results = variants.map(variant => {
12421
+ const count = counts.get(variant.id) ?? 0;
12422
+ return {
12423
+ ...variant,
12424
+ count,
12425
+ percent: totalVotes ? Math.round((count / totalVotes) * PERCENT_BASE) : 0,
12426
+ };
12427
+ });
12428
+ return { results, totalVotes };
12429
+ };
12430
+ /**
12431
+ * Сравнивает варианты из формы с существующими в БД и раскладывает на наборы операций:
12432
+ * создать (новые без id), обновить (изменился текст) и удалить (исчезли из формы).
12433
+ */
12434
+ const diffVariants = (existing, next) => {
12435
+ const nextIds = new Set(next.filter(({ id }) => id).map(({ id }) => id));
12436
+ const existingById = new Map(existing.map(variant => [variant.id, variant]));
12437
+ const toCreate = next.filter(({ id, text }) => !id && text.trim()).map(({ text }) => text.trim());
12438
+ const toUpdate = next
12439
+ .filter(({ id, text }) => id && existingById.get(id) && existingById.get(id)?.text !== text.trim())
12440
+ .map(({ id, text }) => ({ id: id, text: text.trim() }));
12441
+ const toDeleteIds = existing.filter(({ id }) => !nextIds.has(id)).map(({ id }) => id);
12442
+ return { toCreate, toUpdate, toDeleteIds };
12443
+ };
12444
+
12445
+ /**
12446
+ * Операции записи контейнера: создание голосования, отправка ответа, редактирование и удаление.
12447
+ * Все операции доступны только авторизованному пользователю.
12448
+ */
12449
+ const useVoteActions = ({ dataSources, attributeName, featureId, featureLayerName, username, question, variants, answers, onQuestionIdChange, reload, }) => {
12450
+ const { api } = useGlobalContext();
12451
+ const { questionDataSource, variantDataSource, answerDataSource } = dataSources;
12452
+ const [submitting, setSubmitting] = useState(false);
12453
+ const writeFeatureAttribute = useCallback(async (value) => {
12454
+ if (!api || !featureLayerName || !featureId || !attributeName)
12455
+ return;
12456
+ await api.layers.updateFeature(featureLayerName, [{ id: featureId, attributes: { [attributeName]: value } }]);
12457
+ }, [api, featureLayerName, featureId, attributeName]);
12458
+ const createVote = useCallback(async (values) => {
12459
+ if (!api)
12460
+ return;
12461
+ setSubmitting(true);
12462
+ try {
12463
+ const questionResult = await api.layers.createFeatures({ name: questionDataSource }, [
12464
+ {
12465
+ properties: {
12466
+ [VOTE_FIELDS.question.text]: values.text,
12467
+ [VOTE_FIELDS.question.categoryId]: values.categoryId,
12468
+ [VOTE_FIELDS.question.userName]: username,
12469
+ [VOTE_FIELDS.question.multiSelect]: values.multiSelect,
12470
+ },
12471
+ },
12472
+ ]);
12473
+ const newQuestionId = questionResult.createdIds?.[0];
12474
+ if (!newQuestionId)
12475
+ return;
12476
+ await api.layers.createFeatures({ name: variantDataSource }, values.variants
12477
+ .filter(({ text }) => text.trim())
12478
+ .map(({ text }) => ({
12479
+ properties: {
12480
+ [VOTE_FIELDS.variant.questionId]: newQuestionId,
12481
+ [VOTE_FIELDS.variant.text]: text.trim(),
12482
+ },
12483
+ })));
12484
+ await writeFeatureAttribute(newQuestionId);
12485
+ onQuestionIdChange(newQuestionId);
12486
+ reload();
12487
+ }
12488
+ finally {
12489
+ setSubmitting(false);
12490
+ }
12491
+ }, [api, questionDataSource, variantDataSource, username, writeFeatureAttribute, onQuestionIdChange, reload]);
12492
+ const submitAnswer = useCallback(async (variantIds) => {
12493
+ if (!api || !question || !variantIds.length)
12494
+ return;
12495
+ setSubmitting(true);
12496
+ try {
12497
+ // Заложено на будущее (переголосование без UI): сначала снимаем прежние ответы пользователя.
12498
+ const previousAnswerIds = getUserAnswerIds(answers, username);
12499
+ if (previousAnswerIds.length) {
12500
+ await api.layers.deleteFeatures({ name: answerDataSource, ids: previousAnswerIds });
12501
+ }
12502
+ await api.layers.createFeatures({ name: answerDataSource }, variantIds.map(variantId => ({
12503
+ properties: {
12504
+ [VOTE_FIELDS.answer.questionId]: question.id,
12505
+ [VOTE_FIELDS.answer.userName]: username,
12506
+ [VOTE_FIELDS.answer.variantId]: variantId,
12507
+ },
12508
+ })));
12509
+ reload();
12510
+ }
12511
+ finally {
12512
+ setSubmitting(false);
12513
+ }
12514
+ }, [api, question, answers, username, answerDataSource, reload]);
12515
+ const editVote = useCallback(async (values) => {
12516
+ if (!api || !question)
12517
+ return;
12518
+ setSubmitting(true);
12519
+ try {
12520
+ await api.layers.updateFeature(questionDataSource, [
12521
+ {
12522
+ id: question.id,
12523
+ attributes: {
12524
+ [VOTE_FIELDS.question.text]: values.text,
12525
+ [VOTE_FIELDS.question.categoryId]: values.categoryId,
12526
+ [VOTE_FIELDS.question.multiSelect]: values.multiSelect,
12527
+ },
12528
+ },
12529
+ ]);
12530
+ const { toCreate, toUpdate, toDeleteIds } = diffVariants(variants, values.variants);
12531
+ if (toCreate.length) {
12532
+ await api.layers.createFeatures({ name: variantDataSource }, toCreate.map(text => ({
12533
+ properties: { [VOTE_FIELDS.variant.questionId]: question.id, [VOTE_FIELDS.variant.text]: text },
12534
+ })));
12535
+ }
12536
+ if (toUpdate.length) {
12537
+ await api.layers.updateFeature(variantDataSource, toUpdate.map(({ id, text }) => ({ id, attributes: { [VOTE_FIELDS.variant.text]: text } })));
12538
+ }
12539
+ if (toDeleteIds.length) {
12540
+ await api.layers.deleteFeatures({ name: variantDataSource, ids: toDeleteIds });
12541
+ }
12542
+ reload();
12543
+ }
12544
+ finally {
12545
+ setSubmitting(false);
12546
+ }
12547
+ }, [api, question, questionDataSource, variantDataSource, variants, reload]);
12548
+ const deleteVote = useCallback(async () => {
12549
+ if (!api || !question)
12550
+ return;
12551
+ setSubmitting(true);
12552
+ try {
12553
+ const variantIds = variants.map(({ id }) => id).filter(Boolean);
12554
+ if (variantIds.length) {
12555
+ await api.layers.deleteFeatures({ name: variantDataSource, ids: variantIds });
12556
+ }
12557
+ await api.layers.deleteFeature({ name: questionDataSource, id: question.id });
12558
+ await writeFeatureAttribute(null);
12559
+ onQuestionIdChange("");
12560
+ reload();
12561
+ }
12562
+ finally {
12563
+ setSubmitting(false);
12564
+ }
12565
+ }, [api, question, variants, variantDataSource, questionDataSource, writeFeatureAttribute, onQuestionIdChange, reload]);
12566
+ return { submitting, createVote, submitAnswer, editVote, deleteVote };
12567
+ };
12568
+
12569
+ /**
12570
+ * Загружает справочник категорий, а по `questionId` — само голосование, его варианты и ответы.
12571
+ * Перечитывается при смене `questionId` или `reloadToken` (после создания/голосования/правки).
12572
+ */
12573
+ const useVoteData = ({ dataSources, questionId, reloadToken }) => {
12574
+ const { api } = useGlobalContext();
12575
+ const { categoryDataSource, questionDataSource, variantDataSource, answerDataSource } = dataSources;
12576
+ const [categories, setCategories] = useState([]);
12577
+ const [question, setQuestion] = useState(null);
12578
+ const [variants, setVariants] = useState([]);
12579
+ const [answers, setAnswers] = useState([]);
12580
+ const [loading, setLoading] = useState(false);
12581
+ const [error, setError] = useState(false);
12582
+ useEffect(() => {
12583
+ let active = true;
12584
+ if (api && categoryDataSource) {
12585
+ api.layers
12586
+ .getFeatures(categoryDataSource, { withGeom: false, limit: VOTE_FETCH_LIMIT })
12587
+ .then(response => {
12588
+ if (active)
12589
+ setCategories(mapCategories(response.features ?? []));
12590
+ })
12591
+ .catch(() => undefined);
12592
+ }
12593
+ return () => {
12594
+ active = false;
12595
+ };
12596
+ }, [api, categoryDataSource]);
12597
+ useEffect(() => {
12598
+ let active = true;
12599
+ if (!api || !questionId) {
12600
+ setQuestion(null);
12601
+ setVariants([]);
12602
+ setAnswers([]);
12603
+ return () => {
12604
+ active = false;
12605
+ };
12606
+ }
12607
+ setLoading(true);
12608
+ setError(false);
12609
+ Promise.all([
12610
+ api.layers.getFeatures(questionDataSource, {
12611
+ conditions: [eqCondition(VOTE_FIELDS.question.id, questionId)],
12612
+ withGeom: false,
12613
+ limit: 1,
12614
+ }),
12615
+ api.layers.getFeatures(variantDataSource, {
12616
+ conditions: [eqCondition(VOTE_FIELDS.variant.questionId, questionId)],
12617
+ withGeom: false,
12618
+ limit: VOTE_FETCH_LIMIT,
12619
+ }),
12620
+ api.layers.getFeatures(answerDataSource, {
12621
+ conditions: [eqCondition(VOTE_FIELDS.answer.questionId, questionId)],
12622
+ withGeom: false,
12623
+ limit: VOTE_FETCH_LIMIT,
12624
+ }),
12625
+ ])
12626
+ .then(([questionResponse, variantResponse, answerResponse]) => {
12627
+ if (!active)
12628
+ return;
12629
+ const questionFeature = questionResponse.features?.[0];
12630
+ setQuestion(questionFeature ? mapQuestion(questionFeature) : null);
12631
+ setVariants(mapVariants(variantResponse.features ?? []));
12632
+ setAnswers(answerResponse.features ?? []);
12633
+ })
12634
+ .catch(() => {
12635
+ if (active)
12636
+ setError(true);
12637
+ })
12638
+ .finally(() => {
12639
+ if (active)
12640
+ setLoading(false);
12641
+ });
12642
+ return () => {
12643
+ active = false;
12644
+ };
12645
+ }, [api, questionId, reloadToken, questionDataSource, variantDataSource, answerDataSource]);
12646
+ const { results, totalVotes } = useMemo(() => computeResults(variants, answers), [variants, answers]);
12647
+ return { categories, question, variants, answers, results, totalVotes, loading, error };
12648
+ };
12649
+
12650
+ /**
12651
+ * Текущий пользователь для контейнера голосования. Авторизация обязательна — неавторизованный
12652
+ * пользователь видит только результаты. Источник — `api.account.getUserInfo()`; ошибка/отсутствие
12653
+ * `username` трактуются как «не авторизован».
12654
+ *
12655
+ * Публичный аккаунт портала (`PUBLIC_USER_NAME` в `username` или в ролях) авторизацией не
12656
+ * считается: под ним ходят ВСЕ анонимные посетители шаренного проекта, поэтому и «мой голос»,
12657
+ * и владение голосованием опознались бы у них общими на всех.
12658
+ */
12659
+ const useVoteUser = () => {
12660
+ const { api } = useGlobalContext();
12661
+ const [username, setUsername] = useState("");
12662
+ const [roles, setRoles] = useState([]);
12663
+ const [loading, setLoading] = useState(true);
12664
+ useEffect(() => {
12665
+ let active = true;
12666
+ if (!api) {
12667
+ setLoading(false);
12668
+ return () => {
12669
+ active = false;
12670
+ };
12671
+ }
12672
+ api.account
12673
+ .getUserInfo()
12674
+ .then(info => {
12675
+ if (!active)
12676
+ return;
12677
+ setUsername(info?.username ?? "");
12678
+ setRoles(info?.roles ?? []);
12679
+ })
12680
+ .catch(() => {
12681
+ if (!active)
12682
+ return;
12683
+ setUsername("");
12684
+ setRoles([]);
12685
+ })
12686
+ .finally(() => {
12687
+ if (active)
12688
+ setLoading(false);
12689
+ });
12690
+ return () => {
12691
+ active = false;
12692
+ };
12693
+ }, [api]);
12694
+ const isPublic = useMemo(() => username === PUBLIC_USER_NAME || roles.includes(PUBLIC_USER_NAME), [username, roles]);
12695
+ const isAuthenticated = !!username && !isPublic;
12696
+ return { username, isAuthenticated, loading };
12697
+ };
12698
+
12699
+ const normalizeAttributeName = (attributeName) => {
12700
+ if (Array.isArray(attributeName))
12701
+ return attributeName[0] ?? "";
12702
+ return attributeName ?? "";
12703
+ };
12704
+ /**
12705
+ * Вью-модель контейнера голосования: определяет текущий экран и собирает данные/действия.
12706
+ * Привязка к объекту карты — через `elementConfig.attributeName` (хранит `question_id`).
12707
+ */
12708
+ const useVoteContainer = (elementConfig) => {
12709
+ const { feature, layerInfo } = useWidgetContext(WidgetType.FeatureCard);
12710
+ const options = elementConfig?.options;
12711
+ const attributeName = normalizeAttributeName(elementConfig?.attributeName);
12712
+ const dataSources = useMemo(() => ({
12713
+ categoryDataSource: options?.categoryDataSource ?? "",
12714
+ questionDataSource: options?.questionDataSource ?? "",
12715
+ variantDataSource: options?.variantDataSource ?? "",
12716
+ answerDataSource: options?.answerDataSource ?? "",
12717
+ }), [options]);
12718
+ const configured = useMemo(() => Boolean(attributeName &&
12719
+ dataSources.categoryDataSource &&
12720
+ dataSources.questionDataSource &&
12721
+ dataSources.variantDataSource &&
12722
+ dataSources.answerDataSource), [attributeName, dataSources]);
12723
+ const { username, isAuthenticated, loading: userLoading } = useVoteUser();
12724
+ const featureId = feature?.id;
12725
+ const featureQuestionId = toId(feature?.properties?.[attributeName]);
12726
+ const [overrideQuestionId, setOverrideQuestionId] = useState(null);
12727
+ useEffect(() => {
12728
+ setOverrideQuestionId(null);
12729
+ }, [featureId]);
12730
+ const questionId = overrideQuestionId ?? featureQuestionId;
12731
+ const [reloadToken, setReloadToken] = useState(0);
12732
+ const reload = useCallback(() => setReloadToken(token => token + 1), []);
12733
+ const onQuestionIdChange = useCallback((id) => setOverrideQuestionId(id), []);
12734
+ const { categories, question, variants, answers, results, totalVotes, loading } = useVoteData({
12735
+ dataSources,
12736
+ questionId,
12737
+ reloadToken,
12738
+ });
12739
+ const [isEditing, setIsEditing] = useState(false);
12740
+ const actions = useVoteActions({
12741
+ dataSources,
12742
+ attributeName,
12743
+ featureId,
12744
+ featureLayerName: layerInfo?.name,
12745
+ username,
12746
+ question,
12747
+ variants,
12748
+ answers,
12749
+ onQuestionIdChange,
12750
+ reload,
12751
+ });
12752
+ const hasAnswers = answers.length > 0;
12753
+ const isOwner = isAuthenticated && !!question && question.userName === username;
12754
+ const hasVoted = useMemo(() => getUserVariantIds(answers, username).length > 0, [answers, username]);
12755
+ const category = useMemo(() => categories.find(({ id }) => id === question?.categoryId), [categories, question?.categoryId]);
12756
+ const screen = useMemo(() => {
12757
+ if (!configured)
12758
+ return null;
12759
+ if (userLoading)
12760
+ return "loading";
12761
+ if (!questionId)
12762
+ return isAuthenticated ? "create" : null;
12763
+ if (loading && !question)
12764
+ return "loading";
12765
+ if (!question)
12766
+ return null;
12767
+ if (!isAuthenticated)
12768
+ return "unauthenticated";
12769
+ return hasVoted ? "voted" : "voting";
12770
+ }, [configured, userLoading, questionId, isAuthenticated, loading, question, hasVoted]);
12771
+ const editInitialValues = useMemo(() => question
12772
+ ? {
12773
+ categoryId: question.categoryId,
12774
+ text: question.text,
12775
+ multiSelect: question.multiSelect,
12776
+ variants: variants.map(({ id, text }) => ({ id, text })),
12777
+ }
12778
+ : undefined, [question, variants]);
12779
+ return {
12780
+ configured,
12781
+ screen,
12782
+ categories,
12783
+ question,
12784
+ category,
12785
+ results,
12786
+ totalVotes,
12787
+ isOwner,
12788
+ hasAnswers,
12789
+ isEditing,
12790
+ setIsEditing,
12791
+ editInitialValues,
12792
+ ...actions,
12793
+ };
12794
+ };
12795
+
12796
+ /**
12797
+ * Контейнер «Голосование»: привязывает опрос к объекту карты через `attributeName`
12798
+ * и показывает один из экранов — создание, голосование, после голосования
12799
+ * или (для неавторизованных) только результаты.
12800
+ *
12801
+ * Корнем владеет сам (`useContainerRoot`), поэтому шаблон записан в `ROOT_OWNING_TEMPLATES`:
12802
+ * размеры, авторский `style`, фон и заголовок живут на одном узле, без внешней обёртки.
12803
+ */
12804
+ const VoteContainer = memo(({ type, elementConfig, isVisible, renderElement }) => {
12805
+ const { root, body } = useContainerRoot({ elementConfig });
12806
+ const { configured, ...vote } = useVoteContainer(elementConfig);
12807
+ // Без имени атрибута или хотя бы одного источника контейнеру не с чем работать: раньше он
12808
+ // молча возвращал null, и опечатка в конфиге выглядела как «блока просто нет».
12809
+ if (!configured) {
12810
+ return jsx(DataSourceError, { name: elementConfig?.templateName ?? ContainerTemplate.Vote });
12811
+ }
12812
+ // Голосования нет, а создавать его посетитель не вправе — показывать нечего. Пустой заголовок
12813
+ // в карточке в этом случае тоже не нужен, поэтому не рисуем и корень.
12814
+ if (!vote.screen)
12815
+ return null;
12816
+ return (jsxs(ContainerRoot, { ...root, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsx(Container, { ...body, isColumn: true, children: jsx(VoteScreen, { ...vote, screen: vote.screen }) }))] }));
12817
+ });
12818
+
12026
12819
  const ExpandableTitle = memo(({ elementConfig, type, renderElement }) => {
12027
12820
  const { expandedContainers } = useWidgetContext(type);
12028
12821
  const { id, options, templateName, children } = elementConfig || {};
@@ -13247,6 +14040,8 @@ const EditAttachmentContainer = memo(({ type, elementConfig, renderElement }) =>
13247
14040
  setPreviewIndex(idx);
13248
14041
  }, [items]);
13249
14042
  const handleClosePreview = useCallback(() => setPreviewIndex(null), []);
14043
+ const downloadByIndex = useAttachmentDownload(items);
14044
+ const handleDownload = useCallback((_image, index) => downloadByIndex(index), [downloadByIndex]);
13250
14045
  const handleShowMore = useCallback(() => setShowMore(true), [setShowMore]);
13251
14046
  const handleDelete = useCallback((link) => {
13252
14047
  persist(items.filter(item => item.link !== link));
@@ -13297,7 +14092,7 @@ const EditAttachmentContainer = memo(({ type, elementConfig, renderElement }) =>
13297
14092
  setUploading(false);
13298
14093
  }
13299
14094
  }, [api, items, parentResourceId, persist]);
13300
- return (jsxs(AttachmentsContainer, { id: id, style: { ...BASE_CONTAINER_STYLE, ...style }, ...bgHost, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(AttachmentsHeader, { alias: renderElement?.({ id: "alias" }), count: items.length, viewMode: viewMode, onChangeViewMode: setViewMode }), jsx(AttachmentsContent, { children: viewMode === "grid" ? (jsx(AttachmentsGrid, { items: visibleItems, isEdit: true, onPreview: handlePreview, onDelete: handleDelete })) : (jsx(AttachmentsList, { items: visibleItems, isEdit: true, onPreview: handlePreview, onDelete: handleDelete })) }), hasMore && !showMore && (jsx(ShowMoreButton, { hiddenCount: hiddenCount, onClick: handleShowMore })), jsx(AddButton, { accept: fileExtensions, onSelectFiles: uploading ? () => undefined : handleUpload, onSelectFromCatalog: handleSelectFromCatalog, onSelectFromLink: handleOpenLinkDialog }), jsx(AttachmentLinkDialog, { isOpen: isLinkDialogOpen, onClose: handleCloseLinkDialog, onSubmit: handleAddByLink }), previewIndex !== null && (jsx(Preview, { images: previewImages, initialIndex: previewIndex, isOpen: previewIndex !== null, onClose: handleClosePreview }, previewIndex))] }));
14095
+ return (jsxs(AttachmentsContainer, { id: id, style: { ...BASE_CONTAINER_STYLE, ...style }, ...bgHost, children: [jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsx(AttachmentsHeader, { alias: renderElement?.({ id: "alias" }), count: items.length, viewMode: viewMode, onChangeViewMode: setViewMode }), jsx(AttachmentsContent, { children: viewMode === "grid" ? (jsx(AttachmentsGrid, { items: visibleItems, isEdit: true, onPreview: handlePreview, onDelete: handleDelete })) : (jsx(AttachmentsList, { items: visibleItems, isEdit: true, onPreview: handlePreview, onDelete: handleDelete })) }), hasMore && !showMore && (jsx(ShowMoreButton, { hiddenCount: hiddenCount, onClick: handleShowMore })), jsx(AddButton, { accept: fileExtensions, onSelectFiles: uploading ? () => undefined : handleUpload, onSelectFromCatalog: handleSelectFromCatalog, onSelectFromLink: handleOpenLinkDialog }), jsx(AttachmentLinkDialog, { isOpen: isLinkDialogOpen, onClose: handleCloseLinkDialog, onSubmit: handleAddByLink }), previewIndex !== null && (jsx(Preview, { images: previewImages, initialIndex: previewIndex, isOpen: previewIndex !== null, onClose: handleClosePreview, onDownload: handleDownload }, previewIndex))] }));
13301
14096
  });
13302
14097
 
13303
14098
  // `ProgressContainer` и `RoundedBackgroundContainer` исторически принимают `InnerContainerProps`
@@ -13351,6 +14146,7 @@ const createContainerComponents = () => ({
13351
14146
  [ContainerTemplate.EditAttachment]: EditAttachmentContainer,
13352
14147
  [ContainerTemplate.Attachment]: AttachmentContainer,
13353
14148
  [ContainerTemplate.EditGroup]: EditGroupContainer,
14149
+ [ContainerTemplate.Vote]: VoteContainer,
13354
14150
  [ContainerTemplate.ContainersGroup]: ContainersGroupContainer,
13355
14151
  [ContainerTemplate.GridRow]: GridRowContainer,
13356
14152
  [ContainerTemplate.StructuredData]: StructuredDataContainer,
@@ -13386,6 +14182,7 @@ const ROOT_OWNING_TEMPLATES = new Set([
13386
14182
  ContainerTemplate.StructuredData,
13387
14183
  ContainerTemplate.Task,
13388
14184
  ContainerTemplate.Upload,
14185
+ ContainerTemplate.Vote,
13389
14186
  ]);
13390
14187
  let cachedContainerComponents = null;
13391
14188
  const getContainerComponents = () => {
@@ -18454,5 +19251,5 @@ const DEFAULT_HEATMAP_STYLE = {
18454
19251
  ],
18455
19252
  };
18456
19253
 
18457
- export { ALIGNMENTS, ALIGN_ITEMS, ATTRIBUTE_ICON_ELEMENT_TYPES, AddButtonRow, AddFeatureButton, AddFeatureContainer, AlertIconContainer, AttachmentContainer, AttributeGalleryContainer, AttributeLabel, BASE_CONTAINER_STYLE, BG_IMAGE_SLOT_ID, BaseMapTheme, CHART_TYPES, COMPACT_FRACTION_DIGITS, CONFIG_PAGES_ID, CONFIG_PAGE_ID, CONTAINERS_GROUP_DEFAULTS, CONTAINER_BODY_ATTRIBUTE, CONTAINER_BODY_FILL_STYLE, CameraContainer, Chart, ChartContainer, ChartLegend, ChartLoading, Container, ContainerBackground, ContainerChildren, ContainerLoading, ContainerRoot, ContainerTemplate, ContainerWrapper, ContainersGroupContainer, DASHBOARD_OVERLAY_Z_INDEX, DEFAULT_ATTRIBUTE_NAME, DEFAULT_BARCHART_RADIUS, DEFAULT_BASE_MAP, DEFAULT_BLUR, DEFAULT_CHART_ANGLE, DEFAULT_CHART_HEIGHT, DEFAULT_CHART_WIDTH, DEFAULT_CIRCLE_RADIUS, DEFAULT_CIRCLE_STROKE_WIDTH, DEFAULT_CIRCLE_STYLE, DEFAULT_COLOR, DEFAULT_DASHBOARD_CONFIG, DEFAULT_DATA_SOURCE_LIMIT, DEFAULT_DROPDOWN_WIDTH, DEFAULT_FILL_EXTRUSION_BASE, DEFAULT_FILL_EXTRUSION_HEIGHT, DEFAULT_FILL_EXTRUSION_STYLE, DEFAULT_FILL_EXTRUSION_VERTICAL_GRADIENT, DEFAULT_FILL_STYLE, DEFAULT_FILTER_PADDING, DEFAULT_GRID_GAP, DEFAULT_HEATMAP_COLOR, DEFAULT_HEATMAP_INTENSITY, DEFAULT_HEATMAP_RADIUS, DEFAULT_HEATMAP_STYLE, DEFAULT_HEATMAP_WEIGHT, DEFAULT_ICON_ANCHOR, DEFAULT_ICON_OVERLAP, DEFAULT_ICON_PADDING, DEFAULT_ICON_ROTATE, DEFAULT_ICON_SIZE, DEFAULT_ID_ATTRIBUTE_NAME, DEFAULT_LAT, DEFAULT_LINE_CAP, DEFAULT_LINE_JOIN, DEFAULT_LINE_STYLE, DEFAULT_LINE_WIDTH, DEFAULT_LNG, DEFAULT_OPACITY, DEFAULT_PAGES_CONFIG, DEFAULT_PIECHART_RADIUS, DEFAULT_SYMBOL_COLOR, DEFAULT_SYMBOL_HALO_COLOR, DEFAULT_SYMBOL_PLACEMENT, DEFAULT_SYMBOL_SPACING, DEFAULT_SYMBOL_STYLE, DEFAULT_TEXT_ANCHOR, DEFAULT_TEXT_JUSTIFY, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_TRANSFORM, DEFAULT_TRACK_FR, DEFAULT_TRANSLATE, DEFAULT_ZOOM, DRAG_THRESHOLD_PX, Dashboard, DashboardCheckbox, DashboardChip, DashboardContent, DashboardContext, DashboardDefaultHeader, DashboardHeader, DashboardLoading, DashboardPlaceholder, DashboardPlaceholderWrap, DashboardProvider, DashboardWrapper, DataSourceContainer, DataSourceError, DataSourceErrorContainer, DataSourceInnerContainer, DataSourceProgressContainer, DateFormat, DefaultAttributesContainer, DefaultHeaderContainer, DefaultHeaderWrapper, DividerContainer, EMPTY_DATA_SOURCE_LAYER_INFO, EXTENT_FILTER_NAME, EditGeometryType, ElementButton, ElementCamera, ElementChart, ElementChips, ElementControl, ElementIcon, ElementImage, ElementLegend, ElementLink, ElementMarkdown, ElementSlideshow, ElementSvg, ElementTable, ElementTooltip, ElementValueWrapper, ExpandableTitle, FEATURE_CARD_DEFAULT_COLORS, FEATURE_CARD_OTHER_COLOR, FILL_SIZE, FILTERED_VALUE_OPACITY, FILTER_PREFIX, FR_PRECISION, FeatureCardBackgroundHeader, FeatureCardButtons, FeatureCardContext, FeatureCardDefaultHeader, FeatureCardHeader, FeatureCardProvider, FeatureCardSlideshowHeader, FeatureCardTitle, FeatureControls, FeatureTitleContainer, FiltersContainer, GEOMETRY_ATTRIBUTE, GEOMETRY_FILTER_NAME, GRID_AUTO_FILL_DEFAULTS, GRID_CELL_ATTR, GRID_CELL_ID_PREFIX, GRID_DRAGGING_ATTR, GRID_DRAG_SOURCE_ATTR, GRID_DROP_TARGET_ATTR, GRID_FILL_DEFAULTS, GRID_HANDLE_ATTR, GRID_HANDLE_PROPS, GRID_ROW_ID_PREFIX, GlobalContext, GlobalProvider, GridRowContainer, HANDLE_BLEED_PX, Header, HeaderContainer, HeaderFontColorMixin, HeaderFrontView, HeaderTemplate, HeaderTitleContainer, HiddenTitleItems, IconContainer, ImageContainer, LEFT_PANEL_HEADER_HEIGHT, Layer, LayerDescription, LayerGroupList, LayerIcon, LayerIconContainer, LayerListContainer, LayerTree, LayersContainer, LayersListWrapper, LinearProgressContainer, LogTerminal, LogoContainer, MAP_VIEW_FILTER_NAMES, MAX_CHART_WIDTH, MAX_TRACKS, MIN_TRACK_PX, MIN_TRACK_RATIO, Map$1 as Map, MapContext, MapProvider, NON_TRACK_SLOT_IDS, NO_CELL_DRAG_SELECTOR, NO_CONTENT_VALUE, NUMERIC_ATTRIBUTE_TYPES, NoLiveSnapshotContainer, OBJECT_FITS, OVERFLOWS, OneColumnContainer, PIE_CHART_TOOLTIP_STYLE, POLL_SUBTASK_INTERVAL_MS, POLL_SUBTASK_TIMEOUT_MS, PROVIDER_PREFIX, PageNavigator, PageTitle, PageTitleContainer, PagesContainer, Pagination, PresentationHeader, PresentationHeaderButtons, PresentationHeaderTools, PresentationPanelContainer, PresentationPanelWrapper, PresentationWrapper, ProgressContainer, ProviderPrefix, QUERY_DESCRIPTION_CACHE_TTL, RoundedBackgroundContainer, SAVE_HOOK_RESULT_DURATION, SERVER_NOTIFICATION_EVENT, STACK_BAR_TOTAL_HEIGHT, ScalingFactor, ServerNotificationsContext, ServerNotificationsProvider, SlideshowContainer, SmallPreviewContainer$1 as SmallPreviewContainer, SmallPreviewControl, SmallPreviewCounter, SmallPreviewImages, SmallPreviewLeft, SmallPreviewRight, StackBar, StructuredDataContainer, SvgImage, TILE_ALIGNMENTS, TIME_ZONE_FORMAT, TITLE_SLOT_IDS, TabsContainer, TextTrim, ThemeName, TitleContainer, TopContainer, TopContainerButtons, TwoColumnContainer, UploadContainer, VIEW_MODES, WidgetType, ZOOM_FILTER_NAME, addDataSource, addDataSources, adjustColor, applyFiltersToCondition, applyQueryFilters, applyTreeFilterToCondition, applyVarsToCondition, asAttributeName, asChartId, asContainerId, asDataSourceName, asFilterName, asLayerName, asModalId, asResourceId, asTabId, buildGridTemplate, buildTrackTemplate, checkEqualOrIncludes, checkIsLoading, collectConfigIds, containsNodeId, createConfigLayer, createConfigPage, createGridCell, createGridIdFactory, createGridRow, createNewPageId, createSaveNotificationId, dateOptions, debounce, decimalOpacityToHex, enrichStyleItemsWithIds, enrichStyleModelsWithIds, eqlParametersToPayload, fetchQueryDescription, findAttributeInExpression, findCellContext, formatArea, formatAttributeValue, formatChartRelatedValue, formatConditionValue, formatDataSourceCondition, formatDate$1 as formatDate, formatElementValue, formatLength, formatNumber, formatPolygonMeasure, geometryToEwkt, getActualExtrusionHeight, getAttributeByName, getAttributeConfigurationByName, getAttributeIconElement, getAttributeIconUrl, getAttributeValue, getAttributesConfiguration, getAverageTrackSize, getChartAxes, getChartFilterName, getChartMarkers, getConfigFilter, getContainerComponent, getDashboardHeader, getDataFromAttributes, getDataFromRelatedFeatures, getDataSource, getDataSourceFilterValue, getDataSourceLayerInfo, getDate, getDefaultConfig, getDisplayTemplateNameFromAttribute, getElementValue, getFeatureAttributes, getFeatureCardHeader, getFilterComponent, getFilterSelectedItems, getFilterValue, getFormattedAttributes, getGradientColors, getImageUrl, getLayerClientStyle, getLayerInfo, getLayerInfoAttribute, getLayerInfoFromDataSources, getLayoutChildren, getMapViewDataSources, getPagesFromConfig, getPagesFromProjectInfo, getProxyService, getRelatedAttribute, getRenderElement, getResourceUrl, getRootElementId, getSelectedFilterValue, getSlideshowImages, getStyleAttributes, getSvgUrl, getTemplateNameFromAttribute, getThemeByName, getTotalFromAttributes, getTotalFromRelatedFeatures, getTrackSize, getTrackSizeKey, getTrackSizes, getWrapperSizeStyle, hasContainerBgImage, hexToRgba, isCrossOriginUrl, isEmptyElementValue, isEmptyValue, isFeaturesFilterValue, isFillSize, isFrSize, isGridNode, isHiddenEmptyValue, isHookActive, isLayerService, isNotValidSelectedTab, isNumeric, isObject, isProxyService, isRootOwningContainer, isTreeFilterValue, isVisibleContainer, mapNodeById, mergeAttributeConfigurations, metersPerPixel, noMarginMixin, numberOptions, parseFrValue, parseIconNames, parseIconNamesFromClientStyle, pieChartTooltipFromAttributes, pieChartTooltipFromRelatedFeatures, pointOptions, removeDataSource, removeTracks, replaceNodesByIds, rgbToHex, roundFr, roundTotalSum, setLayoutChildren, sizeCssMixin, sliceShownOtherItems, stretchPalette, timeOptions, toConditionsArray, toCssSize, toFrSize, toPxNumber, toRenderableValue, tooltipNameFromAttributes, tooltipValueFromAttributes, tooltipValueFromRelatedFeatures, transparentizeColor, updateDataSource, useAfterSave, useAppHeight, useAttachmentItems, useAttachmentPreviewImages, useAutoCompleteControl, useBeforeSave, useBgImageHost, useChartChange, useChartData, useContainerAttributes, useContainerRoot, useCurrentPageLayers, useCustomFeatureSelect, useDashboardHeader, useDataSourceLoading, useDataSources, useDebouncedCallback, useDiffPage, useEditGroupAttributes, useEqualTileWidth, useExpandableContainers, useExportPdf, useFeatureSaveHooks, useFetchImageWithAuth, useFetchWithAuth, useGetConfigLayer, useGlobalContext, useHeaderRender, useHideIfEmptyDataSource, useIconsFromLayers, useLayerHiddenAttributes, useLayerParams, useMapContext, useMapDraw, useMapImages, useMaxZoomTo, useProjectDashboardInit, usePythonSandbox, usePythonTask, useRedrawLayer, useRelatedDataSourceAttributes, useRemoteTask, useRenderElement, useResizeBox, useSavePrototypeBuilder, useServerNotificationsContext, useShownOtherItems, useToggle, useUpdateDataSource, useVisibleProjectItems, useWidgetConfig, useWidgetContext, useWidgetFilters, useWidgetPage, useWindowResize, useWrapperSize, useZoomToFeatures, useZoomToPoint, withTrackSize };
19254
+ export { ALIGNMENTS, ALIGN_ITEMS, ATTRIBUTE_ICON_ELEMENT_TYPES, AddButtonRow, AddFeatureButton, AddFeatureContainer, AlertIconContainer, AttachmentContainer, AttributeGalleryContainer, AttributeLabel, BASE_CONTAINER_STYLE, BG_IMAGE_SLOT_ID, BaseMapTheme, CHART_TYPES, COMPACT_FRACTION_DIGITS, CONFIG_PAGES_ID, CONFIG_PAGE_ID, CONTAINERS_GROUP_DEFAULTS, CONTAINER_BODY_ATTRIBUTE, CONTAINER_BODY_FILL_STYLE, CameraContainer, Chart, ChartContainer, ChartLegend, ChartLoading, Container, ContainerBackground, ContainerChildren, ContainerLoading, ContainerRoot, ContainerTemplate, ContainerWrapper, ContainersGroupContainer, DASHBOARD_OVERLAY_Z_INDEX, DEFAULT_ATTRIBUTE_NAME, DEFAULT_BARCHART_RADIUS, DEFAULT_BASE_MAP, DEFAULT_BLUR, DEFAULT_CHART_ANGLE, DEFAULT_CHART_HEIGHT, DEFAULT_CHART_WIDTH, DEFAULT_CIRCLE_RADIUS, DEFAULT_CIRCLE_STROKE_WIDTH, DEFAULT_CIRCLE_STYLE, DEFAULT_COLOR, DEFAULT_DASHBOARD_CONFIG, DEFAULT_DATA_SOURCE_LIMIT, DEFAULT_DROPDOWN_WIDTH, DEFAULT_FILL_EXTRUSION_BASE, DEFAULT_FILL_EXTRUSION_HEIGHT, DEFAULT_FILL_EXTRUSION_STYLE, DEFAULT_FILL_EXTRUSION_VERTICAL_GRADIENT, DEFAULT_FILL_STYLE, DEFAULT_FILTER_PADDING, DEFAULT_GRID_GAP, DEFAULT_HEATMAP_COLOR, DEFAULT_HEATMAP_INTENSITY, DEFAULT_HEATMAP_RADIUS, DEFAULT_HEATMAP_STYLE, DEFAULT_HEATMAP_WEIGHT, DEFAULT_ICON_ANCHOR, DEFAULT_ICON_OVERLAP, DEFAULT_ICON_PADDING, DEFAULT_ICON_ROTATE, DEFAULT_ICON_SIZE, DEFAULT_ID_ATTRIBUTE_NAME, DEFAULT_LAT, DEFAULT_LINE_CAP, DEFAULT_LINE_JOIN, DEFAULT_LINE_STYLE, DEFAULT_LINE_WIDTH, DEFAULT_LNG, DEFAULT_OPACITY, DEFAULT_PAGES_CONFIG, DEFAULT_PIECHART_RADIUS, DEFAULT_SYMBOL_COLOR, DEFAULT_SYMBOL_HALO_COLOR, DEFAULT_SYMBOL_PLACEMENT, DEFAULT_SYMBOL_SPACING, DEFAULT_SYMBOL_STYLE, DEFAULT_TEXT_ANCHOR, DEFAULT_TEXT_JUSTIFY, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_TRANSFORM, DEFAULT_TRACK_FR, DEFAULT_TRANSLATE, DEFAULT_ZOOM, DRAG_THRESHOLD_PX, Dashboard, DashboardCheckbox, DashboardChip, DashboardContent, DashboardContext, DashboardDefaultHeader, DashboardHeader, DashboardLoading, DashboardPlaceholder, DashboardPlaceholderWrap, DashboardProvider, DashboardWrapper, DataSourceContainer, DataSourceError, DataSourceErrorContainer, DataSourceInnerContainer, DataSourceProgressContainer, DateFormat, DefaultAttributesContainer, DefaultHeaderContainer, DefaultHeaderWrapper, DividerContainer, EMPTY_DATA_SOURCE_LAYER_INFO, EXTENT_FILTER_NAME, EditGeometryType, ElementButton, ElementCamera, ElementChart, ElementChips, ElementControl, ElementIcon, ElementImage, ElementLegend, ElementLink, ElementMarkdown, ElementSlideshow, ElementSvg, ElementTable, ElementTooltip, ElementValueWrapper, ExpandableTitle, FEATURE_CARD_DEFAULT_COLORS, FEATURE_CARD_OTHER_COLOR, FILL_SIZE, FILTERED_VALUE_OPACITY, FILTER_PREFIX, FR_PRECISION, FeatureCardBackgroundHeader, FeatureCardButtons, FeatureCardContext, FeatureCardDefaultHeader, FeatureCardHeader, FeatureCardProvider, FeatureCardSlideshowHeader, FeatureCardTitle, FeatureControls, FeatureTitleContainer, FiltersContainer, GEOMETRY_ATTRIBUTE, GEOMETRY_FILTER_NAME, GRID_AUTO_FILL_DEFAULTS, GRID_CELL_ATTR, GRID_CELL_ID_PREFIX, GRID_DRAGGING_ATTR, GRID_DRAG_SOURCE_ATTR, GRID_DROP_TARGET_ATTR, GRID_FILL_DEFAULTS, GRID_HANDLE_ATTR, GRID_HANDLE_PROPS, GRID_ROW_ID_PREFIX, GlobalContext, GlobalProvider, GridRowContainer, HANDLE_BLEED_PX, Header, HeaderContainer, HeaderFontColorMixin, HeaderFrontView, HeaderTemplate, HeaderTitleContainer, HiddenTitleItems, IconContainer, ImageContainer, LEFT_PANEL_HEADER_HEIGHT, Layer, LayerDescription, LayerGroupList, LayerIcon, LayerIconContainer, LayerListContainer, LayerTree, LayersContainer, LayersListWrapper, LinearProgressContainer, LogTerminal, LogoContainer, MAP_VIEW_FILTER_NAMES, MAX_CHART_WIDTH, MAX_TRACKS, MIN_TRACK_PX, MIN_TRACK_RATIO, Map$1 as Map, MapContext, MapProvider, NON_TRACK_SLOT_IDS, NO_CELL_DRAG_SELECTOR, NO_CONTENT_VALUE, NUMERIC_ATTRIBUTE_TYPES, NoLiveSnapshotContainer, OBJECT_FITS, OVERFLOWS, OneColumnContainer, PIE_CHART_TOOLTIP_STYLE, POLL_SUBTASK_INTERVAL_MS, POLL_SUBTASK_TIMEOUT_MS, PROVIDER_PREFIX, PageNavigator, PageTitle, PageTitleContainer, PagesContainer, Pagination, PresentationHeader, PresentationHeaderButtons, PresentationHeaderTools, PresentationPanelContainer, PresentationPanelWrapper, PresentationWrapper, ProgressContainer, ProviderPrefix, QUERY_DESCRIPTION_CACHE_TTL, RoundedBackgroundContainer, SAVE_HOOK_RESULT_DURATION, SERVER_NOTIFICATION_EVENT, STACK_BAR_TOTAL_HEIGHT, ScalingFactor, ServerNotificationsContext, ServerNotificationsProvider, SlideshowContainer, SmallPreviewContainer$1 as SmallPreviewContainer, SmallPreviewControl, SmallPreviewCounter, SmallPreviewImages, SmallPreviewLeft, SmallPreviewRight, StackBar, StructuredDataContainer, SvgImage, TILE_ALIGNMENTS, TIME_ZONE_FORMAT, TITLE_SLOT_IDS, TabsContainer, TextTrim, ThemeName, TitleContainer, TopContainer, TopContainerButtons, TwoColumnContainer, UploadContainer, VIEW_MODES, VoteContainer, WidgetType, ZOOM_FILTER_NAME, addDataSource, addDataSources, adjustColor, applyFiltersToCondition, applyQueryFilters, applyTreeFilterToCondition, applyVarsToCondition, asAttributeName, asChartId, asContainerId, asDataSourceName, asFilterName, asLayerName, asModalId, asResourceId, asTabId, buildGridTemplate, buildTrackTemplate, checkEqualOrIncludes, checkIsLoading, collectConfigIds, containsNodeId, createConfigLayer, createConfigPage, createGridCell, createGridIdFactory, createGridRow, createNewPageId, createSaveNotificationId, dateOptions, debounce, decimalOpacityToHex, enrichStyleItemsWithIds, enrichStyleModelsWithIds, eqlParametersToPayload, fetchQueryDescription, findAttributeInExpression, findCellContext, formatArea, formatAttributeValue, formatChartRelatedValue, formatConditionValue, formatDataSourceCondition, formatDate$1 as formatDate, formatElementValue, formatLength, formatNumber, formatPolygonMeasure, geometryToEwkt, getActualExtrusionHeight, getAttributeByName, getAttributeConfigurationByName, getAttributeIconElement, getAttributeIconUrl, getAttributeValue, getAttributesConfiguration, getAverageTrackSize, getChartAxes, getChartFilterName, getChartMarkers, getConfigFilter, getContainerComponent, getDashboardHeader, getDataFromAttributes, getDataFromRelatedFeatures, getDataSource, getDataSourceFilterValue, getDataSourceLayerInfo, getDate, getDefaultConfig, getDisplayTemplateNameFromAttribute, getElementValue, getFeatureAttributes, getFeatureCardHeader, getFilterComponent, getFilterSelectedItems, getFilterValue, getFormattedAttributes, getGradientColors, getImageUrl, getLayerClientStyle, getLayerInfo, getLayerInfoAttribute, getLayerInfoFromDataSources, getLayoutChildren, getMapViewDataSources, getPagesFromConfig, getPagesFromProjectInfo, getProxyService, getRelatedAttribute, getRenderElement, getResourceUrl, getRootElementId, getSelectedFilterValue, getSlideshowImages, getStyleAttributes, getSvgUrl, getTemplateNameFromAttribute, getThemeByName, getTotalFromAttributes, getTotalFromRelatedFeatures, getTrackSize, getTrackSizeKey, getTrackSizes, getWrapperSizeStyle, hasContainerBgImage, hexToRgba, isCrossOriginUrl, isEmptyElementValue, isEmptyValue, isFeaturesFilterValue, isFillSize, isFrSize, isGridNode, isHiddenEmptyValue, isHookActive, isLayerService, isNotValidSelectedTab, isNumeric, isObject, isProxyService, isRootOwningContainer, isTreeFilterValue, isVisibleContainer, mapNodeById, mergeAttributeConfigurations, metersPerPixel, noMarginMixin, numberOptions, parseFrValue, parseIconNames, parseIconNamesFromClientStyle, pieChartTooltipFromAttributes, pieChartTooltipFromRelatedFeatures, pointOptions, removeDataSource, removeTracks, replaceNodesByIds, rgbToHex, roundFr, roundTotalSum, setLayoutChildren, sizeCssMixin, sliceShownOtherItems, stretchPalette, timeOptions, toConditionsArray, toCssSize, toFrSize, toPxNumber, toRenderableValue, tooltipNameFromAttributes, tooltipValueFromAttributes, tooltipValueFromRelatedFeatures, transparentizeColor, updateDataSource, useAfterSave, useAppHeight, useAttachmentDownload, useAttachmentItems, useAttachmentPreviewImages, useAutoCompleteControl, useBeforeSave, useBgImageHost, useChartChange, useChartData, useContainerAttributes, useContainerRoot, useCurrentPageLayers, useCustomFeatureSelect, useDashboardHeader, useDataSourceLoading, useDataSources, useDebouncedCallback, useDiffPage, useEditGroupAttributes, useEqualTileWidth, useExpandableContainers, useExportPdf, useFeatureSaveHooks, useFetchImageWithAuth, useFetchWithAuth, useGetConfigLayer, useGlobalContext, useHeaderRender, useHideIfEmptyDataSource, useIconsFromLayers, useLayerHiddenAttributes, useLayerParams, useMapContext, useMapDraw, useMapImages, useMaxZoomTo, useProjectDashboardInit, usePythonSandbox, usePythonTask, useRedrawLayer, useRelatedDataSourceAttributes, useRemoteTask, useRenderElement, useResizeBox, useSavePrototypeBuilder, useServerNotificationsContext, useShownOtherItems, useToggle, useUpdateDataSource, useVisibleProjectItems, useWidgetConfig, useWidgetContext, useWidgetFilters, useWidgetPage, useWindowResize, useWrapperSize, useZoomToFeatures, useZoomToPoint, withTrackSize };
18458
19255
  //# sourceMappingURL=react.esm.js.map