@evergis/react 4.0.137 → 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/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/types.d.ts +16 -2
- package/dist/index.js +961 -229
- package/dist/index.js.map +1 -1
- package/dist/react.esm.js +963 -232
- package/dist/react.esm.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -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 });
|
|
@@ -8667,61 +8505,6 @@ const useUpdateDataSource = ({ dataSource, config, filters, attributes, layerPar
|
|
|
8667
8505
|
}, [dataSource, getDataSourcePromises, getUpdatedDataSources, dataSources]);
|
|
8668
8506
|
};
|
|
8669
8507
|
|
|
8670
|
-
const useRenderContainer = ({ elementConfig, type, renderElement, renderBody, }) => {
|
|
8671
|
-
const { attributes } = useWidgetContext(type);
|
|
8672
|
-
const { getRenderContainerItem, attributesToRender } = useContainerAttributes({
|
|
8673
|
-
elementConfig,
|
|
8674
|
-
type,
|
|
8675
|
-
renderElement,
|
|
8676
|
-
});
|
|
8677
|
-
const renderContainer = React.useCallback((attribute) => {
|
|
8678
|
-
const item = getRenderContainerItem(elementConfig, attribute);
|
|
8679
|
-
const itemAttribute = attribute
|
|
8680
|
-
? attributes?.find(({ attributeName }) => attributeName === attribute)
|
|
8681
|
-
: undefined;
|
|
8682
|
-
const overrideTemplateName = getDisplayTemplateNameFromAttribute(itemAttribute);
|
|
8683
|
-
if (overrideTemplateName) {
|
|
8684
|
-
const OverrideContainer = getContainerComponent(overrideTemplateName);
|
|
8685
|
-
if (OverrideContainer) {
|
|
8686
|
-
const itemConfig = {
|
|
8687
|
-
...elementConfig,
|
|
8688
|
-
children: (elementConfig?.children ?? []).map(child => ({
|
|
8689
|
-
...child,
|
|
8690
|
-
attributeName: attribute,
|
|
8691
|
-
})),
|
|
8692
|
-
templateName: overrideTemplateName,
|
|
8693
|
-
};
|
|
8694
|
-
return (jsxRuntime.jsx(OverrideContainer, { type: type, elementConfig: itemConfig, renderElement: item.render }, attribute));
|
|
8695
|
-
}
|
|
8696
|
-
}
|
|
8697
|
-
if (isEmptyElementValue(item.value) && item.hideEmpty)
|
|
8698
|
-
return null;
|
|
8699
|
-
return renderBody(item, attribute);
|
|
8700
|
-
}, [getRenderContainerItem, elementConfig, attributes, type, renderBody]);
|
|
8701
|
-
return { renderContainer, attributesToRender };
|
|
8702
|
-
};
|
|
8703
|
-
|
|
8704
|
-
const OneColumnContainer = React.memo(({ type, elementConfig, renderElement }) => {
|
|
8705
|
-
const { width, height, overflow } = elementConfig?.options || {};
|
|
8706
|
-
const templateName = elementConfig?.templateName;
|
|
8707
|
-
const bgHost = useBgImageHost(elementConfig);
|
|
8708
|
-
const renderBody = React.useCallback(({ id, value, style, hasUnits, render }) => (jsxRuntime.jsxs(Container, { id: id, isColumn: true, "data-templatename": templateName, style: style, "$sizeCss": getWrapperSizeStyle({
|
|
8709
|
-
style,
|
|
8710
|
-
width,
|
|
8711
|
-
height,
|
|
8712
|
-
overflow,
|
|
8713
|
-
defaults: BASE_CONTAINER_STYLE,
|
|
8714
|
-
defaultWidth: FILL_SIZE,
|
|
8715
|
-
}), ...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]);
|
|
8716
|
-
const { renderContainer, attributesToRender } = useRenderContainer({
|
|
8717
|
-
type,
|
|
8718
|
-
elementConfig,
|
|
8719
|
-
renderElement,
|
|
8720
|
-
renderBody,
|
|
8721
|
-
});
|
|
8722
|
-
return attributesToRender ? (jsxRuntime.jsx(jsxRuntime.Fragment, { children: attributesToRender.map(renderContainer) })) : (renderContainer());
|
|
8723
|
-
});
|
|
8724
|
-
|
|
8725
8508
|
/** Обычная (не сеточная) раскладка группы: дети в колонку или в строку. */
|
|
8726
8509
|
const FlowGroup = React.memo(({ elementConfig, type, renderElement }) => {
|
|
8727
8510
|
const { expandedContainers } = useWidgetContext(type);
|
|
@@ -9274,18 +9057,181 @@ const AttachmentContainer = React.memo(({ type, elementConfig, renderElement })
|
|
|
9274
9057
|
}) }, previewIndex))] }) }))] }));
|
|
9275
9058
|
});
|
|
9276
9059
|
|
|
9277
|
-
const
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
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" }) `
|
|
9066
|
+
cursor: ${({ onClick }) => (onClick ? "pointer" : "default")};
|
|
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)};
|
|
9074
|
+
`;
|
|
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};
|
|
9081
|
+
`;
|
|
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};
|
|
9086
|
+
`;
|
|
9285
9087
|
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
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
|
+
*
|
|
9289
9235
|
* Компонент `Chart` рендерится через `renderElement({ id: "chart" })` и получает только дочерний
|
|
9290
9236
|
* узел `{ id: "chart" }` — опции контейнера до него не доходят. `ChartContainer` оборачивает
|
|
9291
9237
|
* график в провайдер этого контекста, а `Chart` читает значение через `useContext`.
|
|
@@ -10921,6 +10867,61 @@ const LayersContainer = React.memo(({ type, elementConfig, renderElement }) => {
|
|
|
10921
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 }) }))] }));
|
|
10922
10868
|
});
|
|
10923
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
|
+
|
|
10924
10925
|
const PagesContainer = React.memo(({ type = exports.WidgetType.Dashboard, noBorders }) => {
|
|
10925
10926
|
const { config } = useWidgetConfig(type);
|
|
10926
10927
|
const { pageIndex, currentPage } = useWidgetPage(type);
|
|
@@ -12089,6 +12090,734 @@ const UploadContainer = React.memo(({ type, elementConfig, renderElement }) => {
|
|
|
12089
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 }) }))] }));
|
|
12090
12091
|
});
|
|
12091
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
|
+
|
|
12092
12821
|
const ExpandableTitle = React.memo(({ elementConfig, type, renderElement }) => {
|
|
12093
12822
|
const { expandedContainers } = useWidgetContext(type);
|
|
12094
12823
|
const { id, options, templateName, children } = elementConfig || {};
|
|
@@ -13419,6 +14148,7 @@ const createContainerComponents = () => ({
|
|
|
13419
14148
|
[exports.ContainerTemplate.EditAttachment]: EditAttachmentContainer,
|
|
13420
14149
|
[exports.ContainerTemplate.Attachment]: AttachmentContainer,
|
|
13421
14150
|
[exports.ContainerTemplate.EditGroup]: EditGroupContainer,
|
|
14151
|
+
[exports.ContainerTemplate.Vote]: VoteContainer,
|
|
13422
14152
|
[exports.ContainerTemplate.ContainersGroup]: ContainersGroupContainer,
|
|
13423
14153
|
[exports.ContainerTemplate.GridRow]: GridRowContainer,
|
|
13424
14154
|
[exports.ContainerTemplate.StructuredData]: StructuredDataContainer,
|
|
@@ -13454,6 +14184,7 @@ const ROOT_OWNING_TEMPLATES = new Set([
|
|
|
13454
14184
|
exports.ContainerTemplate.StructuredData,
|
|
13455
14185
|
exports.ContainerTemplate.Task,
|
|
13456
14186
|
exports.ContainerTemplate.Upload,
|
|
14187
|
+
exports.ContainerTemplate.Vote,
|
|
13457
14188
|
]);
|
|
13458
14189
|
let cachedContainerComponents = null;
|
|
13459
14190
|
const getContainerComponents = () => {
|
|
@@ -18761,6 +19492,7 @@ exports.TopContainerButtons = TopContainerButtons;
|
|
|
18761
19492
|
exports.TwoColumnContainer = TwoColumnContainer;
|
|
18762
19493
|
exports.UploadContainer = UploadContainer;
|
|
18763
19494
|
exports.VIEW_MODES = VIEW_MODES;
|
|
19495
|
+
exports.VoteContainer = VoteContainer;
|
|
18764
19496
|
exports.ZOOM_FILTER_NAME = ZOOM_FILTER_NAME;
|
|
18765
19497
|
exports.addDataSource = addDataSource;
|
|
18766
19498
|
exports.addDataSources = addDataSources;
|