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