@micha.bigler/survey-renderer 0.1.0

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.
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Button, Paper, Stack, Typography } from "@mui/material";
4
+ const SurveyCompletionLayout = ({ actionButtons = [], body = "", headline = "", primaryCard = null, resourceCard = null, rootId = "survey-completion-layout", }) => (_jsx(Paper, { id: rootId, elevation: 0, sx: {
5
+ border: "1px solid rgba(20, 83, 45, 0.08)",
6
+ boxShadow: "none",
7
+ p: { xs: 3, md: 4 },
8
+ }, children: _jsxs(Stack, { spacing: 3, children: [_jsxs(Stack, { spacing: 1.25, children: [_jsx(Typography, { variant: "h3", children: headline }), body ? (_jsx(Typography, { color: "text.secondary", sx: { maxWidth: 980, whiteSpace: "pre-line" }, children: body })) : null] }), primaryCard || resourceCard ? (_jsxs(Box, { sx: {
9
+ display: "grid",
10
+ gap: 2,
11
+ gridTemplateColumns: {
12
+ xs: "1fr",
13
+ md: primaryCard && resourceCard ? "minmax(0, 1.6fr) minmax(0, 1fr)" : "1fr",
14
+ },
15
+ }, children: [primaryCard ? (_jsx(Paper, { elevation: 0, sx: {
16
+ backgroundColor: "rgba(255, 255, 255, 0.26)",
17
+ border: "1px solid rgba(17, 17, 17, 0.08)",
18
+ borderRadius: 3,
19
+ p: { xs: 2, md: 2.5 },
20
+ }, children: _jsxs(Stack, { spacing: 1.5, children: [primaryCard.title ? _jsx(Typography, { variant: "h5", children: primaryCard.title }) : null, primaryCard.body ? (_jsx(Typography, { color: "text.secondary", sx: { maxWidth: 920, whiteSpace: "pre-line" }, children: primaryCard.body })) : null, primaryCard.note ? (_jsx(Typography, { color: "text.secondary", sx: { maxWidth: 920, whiteSpace: "pre-line" }, children: primaryCard.note })) : null, Array.isArray(primaryCard.actions) && primaryCard.actions.length > 0 ? (_jsx(Stack, { direction: { xs: "column", sm: "row" }, spacing: 1.5, children: primaryCard.actions.map((action) => (_jsx(Button, { component: action.href ? "a" : "button", href: action.href, onClick: action.onClick, rel: action.href ? "noreferrer" : undefined, sx: { minHeight: 52, px: 3 }, target: action.href ? action.target || "_self" : undefined, variant: action.variant || "contained", children: action.label }, action.key))) })) : null] }) })) : null, resourceCard ? (_jsx(Paper, { elevation: 0, sx: {
21
+ backgroundColor: "rgba(16, 84, 43, 0.04)",
22
+ border: "1px solid rgba(17, 17, 17, 0.08)",
23
+ borderRadius: 3,
24
+ p: { xs: 2, md: 2.5 },
25
+ }, children: _jsxs(Stack, { spacing: 1.25, children: [resourceCard.title ? _jsx(Typography, { variant: "h6", children: resourceCard.title }) : null, resourceCard.body ? (_jsx(Typography, { color: "text.secondary", sx: { whiteSpace: "pre-line" }, children: resourceCard.body })) : null, resourceCard.action ? (_jsx(Button, { component: "a", href: resourceCard.action.href, rel: "noreferrer", sx: { alignSelf: "flex-start", minHeight: 48, px: 2.5 }, target: "_blank", variant: "outlined", children: resourceCard.action.label })) : null] }) })) : null] })) : null, actionButtons.length > 0 ? (_jsx(Stack, { direction: { xs: "column", sm: "row" }, spacing: 1.5, children: actionButtons.map((action) => (_jsx(Button, { component: action.href ? "a" : "button", href: action.href, onClick: action.onClick, rel: action.href ? "noreferrer" : undefined, sx: { minHeight: 56, px: 3 }, target: action.href ? action.target || "_self" : undefined, variant: action.variant || "outlined", children: action.label }, action.key))) })) : null] }) }));
26
+ export default SurveyCompletionLayout;
@@ -0,0 +1,271 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { createElement as _createElement } from "react";
13
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
14
+ import React from "react";
15
+ import CheckRoundedIcon from "@mui/icons-material/CheckRounded";
16
+ import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
17
+ import { Alert, Autocomplete, Box, Button, Chip, Paper, Stack, TextField, Typography } from "@mui/material";
18
+ import { useTranslation } from "react-i18next";
19
+ import { getDiscoveryDataset } from "./discoveryDatasets";
20
+ import { buildDiscoveryDeck, buildDiscoveryItems, filterDiscoveryItemsByGate, resolveDiscoveryStrategy, serializeDiscoveryValue, } from "./discoveryEngine";
21
+ import SurveyGeoSelect from "./SurveyGeoSelect";
22
+ import SurveyQuestionHeader from "./SurveyQuestionHeader";
23
+ const SWIPE_THRESHOLD = 110;
24
+ function DiscoveryCard({ active, item, onSkip, onKeep }) {
25
+ const [dragX, setDragX] = React.useState(0);
26
+ const [dragging, setDragging] = React.useState(false);
27
+ const [imageFailed, setImageFailed] = React.useState(false);
28
+ const startXRef = React.useRef(0);
29
+ React.useEffect(() => {
30
+ setDragX(0);
31
+ setDragging(false);
32
+ setImageFailed(false);
33
+ }, [item === null || item === void 0 ? void 0 : item.imageUrl, item === null || item === void 0 ? void 0 : item.value]);
34
+ const rotation = dragX / 16;
35
+ const showImage = Boolean(item === null || item === void 0 ? void 0 : item.imageUrl) && !imageFailed;
36
+ const handlePointerDown = (event) => {
37
+ var _a, _b;
38
+ if (!active) {
39
+ return;
40
+ }
41
+ startXRef.current = event.clientX;
42
+ setDragging(true);
43
+ (_b = (_a = event.currentTarget).setPointerCapture) === null || _b === void 0 ? void 0 : _b.call(_a, event.pointerId);
44
+ };
45
+ const handlePointerMove = (event) => {
46
+ if (!dragging || !active) {
47
+ return;
48
+ }
49
+ setDragX(event.clientX - startXRef.current);
50
+ };
51
+ const handlePointerEnd = () => {
52
+ if (!active) {
53
+ return;
54
+ }
55
+ const delta = dragX;
56
+ setDragging(false);
57
+ setDragX(0);
58
+ if (delta >= SWIPE_THRESHOLD) {
59
+ onKeep(item.value);
60
+ return;
61
+ }
62
+ if (delta <= -SWIPE_THRESHOLD) {
63
+ onSkip(item.value);
64
+ }
65
+ };
66
+ if (!item) {
67
+ return null;
68
+ }
69
+ return (_jsx(Paper, { elevation: 0, onPointerCancel: handlePointerEnd, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerEnd, sx: {
70
+ border: "1px solid rgba(17, 17, 17, 0.08)",
71
+ borderRadius: 4,
72
+ cursor: active ? "grab" : "default",
73
+ overflow: "hidden",
74
+ position: "relative",
75
+ touchAction: "pan-y",
76
+ transform: active ? `translateX(${dragX}px) rotate(${rotation}deg)` : "scale(1)",
77
+ transition: dragging ? "none" : "transform 220ms ease, opacity 220ms ease",
78
+ userSelect: "none",
79
+ }, children: _jsxs(Box, { sx: {
80
+ aspectRatio: "16 / 9",
81
+ alignItems: "flex-end",
82
+ background: "#f7eef3",
83
+ color: "#171117",
84
+ display: "flex",
85
+ minHeight: { xs: 260, sm: 320 },
86
+ p: { xs: 1.25, sm: 2 },
87
+ position: "relative",
88
+ width: "100%",
89
+ }, children: [showImage ? (_jsx(Box, { alt: item.imageAlt || item.label, component: "img", onError: () => setImageFailed(true), src: item.imageUrl, sx: {
90
+ height: "100%",
91
+ inset: 0,
92
+ objectFit: "cover",
93
+ position: "absolute",
94
+ width: "100%",
95
+ } })) : null, _jsx(Box, { sx: {
96
+ background: showImage
97
+ ? "linear-gradient(180deg, rgba(10,10,10,0.05) 0%, rgba(10,10,10,0.72) 100%)"
98
+ : "none",
99
+ inset: 0,
100
+ position: "absolute",
101
+ } }), _jsx(Stack, { justifyContent: "flex-end", spacing: 1, sx: { position: "relative", width: "100%", zIndex: 1 }, children: _jsx(Typography, { sx: {
102
+ color: showImage ? "#ffffff" : "#171117",
103
+ fontSize: { xs: 28, sm: 34 },
104
+ fontWeight: 700,
105
+ lineHeight: 1.05,
106
+ }, children: item.label }) })] }) }));
107
+ }
108
+ function buildNextQueue(items, state, strategy) {
109
+ return buildDiscoveryDeck(items, state, strategy);
110
+ }
111
+ function selectedGateLabels(gateOptions, gateValues) {
112
+ const selectedValueKeys = new Set((gateValues || []).map(serializeDiscoveryValue));
113
+ return (gateOptions || [])
114
+ .filter((option) => selectedValueKeys.has(serializeDiscoveryValue(option.value)))
115
+ .map((option) => option.label || "");
116
+ }
117
+ const SurveyDiscoverySelect = ({ allOptionsByName, errorText, hint, label, onChange, options, uiConfig, value }) => {
118
+ const { t } = useTranslation();
119
+ const dataset = React.useMemo(() => getDiscoveryDataset(uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.datasetKey), [uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.datasetKey]);
120
+ const gatingConfig = (uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.gating) || null;
121
+ const hasGateStep = (gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.type) === "geo-select";
122
+ const strategy = React.useMemo(() => resolveDiscoveryStrategy((dataset === null || dataset === void 0 ? void 0 : dataset.strategy) || {}, (uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.strategy) || {}, {
123
+ deckSize: uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.deckSize,
124
+ maxSelections: uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.maxSelections,
125
+ randomShare: uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.randomShare,
126
+ }), [dataset === null || dataset === void 0 ? void 0 : dataset.strategy, uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.deckSize, uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.maxSelections, uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.randomShare, uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.strategy]);
127
+ const items = React.useMemo(() => buildDiscoveryItems(options, dataset), [dataset, options]);
128
+ const gateOptions = React.useMemo(() => (hasGateStep ? (allOptionsByName === null || allOptionsByName === void 0 ? void 0 : allOptionsByName[gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.sourceQuestionName]) || [] : []), [allOptionsByName, gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.sourceQuestionName, hasGateStep]);
129
+ const [gateValues, setGateValues] = React.useState([]);
130
+ const [seenValues, setSeenValues] = React.useState([]);
131
+ const [dismissedValues, setDismissedValues] = React.useState([]);
132
+ const [queue, setQueue] = React.useState([]);
133
+ const [queueCursor, setQueueCursor] = React.useState(0);
134
+ const preservedNextValueKeyRef = React.useRef("");
135
+ const previousGateKeyRef = React.useRef("");
136
+ const selectedValues = Array.isArray(value) ? value : [];
137
+ const selectedValueKeys = React.useMemo(() => selectedValues.map(serializeDiscoveryValue), [selectedValues]);
138
+ const seenValueKeys = React.useMemo(() => seenValues.map(serializeDiscoveryValue), [seenValues]);
139
+ const dismissedValueKeys = React.useMemo(() => dismissedValues.map(serializeDiscoveryValue), [dismissedValues]);
140
+ const gateLabels = React.useMemo(() => selectedGateLabels(gateOptions, gateValues), [gateOptions, gateValues]);
141
+ const filteredItems = React.useMemo(() => {
142
+ if (!hasGateStep) {
143
+ return items;
144
+ }
145
+ return filterDiscoveryItemsByGate(items, gateLabels, gatingConfig);
146
+ }, [gateLabels, gatingConfig, hasGateStep, items]);
147
+ const filteredOptionValueKeys = React.useMemo(() => new Set(filteredItems.map((item) => item.valueKey)), [filteredItems]);
148
+ const filteredOptions = React.useMemo(() => options.filter((option) => filteredOptionValueKeys.has(serializeDiscoveryValue(option.value))), [filteredOptionValueKeys, options]);
149
+ React.useEffect(() => {
150
+ setGateValues([]);
151
+ }, [gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.datasetKey, gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.sourceQuestionName]);
152
+ React.useEffect(() => {
153
+ if (!hasGateStep) {
154
+ return;
155
+ }
156
+ const gateKey = gateValues.map(serializeDiscoveryValue).sort().join("|");
157
+ if (gateKey === previousGateKeyRef.current) {
158
+ return;
159
+ }
160
+ previousGateKeyRef.current = gateKey;
161
+ preservedNextValueKeyRef.current = "";
162
+ setSeenValues([]);
163
+ setDismissedValues([]);
164
+ setQueue([]);
165
+ setQueueCursor(0);
166
+ const allowedValueKeys = new Set(filteredItems.map((item) => item.valueKey));
167
+ const nextSelectedValues = selectedValues.filter((entry) => allowedValueKeys.has(serializeDiscoveryValue(entry)));
168
+ if (nextSelectedValues.length !== selectedValues.length) {
169
+ onChange(nextSelectedValues);
170
+ }
171
+ }, [filteredItems, gateValues, hasGateStep, onChange, selectedValues]);
172
+ React.useEffect(() => {
173
+ const nextQueue = buildNextQueue(filteredItems, {
174
+ dismissedValues,
175
+ seenValues,
176
+ selectedValues,
177
+ }, strategy);
178
+ const preservedNextValueKey = preservedNextValueKeyRef.current;
179
+ if (preservedNextValueKey) {
180
+ const preservedIndex = nextQueue.findIndex((item) => item.valueKey === preservedNextValueKey);
181
+ if (preservedIndex > 0) {
182
+ const [preservedItem] = nextQueue.splice(preservedIndex, 1);
183
+ nextQueue.unshift(preservedItem);
184
+ }
185
+ preservedNextValueKeyRef.current = "";
186
+ }
187
+ setQueue(nextQueue);
188
+ setQueueCursor(0);
189
+ }, [dismissedValues, filteredItems, seenValues, selectedValues, strategy]);
190
+ const currentItem = queue[queueCursor] || null;
191
+ const previewItems = queue.slice(queueCursor + 1, queueCursor + 3);
192
+ const selectedItems = filteredItems.filter((item) => selectedValueKeys.includes(item.valueKey));
193
+ const maxReached = selectedValues.length >= strategy.maxSelections;
194
+ const commitSelection = React.useCallback((nextValues) => {
195
+ onChange(nextValues.slice(0, strategy.maxSelections));
196
+ }, [onChange, strategy.maxSelections]);
197
+ const markSeen = React.useCallback((nextValue) => {
198
+ setSeenValues((current) => (current.includes(nextValue) ? current : [...current, nextValue]));
199
+ }, []);
200
+ const moveForward = React.useCallback(() => {
201
+ var _a;
202
+ preservedNextValueKeyRef.current = ((_a = queue[queueCursor + 1]) === null || _a === void 0 ? void 0 : _a.valueKey) || "";
203
+ setQueueCursor((current) => current + 1);
204
+ }, [queue, queueCursor]);
205
+ const handleSkip = React.useCallback((nextValue) => {
206
+ markSeen(nextValue);
207
+ setDismissedValues((current) => (current.includes(nextValue) ? current : [...current, nextValue]));
208
+ moveForward();
209
+ }, [markSeen, moveForward]);
210
+ const handleKeep = React.useCallback((nextValue) => {
211
+ markSeen(nextValue);
212
+ if (!selectedValueKeys.includes(serializeDiscoveryValue(nextValue)) && selectedValues.length < strategy.maxSelections) {
213
+ commitSelection([...selectedValues, nextValue]);
214
+ }
215
+ moveForward();
216
+ }, [commitSelection, markSeen, moveForward, selectedValueKeys, selectedValues, strategy.maxSelections]);
217
+ const handleManualSelection = (_event, nextOptions) => {
218
+ const nextValues = nextOptions.map((option) => option.value).slice(0, strategy.maxSelections);
219
+ commitSelection(nextValues);
220
+ };
221
+ const hasRemainingDiscoverable = filteredItems.some((item) => item.discoverable &&
222
+ !selectedValueKeys.includes(item.valueKey) &&
223
+ !dismissedValueKeys.includes(item.valueKey) &&
224
+ !seenValueKeys.includes(item.valueKey));
225
+ return (_jsxs(Stack, { spacing: 1.5, children: [_jsx(SurveyQuestionHeader, { errorText: errorText, hint: hint, label: label }), hasGateStep ? (_jsx(SurveyGeoSelect, { hint: t("surveyRenderer.DISCOVERY_GATE_HINT"), label: t("surveyRenderer.DISCOVERY_GATE_LABEL", { max: (gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.maxSelections) || 1 }), maxSelections: (gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.maxSelections) || 1, multiple: true, onChange: setGateValues, options: gateOptions, uiConfig: {
226
+ autocompletePlaceholderKey: (gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.autocompletePlaceholderKey) || "surveyRenderer.DISCOVERY_GATE_PLACEHOLDER",
227
+ datasetKey: gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.datasetKey,
228
+ emptyMessageKey: (gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.emptyMessageKey) || "surveyRenderer.DISCOVERY_GATE_EMPTY",
229
+ featureLabelProperty: gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.featureLabelProperty,
230
+ loadingMessageKey: (gatingConfig === null || gatingConfig === void 0 ? void 0 : gatingConfig.loadingMessageKey) || "surveyRenderer.MAP_LOADING",
231
+ }, value: gateValues })) : null, !hasGateStep || gateValues.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Stack, { spacing: 1.5, children: [_jsxs(Stack, { alignItems: { xs: "flex-start", sm: "center" }, direction: { xs: "column", sm: "row" }, justifyContent: "space-between", spacing: 1, children: [_jsx(Box, {}), _jsx(Typography, { color: "text.secondary", variant: "body2", children: t("surveyRenderer.DISCOVERY_HELP") })] }), maxReached ? (_jsx(Alert, { severity: "info", children: t("surveyRenderer.DISCOVERY_MAX_REACHED", { max: strategy.maxSelections }) })) : currentItem ? (_jsxs(Box, { sx: { position: "relative" }, children: [previewItems[1] ? (_jsx(Box, { sx: { left: 0, px: { xs: 1.25, sm: 3 }, position: "absolute", right: 0, top: 22, zIndex: 0 }, children: _jsx(Paper, { elevation: 0, sx: { backgroundColor: "rgba(255, 243, 249, 0.6)", borderRadius: 4, height: 22 } }) })) : null, previewItems[0] ? (_jsx(Box, { sx: { left: 0, px: { xs: 0.75, sm: 1.5 }, position: "absolute", right: 0, top: 12, zIndex: 0 }, children: _jsx(Paper, { elevation: 0, sx: { backgroundColor: "rgba(255, 243, 249, 0.78)", borderRadius: 4, height: 24 } }) })) : null, _jsx(Box, { sx: { position: "relative", zIndex: 1 }, children: _jsx(DiscoveryCard, { active: true, item: currentItem, onKeep: handleKeep, onSkip: handleSkip }) })] })) : (_jsx(Alert, { severity: hasRemainingDiscoverable ? "info" : "success", children: hasRemainingDiscoverable ? t("surveyRenderer.DISCOVERY_REFRESH") : t("surveyRenderer.DISCOVERY_DONE") })), _jsxs(Stack, { direction: "row", spacing: 1.25, sx: {
232
+ display: { xs: "none", sm: "flex" },
233
+ }, children: [_jsx(Button, { disabled: !currentItem || maxReached, onClick: () => currentItem && handleSkip(currentItem.value), startIcon: _jsx(CloseRoundedIcon, {}), sx: {
234
+ backgroundColor: "#f5bfd8",
235
+ color: "#7a2455",
236
+ flex: 1,
237
+ minHeight: 52,
238
+ "&:hover": {
239
+ backgroundColor: "#eeabc9",
240
+ },
241
+ }, variant: "contained", children: t("surveyRenderer.DISCOVERY_SKIP") }), _jsx(Button, { disabled: !currentItem || maxReached, onClick: () => currentItem && handleKeep(currentItem.value), startIcon: _jsx(CheckRoundedIcon, {}), sx: { flex: 1, minHeight: 52 }, variant: "contained", children: t("surveyRenderer.DISCOVERY_SAVE") })] })] }), _jsx(Autocomplete, { fullWidth: true, getOptionLabel: (option) => option.label || "", isOptionEqualToValue: (option, candidate) => option.value === candidate.value, multiple: true, onChange: handleManualSelection, options: filteredOptions, renderTags: (tagValue, getTagProps) => tagValue.map((option, index) => {
242
+ const _a = getTagProps({ index }), { key } = _a, tagProps = __rest(_a, ["key"]);
243
+ return (_createElement(Chip, Object.assign({}, tagProps, { key: key, label: option.label, sx: {
244
+ backgroundColor: "#ffffff",
245
+ border: "1px solid rgba(15, 23, 42, 0.10)",
246
+ borderRadius: 999,
247
+ fontSize: 14,
248
+ fontWeight: 600,
249
+ height: 34,
250
+ px: 0.5,
251
+ ".MuiChip-deleteIcon": {
252
+ color: "rgba(15, 23, 42, 0.52)",
253
+ fontSize: 18,
254
+ "&:hover": {
255
+ color: "#111111",
256
+ },
257
+ },
258
+ } })));
259
+ }), renderInput: (params) => (_jsx(TextField, Object.assign({}, params, { placeholder: t("surveyRenderer.DISCOVERY_AUTOCOMPLETE_PLACEHOLDER"), sx: {
260
+ ".MuiAutocomplete-tag": {
261
+ margin: "4px 6px 4px 0",
262
+ },
263
+ ".MuiOutlinedInput-root": {
264
+ backgroundColor: "rgba(255, 243, 249, 0.96)",
265
+ borderRadius: 3,
266
+ paddingBottom: "8px !important",
267
+ paddingTop: "8px !important",
268
+ },
269
+ } }))), value: selectedItems })] })) : (_jsx(Alert, { severity: "info", children: t("surveyRenderer.DISCOVERY_GATE_REQUIRED") }))] }));
270
+ };
271
+ export default SurveyDiscoverySelect;
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import SearchRoundedIcon from "@mui/icons-material/SearchRounded";
4
+ import { Alert, Box, InputAdornment, Stack, TextField, Typography } from "@mui/material";
5
+ import { useTranslation } from "react-i18next";
6
+ import { getDiscoveryDataset } from "./discoveryDatasets";
7
+ import { buildDiscoveryItems, filterDiscoveryItemsByGate, serializeDiscoveryValue } from "./discoveryEngine";
8
+ import SurveyQuestionHeader from "./SurveyQuestionHeader";
9
+ function FilteredChoiceList({ maxSelections, onChange, options, selectedValues }) {
10
+ const selectedValueKeys = new Set((selectedValues || []).map(serializeDiscoveryValue));
11
+ return (_jsx(Stack, { spacing: 1.1, children: options.map((option) => {
12
+ const isSelected = selectedValueKeys.has(serializeDiscoveryValue(option.value));
13
+ return (_jsx(Box, { onClick: () => {
14
+ if (isSelected) {
15
+ onChange(selectedValues.filter((entry) => serializeDiscoveryValue(entry) !== serializeDiscoveryValue(option.value)));
16
+ return;
17
+ }
18
+ if ((selectedValues || []).length >= maxSelections) {
19
+ return;
20
+ }
21
+ onChange([...(selectedValues || []), option.value]);
22
+ }, sx: {
23
+ backgroundColor: "rgba(255, 243, 249, 0.96)",
24
+ border: "1px solid",
25
+ borderColor: isSelected ? "primary.main" : "divider",
26
+ borderRadius: 2,
27
+ cursor: "pointer",
28
+ p: 1.35,
29
+ transition: "border-color 150ms ease, background-color 150ms ease",
30
+ bgcolor: isSelected ? "rgba(13, 174, 209, 0.16)" : "rgba(255, 243, 249, 0.96)",
31
+ }, children: _jsxs(Stack, { spacing: 0.25, children: [_jsx(Typography, { children: option.label }), option.hint ? (_jsx(Typography, { color: "text.secondary", sx: { whiteSpace: "pre-line" }, variant: "body2", children: option.hint })) : null] }) }, option.value));
32
+ }) }));
33
+ }
34
+ const SurveyFilteredPlaceSelect = ({ allOptionsByName, answers, errorText, hint, label, onChange, options, uiConfig, value, }) => {
35
+ const { t } = useTranslation();
36
+ const dataset = React.useMemo(() => getDiscoveryDataset(uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.datasetKey), [uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.datasetKey]);
37
+ const sourceQuestionName = (uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.sourceQuestionName) || "";
38
+ const filterKey = (uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.filterKey) || "quarters";
39
+ const maxSelections = Math.max(1, (uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.maxSelections) || 5);
40
+ const sourceOptions = (allOptionsByName === null || allOptionsByName === void 0 ? void 0 : allOptionsByName[sourceQuestionName]) || [];
41
+ const sourceValues = Array.isArray(answers === null || answers === void 0 ? void 0 : answers[sourceQuestionName]) ? answers[sourceQuestionName] : [];
42
+ const selectedSourceValueKeys = React.useMemo(() => new Set(sourceValues.map(serializeDiscoveryValue)), [sourceValues]);
43
+ const selectedSourceLabels = React.useMemo(() => sourceOptions
44
+ .filter((option) => selectedSourceValueKeys.has(serializeDiscoveryValue(option.value)))
45
+ .map((option) => option.label || ""), [selectedSourceValueKeys, sourceOptions]);
46
+ const items = React.useMemo(() => buildDiscoveryItems(options, dataset), [dataset, options]);
47
+ const filteredItems = React.useMemo(() => filterDiscoveryItemsByGate(items, selectedSourceLabels, { filterKey }), [filterKey, items, selectedSourceLabels]);
48
+ const filteredOptionValueKeys = React.useMemo(() => new Set(filteredItems.map((item) => item.valueKey)), [filteredItems]);
49
+ const filteredOptions = React.useMemo(() => options.filter((option) => filteredOptionValueKeys.has(serializeDiscoveryValue(option.value))), [filteredOptionValueKeys, options]);
50
+ const [searchValue, setSearchValue] = React.useState("");
51
+ const selectedValues = Array.isArray(value) ? value : [];
52
+ const visibleOptions = React.useMemo(() => {
53
+ const normalizedSearch = searchValue.trim().toLowerCase();
54
+ if (!normalizedSearch) {
55
+ return filteredOptions;
56
+ }
57
+ return filteredOptions.filter((option) => (option.label || "").toLowerCase().includes(normalizedSearch));
58
+ }, [filteredOptions, searchValue]);
59
+ React.useEffect(() => {
60
+ const allowedValues = new Set(filteredOptions.map((option) => serializeDiscoveryValue(option.value)));
61
+ const nextSelectedValues = selectedValues.filter((entry) => allowedValues.has(serializeDiscoveryValue(entry)));
62
+ if (nextSelectedValues.length !== selectedValues.length) {
63
+ onChange(nextSelectedValues);
64
+ }
65
+ }, [filteredOptions, onChange, selectedValues]);
66
+ return (_jsxs(Stack, { spacing: 1.5, children: [_jsx(SurveyQuestionHeader, { errorText: errorText, hint: hint, label: label }), selectedSourceLabels.length === 0 ? (_jsx(Alert, { severity: "info", children: t("surveyRenderer.FILTERED_SELECT_REQUIRED") })) : null, selectedSourceLabels.length > 0 ? (_jsxs(_Fragment, { children: [selectedValues.length >= maxSelections ? (_jsx(Alert, { severity: "info", children: t("surveyRenderer.FILTERED_SELECT_MAX_REACHED", { max: maxSelections }) })) : null, !(uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.hideSearch) && (_jsx(TextField, { fullWidth: true, onChange: (event) => setSearchValue(event.target.value), placeholder: t("surveyRenderer.FILTERED_SELECT_SEARCH_PLACEHOLDER"), sx: {
67
+ ".MuiOutlinedInput-root": {
68
+ backgroundColor: "rgba(255, 243, 249, 0.96)",
69
+ },
70
+ }, value: searchValue, InputProps: {
71
+ startAdornment: (_jsx(InputAdornment, { position: "start", children: _jsx(SearchRoundedIcon, { fontSize: "small" }) })),
72
+ } })), visibleOptions.length > 0 ? (_jsx(FilteredChoiceList, { maxSelections: maxSelections, onChange: onChange, options: visibleOptions, selectedValues: selectedValues })) : (_jsx(Alert, { severity: "info", children: t("surveyRenderer.FILTERED_SELECT_EMPTY") }))] })) : null] }));
73
+ };
74
+ export default SurveyFilteredPlaceSelect;