@ydtb/tk-scope-capture 0.22.0 → 0.23.6
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/src/client/components/CaptureFormSurface.d.ts +58 -0
- package/dist/src/client/components/CaptureFormSurface.d.ts.map +1 -0
- package/dist/src/client/components/CaptureFormSurface.js +301 -0
- package/dist/src/client/components/CaptureFormSurface.js.map +1 -0
- package/dist/src/client/components/CaptureSidebar.d.ts +43 -0
- package/dist/src/client/components/CaptureSidebar.d.ts.map +1 -0
- package/dist/src/client/components/CaptureSidebar.js +38 -0
- package/dist/src/client/components/CaptureSidebar.js.map +1 -0
- package/dist/src/client/pages/CaptureBuilderPage.d.ts.map +1 -1
- package/dist/src/client/pages/CaptureBuilderPage.js +1869 -129
- package/dist/src/client/pages/CaptureBuilderPage.js.map +1 -1
- package/dist/src/client/pages/CaptureSubmissionDetailPage.d.ts.map +1 -1
- package/dist/src/client/pages/CaptureSubmissionDetailPage.js +61 -10
- package/dist/src/client/pages/CaptureSubmissionDetailPage.js.map +1 -1
- package/dist/src/client/pages/CaptureSubmissionsPage.d.ts.map +1 -1
- package/dist/src/client/pages/CaptureSubmissionsPage.js +14 -2
- package/dist/src/client/pages/CaptureSubmissionsPage.js.map +1 -1
- package/dist/src/client/pages/PublicFormPage.d.ts.map +1 -1
- package/dist/src/client/pages/PublicFormPage.js +13 -56
- package/dist/src/client/pages/PublicFormPage.js.map +1 -1
- package/dist/src/client/pages/PublicQuizPage.d.ts.map +1 -1
- package/dist/src/client/pages/PublicQuizPage.js +23 -11
- package/dist/src/client/pages/PublicQuizPage.js.map +1 -1
- package/dist/src/client/pages/QuizBuilderPage.d.ts.map +1 -1
- package/dist/src/client/pages/QuizBuilderPage.js +6 -2
- package/dist/src/client/pages/QuizBuilderPage.js.map +1 -1
- package/dist/src/client.d.ts +858 -15
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +2 -0
- package/dist/src/client.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/server/api/action-adapters.d.ts +2 -0
- package/dist/src/server/api/action-adapters.d.ts.map +1 -1
- package/dist/src/server/api/action-adapters.js +1 -0
- package/dist/src/server/api/action-adapters.js.map +1 -1
- package/dist/src/server/api/router.d.ts +884 -16
- package/dist/src/server/api/router.d.ts.map +1 -1
- package/dist/src/server/api/router.js +547 -22
- package/dist/src/server/api/router.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +51 -1
- package/dist/src/server.js.map +1 -1
- package/dist/src/shared/conditions.d.ts +2 -2
- package/dist/src/shared/conditions.d.ts.map +1 -1
- package/dist/src/shared/conditions.js +47 -0
- package/dist/src/shared/conditions.js.map +1 -1
- package/dist/src/shared/db/schema.d.ts +414 -0
- package/dist/src/shared/db/schema.d.ts.map +1 -1
- package/dist/src/shared/db/schema.js +40 -0
- package/dist/src/shared/db/schema.js.map +1 -1
- package/dist/src/shared/document.d.ts +120 -0
- package/dist/src/shared/document.d.ts.map +1 -0
- package/dist/src/shared/document.js +111 -0
- package/dist/src/shared/document.js.map +1 -0
- package/dist/src/shared/field-types.d.ts +2 -0
- package/dist/src/shared/field-types.d.ts.map +1 -1
- package/dist/src/shared/field-types.js +43 -10
- package/dist/src/shared/field-types.js.map +1 -1
- package/dist/src/shared/types.d.ts +90 -3
- package/dist/src/shared/types.d.ts.map +1 -1
- package/dist/src/shared/types.js.map +1 -1
- package/dist/src/shared/validation.d.ts +6 -0
- package/dist/src/shared/validation.d.ts.map +1 -0
- package/dist/src/shared/validation.js +87 -0
- package/dist/src/shared/validation.js.map +1 -0
- package/package.json +11 -7
|
@@ -1,80 +1,311 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { useNavigate, useParams } from "@tanstack/react-router";
|
|
3
|
+
import { useLocation, useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
|
4
4
|
import { formatRelativeTime } from "@ydtb/tk-scope-lib/date";
|
|
5
|
-
import {
|
|
5
|
+
import { AlertCircle, ArrowLeft, ArrowRight, CheckCheck, CheckCircle2, ChevronLeft, ChevronRight, Columns3, Copy, Eye, Filter, ExternalLink, FileText, GitBranch, ImageIcon, Info, Inbox, Mail, Monitor, PanelRight, ListTree, Plus, Save, Search, Settings2, Send, Smartphone, Sparkles, Trash2, UserRound, } from "lucide-react";
|
|
6
|
+
import { StorageAssetPicker, useAssetPickerHooks } from "@ydtb/tk-scope-asset-picker";
|
|
7
|
+
import { WorkbenchColumnPanel, WorkbenchFilterPanel, WorkbenchSaveResetControls, WorkbenchTable, adaptWorkbenchColumns, fieldsToFilterFields, isWorkbenchStateDirty, persistWorkbenchColumns, useWorkbenchOffsetPagination, } from "@ydtb/tk-scope-data-workbench";
|
|
6
8
|
import { useMutation, useQuery, useQueryClient } from "@ydtb/tk-scope-query-client";
|
|
7
|
-
import { HeaderPortal,
|
|
9
|
+
import { HeaderPortal, useContributions, useScopeLink } from "@ydtb/tk-scope/client";
|
|
8
10
|
import { ToolPageHeader } from "@ydtb/tk-scope-shell";
|
|
11
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@ydtb/tk-scope-ui/components/alert-dialog";
|
|
12
|
+
import { BulkActionBar, BulkActionButton } from "@ydtb/tk-scope-ui/components/bulk-action-bar";
|
|
9
13
|
import { Button } from "@ydtb/tk-scope-ui/components/button";
|
|
14
|
+
import { InlineCellEditor } from "@ydtb/tk-scope-ui/components/data-table";
|
|
15
|
+
import { FilterPanel } from "@ydtb/tk-scope-ui/components/filter-panel";
|
|
16
|
+
import { AvatarPreview, COLORS, ICON_LIST, getColorStyle } from "@ydtb/tk-scope-ui/components/icon-picker";
|
|
17
|
+
import { InlineEditableText } from "@ydtb/tk-scope-ui/components/inline-edit";
|
|
10
18
|
import { Input } from "@ydtb/tk-scope-ui/components/input";
|
|
11
19
|
import { Label } from "@ydtb/tk-scope-ui/components/label";
|
|
12
20
|
import { TabbedPanel } from "@ydtb/tk-scope-ui/components/tabbed-panel";
|
|
21
|
+
import { Skeleton } from "@ydtb/tk-scope-ui/components/skeleton";
|
|
13
22
|
import { Switch } from "@ydtb/tk-scope-ui/components/switch";
|
|
14
23
|
import { Textarea } from "@ydtb/tk-scope-ui/components/textarea";
|
|
15
24
|
import { toast } from "@ydtb/tk-scope-ui/components/sonner";
|
|
16
25
|
import { cn } from "@ydtb/tk-scope-ui/lib/utils";
|
|
17
26
|
import { captureApi } from "../../client.js";
|
|
27
|
+
import { createCapturePageHeaderItem, createCapturePageProgressItem, deriveCaptureDocumentFromFields, extractCaptureAnswerFields, extractCaptureAnswerItems, resolveCaptureDocument, } from "../../shared/document.js";
|
|
18
28
|
import { BASE_CAPTURE_FIELD_TYPES, getCaptureFieldType } from "../../shared/field-types.js";
|
|
19
|
-
import {
|
|
29
|
+
import { CAPTURE_RATING_COLORS, CaptureFormSurface, getFieldLayout, getFieldPlaceholder, getImageClassName, getRatingAlign, getRatingColor, getRatingIcon } from "../components/CaptureFormSurface.js";
|
|
30
|
+
import { CaptureSidebar } from "../components/CaptureSidebar.js";
|
|
20
31
|
const CAPTURE_BUILDER_DRAG_TYPE = "application/vnd.ydtb.capture.palette-item+json";
|
|
21
|
-
const
|
|
32
|
+
const CAPTURE_BUILDER_CONTENT_DRAG_TYPE = "application/vnd.ydtb.capture.content-kind";
|
|
22
33
|
const FORM_PANEL_TABS = [
|
|
23
34
|
{ id: "outline", icon: ListTree, label: "Outline" },
|
|
24
35
|
{ id: "inspector", icon: CheckCircle2, label: "Inspector" },
|
|
25
|
-
{ id: "
|
|
26
|
-
{ id: "
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
{ id: "conditions", icon: GitBranch, label: "Conditional Visibility" },
|
|
37
|
+
{ id: "validation", icon: CheckCheck, label: "Validation" },
|
|
38
|
+
];
|
|
39
|
+
const CAPTURE_LEAD_ACTIONS = [
|
|
40
|
+
{ id: "contacts", label: "Contact update", description: "Create or update a Contact from answers.", icon: UserRound },
|
|
41
|
+
{ id: "email", label: "Email send", description: "Send operator or respondent notifications.", icon: Mail },
|
|
29
42
|
];
|
|
30
|
-
const
|
|
43
|
+
const CAPTURE_LIST_BASE_PANEL_TABS = [
|
|
31
44
|
{ id: "overview", icon: Info, label: "Overview" },
|
|
32
45
|
{ id: "submissions", icon: Inbox, label: "Submissions" },
|
|
33
46
|
{ id: "publish", icon: Send, label: "Publish" },
|
|
34
|
-
{ id: "actions", icon: GitBranch, label: "
|
|
47
|
+
{ id: "actions", icon: GitBranch, label: "Lead actions" },
|
|
35
48
|
];
|
|
49
|
+
function buildCaptureListPanelTabs(config) {
|
|
50
|
+
return [
|
|
51
|
+
...CAPTURE_LIST_BASE_PANEL_TABS,
|
|
52
|
+
...CAPTURE_LEAD_ACTIONS.filter((action) => Boolean(config[action.id]?.enabled)).map((action) => ({
|
|
53
|
+
id: action.id,
|
|
54
|
+
icon: action.icon,
|
|
55
|
+
label: action.label,
|
|
56
|
+
})),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
const HONEYPOT_FIELD_KEYS = ["email", "name", "phone", "company", "website"];
|
|
60
|
+
function createDefaultFieldKey(tile, items) {
|
|
61
|
+
const suffix = Date.now().toString(36);
|
|
62
|
+
if (tile.defaultField.type !== "honeypot")
|
|
63
|
+
return `${tile.id}_${suffix}`;
|
|
64
|
+
const existingKeys = new Set(items.filter((item) => item.kind === "input").map((item) => item.key));
|
|
65
|
+
const availableKey = HONEYPOT_FIELD_KEYS.find((key) => !existingKeys.has(key));
|
|
66
|
+
return availableKey ?? `website_${suffix}`;
|
|
67
|
+
}
|
|
68
|
+
const CAPTURE_CONDITION_OPERATORS = {
|
|
69
|
+
text: ["is", "is_not", "contains", "does_not_contain", "starts_with", "ends_with", "regex", "is_empty", "is_not_empty"],
|
|
70
|
+
email: ["is", "is_not", "contains", "does_not_contain", "regex", "is_empty", "is_not_empty"],
|
|
71
|
+
phone: ["is", "is_not", "contains", "does_not_contain", "is_empty", "is_not_empty"],
|
|
72
|
+
textarea: ["contains", "does_not_contain", "regex", "is_empty", "is_not_empty"],
|
|
73
|
+
select: ["is", "is_not", "is_empty", "is_not_empty"],
|
|
74
|
+
rating: ["is", "is_not", "greater_than", "less_than", "greater_than_or_equal", "less_than_or_equal", "is_empty", "is_not_empty"],
|
|
75
|
+
};
|
|
76
|
+
function isConditionGroup(value) {
|
|
77
|
+
return Boolean(value && "conditions" in value);
|
|
78
|
+
}
|
|
79
|
+
function toConditionGroup(value) {
|
|
80
|
+
if (!value)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (isConditionGroup(value))
|
|
83
|
+
return value;
|
|
84
|
+
const operator = value.operator === "notEquals"
|
|
85
|
+
? "is_not"
|
|
86
|
+
: value.operator === "isEmpty"
|
|
87
|
+
? "is_empty"
|
|
88
|
+
: value.operator === "isNotEmpty"
|
|
89
|
+
? "is_not_empty"
|
|
90
|
+
: "is";
|
|
91
|
+
return {
|
|
92
|
+
id: crypto.randomUUID().slice(0, 12),
|
|
93
|
+
logic: "and",
|
|
94
|
+
conditions: [{
|
|
95
|
+
id: crypto.randomUUID().slice(0, 12),
|
|
96
|
+
fieldKey: value.fieldKey,
|
|
97
|
+
operator,
|
|
98
|
+
value: value.value == null ? undefined : String(value.value),
|
|
99
|
+
}],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function createDefaultValidationRule(field) {
|
|
103
|
+
return {
|
|
104
|
+
id: crypto.randomUUID().slice(0, 12),
|
|
105
|
+
message: `${field.label} is invalid`,
|
|
106
|
+
expression: {
|
|
107
|
+
id: crypto.randomUUID().slice(0, 12),
|
|
108
|
+
logic: "and",
|
|
109
|
+
conditions: [],
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
36
113
|
export function CaptureBuilderPage() {
|
|
37
114
|
const { id } = useParams({ strict: false });
|
|
38
115
|
const navigate = useNavigate();
|
|
116
|
+
const location = useLocation();
|
|
117
|
+
const search = useSearch({ strict: false });
|
|
39
118
|
const sl = useScopeLink();
|
|
40
119
|
const queryClient = useQueryClient();
|
|
120
|
+
const urlSubmissionViewId = typeof search.view === "string" && search.view.trim().length > 0 ? search.view : undefined;
|
|
41
121
|
const [newFormName] = useState("Lead capture form");
|
|
42
122
|
const [landingSearch, setLandingSearch] = useState("");
|
|
43
123
|
const [selectedLandingDefinition, setSelectedLandingDefinition] = useState(null);
|
|
44
124
|
const [activeLandingDetailTab, setActiveLandingDetailTab] = useState("overview");
|
|
125
|
+
const [activeWorkbenchViewId, setActiveWorkbenchViewId] = useState("all");
|
|
126
|
+
const [activeWorkbenchPanel, setActiveWorkbenchPanel] = useState(null);
|
|
127
|
+
const [captureItemsFilters, setCaptureItemsFilters] = useState(undefined);
|
|
128
|
+
const [captureItemsColumns, setCaptureItemsColumns] = useState(() => adaptWorkbenchColumns({
|
|
129
|
+
fields: CAPTURE_ITEMS_FIELDS,
|
|
130
|
+
persisted: DEFAULT_CAPTURE_ITEMS_COLUMNS,
|
|
131
|
+
}).configs);
|
|
132
|
+
const [savedCaptureItemsColumns, setSavedCaptureItemsColumns] = useState(captureItemsColumns);
|
|
133
|
+
const [captureItemsSort, setCaptureItemsSort] = useState({ field: "updatedAt", direction: "desc" });
|
|
134
|
+
const [savedCaptureItemsSort, setSavedCaptureItemsSort] = useState(captureItemsSort);
|
|
135
|
+
const [savedCaptureItemsFilters, setSavedCaptureItemsFilters] = useState(undefined);
|
|
136
|
+
const [activeSubmissionViewId, setActiveSubmissionViewId] = useState("all");
|
|
137
|
+
const [activeSubmissionPanel, setActiveSubmissionPanel] = useState(null);
|
|
138
|
+
const [submissionViewName, setSubmissionViewName] = useState("New submission view");
|
|
139
|
+
const [submissionViewNameError, setSubmissionViewNameError] = useState(null);
|
|
140
|
+
const [submissionFilters, setSubmissionFilters] = useState(undefined);
|
|
141
|
+
const [submissionColumns, setSubmissionColumns] = useState(() => adaptWorkbenchColumns({
|
|
142
|
+
fields: CAPTURE_SUBMISSION_SYSTEM_FIELDS,
|
|
143
|
+
persisted: DEFAULT_CAPTURE_SUBMISSION_COLUMNS,
|
|
144
|
+
}).configs);
|
|
145
|
+
const [savedSubmissionColumns, setSavedSubmissionColumns] = useState(submissionColumns);
|
|
146
|
+
const [submissionSort, setSubmissionSort] = useState({ field: "submittedAt", direction: "desc" });
|
|
147
|
+
const [savedSubmissionSort, setSavedSubmissionSort] = useState(submissionSort);
|
|
148
|
+
const [savedSubmissionFilters, setSavedSubmissionFilters] = useState(undefined);
|
|
45
149
|
const [formName, setFormName] = useState("Untitled form");
|
|
46
|
-
const [
|
|
150
|
+
const [formDocument, setFormDocument] = useState(() => deriveCaptureDocumentFromFields([]));
|
|
47
151
|
const [actionConfig, setActionConfig] = useState({ contacts: { enabled: false, customFieldMappings: {} } });
|
|
48
152
|
const [selectedId, setSelectedId] = useState(null);
|
|
49
|
-
const [
|
|
153
|
+
const [selectedItemIds, setSelectedItemIds] = useState(() => new Set());
|
|
154
|
+
const [currentPageId, setCurrentPageId] = useState("page-1");
|
|
50
155
|
const [detailsOpen, setDetailsOpen] = useState(true);
|
|
51
156
|
const [activePanel, setActivePanel] = useState("outline");
|
|
157
|
+
const [previewDevice, setPreviewDevice] = useState("desktop");
|
|
158
|
+
const [savedFieldIds, setSavedFieldIds] = useState(() => new Set());
|
|
52
159
|
const captureContributions = useContributions("capture");
|
|
53
160
|
const fieldTypes = useMemo(() => [...BASE_CAPTURE_FIELD_TYPES, ...captureContributions.flatMap((contribution) => contribution.fieldTypes ?? [])], [captureContributions]);
|
|
54
161
|
const definitionsQuery = useQuery(captureApi.definitions.list.queryOptions({ input: {} }));
|
|
162
|
+
const itemViewsQuery = useQuery(captureApi.definitions.views.list.queryOptions({ input: {} }));
|
|
163
|
+
const submissionViewsQuery = useQuery({
|
|
164
|
+
...captureApi.submissions.views.list.queryOptions({ input: { definitionId: id ?? "" } }),
|
|
165
|
+
enabled: Boolean(id),
|
|
166
|
+
});
|
|
55
167
|
const definitionQuery = useQuery({
|
|
56
168
|
...captureApi.definitions.get.queryOptions({ input: { id: id ?? "" } }),
|
|
57
169
|
enabled: Boolean(id),
|
|
58
170
|
});
|
|
59
171
|
const submissionsQuery = useQuery(captureApi.submissions.list.queryOptions({ input: {} }));
|
|
60
|
-
const workflowsQuery = useQuery({
|
|
61
|
-
|
|
62
|
-
enabled: Boolean(id),
|
|
63
|
-
});
|
|
172
|
+
const workflowsQuery = useQuery(captureApi.definitions.listWorkflows.queryOptions({ input: {} }));
|
|
173
|
+
const contactCustomFieldsQuery = useQuery(captureApi.definitions.listContactCustomFields.queryOptions({ input: {} }));
|
|
64
174
|
useEffect(() => {
|
|
65
175
|
const definition = definitionQuery.data;
|
|
66
176
|
if (!definition)
|
|
67
177
|
return;
|
|
178
|
+
const nextDocument = resolveCaptureDocument(definition.fields, definition.document);
|
|
68
179
|
setFormName(definition.name);
|
|
69
|
-
|
|
180
|
+
setFormDocument(nextDocument);
|
|
181
|
+
const firstPageId = nextDocument.pages[0]?.id ?? "page-1";
|
|
70
182
|
setActionConfig(definition.actionConfig ?? { contacts: { enabled: false, customFieldMappings: {} } });
|
|
71
|
-
|
|
183
|
+
setSavedFieldIds(new Set(extractCaptureAnswerItems(nextDocument).map((field) => field.id)));
|
|
184
|
+
setCurrentPageId(firstPageId);
|
|
185
|
+
const firstItemId = nextDocument.items.find((item) => item.pageId === firstPageId)?.id ?? null;
|
|
186
|
+
setSelectedId(firstItemId);
|
|
187
|
+
setSelectedItemIds(firstItemId ? new Set([firstItemId]) : new Set());
|
|
72
188
|
}, [definitionQuery.data]);
|
|
73
|
-
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!selectedLandingDefinition)
|
|
191
|
+
return;
|
|
192
|
+
const freshDefinition = definitionsQuery.data?.find((definition) => definition.id === selectedLandingDefinition.id);
|
|
193
|
+
if (freshDefinition && freshDefinition !== selectedLandingDefinition)
|
|
194
|
+
setSelectedLandingDefinition(freshDefinition);
|
|
195
|
+
}, [definitionsQuery.data, selectedLandingDefinition]);
|
|
196
|
+
const fields = useMemo(() => extractCaptureAnswerFields(formDocument), [formDocument]);
|
|
197
|
+
const submissionFields = useMemo(() => buildSubmissionWorkbenchFields(fields), [fields]);
|
|
198
|
+
const sortedPages = useMemo(() => [...formDocument.pages].sort((a, b) => a.order - b.order), [formDocument.pages]);
|
|
199
|
+
const currentPage = useMemo(() => sortedPages.find((page) => page.id === currentPageId) ?? sortedPages[0] ?? null, [currentPageId, sortedPages]);
|
|
200
|
+
const currentPageIndex = Math.max(0, sortedPages.findIndex((page) => page.id === currentPage?.id));
|
|
201
|
+
const currentPageItems = useMemo(() => formDocument.items
|
|
202
|
+
.filter((item) => item.pageId === currentPage?.id)
|
|
203
|
+
.sort((a, b) => a.order - b.order), [currentPage?.id, formDocument.items]);
|
|
204
|
+
const selectedPageId = selectedId?.startsWith("page:") ? selectedId.slice("page:".length) : null;
|
|
205
|
+
const selectedPage = selectedPageId ? sortedPages.find((page) => page.id === selectedPageId) ?? null : null;
|
|
206
|
+
const selectedItem = useMemo(() => formDocument.items.find((item) => item.id === selectedId) ?? null, [formDocument.items, selectedId]);
|
|
207
|
+
const selectedItems = useMemo(() => formDocument.items.filter((item) => selectedItemIds.has(item.id)), [formDocument.items, selectedItemIds]);
|
|
208
|
+
const selectedInputItems = useMemo(() => selectedItems.filter((item) => item.kind === "input"), [selectedItems]);
|
|
209
|
+
const hasMultiSelection = selectedItems.length > 1;
|
|
210
|
+
const selectedField = !hasMultiSelection && selectedItem?.kind === "input" ? selectedItem : null;
|
|
211
|
+
const selectedTextBlock = !hasMultiSelection && selectedItem?.kind === "content.text" ? selectedItem : null;
|
|
212
|
+
const selectedIconBlock = !hasMultiSelection && selectedItem?.kind === "content.icon" ? selectedItem : null;
|
|
213
|
+
const selectedImageBlock = !hasMultiSelection && selectedItem?.kind === "content.image" ? selectedItem : null;
|
|
214
|
+
const selectedDivider = !hasMultiSelection && selectedItem?.kind === "content.divider" ? selectedItem : null;
|
|
215
|
+
const selectedPageHeader = !hasMultiSelection && selectedItem?.kind === "content.page-header" ? selectedItem : null;
|
|
216
|
+
const selectedPageProgress = !hasMultiSelection && selectedItem?.kind === "content.page-progress" ? selectedItem : null;
|
|
217
|
+
const selectedVisibilityTarget = selectedPage ?? selectedField ?? selectedTextBlock ?? selectedIconBlock ?? selectedImageBlock ?? selectedDivider ?? selectedPageHeader ?? selectedPageProgress;
|
|
218
|
+
const selectedIsStaticallyHidden = Boolean(selectedVisibilityTarget?.hidden);
|
|
219
|
+
const showValidationPanel = Boolean(selectedField?.required);
|
|
220
|
+
const showConditionsPanel = Boolean(selectedVisibilityTarget && !selectedIsStaticallyHidden);
|
|
221
|
+
const formPanelTabs = useMemo(() => FORM_PANEL_TABS.filter((tab) => {
|
|
222
|
+
if (tab.id === "validation")
|
|
223
|
+
return showValidationPanel;
|
|
224
|
+
if (tab.id === "conditions")
|
|
225
|
+
return showConditionsPanel;
|
|
226
|
+
return true;
|
|
227
|
+
}), [showConditionsPanel, showValidationPanel]);
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (((!showValidationPanel && activePanel === "validation") || (!showConditionsPanel && activePanel === "conditions")))
|
|
230
|
+
setActivePanel("inspector");
|
|
231
|
+
}, [activePanel, showConditionsPanel, showValidationPanel]);
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
const availableKeys = new Set(submissionFields.map((field) => field.key));
|
|
234
|
+
setSubmissionColumns((current) => adaptWorkbenchColumns({
|
|
235
|
+
fields: submissionFields,
|
|
236
|
+
persisted: persistWorkbenchColumns(current).filter((column) => availableKeys.has(column.fieldKey)),
|
|
237
|
+
}).configs);
|
|
238
|
+
setSavedSubmissionColumns((current) => adaptWorkbenchColumns({
|
|
239
|
+
fields: submissionFields,
|
|
240
|
+
persisted: persistWorkbenchColumns(current).filter((column) => availableKeys.has(column.fieldKey)),
|
|
241
|
+
}).configs);
|
|
242
|
+
}, [submissionFields]);
|
|
243
|
+
const selectedFieldKeyDuplicate = selectedField
|
|
244
|
+
? fields.some((field) => field.id !== selectedField.id && field.key.trim() === selectedField.key.trim())
|
|
245
|
+
: false;
|
|
246
|
+
const selectedItemDeleteLabel = selectedField
|
|
247
|
+
? "Delete field"
|
|
248
|
+
: selectedTextBlock
|
|
249
|
+
? "Delete text block"
|
|
250
|
+
: selectedIconBlock
|
|
251
|
+
? "Delete icon"
|
|
252
|
+
: selectedImageBlock
|
|
253
|
+
? "Delete image"
|
|
254
|
+
: selectedDivider
|
|
255
|
+
? "Delete divider"
|
|
256
|
+
: selectedPageHeader
|
|
257
|
+
? "Delete page header"
|
|
258
|
+
: selectedPageProgress
|
|
259
|
+
? "Delete page progress"
|
|
260
|
+
: null;
|
|
261
|
+
const selectedContextLabel = selectedPage
|
|
262
|
+
? (selectedPage.title || "Page")
|
|
263
|
+
: selectedField
|
|
264
|
+
? selectedField.label
|
|
265
|
+
: selectedTextBlock
|
|
266
|
+
? (selectedTextBlock.title || "Text")
|
|
267
|
+
: selectedIconBlock
|
|
268
|
+
? "Icon"
|
|
269
|
+
: selectedImageBlock
|
|
270
|
+
? "Image"
|
|
271
|
+
: selectedDivider
|
|
272
|
+
? "Divider"
|
|
273
|
+
: selectedPageHeader
|
|
274
|
+
? "Page header"
|
|
275
|
+
: selectedPageProgress
|
|
276
|
+
? "Page progress"
|
|
277
|
+
: null;
|
|
278
|
+
const detailsPanelTitle = activePanel === "conditions" && selectedContextLabel
|
|
279
|
+
? `Conditional Visibility - ${selectedContextLabel}`
|
|
280
|
+
: activePanel === "validation" && selectedContextLabel
|
|
281
|
+
? `Validation - ${selectedContextLabel}`
|
|
282
|
+
: formPanelTabs.find((tab) => tab.id === activePanel)?.label;
|
|
74
283
|
const invalidateCapture = useCallback(() => {
|
|
75
284
|
queryClient.invalidateQueries({ queryKey: captureApi.definitions.key() });
|
|
76
285
|
queryClient.invalidateQueries({ queryKey: captureApi.submissions.key() });
|
|
77
286
|
}, [queryClient]);
|
|
287
|
+
const invalidateItemViews = useCallback(() => {
|
|
288
|
+
queryClient.invalidateQueries({ queryKey: captureApi.definitions.views.list.key() });
|
|
289
|
+
}, [queryClient]);
|
|
290
|
+
const createItemViewMutation = useMutation(captureApi.definitions.views.create.mutationOptions({ onSuccess: invalidateItemViews }));
|
|
291
|
+
const updateItemViewMutation = useMutation(captureApi.definitions.views.update.mutationOptions({ onSuccess: invalidateItemViews }));
|
|
292
|
+
const invalidateSubmissionViews = useCallback(() => {
|
|
293
|
+
queryClient.invalidateQueries({ queryKey: captureApi.submissions.views.list.key() });
|
|
294
|
+
}, [queryClient]);
|
|
295
|
+
const createSubmissionViewMutation = useMutation(captureApi.submissions.views.create.mutationOptions({ onSuccess: invalidateSubmissionViews }));
|
|
296
|
+
const updateSubmissionViewMutation = useMutation(captureApi.submissions.views.update.mutationOptions({ onSuccess: invalidateSubmissionViews }));
|
|
297
|
+
const updateTagsMutation = useMutation(captureApi.definitions.updateTags.mutationOptions({
|
|
298
|
+
onSuccess: invalidateCapture,
|
|
299
|
+
onError: () => toast.error("Failed to update tags"),
|
|
300
|
+
}));
|
|
301
|
+
const updateActionConfigMutation = useMutation(captureApi.definitions.updateActionConfig.mutationOptions({
|
|
302
|
+
onSuccess: (definition) => {
|
|
303
|
+
invalidateCapture();
|
|
304
|
+
setSelectedLandingDefinition((current) => current?.id === definition.id ? { ...current, ...definition } : current);
|
|
305
|
+
toast.success("Capture actions saved");
|
|
306
|
+
},
|
|
307
|
+
onError: () => toast.error("Failed to update capture actions"),
|
|
308
|
+
}));
|
|
78
309
|
const createMutation = useMutation(captureApi.definitions.create.mutationOptions({
|
|
79
310
|
onSuccess: (definition) => {
|
|
80
311
|
invalidateCapture();
|
|
@@ -97,73 +328,812 @@ export function CaptureBuilderPage() {
|
|
|
97
328
|
},
|
|
98
329
|
onError: () => toast.error("Failed to publish form"),
|
|
99
330
|
}));
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return;
|
|
104
|
-
setFormName(template.title);
|
|
105
|
-
setFields(template.fields.map((field) => ({ ...field, metadata: field.metadata ? { ...field.metadata } : undefined, options: field.options ? [...field.options] : undefined })));
|
|
106
|
-
setActionConfig(template.actionConfig ?? { contacts: { enabled: false, customFieldMappings: {} } });
|
|
107
|
-
setSelectedId(template.fields[0]?.id ?? null);
|
|
108
|
-
toast.success(`Applied ${template.label}`);
|
|
331
|
+
const setSingleSelectedItem = useCallback((itemId) => {
|
|
332
|
+
setSelectedId(itemId);
|
|
333
|
+
setSelectedItemIds(itemId ? new Set([itemId]) : new Set());
|
|
109
334
|
}, []);
|
|
110
335
|
const insertField = useCallback((tile) => {
|
|
111
336
|
const fieldId = `field-${Date.now().toString(36)}`;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
337
|
+
const targetPageId = currentPage?.id ?? currentPageId;
|
|
338
|
+
setFormDocument((current) => {
|
|
339
|
+
const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
|
|
340
|
+
const newField = {
|
|
341
|
+
id: fieldId,
|
|
342
|
+
key: createDefaultFieldKey(tile, current.items),
|
|
343
|
+
label: tile.defaultField.label,
|
|
344
|
+
type: tile.defaultField.type,
|
|
345
|
+
required: tile.defaultField.required ?? false,
|
|
346
|
+
...(tile.defaultField.options ? { options: tile.defaultField.options } : {}),
|
|
347
|
+
...(tile.defaultField.metadata ? { metadata: tile.defaultField.metadata } : {}),
|
|
348
|
+
kind: "input",
|
|
349
|
+
pageId,
|
|
350
|
+
order: current.items.filter((item) => item.pageId === pageId).length,
|
|
351
|
+
};
|
|
352
|
+
return { ...current, items: [...current.items, newField] };
|
|
353
|
+
});
|
|
354
|
+
setSingleSelectedItem(fieldId);
|
|
355
|
+
}, [currentPage?.id, currentPageId, setSingleSelectedItem]);
|
|
356
|
+
const insertTextBlock = useCallback(() => {
|
|
357
|
+
const blockId = `text-${Date.now().toString(36)}`;
|
|
358
|
+
const targetPageId = currentPage?.id ?? currentPageId;
|
|
359
|
+
setFormDocument((current) => {
|
|
360
|
+
const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
|
|
361
|
+
const textBlock = {
|
|
362
|
+
id: blockId,
|
|
363
|
+
kind: "content.text",
|
|
364
|
+
pageId,
|
|
365
|
+
order: current.items.filter((item) => item.pageId === pageId).length,
|
|
366
|
+
title: "Section intro",
|
|
367
|
+
body: "Add helpful context before respondents answer the next question.",
|
|
368
|
+
};
|
|
369
|
+
return { ...current, items: [...current.items, textBlock] };
|
|
370
|
+
});
|
|
371
|
+
setSingleSelectedItem(blockId);
|
|
372
|
+
setActivePanel("inspector");
|
|
373
|
+
setDetailsOpen(true);
|
|
374
|
+
}, [currentPage?.id, currentPageId, setSingleSelectedItem]);
|
|
375
|
+
const insertImageBlock = useCallback(() => {
|
|
376
|
+
const imageId = `image-${Date.now().toString(36)}`;
|
|
377
|
+
const targetPageId = currentPage?.id ?? currentPageId;
|
|
378
|
+
setFormDocument((current) => {
|
|
379
|
+
const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
|
|
380
|
+
const imageBlock = {
|
|
381
|
+
id: imageId,
|
|
382
|
+
kind: "content.image",
|
|
383
|
+
pageId,
|
|
384
|
+
order: current.items.filter((item) => item.pageId === pageId).length,
|
|
385
|
+
imageUrl: "",
|
|
386
|
+
shape: "square",
|
|
387
|
+
align: "center",
|
|
388
|
+
size: "medium",
|
|
389
|
+
fit: "cover",
|
|
390
|
+
source: { type: "upload" },
|
|
391
|
+
};
|
|
392
|
+
return { ...current, items: [...current.items, imageBlock] };
|
|
393
|
+
});
|
|
394
|
+
setSingleSelectedItem(imageId);
|
|
395
|
+
setActivePanel("inspector");
|
|
396
|
+
setDetailsOpen(true);
|
|
397
|
+
}, [currentPage?.id, currentPageId, setSingleSelectedItem]);
|
|
398
|
+
const insertIconBlock = useCallback(() => {
|
|
399
|
+
const iconId = `icon-${Date.now().toString(36)}`;
|
|
400
|
+
const targetPageId = currentPage?.id ?? currentPageId;
|
|
401
|
+
setFormDocument((current) => {
|
|
402
|
+
const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
|
|
403
|
+
const iconBlock = {
|
|
404
|
+
id: iconId,
|
|
405
|
+
kind: "content.icon",
|
|
406
|
+
pageId,
|
|
407
|
+
order: current.items.filter((item) => item.pageId === pageId).length,
|
|
408
|
+
icon: "sparkles",
|
|
409
|
+
iconColor: "indigo",
|
|
410
|
+
align: "center",
|
|
411
|
+
};
|
|
412
|
+
return { ...current, items: [...current.items, iconBlock] };
|
|
413
|
+
});
|
|
414
|
+
setSingleSelectedItem(iconId);
|
|
415
|
+
setActivePanel("inspector");
|
|
416
|
+
setDetailsOpen(true);
|
|
417
|
+
}, [currentPage?.id, currentPageId, setSingleSelectedItem]);
|
|
418
|
+
const insertDivider = useCallback(() => {
|
|
419
|
+
const dividerId = `divider-${Date.now().toString(36)}`;
|
|
420
|
+
const targetPageId = currentPage?.id ?? currentPageId;
|
|
421
|
+
setFormDocument((current) => {
|
|
422
|
+
const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
|
|
423
|
+
const divider = {
|
|
424
|
+
id: dividerId,
|
|
425
|
+
kind: "content.divider",
|
|
426
|
+
pageId,
|
|
427
|
+
order: current.items.filter((item) => item.pageId === pageId).length,
|
|
428
|
+
};
|
|
429
|
+
return { ...current, items: [...current.items, divider] };
|
|
430
|
+
});
|
|
431
|
+
setSingleSelectedItem(dividerId);
|
|
432
|
+
setActivePanel("inspector");
|
|
433
|
+
setDetailsOpen(true);
|
|
434
|
+
}, [currentPage?.id, currentPageId, setSingleSelectedItem]);
|
|
435
|
+
const insertPageHeader = useCallback(() => {
|
|
436
|
+
const headerId = `header-${Date.now().toString(36)}`;
|
|
437
|
+
const targetPageId = currentPage?.id ?? currentPageId;
|
|
438
|
+
setFormDocument((current) => {
|
|
439
|
+
const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
|
|
440
|
+
const page = current.pages.find((candidate) => candidate.id === pageId);
|
|
441
|
+
const header = {
|
|
442
|
+
...createCapturePageHeaderItem(pageId, current.items.filter((item) => item.pageId === pageId).length, {
|
|
443
|
+
preHeading: page?.preHeading,
|
|
444
|
+
title: page?.title,
|
|
445
|
+
description: page?.description,
|
|
446
|
+
}),
|
|
447
|
+
id: headerId,
|
|
448
|
+
};
|
|
449
|
+
return { ...current, items: [...current.items, header] };
|
|
450
|
+
});
|
|
451
|
+
setSingleSelectedItem(headerId);
|
|
452
|
+
setActivePanel("inspector");
|
|
453
|
+
setDetailsOpen(true);
|
|
454
|
+
}, [currentPage?.id, currentPageId, setSingleSelectedItem]);
|
|
455
|
+
const insertPageProgress = useCallback(() => {
|
|
456
|
+
const progressId = `progress-${Date.now().toString(36)}`;
|
|
457
|
+
const targetPageId = currentPage?.id ?? currentPageId;
|
|
458
|
+
setFormDocument((current) => {
|
|
459
|
+
const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
|
|
460
|
+
const progress = {
|
|
461
|
+
...createCapturePageProgressItem(pageId, current.items.filter((item) => item.pageId === pageId).length),
|
|
462
|
+
id: progressId,
|
|
463
|
+
};
|
|
464
|
+
return { ...current, items: [...current.items, progress] };
|
|
465
|
+
});
|
|
466
|
+
setSingleSelectedItem(progressId);
|
|
467
|
+
setActivePanel("inspector");
|
|
468
|
+
setDetailsOpen(true);
|
|
469
|
+
}, [currentPage?.id, currentPageId, setSingleSelectedItem]);
|
|
470
|
+
const handleCanvasItemSelect = useCallback((item, event) => {
|
|
471
|
+
const additive = Boolean(event?.metaKey || event?.ctrlKey || event?.shiftKey);
|
|
472
|
+
if (!additive) {
|
|
473
|
+
setSingleSelectedItem(item.id);
|
|
474
|
+
setActivePanel("inspector");
|
|
475
|
+
setDetailsOpen(true);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
setSelectedItemIds((current) => {
|
|
479
|
+
const next = new Set(current);
|
|
480
|
+
if (next.has(item.id) && next.size > 1)
|
|
481
|
+
next.delete(item.id);
|
|
482
|
+
else
|
|
483
|
+
next.add(item.id);
|
|
484
|
+
const nextIds = formDocument.items.filter((candidate) => next.has(candidate.id)).map((candidate) => candidate.id);
|
|
485
|
+
setSelectedId(nextIds[0] ?? null);
|
|
486
|
+
if (nextIds.length > 1) {
|
|
487
|
+
setActivePanel("outline");
|
|
488
|
+
setDetailsOpen(true);
|
|
489
|
+
}
|
|
490
|
+
return new Set(nextIds);
|
|
491
|
+
});
|
|
492
|
+
}, [formDocument.items, setSingleSelectedItem]);
|
|
124
493
|
const updateSelectedField = useCallback((patch) => {
|
|
125
494
|
if (!selectedId)
|
|
126
495
|
return;
|
|
127
|
-
|
|
496
|
+
setFormDocument((current) => ({
|
|
497
|
+
...current,
|
|
498
|
+
items: current.items.map((item) => (item.kind === "input" && item.id === selectedId ? { ...item, ...patch } : item)),
|
|
499
|
+
}));
|
|
500
|
+
}, [selectedId]);
|
|
501
|
+
const updateSelectedTextBlock = useCallback((patch) => {
|
|
502
|
+
if (!selectedId)
|
|
503
|
+
return;
|
|
504
|
+
setFormDocument((current) => ({
|
|
505
|
+
...current,
|
|
506
|
+
items: current.items.map((item) => (item.kind === "content.text" && item.id === selectedId ? { ...item, ...patch } : item)),
|
|
507
|
+
}));
|
|
508
|
+
}, [selectedId]);
|
|
509
|
+
const updateSelectedIconBlock = useCallback((patch) => {
|
|
510
|
+
if (!selectedId)
|
|
511
|
+
return;
|
|
512
|
+
setFormDocument((current) => ({
|
|
513
|
+
...current,
|
|
514
|
+
items: current.items.map((item) => (item.kind === "content.icon" && item.id === selectedId ? { ...item, ...patch } : item)),
|
|
515
|
+
}));
|
|
516
|
+
}, [selectedId]);
|
|
517
|
+
const updateSelectedImageBlock = useCallback((patch) => {
|
|
518
|
+
if (!selectedId)
|
|
519
|
+
return;
|
|
520
|
+
setFormDocument((current) => ({
|
|
521
|
+
...current,
|
|
522
|
+
items: current.items.map((item) => (item.kind === "content.image" && item.id === selectedId ? { ...item, ...patch } : item)),
|
|
523
|
+
}));
|
|
524
|
+
}, [selectedId]);
|
|
525
|
+
const updateSelectedDivider = useCallback((patch) => {
|
|
526
|
+
if (!selectedId)
|
|
527
|
+
return;
|
|
528
|
+
setFormDocument((current) => ({
|
|
529
|
+
...current,
|
|
530
|
+
items: current.items.map((item) => (item.kind === "content.divider" && item.id === selectedId ? { ...item, ...patch } : item)),
|
|
531
|
+
}));
|
|
532
|
+
}, [selectedId]);
|
|
533
|
+
const updateSelectedPageHeader = useCallback((patch) => {
|
|
534
|
+
if (!selectedId)
|
|
535
|
+
return;
|
|
536
|
+
setFormDocument((current) => ({
|
|
537
|
+
...current,
|
|
538
|
+
items: current.items.map((item) => (item.kind === "content.page-header" && item.id === selectedId ? { ...item, ...patch } : item)),
|
|
539
|
+
}));
|
|
540
|
+
}, [selectedId]);
|
|
541
|
+
const updateSelectedPageProgress = useCallback((patch) => {
|
|
542
|
+
if (!selectedId)
|
|
543
|
+
return;
|
|
544
|
+
setFormDocument((current) => ({
|
|
545
|
+
...current,
|
|
546
|
+
items: current.items.map((item) => (item.kind === "content.page-progress" && item.id === selectedId ? { ...item, ...patch } : item)),
|
|
547
|
+
}));
|
|
128
548
|
}, [selectedId]);
|
|
549
|
+
const deleteSelectedItem = useCallback(() => {
|
|
550
|
+
if (!selectedItem)
|
|
551
|
+
return;
|
|
552
|
+
const pageId = selectedItem.pageId;
|
|
553
|
+
setFormDocument((current) => {
|
|
554
|
+
const remainingItems = current.items.filter((item) => item.id !== selectedItem.id);
|
|
555
|
+
const reorderedPageItems = remainingItems
|
|
556
|
+
.filter((item) => item.pageId === pageId)
|
|
557
|
+
.sort((a, b) => a.order - b.order)
|
|
558
|
+
.map((item, order) => ({ ...item, order }));
|
|
559
|
+
const reorderedIds = new Set(reorderedPageItems.map((item) => item.id));
|
|
560
|
+
return {
|
|
561
|
+
...current,
|
|
562
|
+
items: remainingItems.map((item) => reorderedIds.has(item.id)
|
|
563
|
+
? reorderedPageItems.find((pageItem) => pageItem.id === item.id) ?? item
|
|
564
|
+
: item),
|
|
565
|
+
};
|
|
566
|
+
});
|
|
567
|
+
const nextItem = formDocument.items
|
|
568
|
+
.filter((item) => item.pageId === pageId && item.id !== selectedItem.id)
|
|
569
|
+
.sort((a, b) => a.order - b.order)[0];
|
|
570
|
+
setSingleSelectedItem(nextItem?.id ?? `page:${pageId}`);
|
|
571
|
+
setActivePanel("inspector");
|
|
572
|
+
}, [formDocument.items, selectedItem, setSingleSelectedItem]);
|
|
573
|
+
const updateSelectedInputItems = useCallback((update) => {
|
|
574
|
+
if (selectedInputItems.length === 0)
|
|
575
|
+
return;
|
|
576
|
+
const inputIds = new Set(selectedInputItems.map((item) => item.id));
|
|
577
|
+
setFormDocument((current) => ({
|
|
578
|
+
...current,
|
|
579
|
+
items: current.items.map((item) => item.kind === "input" && inputIds.has(item.id) ? update(item) : item),
|
|
580
|
+
}));
|
|
581
|
+
}, [selectedInputItems]);
|
|
582
|
+
const updateSelectedInputLayout = useCallback((patch) => {
|
|
583
|
+
updateSelectedInputItems((field) => ({
|
|
584
|
+
...field,
|
|
585
|
+
metadata: {
|
|
586
|
+
...(field.metadata ?? {}),
|
|
587
|
+
layout: {
|
|
588
|
+
...getFieldLayout(field),
|
|
589
|
+
...patch,
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
}));
|
|
593
|
+
}, [updateSelectedInputItems]);
|
|
594
|
+
const updateSelectedInputRequired = useCallback((required) => {
|
|
595
|
+
updateSelectedInputItems((field) => ({ ...field, required }));
|
|
596
|
+
}, [updateSelectedInputItems]);
|
|
597
|
+
const deleteSelectedItems = useCallback(() => {
|
|
598
|
+
if (selectedItems.length === 0)
|
|
599
|
+
return;
|
|
600
|
+
const idsToDelete = new Set(selectedItems.map((item) => item.id));
|
|
601
|
+
const touchedPageIds = new Set(selectedItems.map((item) => item.pageId));
|
|
602
|
+
const fallbackPageId = selectedItems[0]?.pageId ?? currentPage?.id ?? currentPageId;
|
|
603
|
+
setFormDocument((current) => {
|
|
604
|
+
const remainingItems = current.items.filter((item) => !idsToDelete.has(item.id));
|
|
605
|
+
const reorderedById = new Map();
|
|
606
|
+
for (const pageId of touchedPageIds) {
|
|
607
|
+
remainingItems
|
|
608
|
+
.filter((item) => item.pageId === pageId)
|
|
609
|
+
.sort((a, b) => a.order - b.order)
|
|
610
|
+
.forEach((item, order) => reorderedById.set(item.id, { ...item, order }));
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
...current,
|
|
614
|
+
items: remainingItems.map((item) => reorderedById.get(item.id) ?? item),
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
const nextItem = formDocument.items
|
|
618
|
+
.filter((item) => !idsToDelete.has(item.id) && touchedPageIds.has(item.pageId))
|
|
619
|
+
.sort((a, b) => a.order - b.order)[0];
|
|
620
|
+
setSingleSelectedItem(nextItem?.id ?? `page:${fallbackPageId}`);
|
|
621
|
+
setActivePanel("inspector");
|
|
622
|
+
}, [currentPage?.id, currentPageId, formDocument.items, selectedItems, setSingleSelectedItem]);
|
|
623
|
+
useEffect(() => {
|
|
624
|
+
if (selectedItems.length === 0)
|
|
625
|
+
return;
|
|
626
|
+
const handleDeleteKey = (event) => {
|
|
627
|
+
if (event.key !== "Delete" || event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey)
|
|
628
|
+
return;
|
|
629
|
+
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
630
|
+
const isEditableTarget = target
|
|
631
|
+
? target.isContentEditable
|
|
632
|
+
|| ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)
|
|
633
|
+
|| target.getAttribute("role") === "textbox"
|
|
634
|
+
: false;
|
|
635
|
+
if (isEditableTarget)
|
|
636
|
+
return;
|
|
637
|
+
event.preventDefault();
|
|
638
|
+
if (selectedItems.length > 1)
|
|
639
|
+
deleteSelectedItems();
|
|
640
|
+
else
|
|
641
|
+
deleteSelectedItem();
|
|
642
|
+
};
|
|
643
|
+
window.addEventListener("keydown", handleDeleteKey);
|
|
644
|
+
return () => window.removeEventListener("keydown", handleDeleteKey);
|
|
645
|
+
}, [deleteSelectedItem, deleteSelectedItems, selectedItems]);
|
|
646
|
+
const addPage = useCallback(() => {
|
|
647
|
+
const pageId = `page-${Date.now().toString(36)}`;
|
|
648
|
+
setFormDocument((current) => {
|
|
649
|
+
const pageNumber = current.pages.length + 1;
|
|
650
|
+
const page = {
|
|
651
|
+
id: pageId,
|
|
652
|
+
title: `Page ${pageNumber}`,
|
|
653
|
+
preHeading: "Capture Form",
|
|
654
|
+
headerContentInitialized: true,
|
|
655
|
+
order: current.pages.length,
|
|
656
|
+
};
|
|
657
|
+
return {
|
|
658
|
+
...current,
|
|
659
|
+
pages: [...current.pages, page],
|
|
660
|
+
items: [
|
|
661
|
+
...current.items,
|
|
662
|
+
createCapturePageHeaderItem(pageId, 0, {
|
|
663
|
+
preHeading: page.preHeading,
|
|
664
|
+
title: page.title,
|
|
665
|
+
description: page.description,
|
|
666
|
+
}),
|
|
667
|
+
createCapturePageProgressItem(pageId, 1),
|
|
668
|
+
],
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
setCurrentPageId(pageId);
|
|
672
|
+
setSingleSelectedItem(null);
|
|
673
|
+
setActivePanel("inspector");
|
|
674
|
+
setDetailsOpen(true);
|
|
675
|
+
}, [setSingleSelectedItem]);
|
|
676
|
+
const selectBuilderPage = useCallback((pageId) => {
|
|
677
|
+
setCurrentPageId(pageId);
|
|
678
|
+
const firstItem = formDocument.items
|
|
679
|
+
.filter((item) => item.pageId === pageId)
|
|
680
|
+
.sort((a, b) => a.order - b.order)[0];
|
|
681
|
+
setSingleSelectedItem(firstItem?.id ?? null);
|
|
682
|
+
}, [formDocument.items, setSingleSelectedItem]);
|
|
683
|
+
const selectPageForInspection = useCallback((pageId) => {
|
|
684
|
+
setCurrentPageId(pageId);
|
|
685
|
+
setSingleSelectedItem(`page:${pageId}`);
|
|
686
|
+
setActivePanel("inspector");
|
|
687
|
+
setDetailsOpen(true);
|
|
688
|
+
}, [setSingleSelectedItem]);
|
|
689
|
+
const goToPreviousPage = useCallback(() => {
|
|
690
|
+
const previousPage = sortedPages[currentPageIndex - 1];
|
|
691
|
+
if (previousPage)
|
|
692
|
+
selectBuilderPage(previousPage.id);
|
|
693
|
+
}, [currentPageIndex, selectBuilderPage, sortedPages]);
|
|
694
|
+
const goToNextPage = useCallback(() => {
|
|
695
|
+
const nextPage = sortedPages[currentPageIndex + 1];
|
|
696
|
+
if (nextPage)
|
|
697
|
+
selectBuilderPage(nextPage.id);
|
|
698
|
+
}, [currentPageIndex, selectBuilderPage, sortedPages]);
|
|
699
|
+
const updateCurrentPage = useCallback((patch) => {
|
|
700
|
+
if (!currentPage)
|
|
701
|
+
return;
|
|
702
|
+
setFormDocument((current) => ({
|
|
703
|
+
...current,
|
|
704
|
+
pages: current.pages.map((page) => (page.id === currentPage.id ? { ...page, ...patch } : page)),
|
|
705
|
+
}));
|
|
706
|
+
}, [currentPage]);
|
|
707
|
+
const deleteCurrentPage = useCallback(() => {
|
|
708
|
+
if (!currentPage || formDocument.pages.length <= 1)
|
|
709
|
+
return;
|
|
710
|
+
const sorted = [...formDocument.pages].sort((a, b) => a.order - b.order);
|
|
711
|
+
const currentIndex = sorted.findIndex((page) => page.id === currentPage.id);
|
|
712
|
+
const fallbackPage = sorted[currentIndex + 1] ?? sorted[currentIndex - 1] ?? sorted.find((page) => page.id !== currentPage.id);
|
|
713
|
+
setFormDocument((current) => {
|
|
714
|
+
const remainingPages = current.pages
|
|
715
|
+
.filter((page) => page.id !== currentPage.id)
|
|
716
|
+
.sort((a, b) => a.order - b.order)
|
|
717
|
+
.map((page, index) => ({ ...page, order: index }));
|
|
718
|
+
return {
|
|
719
|
+
...current,
|
|
720
|
+
pages: remainingPages,
|
|
721
|
+
items: current.items.filter((item) => item.pageId !== currentPage.id),
|
|
722
|
+
};
|
|
723
|
+
});
|
|
724
|
+
if (fallbackPage) {
|
|
725
|
+
setCurrentPageId(fallbackPage.id);
|
|
726
|
+
setSingleSelectedItem(`page:${fallbackPage.id}`);
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
setSingleSelectedItem(null);
|
|
730
|
+
}
|
|
731
|
+
setActivePanel("inspector");
|
|
732
|
+
setDetailsOpen(true);
|
|
733
|
+
}, [currentPage, formDocument.pages, setSingleSelectedItem]);
|
|
734
|
+
const reorderItems = useCallback((draggedItemId, targetItemId, placement) => {
|
|
735
|
+
if (draggedItemId === targetItemId)
|
|
736
|
+
return;
|
|
737
|
+
setFormDocument((current) => {
|
|
738
|
+
const draggedItem = current.items.find((item) => item.id === draggedItemId);
|
|
739
|
+
const targetItem = current.items.find((item) => item.id === targetItemId);
|
|
740
|
+
if (!draggedItem || !targetItem || draggedItem.pageId !== targetItem.pageId)
|
|
741
|
+
return current;
|
|
742
|
+
const pageItems = current.items
|
|
743
|
+
.filter((item) => item.pageId === targetItem.pageId)
|
|
744
|
+
.sort((a, b) => a.order - b.order)
|
|
745
|
+
.filter((item) => item.id !== draggedItemId);
|
|
746
|
+
const targetIndex = pageItems.findIndex((item) => item.id === targetItemId);
|
|
747
|
+
if (targetIndex === -1)
|
|
748
|
+
return current;
|
|
749
|
+
const insertIndex = placement === "before" ? targetIndex : targetIndex + 1;
|
|
750
|
+
const reorderedPageItems = [
|
|
751
|
+
...pageItems.slice(0, insertIndex),
|
|
752
|
+
draggedItem,
|
|
753
|
+
...pageItems.slice(insertIndex),
|
|
754
|
+
].map((item, order) => ({ ...item, order }));
|
|
755
|
+
const reorderedIds = new Set(reorderedPageItems.map((item) => item.id));
|
|
756
|
+
return {
|
|
757
|
+
...current,
|
|
758
|
+
items: current.items.map((item) => reorderedIds.has(item.id)
|
|
759
|
+
? reorderedPageItems.find((pageItem) => pageItem.id === item.id) ?? item
|
|
760
|
+
: item),
|
|
761
|
+
};
|
|
762
|
+
});
|
|
763
|
+
}, []);
|
|
129
764
|
const handleDragStart = useCallback((event, tile) => {
|
|
130
765
|
event.dataTransfer.effectAllowed = "copy";
|
|
131
766
|
event.dataTransfer.setData(CAPTURE_BUILDER_DRAG_TYPE, JSON.stringify(tile));
|
|
132
767
|
event.dataTransfer.setData("text/plain", tile.label);
|
|
133
768
|
}, []);
|
|
769
|
+
const handleContentDragStart = useCallback((event, kind) => {
|
|
770
|
+
event.dataTransfer.effectAllowed = "copy";
|
|
771
|
+
event.dataTransfer.setData(CAPTURE_BUILDER_CONTENT_DRAG_TYPE, kind);
|
|
772
|
+
event.dataTransfer.setData("text/plain", kind === "text" ? "Text block" : kind === "icon" ? "Icon" : kind === "image" ? "Image" : kind === "divider" ? "Divider" : kind === "page-header" ? "Page header" : "Page progress");
|
|
773
|
+
}, []);
|
|
774
|
+
const insertPaletteItemAt = useCallback((event, target) => {
|
|
775
|
+
const contentKind = event.dataTransfer.getData(CAPTURE_BUILDER_CONTENT_DRAG_TYPE);
|
|
776
|
+
const raw = event.dataTransfer.getData(CAPTURE_BUILDER_DRAG_TYPE);
|
|
777
|
+
if (!contentKind && !raw)
|
|
778
|
+
return;
|
|
779
|
+
const newId = `${contentKind || "field"}-${Date.now().toString(36)}`;
|
|
780
|
+
setFormDocument((current) => {
|
|
781
|
+
const targetItem = current.items.find((item) => item.id === target.itemId);
|
|
782
|
+
const pageId = targetItem?.pageId ?? currentPage?.id ?? currentPageId;
|
|
783
|
+
if (!current.pages.some((page) => page.id === pageId))
|
|
784
|
+
return current;
|
|
785
|
+
const order = 0;
|
|
786
|
+
const page = current.pages.find((candidate) => candidate.id === pageId);
|
|
787
|
+
let newItem = null;
|
|
788
|
+
if (contentKind === "text") {
|
|
789
|
+
newItem = { id: newId, kind: "content.text", pageId, order, title: "Section intro", body: "Add helpful context before respondents answer the next question." };
|
|
790
|
+
}
|
|
791
|
+
else if (contentKind === "icon") {
|
|
792
|
+
newItem = { id: newId, kind: "content.icon", pageId, order, icon: "sparkles", iconColor: "indigo", align: "center" };
|
|
793
|
+
}
|
|
794
|
+
else if (contentKind === "image") {
|
|
795
|
+
newItem = { id: newId, kind: "content.image", pageId, order, imageUrl: "", shape: "square", align: "center", size: "medium", fit: "cover", source: { type: "upload" } };
|
|
796
|
+
}
|
|
797
|
+
else if (contentKind === "divider") {
|
|
798
|
+
newItem = { id: newId, kind: "content.divider", pageId, order };
|
|
799
|
+
}
|
|
800
|
+
else if (contentKind === "page-header") {
|
|
801
|
+
newItem = { ...createCapturePageHeaderItem(pageId, order, { preHeading: page?.preHeading, title: page?.title, description: page?.description }), id: newId };
|
|
802
|
+
}
|
|
803
|
+
else if (contentKind === "page-progress") {
|
|
804
|
+
newItem = { ...createCapturePageProgressItem(pageId, order), id: newId };
|
|
805
|
+
}
|
|
806
|
+
else if (raw) {
|
|
807
|
+
const tile = JSON.parse(raw);
|
|
808
|
+
newItem = {
|
|
809
|
+
id: newId,
|
|
810
|
+
key: createDefaultFieldKey(tile, current.items),
|
|
811
|
+
label: tile.defaultField.label,
|
|
812
|
+
type: tile.defaultField.type,
|
|
813
|
+
required: tile.defaultField.required ?? false,
|
|
814
|
+
...(tile.defaultField.options ? { options: tile.defaultField.options } : {}),
|
|
815
|
+
...(tile.defaultField.metadata ? { metadata: tile.defaultField.metadata } : {}),
|
|
816
|
+
kind: "input",
|
|
817
|
+
pageId,
|
|
818
|
+
order,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
if (!newItem)
|
|
822
|
+
return current;
|
|
823
|
+
const pageItems = current.items.filter((item) => item.pageId === pageId).sort((a, b) => a.order - b.order);
|
|
824
|
+
const targetIndex = Math.max(0, pageItems.findIndex((item) => item.id === target.itemId));
|
|
825
|
+
const insertIndex = target.placement === "before" ? targetIndex : targetIndex + 1;
|
|
826
|
+
const reorderedPageItems = [
|
|
827
|
+
...pageItems.slice(0, insertIndex),
|
|
828
|
+
newItem,
|
|
829
|
+
...pageItems.slice(insertIndex),
|
|
830
|
+
].map((item, nextOrder) => ({ ...item, order: nextOrder }));
|
|
831
|
+
const reorderedIds = new Set(reorderedPageItems.map((item) => item.id));
|
|
832
|
+
return {
|
|
833
|
+
...current,
|
|
834
|
+
items: [
|
|
835
|
+
...current.items.filter((item) => item.pageId !== pageId),
|
|
836
|
+
...reorderedPageItems,
|
|
837
|
+
].map((item) => reorderedIds.has(item.id) ? reorderedPageItems.find((pageItem) => pageItem.id === item.id) ?? item : item),
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
setSingleSelectedItem(newId);
|
|
841
|
+
setActivePanel("inspector");
|
|
842
|
+
setDetailsOpen(true);
|
|
843
|
+
}, [currentPage?.id, currentPageId, setSingleSelectedItem]);
|
|
134
844
|
const handleDragOver = useCallback((event) => {
|
|
135
|
-
if (event.dataTransfer.types.includes(CAPTURE_BUILDER_DRAG_TYPE)) {
|
|
845
|
+
if (event.dataTransfer.types.includes(CAPTURE_BUILDER_DRAG_TYPE) || event.dataTransfer.types.includes(CAPTURE_BUILDER_CONTENT_DRAG_TYPE)) {
|
|
136
846
|
event.preventDefault();
|
|
137
847
|
event.dataTransfer.dropEffect = "copy";
|
|
138
|
-
setIsDragOver(true);
|
|
139
848
|
}
|
|
140
849
|
}, []);
|
|
141
850
|
const handleDrop = useCallback((event) => {
|
|
851
|
+
const contentKind = event.dataTransfer.getData(CAPTURE_BUILDER_CONTENT_DRAG_TYPE);
|
|
852
|
+
if (contentKind === "text" || contentKind === "icon" || contentKind === "image" || contentKind === "divider" || contentKind === "page-header" || contentKind === "page-progress") {
|
|
853
|
+
event.preventDefault();
|
|
854
|
+
if (contentKind === "text")
|
|
855
|
+
insertTextBlock();
|
|
856
|
+
else if (contentKind === "icon")
|
|
857
|
+
insertIconBlock();
|
|
858
|
+
else if (contentKind === "image")
|
|
859
|
+
insertImageBlock();
|
|
860
|
+
else if (contentKind === "divider")
|
|
861
|
+
insertDivider();
|
|
862
|
+
else if (contentKind === "page-header")
|
|
863
|
+
insertPageHeader();
|
|
864
|
+
else
|
|
865
|
+
insertPageProgress();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
142
868
|
const raw = event.dataTransfer.getData(CAPTURE_BUILDER_DRAG_TYPE);
|
|
143
869
|
if (!raw)
|
|
144
870
|
return;
|
|
145
871
|
event.preventDefault();
|
|
146
|
-
setIsDragOver(false);
|
|
147
872
|
const parsed = JSON.parse(raw);
|
|
148
873
|
insertField(parsed);
|
|
149
|
-
}, [insertField]);
|
|
874
|
+
}, [insertDivider, insertField, insertIconBlock, insertImageBlock, insertPageHeader, insertPageProgress, insertTextBlock]);
|
|
875
|
+
const validateUniqueFieldKeys = useCallback(() => {
|
|
876
|
+
const seen = new Set();
|
|
877
|
+
for (const field of fields) {
|
|
878
|
+
const key = field.key.trim();
|
|
879
|
+
if (!key) {
|
|
880
|
+
toast.error("Field keys cannot be blank");
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
if (seen.has(key)) {
|
|
884
|
+
toast.error(`Field key “${key}” is already used in this form`);
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
seen.add(key);
|
|
888
|
+
}
|
|
889
|
+
return true;
|
|
890
|
+
}, [fields]);
|
|
891
|
+
const markFieldsSaved = useCallback(() => {
|
|
892
|
+
setSavedFieldIds(new Set(extractCaptureAnswerItems(formDocument).map((field) => field.id)));
|
|
893
|
+
}, [formDocument]);
|
|
150
894
|
const saveForm = useCallback(async () => {
|
|
151
|
-
if (!id)
|
|
895
|
+
if (!id || !validateUniqueFieldKeys())
|
|
152
896
|
return;
|
|
153
|
-
await updateMutation.mutateAsync({ id, name: formName, fields, actionConfig });
|
|
154
|
-
|
|
897
|
+
await updateMutation.mutateAsync({ id, name: formName, fields, document: formDocument, actionConfig });
|
|
898
|
+
markFieldsSaved();
|
|
899
|
+
}, [actionConfig, fields, formDocument, formName, id, markFieldsSaved, updateMutation, validateUniqueFieldKeys]);
|
|
155
900
|
const publishForm = useCallback(async () => {
|
|
156
|
-
if (!id)
|
|
901
|
+
if (!id || !validateUniqueFieldKeys())
|
|
157
902
|
return;
|
|
158
|
-
await updateMutation.mutateAsync({ id, name: formName, fields, actionConfig });
|
|
903
|
+
await updateMutation.mutateAsync({ id, name: formName, fields, document: formDocument, actionConfig });
|
|
904
|
+
markFieldsSaved();
|
|
159
905
|
await publishMutation.mutateAsync({ id });
|
|
160
|
-
}, [actionConfig, fields, formName, id, publishMutation, updateMutation]);
|
|
906
|
+
}, [actionConfig, fields, formDocument, formName, id, markFieldsSaved, publishMutation, updateMutation, validateUniqueFieldKeys]);
|
|
161
907
|
const definitionSubmissions = useMemo(() => {
|
|
162
908
|
const rows = (submissionsQuery.data ?? []);
|
|
163
909
|
return id ? rows.filter((submission) => submission.definitionId === id) : rows;
|
|
164
910
|
}, [id, submissionsQuery.data]);
|
|
911
|
+
const publicationId = definitionQuery.data?.publicationId ?? undefined;
|
|
165
912
|
const publicationSlug = definitionQuery.data?.publicationSlug ?? undefined;
|
|
913
|
+
const formStatus = definitionQuery.data?.status ?? definitionsQuery.data?.find((definition) => definition.id === id)?.status ?? "draft";
|
|
166
914
|
const publicPath = publicationSlug ? `/forms/${publicationSlug}` : null;
|
|
915
|
+
const publicUrl = publicPath ? buildPublicUrl(publicPath) : null;
|
|
916
|
+
const builderSubView = location.pathname.endsWith("/submissions")
|
|
917
|
+
? "submissions"
|
|
918
|
+
: location.pathname.endsWith("/share")
|
|
919
|
+
? "share"
|
|
920
|
+
: "build";
|
|
921
|
+
const isCreating = createMutation.isPending;
|
|
922
|
+
const itemViews = useMemo(() => (itemViewsQuery.data ?? []), [itemViewsQuery.data]);
|
|
923
|
+
const isCaptureItemsViewDirty = isWorkbenchStateDirty({
|
|
924
|
+
current: { columns: persistWorkbenchColumns(captureItemsColumns), filters: captureItemsFilters, sort: captureItemsSort },
|
|
925
|
+
saved: { columns: persistWorkbenchColumns(savedCaptureItemsColumns), filters: savedCaptureItemsFilters, sort: savedCaptureItemsSort },
|
|
926
|
+
});
|
|
927
|
+
const saveCaptureItemsView = useCallback(() => {
|
|
928
|
+
const state = {
|
|
929
|
+
filters: captureItemsFilters ?? null,
|
|
930
|
+
sorts: [captureItemsSort],
|
|
931
|
+
columns: persistWorkbenchColumns(captureItemsColumns),
|
|
932
|
+
};
|
|
933
|
+
const existing = itemViews.find((view) => view.id === activeWorkbenchViewId);
|
|
934
|
+
if (existing) {
|
|
935
|
+
updateItemViewMutation.mutate({ id: existing.id, ...state }, {
|
|
936
|
+
onSuccess: () => {
|
|
937
|
+
setSavedCaptureItemsColumns(captureItemsColumns);
|
|
938
|
+
setSavedCaptureItemsFilters(captureItemsFilters);
|
|
939
|
+
setSavedCaptureItemsSort(captureItemsSort);
|
|
940
|
+
toast.success("View saved");
|
|
941
|
+
},
|
|
942
|
+
onError: () => toast.error("Failed to save view"),
|
|
943
|
+
});
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const name = window.prompt("Name this view", "New Capture view")?.trim();
|
|
947
|
+
if (!name)
|
|
948
|
+
return;
|
|
949
|
+
createItemViewMutation.mutate({ name, ...state }, {
|
|
950
|
+
onSuccess: (view) => {
|
|
951
|
+
if (!view)
|
|
952
|
+
return;
|
|
953
|
+
setActiveWorkbenchViewId(view.id);
|
|
954
|
+
setSavedCaptureItemsColumns(captureItemsColumns);
|
|
955
|
+
setSavedCaptureItemsFilters(captureItemsFilters);
|
|
956
|
+
setSavedCaptureItemsSort(captureItemsSort);
|
|
957
|
+
toast.success("View created");
|
|
958
|
+
},
|
|
959
|
+
onError: () => toast.error("Failed to create view"),
|
|
960
|
+
});
|
|
961
|
+
}, [activeWorkbenchViewId, captureItemsColumns, captureItemsFilters, captureItemsSort, createItemViewMutation, itemViews, updateItemViewMutation]);
|
|
962
|
+
const resetCaptureItemsView = useCallback(() => {
|
|
963
|
+
setCaptureItemsColumns(savedCaptureItemsColumns);
|
|
964
|
+
setCaptureItemsFilters(savedCaptureItemsFilters);
|
|
965
|
+
setCaptureItemsSort(savedCaptureItemsSort);
|
|
966
|
+
}, [savedCaptureItemsColumns, savedCaptureItemsFilters, savedCaptureItemsSort]);
|
|
967
|
+
const workbenchViews = useMemo(() => [
|
|
968
|
+
...BUILT_IN_CAPTURE_ITEMS_WORKBENCH_VIEWS,
|
|
969
|
+
...itemViews.map((view) => ({ id: view.id, name: view.name, icon: _jsx(ListTree, { className: "h-4 w-4" }) })),
|
|
970
|
+
], [itemViews]);
|
|
971
|
+
const applyCaptureItemsView = useCallback((viewId) => {
|
|
972
|
+
setActiveWorkbenchViewId(viewId);
|
|
973
|
+
const view = itemViews.find((candidate) => candidate.id === viewId);
|
|
974
|
+
if (!view) {
|
|
975
|
+
setCaptureItemsFilters(undefined);
|
|
976
|
+
setSavedCaptureItemsFilters(undefined);
|
|
977
|
+
const defaultSort = { field: "updatedAt", direction: "desc" };
|
|
978
|
+
setCaptureItemsSort(defaultSort);
|
|
979
|
+
setSavedCaptureItemsSort(defaultSort);
|
|
980
|
+
const defaultColumns = adaptWorkbenchColumns({ fields: CAPTURE_ITEMS_FIELDS, persisted: DEFAULT_CAPTURE_ITEMS_COLUMNS }).configs;
|
|
981
|
+
setCaptureItemsColumns(defaultColumns);
|
|
982
|
+
setSavedCaptureItemsColumns(defaultColumns);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
setCaptureItemsFilters(view.filters ?? undefined);
|
|
986
|
+
setSavedCaptureItemsFilters(view.filters ?? undefined);
|
|
987
|
+
const nextSort = view.sorts?.[0] ?? { field: "updatedAt", direction: "desc" };
|
|
988
|
+
setCaptureItemsSort(nextSort);
|
|
989
|
+
setSavedCaptureItemsSort(nextSort);
|
|
990
|
+
const nextColumns = adaptWorkbenchColumns({
|
|
991
|
+
fields: CAPTURE_ITEMS_FIELDS,
|
|
992
|
+
persisted: view.columns ?? DEFAULT_CAPTURE_ITEMS_COLUMNS,
|
|
993
|
+
}).configs;
|
|
994
|
+
setCaptureItemsColumns(nextColumns);
|
|
995
|
+
setSavedCaptureItemsColumns(nextColumns);
|
|
996
|
+
}, [itemViews]);
|
|
997
|
+
const submissionViews = useMemo(() => (submissionViewsQuery.data ?? []), [submissionViewsQuery.data]);
|
|
998
|
+
const activeSubmissionSavedView = useMemo(() => submissionViews.find((view) => view.id === activeSubmissionViewId) ?? null, [activeSubmissionViewId, submissionViews]);
|
|
999
|
+
useEffect(() => {
|
|
1000
|
+
const builtInView = BUILT_IN_CAPTURE_SUBMISSION_WORKBENCH_VIEWS.find((view) => view.id === activeSubmissionViewId);
|
|
1001
|
+
setSubmissionViewName(activeSubmissionSavedView?.name ?? (builtInView ? `${builtInView.name} copy` : "New submission view"));
|
|
1002
|
+
setSubmissionViewNameError(null);
|
|
1003
|
+
}, [activeSubmissionSavedView, activeSubmissionViewId]);
|
|
1004
|
+
const isSubmissionViewDirty = isWorkbenchStateDirty({
|
|
1005
|
+
current: { columns: persistWorkbenchColumns(submissionColumns), filters: submissionFilters, sort: submissionSort },
|
|
1006
|
+
saved: { columns: persistWorkbenchColumns(savedSubmissionColumns), filters: savedSubmissionFilters, sort: savedSubmissionSort },
|
|
1007
|
+
});
|
|
1008
|
+
const writeSubmissionViewToUrl = useCallback((viewId, options) => {
|
|
1009
|
+
void navigate({
|
|
1010
|
+
to: ".",
|
|
1011
|
+
search: (previous) => ({ ...previous, view: viewId }),
|
|
1012
|
+
replace: options?.replace,
|
|
1013
|
+
});
|
|
1014
|
+
}, [navigate]);
|
|
1015
|
+
const getSubmissionViewState = useCallback(() => ({
|
|
1016
|
+
filters: submissionFilters ?? null,
|
|
1017
|
+
sorts: [submissionSort],
|
|
1018
|
+
columns: persistWorkbenchColumns(submissionColumns),
|
|
1019
|
+
}), [submissionColumns, submissionFilters, submissionSort]);
|
|
1020
|
+
const markSubmissionViewSaved = useCallback(() => {
|
|
1021
|
+
setSavedSubmissionColumns(submissionColumns);
|
|
1022
|
+
setSavedSubmissionFilters(submissionFilters);
|
|
1023
|
+
setSavedSubmissionSort(submissionSort);
|
|
1024
|
+
}, [submissionColumns, submissionFilters, submissionSort]);
|
|
1025
|
+
const saveSubmissionView = useCallback(() => {
|
|
1026
|
+
if (!id)
|
|
1027
|
+
return;
|
|
1028
|
+
if (!activeSubmissionSavedView) {
|
|
1029
|
+
setActiveSubmissionPanel("details");
|
|
1030
|
+
setSubmissionViewNameError(null);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
updateSubmissionViewMutation.mutate({ id: activeSubmissionSavedView.id, ...getSubmissionViewState() }, {
|
|
1034
|
+
onSuccess: () => {
|
|
1035
|
+
markSubmissionViewSaved();
|
|
1036
|
+
toast.success("Submission view saved");
|
|
1037
|
+
},
|
|
1038
|
+
onError: () => toast.error("Failed to save submission view"),
|
|
1039
|
+
});
|
|
1040
|
+
}, [activeSubmissionSavedView, getSubmissionViewState, id, markSubmissionViewSaved, updateSubmissionViewMutation]);
|
|
1041
|
+
const saveSubmissionViewDetails = useCallback(() => {
|
|
1042
|
+
if (!id)
|
|
1043
|
+
return;
|
|
1044
|
+
const name = submissionViewName.trim();
|
|
1045
|
+
if (!name) {
|
|
1046
|
+
setSubmissionViewNameError("Name is required");
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
setSubmissionViewNameError(null);
|
|
1050
|
+
const state = getSubmissionViewState();
|
|
1051
|
+
if (activeSubmissionSavedView) {
|
|
1052
|
+
updateSubmissionViewMutation.mutate({ id: activeSubmissionSavedView.id, name, ...state }, {
|
|
1053
|
+
onSuccess: () => {
|
|
1054
|
+
markSubmissionViewSaved();
|
|
1055
|
+
toast.success("Submission view saved");
|
|
1056
|
+
},
|
|
1057
|
+
onError: () => toast.error("Failed to save submission view"),
|
|
1058
|
+
});
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
createSubmissionViewMutation.mutate({ definitionId: id, name, ...state }, {
|
|
1062
|
+
onSuccess: (view) => {
|
|
1063
|
+
if (!view)
|
|
1064
|
+
return;
|
|
1065
|
+
setActiveSubmissionViewId(view.id);
|
|
1066
|
+
setSubmissionViewName(view.name);
|
|
1067
|
+
writeSubmissionViewToUrl(view.id);
|
|
1068
|
+
markSubmissionViewSaved();
|
|
1069
|
+
toast.success("Submission view created");
|
|
1070
|
+
},
|
|
1071
|
+
onError: () => toast.error("Failed to create submission view"),
|
|
1072
|
+
});
|
|
1073
|
+
}, [activeSubmissionSavedView, createSubmissionViewMutation, getSubmissionViewState, id, markSubmissionViewSaved, submissionViewName, updateSubmissionViewMutation, writeSubmissionViewToUrl]);
|
|
1074
|
+
const resetSubmissionView = useCallback(() => {
|
|
1075
|
+
setSubmissionColumns(savedSubmissionColumns);
|
|
1076
|
+
setSubmissionFilters(savedSubmissionFilters);
|
|
1077
|
+
setSubmissionSort(savedSubmissionSort);
|
|
1078
|
+
}, [savedSubmissionColumns, savedSubmissionFilters, savedSubmissionSort]);
|
|
1079
|
+
const submissionWorkbenchViews = useMemo(() => [
|
|
1080
|
+
...BUILT_IN_CAPTURE_SUBMISSION_WORKBENCH_VIEWS,
|
|
1081
|
+
...submissionViews.map((view) => ({ id: view.id, name: view.name, icon: _jsx(Inbox, { className: "h-4 w-4" }) })),
|
|
1082
|
+
], [submissionViews]);
|
|
1083
|
+
const applySubmissionViewState = useCallback((viewId) => {
|
|
1084
|
+
setActiveSubmissionViewId(viewId);
|
|
1085
|
+
const view = submissionViews.find((candidate) => candidate.id === viewId);
|
|
1086
|
+
if (!view) {
|
|
1087
|
+
const builtInFilters = viewId === "with-actions" ? createSubmissionActionStatusFilter() : undefined;
|
|
1088
|
+
setSubmissionFilters(builtInFilters);
|
|
1089
|
+
setSavedSubmissionFilters(builtInFilters);
|
|
1090
|
+
const defaultSort = { field: "submittedAt", direction: "desc" };
|
|
1091
|
+
setSubmissionSort(defaultSort);
|
|
1092
|
+
setSavedSubmissionSort(defaultSort);
|
|
1093
|
+
const defaultColumns = adaptWorkbenchColumns({ fields: submissionFields, persisted: DEFAULT_CAPTURE_SUBMISSION_COLUMNS }).configs;
|
|
1094
|
+
setSubmissionColumns(defaultColumns);
|
|
1095
|
+
setSavedSubmissionColumns(defaultColumns);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
setSubmissionFilters(view.filters ?? undefined);
|
|
1099
|
+
setSavedSubmissionFilters(view.filters ?? undefined);
|
|
1100
|
+
const nextSort = view.sorts?.[0] ?? { field: "submittedAt", direction: "desc" };
|
|
1101
|
+
setSubmissionSort(nextSort);
|
|
1102
|
+
setSavedSubmissionSort(nextSort);
|
|
1103
|
+
const nextColumns = adaptWorkbenchColumns({ fields: submissionFields, persisted: view.columns ?? DEFAULT_CAPTURE_SUBMISSION_COLUMNS }).configs;
|
|
1104
|
+
setSubmissionColumns(nextColumns);
|
|
1105
|
+
setSavedSubmissionColumns(nextColumns);
|
|
1106
|
+
}, [submissionFields, submissionViews]);
|
|
1107
|
+
const applySubmissionView = useCallback((viewId) => {
|
|
1108
|
+
applySubmissionViewState(viewId);
|
|
1109
|
+
writeSubmissionViewToUrl(viewId);
|
|
1110
|
+
}, [applySubmissionViewState, writeSubmissionViewToUrl]);
|
|
1111
|
+
useEffect(() => {
|
|
1112
|
+
if (builderSubView !== "submissions")
|
|
1113
|
+
return;
|
|
1114
|
+
if (!urlSubmissionViewId) {
|
|
1115
|
+
if (activeSubmissionViewId !== "all")
|
|
1116
|
+
applySubmissionViewState("all");
|
|
1117
|
+
writeSubmissionViewToUrl("all", { replace: true });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const nextViewId = urlSubmissionViewId;
|
|
1121
|
+
if (nextViewId === activeSubmissionViewId)
|
|
1122
|
+
return;
|
|
1123
|
+
const knownBuiltIn = BUILT_IN_CAPTURE_SUBMISSION_WORKBENCH_VIEWS.some((view) => view.id === nextViewId);
|
|
1124
|
+
const knownSaved = submissionViews.some((view) => view.id === nextViewId);
|
|
1125
|
+
if (!knownBuiltIn && !knownSaved) {
|
|
1126
|
+
if (!urlSubmissionViewId || submissionViewsQuery.isLoading)
|
|
1127
|
+
return;
|
|
1128
|
+
writeSubmissionViewToUrl("all", { replace: true });
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (nextViewId !== activeSubmissionViewId)
|
|
1132
|
+
applySubmissionViewState(nextViewId);
|
|
1133
|
+
}, [activeSubmissionViewId, applySubmissionViewState, builderSubView, submissionViews, submissionViewsQuery.isLoading, urlSubmissionViewId, writeSubmissionViewToUrl]);
|
|
1134
|
+
const isSaving = updateMutation.isPending;
|
|
1135
|
+
const isPublishing = publishMutation.isPending;
|
|
1136
|
+
const isBuilderActionPending = isSaving || isPublishing;
|
|
167
1137
|
const openDefinition = useCallback((definitionId) => {
|
|
168
1138
|
const definition = definitionsQuery.data?.find((item) => item.id === definitionId);
|
|
169
1139
|
if (definition?.surface === "quiz") {
|
|
@@ -172,77 +1142,660 @@ export function CaptureBuilderPage() {
|
|
|
172
1142
|
}
|
|
173
1143
|
void navigate({ to: sl("/capture/forms/$id/builder"), params: { id: definitionId } });
|
|
174
1144
|
}, [definitionsQuery.data, navigate, sl]);
|
|
175
|
-
|
|
1145
|
+
const builderSaveAction = (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void saveForm(), disabled: isBuilderActionPending, children: [_jsx(Save, { className: "h-4 w-4" }), isSaving ? "Saving…" : "Save"] }), _jsxs(Button, { size: "sm", type: "button", onClick: () => void publishForm(), disabled: isBuilderActionPending, children: [_jsx(Send, { className: "h-4 w-4" }), isPublishing ? "Publishing…" : "Publish"] })] }));
|
|
1146
|
+
const copyToClipboard = useCallback(async (value, label) => {
|
|
1147
|
+
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
|
1148
|
+
toast.error("Clipboard is not available");
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
await navigator.clipboard.writeText(value);
|
|
1153
|
+
toast.success(`${label} copied`);
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
toast.error(`Could not copy ${label.toLowerCase()}`);
|
|
1157
|
+
}
|
|
1158
|
+
}, []);
|
|
1159
|
+
const publicFormAction = publicPath ? (_jsx(Button, { variant: "outline", size: "icon", type: "button", className: "h-9 w-9", asChild: true, children: _jsx("a", { href: publicPath, target: "_blank", rel: "noreferrer", "aria-label": "Preview public form", title: "Preview public form", children: _jsx(Eye, { className: "h-4 w-4" }) }) })) : (_jsx(Button, { variant: "outline", size: "icon", type: "button", className: "h-9 w-9", disabled: true, "aria-label": "Preview public form", title: "Publish this form to enable public preview", children: _jsx(Eye, { className: "h-4 w-4" }) }));
|
|
1160
|
+
const builderHeaderLabel = builderSubView === "submissions" ? "Submissions" : "Share / Embed";
|
|
1161
|
+
const builderHeaderDescription = builderSubView === "build"
|
|
1162
|
+
? "Build the public form canvas. Add fields from the sidebar and inspect details in the right panel."
|
|
1163
|
+
: builderSubView === "submissions"
|
|
1164
|
+
? `Review public responses for ${formName || "this form"}. Detail rows open the existing Capture submission route.`
|
|
1165
|
+
: "Manage public links and embed modes for this form. Publish from the header to enable public access.";
|
|
1166
|
+
const goBackToCaptureItems = useCallback(() => {
|
|
1167
|
+
void navigate({ to: sl("/capture/builder") });
|
|
1168
|
+
}, [navigate, sl]);
|
|
1169
|
+
const builderHeaderTitle = (_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "-ml-1 h-8 w-8 shrink-0 rounded-full text-muted-foreground hover:bg-muted hover:text-foreground", onClick: goBackToCaptureItems, "aria-label": "Back to Capture Items", title: "Back to Capture Items", children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [builderSubView === "build" ? (_jsx(InlineEditableText, { value: formName, onCommit: setFormName, ariaLabel: "Form name", variant: "headerTitle", placeholder: "Untitled form", required: true })) : (_jsx("h1", { className: "text-lg font-bold text-foreground", children: builderHeaderLabel })), _jsx(CaptureStatusBadge, { status: formStatus })] }), _jsx("p", { className: "mt-0.5 truncate text-sm text-muted-foreground", children: builderHeaderDescription })] })] }));
|
|
1170
|
+
const previewDeviceControl = (_jsxs("div", { className: "flex rounded-lg border border-border bg-muted/40 p-0.5", "aria-label": "Preview device width", children: [_jsx(Button, { type: "button", variant: previewDevice === "desktop" ? "secondary" : "ghost", size: "icon", className: "h-8 w-9", onClick: () => setPreviewDevice("desktop"), "aria-label": "Desktop preview", title: "Desktop preview", children: _jsx(Monitor, { className: "h-3.5 w-3.5" }) }), _jsx(Button, { type: "button", variant: previewDevice === "mobile" ? "secondary" : "ghost", size: "icon", className: "h-8 w-9", onClick: () => setPreviewDevice("mobile"), "aria-label": "Mobile preview", title: "Mobile preview", children: _jsx(Smartphone, { className: "h-3.5 w-3.5" }) })] }));
|
|
1171
|
+
const builderHeaderActions = builderSubView === "build" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground", children: [currentPageItems.length, " item", currentPageItems.length === 1 ? "" : "s", " on this page"] }), previewDeviceControl, publicFormAction, _jsx(Button, { variant: "outline", size: "icon", type: "button", className: cn("h-9 w-9", detailsOpen && "bg-muted"), onClick: () => setDetailsOpen((open) => !open), "aria-label": detailsOpen ? "Hide details panel" : "Show details panel", "aria-pressed": detailsOpen, title: detailsOpen ? "Hide details panel" : "Show details panel", children: _jsx(PanelRight, { className: "h-4 w-4" }) }), builderSaveAction] })) : builderSubView === "submissions" ? (_jsxs(_Fragment, { children: [!submissionsQuery.isLoading && !submissionsQuery.isError && (_jsxs("span", { className: "rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground", children: [definitionSubmissions.length, " response", definitionSubmissions.length === 1 ? "" : "s"] })), _jsx(WorkbenchSaveResetControls, { isDirty: isSubmissionViewDirty, isSaving: createSubmissionViewMutation.isPending || updateSubmissionViewMutation.isPending, onSave: saveSubmissionView, onReset: resetSubmissionView }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", title: "Filters", "aria-label": "Filters", onClick: () => setActiveSubmissionPanel("filters"), children: _jsx(Filter, { className: "h-4 w-4" }) }), publicFormAction] })) : (_jsxs(_Fragment, { children: [publicUrl && (_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void copyToClipboard(publicUrl, "Public link"), children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy link"] })), publicFormAction, builderSaveAction] }));
|
|
1172
|
+
return (_jsxs(_Fragment, { children: [_jsx(HeaderPortal, { children: _jsx(ToolPageHeader, { title: id ? builderHeaderTitle : "Capture Items", description: id ? undefined : "Manage forms, quizzes, and other Capture-backed intake surfaces.", actions: id ? builderHeaderActions : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative w-64", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { type: "search", placeholder: "Search capture items...", value: landingSearch, onChange: (event) => setLandingSearch(event.target.value), className: "h-9 pl-9" })] }), _jsx(WorkbenchSaveResetControls, { isDirty: isCaptureItemsViewDirty, onSave: saveCaptureItemsView, onReset: resetCaptureItemsView }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", title: "Filters", "aria-label": "Filters", onClick: () => setActiveWorkbenchPanel("filters"), children: _jsx(Filter, { className: "h-4 w-4" }) }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", title: "Columns", "aria-label": "Columns", onClick: () => setActiveWorkbenchPanel("columns"), children: _jsx(Columns3, { className: "h-4 w-4" }) }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", title: "Save as named view", "aria-label": "Save as named view", onClick: saveCaptureItemsView, children: _jsx(Settings2, { className: "h-4 w-4" }) }), selectedLandingDefinition && (_jsx(Button, { variant: "outline", size: "icon", type: "button", className: "h-9 w-9 bg-muted", onClick: () => setSelectedLandingDefinition(null), "aria-label": "Hide details panel", "aria-pressed": "true", title: "Hide details panel", children: _jsx(PanelRight, { className: "h-4 w-4" }) })), _jsxs(Button, { size: "sm", type: "button", onClick: () => createMutation.mutate({ name: newFormName }), disabled: isCreating, children: [_jsx(Plus, { className: "h-4 w-4" }), isCreating ? "Creating…" : "New Form"] })] })) }) }), _jsx(CaptureSidebar, { currentId: id, fieldTypes: fieldTypes, builderSubView: builderSubView, workbenchViews: workbenchViews, activeWorkbenchViewId: activeWorkbenchViewId, onWorkbenchViewSelect: applyCaptureItemsView, submissionWorkbenchViews: submissionWorkbenchViews, activeSubmissionViewId: activeSubmissionViewId, onSubmissionViewSelect: applySubmissionView, onInsertField: insertField, onInsertTextBlock: insertTextBlock, onInsertIconBlock: insertIconBlock, onInsertImageBlock: insertImageBlock, onInsertDivider: insertDivider, onInsertPageHeader: insertPageHeader, onInsertPageProgress: insertPageProgress, onDragStart: handleDragStart, onContentDragStart: handleContentDragStart }), _jsx("main", { className: cn("flex h-full min-h-0 flex-1 flex-col gap-4 p-4 sm:p-6", builderSubView === "build" && id && "bg-muted/40"), "data-testid": "capture-builder-shell", children: !id ? (_jsx(StartPanel, { definitions: definitionsQuery.data ?? [], isLoading: definitionsQuery.isLoading, isError: definitionsQuery.isError, onRetryDefinitions: () => void definitionsQuery.refetch(), search: landingSearch, selectedDefinition: selectedLandingDefinition, activeDetailTab: activeLandingDetailTab, activeWorkbenchViewId: activeWorkbenchViewId, activeWorkbenchPanel: activeWorkbenchPanel, onActiveWorkbenchPanelChange: setActiveWorkbenchPanel, filters: captureItemsFilters, onFiltersChange: setCaptureItemsFilters, columns: captureItemsColumns, onColumnsChange: setCaptureItemsColumns, sort: captureItemsSort, onSortChange: setCaptureItemsSort, onSelectDefinition: (definition) => {
|
|
176
1173
|
setSelectedLandingDefinition(definition);
|
|
177
1174
|
setActiveLandingDetailTab("overview");
|
|
178
|
-
}, onCloseDetails: () => setSelectedLandingDefinition(null), onDetailTabChange: setActiveLandingDetailTab, onCreate: () => createMutation.mutate({ name: newFormName }), onOpen: openDefinition
|
|
179
|
-
|
|
180
|
-
|
|
1175
|
+
}, onCloseDetails: () => setSelectedLandingDefinition(null), onDetailTabChange: setActiveLandingDetailTab, onCreate: () => createMutation.mutate({ name: newFormName }), isCreating: isCreating, onOpen: openDefinition, onTagsChange: (definitionId, tags) => updateTagsMutation.mutate({ id: definitionId, tags }), workflows: workflowsQuery.data ?? [], workflowsLoading: workflowsQuery.isLoading, workflowsError: workflowsQuery.isError, onRetryWorkflows: () => void workflowsQuery.refetch(), contactCustomFields: (contactCustomFieldsQuery.data ?? []), contactCustomFieldsLoading: contactCustomFieldsQuery.isLoading, contactCustomFieldsError: contactCustomFieldsQuery.isError, onRetryContactCustomFields: () => void contactCustomFieldsQuery.refetch(), onActionConfigChange: (definitionId, actionConfig) => updateActionConfigMutation.mutate({ id: definitionId, actionConfig }), isActionConfigSaving: updateActionConfigMutation.isPending })) : definitionQuery.isLoading ? (_jsx(BuilderWorkspaceSkeleton, {})) : definitionQuery.isError || !definitionQuery.data ? (_jsx(QueryErrorState, { title: "Could not load this form", description: "The form may have moved, been deleted, or failed to load. Try again or return to Capture Items.", onRetry: () => void definitionQuery.refetch() })) : (_jsx(_Fragment, { children: builderSubView === "build" ? (_jsxs("div", { className: "flex min-h-0 flex-1 gap-4", children: [_jsx("section", { className: "min-h-0 min-w-0 flex-1 overflow-auto p-4", children: _jsxs("div", { className: "flex min-h-[560px] items-center justify-center gap-3", children: [_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 rounded-full bg-background/90 shadow-sm", onClick: goToPreviousPage, disabled: currentPageIndex === 0, "aria-label": "Previous page", title: "Previous page", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx("div", { className: cn("flex min-h-[560px] flex-1 flex-col rounded-[2rem] border border-border bg-card p-4 shadow-sm transition", previewDevice === "mobile" ? "max-w-sm" : "max-w-3xl"), onDragEnter: currentPageItems.length === 0 ? handleDragOver : undefined, onDragOver: currentPageItems.length === 0 ? handleDragOver : undefined, onDrop: currentPageItems.length === 0 ? handleDrop : undefined, "data-testid": "capture-builder-canvas", children: _jsx(CaptureFormSurface, { title: formName || "Untitled form", pages: sortedPages, items: formDocument.items, currentPageId: currentPage?.id ?? currentPageId, fieldTypes: fieldTypes, mode: "builder", selectedItemId: selectedId, selectedItemIds: selectedItemIds, className: "flex min-h-full flex-1 flex-col gap-4 space-y-0", onSelectItem: handleCanvasItemSelect, onReorderItem: reorderItems, paletteDragTypes: [CAPTURE_BUILDER_DRAG_TYPE, CAPTURE_BUILDER_CONTENT_DRAG_TYPE], onPaletteDrop: insertPaletteItemAt, emptyContent: (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center rounded-xl border border-dashed border-border bg-muted/25 p-8 text-center", children: [_jsx(Plus, { className: "mb-3 h-8 w-8 text-muted-foreground" }), _jsx("h4", { className: "text-sm font-semibold text-foreground", children: "Add the first item to this page" }), _jsxs("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: ["Use the sidebar palette to add fields or explainer text to ", currentPage?.title ?? "the selected page", "."] })] })), footer: sortedPages.length > 1 ? (_jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs(Button, { type: "button", variant: "outline", className: "h-11 rounded-xl sm:w-auto", disabled: currentPageIndex === 0, onClick: goToPreviousPage, children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back"] }), _jsxs(Button, { type: "button", className: "h-11 rounded-xl sm:w-auto", disabled: currentPageIndex >= sortedPages.length - 1, onClick: goToNextPage, children: ["Next", _jsx(ArrowRight, { className: "h-4 w-4" })] })] })) : null }) }), _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 rounded-full bg-background/90 shadow-sm", onClick: currentPageIndex >= sortedPages.length - 1 ? addPage : goToNextPage, "aria-label": currentPageIndex >= sortedPages.length - 1 ? "Add page" : "Next page", title: currentPageIndex >= sortedPages.length - 1 ? "Add page" : "Next page", children: currentPageIndex >= sortedPages.length - 1 ? _jsx(Plus, { className: "h-4 w-4" }) : _jsx(ChevronRight, { className: "h-4 w-4" }) })] }) }), _jsx(TabbedPanel, { open: detailsOpen, onClose: () => setDetailsOpen(false), tabs: formPanelTabs, activeTab: activePanel, onTabChange: setActivePanel, width: "380px", inline: true, flush: true, className: "self-stretch rounded-2xl border shadow-sm", header: _jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: detailsPanelTitle }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: activePanel === "validation"
|
|
1176
|
+
? "When the validation expression matches, respondents are blocked and see the failure message."
|
|
1177
|
+
: "Builder context and selected-field controls." })] }), footer: selectedItemDeleteLabel ? (_jsx("div", { className: "border-t border-border p-3", children: _jsx(DeleteSelectedItemButton, { onDelete: deleteSelectedItem, label: selectedItemDeleteLabel }) })) : null, children: _jsx(FormDetailsPanel, { activePanel: activePanel, pages: sortedPages, items: formDocument.items, currentPage: currentPage, selectedPage: selectedPage, fieldTypes: fieldTypes, selectedField: selectedField, selectedTextBlock: selectedTextBlock, selectedIconBlock: selectedIconBlock, selectedImageBlock: selectedImageBlock, selectedDivider: selectedDivider, selectedPageHeader: selectedPageHeader, selectedPageProgress: selectedPageProgress, selectedId: selectedId, selectedItemIds: selectedItemIds, savedFieldIds: savedFieldIds, selectedFieldKeyDuplicate: selectedFieldKeyDuplicate, onSelectItem: handleCanvasItemSelect, onOpenInspector: (itemId) => { setSingleSelectedItem(itemId); setActivePanel("inspector"); setDetailsOpen(true); }, onSelectPage: selectPageForInspection, updateSelectedField: updateSelectedField, updateSelectedTextBlock: updateSelectedTextBlock, updateSelectedIconBlock: updateSelectedIconBlock, updateSelectedImageBlock: updateSelectedImageBlock, updateSelectedDivider: updateSelectedDivider, updateSelectedPageHeader: updateSelectedPageHeader, updateSelectedPageProgress: updateSelectedPageProgress, updateCurrentPage: updateCurrentPage, deleteCurrentPage: deleteCurrentPage }) })] })) : builderSubView === "submissions" ? (_jsx(BuilderSubmissionsView, { submissions: definitionSubmissions, fields: submissionFields, columns: submissionColumns, filters: submissionFilters, sort: submissionSort, onFiltersChange: setSubmissionFilters, onSortChange: setSubmissionSort, onOpenColumns: () => setActiveSubmissionPanel("columns"), isLoading: submissionsQuery.isLoading, isError: submissionsQuery.isError, onRetry: () => void submissionsQuery.refetch() })) : (_jsx(BuilderShareView, { publicationId: publicationId, publicationSlug: publicationSlug, onCopy: copyToClipboard })) })) }), _jsx(TabbedPanel, { open: builderSubView === "submissions" && activeSubmissionPanel != null, onClose: () => setActiveSubmissionPanel(null), tabs: [
|
|
1178
|
+
{ id: "details", icon: Settings2, label: "View details" },
|
|
1179
|
+
{ id: "filters", icon: Filter, label: "Filters" },
|
|
1180
|
+
{ id: "columns", icon: Columns3, label: "Columns" },
|
|
1181
|
+
], activeTab: activeSubmissionPanel ?? "filters", onTabChange: (tabId) => setActiveSubmissionPanel(tabId), width: "420px", header: (_jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: "Submission view" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Name, save, and adjust this form\u2019s submission view." })] })), footer: activeSubmissionPanel === "details" ? (_jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border px-4 py-3", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setActiveSubmissionPanel(null), disabled: createSubmissionViewMutation.isPending || updateSubmissionViewMutation.isPending, children: "Cancel" }), _jsxs(Button, { type: "button", onClick: saveSubmissionViewDetails, disabled: createSubmissionViewMutation.isPending || updateSubmissionViewMutation.isPending, children: [_jsx(Save, { className: "h-4 w-4" }), createSubmissionViewMutation.isPending || updateSubmissionViewMutation.isPending ? "Saving…" : activeSubmissionSavedView ? "Save view" : "Create view"] })] })) : undefined, children: activeSubmissionPanel === "details" ? (_jsx(SubmissionViewDetailsPanel, { name: submissionViewName, error: submissionViewNameError, isExisting: Boolean(activeSubmissionSavedView), isDirty: isSubmissionViewDirty, onNameChange: (name) => { setSubmissionViewName(name); setSubmissionViewNameError(null); } })) : activeSubmissionPanel === "columns" ? (_jsx(WorkbenchColumnPanel, { columns: submissionColumns, onColumnsChange: setSubmissionColumns })) : (_jsx(WorkbenchFilterPanel, { filters: submissionFilters, onFiltersChange: setSubmissionFilters, fields: fieldsToFilterFields(submissionFields), operatorsByFieldType: CAPTURE_SUBMISSION_FILTER_OPERATORS, resultCount: filterSubmissions(definitionSubmissions, submissionFilters).length })) }), builderSubView === "build" && selectedItems.length > 1 && (_jsx(CaptureBuilderBulkActionBar, { selectedCount: selectedItems.length, selectedFields: selectedInputItems, onDeselectAll: () => setSingleSelectedItem(null), onSetLabelVisible: (labelVisible) => updateSelectedInputLayout({ labelVisible }), onSetWidth: (width) => updateSelectedInputLayout({ width }), onSetWrapAfter: (wrapAfter) => updateSelectedInputLayout({ wrapAfter }), onSetRequired: updateSelectedInputRequired, onDelete: deleteSelectedItems }))] }));
|
|
181
1182
|
}
|
|
182
|
-
function
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
1183
|
+
function CaptureBuilderBulkActionBar({ selectedCount, selectedFields, onDeselectAll, onSetLabelVisible, onSetWidth, onSetWrapAfter, onSetRequired, onDelete, }) {
|
|
1184
|
+
const fieldLayouts = selectedFields.map(getFieldLayout);
|
|
1185
|
+
const allLabelsVisible = selectedFields.length > 0 && fieldLayouts.every((layout) => layout.labelVisible);
|
|
1186
|
+
const allWrapAfter = selectedFields.length > 0 && fieldLayouts.every((layout) => layout.wrapAfter);
|
|
1187
|
+
const allRequired = selectedFields.length > 0 && selectedFields.every((field) => field.required);
|
|
1188
|
+
return (_jsxs(BulkActionBar, { selectedCount: selectedCount, onDeselectAll: onDeselectAll, children: [selectedFields.length > 0 && (_jsxs(_Fragment, { children: [_jsx(BulkActionButton, { onClick: () => onSetLabelVisible(!allLabelsVisible), children: allLabelsVisible ? "Hide labels" : "Show labels" }), _jsx("div", { className: "flex items-center gap-1 rounded-lg bg-zinc-950/40 p-1 dark:bg-zinc-900/50", "aria-label": "Set field width", children: [
|
|
1189
|
+
["1/3", "⅓"],
|
|
1190
|
+
["1/2", "½"],
|
|
1191
|
+
["full", "Full"],
|
|
1192
|
+
].map(([width, label]) => (_jsx(BulkActionButton, { className: "px-2 py-1", onClick: () => onSetWidth(width), children: label }, width))) }), _jsx(BulkActionButton, { onClick: () => onSetWrapAfter(!allWrapAfter), children: allWrapAfter ? "Unwrap" : "Wrap" }), _jsx(BulkActionButton, { onClick: () => onSetRequired(!allRequired), children: allRequired ? "Optional" : "Required" })] })), _jsx(BulkActionButton, { variant: "destructive", onClick: onDelete, "aria-label": "Delete selected", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }));
|
|
1193
|
+
}
|
|
1194
|
+
function FormDetailsPanel({ activePanel, pages, items, selectedPage, fieldTypes, selectedField, selectedTextBlock, selectedIconBlock, selectedImageBlock, selectedDivider, selectedPageHeader, selectedPageProgress, selectedId, selectedItemIds, savedFieldIds, selectedFieldKeyDuplicate, onSelectItem, onOpenInspector, onSelectPage, updateSelectedField, updateSelectedTextBlock, updateSelectedIconBlock, updateSelectedImageBlock, updateSelectedDivider, updateSelectedPageHeader, updateSelectedPageProgress, updateCurrentPage, deleteCurrentPage, }) {
|
|
1195
|
+
const [confirmDeletePageOpen, setConfirmDeletePageOpen] = useState(false);
|
|
1196
|
+
const selectedVisibilityTarget = selectedPage ?? selectedField ?? selectedTextBlock ?? selectedIconBlock ?? selectedImageBlock ?? selectedDivider ?? selectedPageHeader ?? selectedPageProgress;
|
|
1197
|
+
const updateStaticVisibility = (hidden) => {
|
|
1198
|
+
const patch = { hidden: hidden || undefined, ...(hidden ? { visibleWhen: undefined } : {}) };
|
|
1199
|
+
if (selectedPage)
|
|
1200
|
+
updateCurrentPage(patch);
|
|
1201
|
+
else if (selectedField)
|
|
1202
|
+
updateSelectedField(patch);
|
|
1203
|
+
else if (selectedTextBlock)
|
|
1204
|
+
updateSelectedTextBlock(patch);
|
|
1205
|
+
else if (selectedIconBlock)
|
|
1206
|
+
updateSelectedIconBlock(patch);
|
|
1207
|
+
else if (selectedImageBlock)
|
|
1208
|
+
updateSelectedImageBlock(patch);
|
|
1209
|
+
else if (selectedDivider)
|
|
1210
|
+
updateSelectedDivider(patch);
|
|
1211
|
+
else if (selectedPageHeader)
|
|
1212
|
+
updateSelectedPageHeader(patch);
|
|
1213
|
+
else if (selectedPageProgress)
|
|
1214
|
+
updateSelectedPageProgress(patch);
|
|
1215
|
+
};
|
|
1216
|
+
const staticVisibilityControl = selectedVisibilityTarget ? (_jsx(StaticVisibilityControl, { hidden: Boolean(selectedVisibilityTarget.hidden), onHiddenChange: updateStaticVisibility })) : null;
|
|
195
1217
|
if (activePanel === "outline") {
|
|
196
|
-
return (_jsx("div", { className: "p-3", children:
|
|
197
|
-
|
|
198
|
-
|
|
1218
|
+
return (_jsx("div", { className: "p-3", children: pages.length === 0 ? _jsx(EmptyPanelText, { children: "No pages yet." }) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between px-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Pages" }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: [items.length, " total item", items.length === 1 ? "" : "s"] })] }), _jsx("div", { className: "space-y-3", children: pages.map((page, pageIndex) => {
|
|
1219
|
+
const pageItems = items
|
|
1220
|
+
.filter((item) => item.pageId === page.id)
|
|
1221
|
+
.sort((a, b) => a.order - b.order);
|
|
1222
|
+
const pageSelected = selectedId === `page:${page.id}`;
|
|
1223
|
+
return (_jsxs("section", { children: [_jsxs("button", { type: "button", onClick: () => onSelectPage(page.id), className: cn("group flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left transition", pageSelected
|
|
1224
|
+
? "bg-primary/10 text-foreground"
|
|
1225
|
+
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground"), children: [_jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", children: page.title || `Page ${pageIndex + 1}` }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: [pageItems.length, " item", pageItems.length === 1 ? "" : "s"] })] }), pageItems.length > 0 ? (_jsx("div", { className: "mt-1 space-y-0.5 pl-3", children: pageItems.map((item) => (_jsxs("button", { type: "button", onClick: (event) => onSelectItem(item, event), onDoubleClick: () => onOpenInspector(item.id), className: cn("group/item flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition", selectedItemIds.has(item.id)
|
|
1226
|
+
? "bg-muted text-foreground"
|
|
1227
|
+
: "text-muted-foreground hover:bg-muted/60 hover:text-foreground"), children: [_jsx("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/40 group-hover/item:bg-muted-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: item.kind === "content.text" ? (item.title || "Explainer text") : item.kind === "content.icon" ? "Icon" : item.kind === "content.image" ? "Image" : item.kind === "content.divider" ? "Divider" : item.kind === "content.page-header" ? "Page header" : item.kind === "content.page-progress" ? "Page progress" : item.kind === "input" ? item.label : "Content" }), _jsx("span", { className: "text-[10px] text-muted-foreground group-hover/item:text-foreground", children: item.kind === "content.text" ? "Text" : item.kind === "content.icon" ? "Icon" : item.kind === "content.image" ? "Image" : item.kind === "content.divider" ? "Divider" : item.kind === "content.page-header" ? "Header" : item.kind === "content.page-progress" ? "Progress" : item.kind === "input" ? getCaptureFieldType(item.type, fieldTypes).label : "Content" })] }, item.id))) })) : (_jsx("p", { className: "px-2 py-1 pl-5 text-xs text-muted-foreground", children: "No items yet." }))] }, page.id));
|
|
1228
|
+
}) })] })) }));
|
|
199
1229
|
}
|
|
200
1230
|
if (activePanel === "inspector") {
|
|
201
|
-
return (_jsx("div", { className: "p-3", children:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1231
|
+
return (_jsx("div", { className: "h-full p-3", children: selectedPage ? (_jsxs("div", { className: "flex h-full flex-col gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Page details" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "This label identifies the page in the builder, progress indicators, and conditional visibility groups. Header copy lives on Page header content blocks." })] }), staticVisibilityControl, _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "page-title", children: "Page label" }), _jsx(Input, { id: "page-title", value: selectedPage.title, placeholder: "Page label", onChange: (event) => updateCurrentPage({ title: event.target.value || "Untitled page" }) })] }), _jsx("div", { className: "mt-auto rounded-lg border border-destructive/20 bg-destructive/5 p-3", children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "Delete page" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Removes this page and all items on it." })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "shrink-0 border-destructive/30 text-destructive hover:bg-destructive/10", disabled: pages.length <= 1, onClick: () => setConfirmDeletePageOpen(true), children: [_jsx(Trash2, { className: "mr-1.5 h-3.5 w-3.5" }), "Delete"] })] }) }), _jsx(AlertDialog, { open: confirmDeletePageOpen, onOpenChange: setConfirmDeletePageOpen, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { children: ["Delete \u201C", selectedPage.title || "Untitled page", "\u201D?"] }), _jsxs(AlertDialogDescription, { children: ["This will permanently remove the page and ", items.filter((item) => item.pageId === selectedPage.id).length, " item", items.filter((item) => item.pageId === selectedPage.id).length === 1 ? "" : "s", " on it. This cannot be undone after you save the form."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Cancel" }), _jsx(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: () => {
|
|
1232
|
+
deleteCurrentPage();
|
|
1233
|
+
setConfirmDeletePageOpen(false);
|
|
1234
|
+
}, children: "Delete page" })] })] }) })] })) : selectedTextBlock ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "rounded-lg border border-primary/20 bg-primary/5 p-3", children: [_jsxs("p", { className: "flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-primary", children: [_jsx(FileText, { className: "h-3.5 w-3.5" }), " Explainer text"] }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Content blocks render in the form body but are ignored by answer validation and actions." })] }), staticVisibilityControl, _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "text-block-title", children: "Title" }), _jsx(Input, { id: "text-block-title", value: selectedTextBlock.title ?? "", placeholder: "Optional heading", onChange: (event) => updateSelectedTextBlock({ title: event.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "text-block-body", children: "Body" }), _jsx(Textarea, { id: "text-block-body", value: selectedTextBlock.body, placeholder: "Add explanatory copy for respondents.", rows: 6, onChange: (event) => updateSelectedTextBlock({ body: event.target.value }) })] })] })) : selectedField ? (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Label, { htmlFor: "field-label", children: "Label" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Show" }), _jsx(Switch, { checked: getFieldLayout(selectedField).labelVisible, onCheckedChange: (checked) => updateSelectedField({
|
|
1235
|
+
metadata: {
|
|
1236
|
+
...(selectedField.metadata ?? {}),
|
|
1237
|
+
layout: {
|
|
1238
|
+
...getFieldLayout(selectedField),
|
|
1239
|
+
labelVisible: checked,
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
}) })] })] }), _jsx(Input, { id: "field-label", value: selectedField.label, onChange: (event) => updateSelectedField({ label: event.target.value }) })] }), _jsx(FieldLayoutControls, { field: selectedField, onChange: (patch) => updateSelectedField({
|
|
1243
|
+
metadata: {
|
|
1244
|
+
...(selectedField.metadata ?? {}),
|
|
1245
|
+
layout: {
|
|
1246
|
+
...getFieldLayout(selectedField),
|
|
1247
|
+
...patch,
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
}) }), supportsPlaceholder(selectedField) && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "field-placeholder", children: "Placeholder" }), _jsx(Input, { id: "field-placeholder", value: getFieldPlaceholder(selectedField), placeholder: "Sample answer shown before typing", onChange: (event) => {
|
|
1251
|
+
const value = event.target.value;
|
|
1252
|
+
updateSelectedField({
|
|
1253
|
+
metadata: {
|
|
1254
|
+
...(selectedField.metadata ?? {}),
|
|
1255
|
+
...(value.trim() ? { placeholder: value } : { placeholder: undefined }),
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
} }, selectedField.id), _jsx("p", { className: "text-xs text-muted-foreground", children: "Shown in the respondent preview and public form inputs." })] })), selectedField.type === "rating" && (_jsx(RatingFieldControls, { field: selectedField, onChange: (patch) => updateSelectedField({
|
|
1259
|
+
metadata: {
|
|
1260
|
+
...(selectedField.metadata ?? {}),
|
|
1261
|
+
...patch,
|
|
1262
|
+
},
|
|
1263
|
+
}) })), _jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [staticVisibilityControl, _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Required" }), _jsx(Switch, { checked: selectedField.required, onCheckedChange: (checked) => updateSelectedField({ required: checked }) })] })] }), selectedField.type === "select" && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "field-options", children: "Options, one per line" }), _jsx(Textarea, { id: "field-options", value: (selectedField.options ?? []).join("\n"), onChange: (event) => updateSelectedField({
|
|
214
1264
|
options: event.target.value.split("\n").map((option) => option.trim()).filter(Boolean),
|
|
215
|
-
}) })] }))] })) : _jsx(EmptyPanelText, { children: "Select a field to edit its basics." }) }));
|
|
1265
|
+
}) })] })), _jsx(FieldKeyControl, { field: selectedField, locked: savedFieldIds.has(selectedField.id), duplicate: selectedFieldKeyDuplicate, onChange: (key) => updateSelectedField({ key }) })] })) : selectedIconBlock ? (_jsxs("div", { className: "space-y-4", children: [staticVisibilityControl, _jsx(IconBlockControls, { item: selectedIconBlock, onChange: updateSelectedIconBlock })] })) : selectedImageBlock ? (_jsxs("div", { className: "space-y-4", children: [staticVisibilityControl, _jsx(ImageBlockControls, { item: selectedImageBlock, onChange: updateSelectedImageBlock })] })) : selectedDivider ? (_jsxs("div", { className: "space-y-3", children: [staticVisibilityControl, _jsxs("div", { className: "space-y-2", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Divider" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "A visual divider between form items. Drag it on the canvas to reorder it." })] })] })) : selectedPageHeader ? (_jsxs("div", { className: "space-y-3", children: [staticVisibilityControl, _jsxs("div", { className: "space-y-2", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Page header" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Shows heading copy in the form body. Each header block owns its own text, so multiple headers on one page can differ." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "page-header-pre-heading", children: "Pre-heading" }), _jsx(Input, { id: "page-header-pre-heading", value: selectedPageHeader.preHeading ?? "", placeholder: "Capture Form", onChange: (event) => updateSelectedPageHeader({ preHeading: event.target.value || undefined }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "page-header-title", children: "Title" }), _jsx(Input, { id: "page-header-title", value: selectedPageHeader.title, placeholder: "Header title", onChange: (event) => updateSelectedPageHeader({ title: event.target.value || "Untitled page" }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "page-header-description", children: "Description" }), _jsx(Textarea, { id: "page-header-description", value: selectedPageHeader.description ?? "", placeholder: "Optional page intro shown in the form.", rows: 4, onChange: (event) => updateSelectedPageHeader({ description: event.target.value || undefined }) })] })] })) : selectedPageProgress ? (_jsxs("div", { className: "space-y-3", children: [staticVisibilityControl, _jsxs("div", { className: "space-y-2", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Page progress" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Shows the respondent which page they are on. Drag it on the canvas to move it, or delete it if this page should not show progress." })] })] })) : _jsx(EmptyPanelText, { children: "Select a page, field, or content block to edit its basics." }) }));
|
|
216
1266
|
}
|
|
217
|
-
if (activePanel === "
|
|
218
|
-
|
|
1267
|
+
if (activePanel === "conditions") {
|
|
1268
|
+
const selectedConditionItem = selectedPage ?? selectedField ?? selectedTextBlock ?? selectedIconBlock ?? selectedImageBlock ?? selectedDivider ?? selectedPageHeader ?? selectedPageProgress;
|
|
1269
|
+
const selectedPageOrder = selectedPage
|
|
1270
|
+
? selectedPage.order
|
|
1271
|
+
: selectedConditionItem && "pageId" in selectedConditionItem
|
|
1272
|
+
? pages.find((page) => page.id === selectedConditionItem.pageId)?.order ?? 0
|
|
1273
|
+
: 0;
|
|
1274
|
+
const conditionFields = items
|
|
1275
|
+
.filter((item) => item.kind === "input")
|
|
1276
|
+
.filter((item) => item.id !== selectedConditionItem?.id)
|
|
1277
|
+
.filter((item) => (pages.find((page) => page.id === item.pageId)?.order ?? 0) <= selectedPageOrder)
|
|
1278
|
+
.sort((a, b) => {
|
|
1279
|
+
const pageDiff = (pages.find((page) => page.id === a.pageId)?.order ?? 0) - (pages.find((page) => page.id === b.pageId)?.order ?? 0);
|
|
1280
|
+
return pageDiff || a.order - b.order;
|
|
1281
|
+
})
|
|
1282
|
+
.map((field) => {
|
|
1283
|
+
const contribution = fieldTypes.find((type) => type.id === field.type) ?? getCaptureFieldType(field.type);
|
|
1284
|
+
const renderer = contribution?.renderer ?? "text";
|
|
1285
|
+
const page = pages.find((candidate) => candidate.id === field.pageId);
|
|
1286
|
+
return {
|
|
1287
|
+
key: field.key,
|
|
1288
|
+
label: field.label,
|
|
1289
|
+
type: renderer === "rating" ? "rating" : renderer === "select" ? "select" : renderer,
|
|
1290
|
+
group: page?.title ?? "Prior page",
|
|
1291
|
+
options: field.options?.map((option) => ({ value: option, label: option })),
|
|
1292
|
+
};
|
|
1293
|
+
});
|
|
1294
|
+
const filters = toConditionGroup(selectedConditionItem?.visibleWhen);
|
|
1295
|
+
const updateSelectedCondition = (visibleWhen) => {
|
|
1296
|
+
if (selectedPage)
|
|
1297
|
+
updateCurrentPage({ visibleWhen });
|
|
1298
|
+
else if (selectedField)
|
|
1299
|
+
updateSelectedField({ visibleWhen });
|
|
1300
|
+
else if (selectedTextBlock)
|
|
1301
|
+
updateSelectedTextBlock({ visibleWhen });
|
|
1302
|
+
else if (selectedIconBlock)
|
|
1303
|
+
updateSelectedIconBlock({ visibleWhen });
|
|
1304
|
+
else if (selectedImageBlock)
|
|
1305
|
+
updateSelectedImageBlock({ visibleWhen });
|
|
1306
|
+
else if (selectedDivider)
|
|
1307
|
+
updateSelectedDivider({ visibleWhen });
|
|
1308
|
+
else if (selectedPageHeader)
|
|
1309
|
+
updateSelectedPageHeader({ visibleWhen });
|
|
1310
|
+
else if (selectedPageProgress)
|
|
1311
|
+
updateSelectedPageProgress({ visibleWhen });
|
|
1312
|
+
};
|
|
1313
|
+
return (_jsx("div", { className: "space-y-4 p-3", children: selectedConditionItem ? (_jsx("div", { className: "space-y-3", children: conditionFields.length === 0 ? (_jsx("p", { className: "rounded-lg border border-dashed border-border bg-muted/20 p-4 text-sm text-muted-foreground", children: "Add a field on this page or an earlier page before creating a show condition." })) : (_jsx("div", { className: "[&>div>div:first-child]:hidden", children: _jsx(FilterPanel, { filters: filters, onFiltersChange: (nextFilters) => updateSelectedCondition(nextFilters ?? undefined), fields: conditionFields, operatorsByFieldType: CAPTURE_CONDITION_OPERATORS, constrainWorkspaceHeight: false, labels: {
|
|
1314
|
+
title: "Conditional Visibility",
|
|
1315
|
+
helpText: "Use answers from this page or earlier pages to decide whether this item appears.",
|
|
1316
|
+
emptyText: "Use answers from this page or earlier pages to decide whether this item appears.",
|
|
1317
|
+
addFilter: "Add condition",
|
|
1318
|
+
addGroup: "Add group",
|
|
1319
|
+
addNestedFilter: "add condition",
|
|
1320
|
+
clearAllTitle: "Clear all conditions",
|
|
1321
|
+
} }) })) })) : _jsx(EmptyPanelText, { children: "Select a page, field, or content block to configure its show condition." }) }));
|
|
219
1322
|
}
|
|
220
|
-
if (activePanel === "
|
|
221
|
-
|
|
1323
|
+
if (activePanel === "validation") {
|
|
1324
|
+
const selectedPageOrder = selectedField
|
|
1325
|
+
? pages.find((page) => page.id === selectedField.pageId)?.order ?? 0
|
|
1326
|
+
: 0;
|
|
1327
|
+
const validationFields = items
|
|
1328
|
+
.filter((item) => item.kind === "input")
|
|
1329
|
+
.filter((item) => (pages.find((page) => page.id === item.pageId)?.order ?? 0) <= selectedPageOrder)
|
|
1330
|
+
.sort((a, b) => {
|
|
1331
|
+
const pageDiff = (pages.find((page) => page.id === a.pageId)?.order ?? 0) - (pages.find((page) => page.id === b.pageId)?.order ?? 0);
|
|
1332
|
+
return pageDiff || a.order - b.order;
|
|
1333
|
+
})
|
|
1334
|
+
.map((field) => {
|
|
1335
|
+
const contribution = fieldTypes.find((type) => type.id === field.type) ?? getCaptureFieldType(field.type);
|
|
1336
|
+
const renderer = contribution?.renderer ?? "text";
|
|
1337
|
+
const page = pages.find((candidate) => candidate.id === field.pageId);
|
|
1338
|
+
return {
|
|
1339
|
+
key: field.key,
|
|
1340
|
+
label: field.id === selectedField?.id ? `${field.label} (this field)` : field.label,
|
|
1341
|
+
type: renderer === "rating" ? "rating" : renderer === "select" ? "select" : renderer,
|
|
1342
|
+
group: page?.title ?? "Prior page",
|
|
1343
|
+
options: field.options?.map((option) => ({ value: option, label: option })),
|
|
1344
|
+
};
|
|
1345
|
+
});
|
|
1346
|
+
const rule = selectedField?.validationRules?.[0];
|
|
1347
|
+
const filters = rule?.expression;
|
|
1348
|
+
const updateValidation = (nextFilters, message = rule?.message) => {
|
|
1349
|
+
if (!selectedField)
|
|
1350
|
+
return;
|
|
1351
|
+
if (!nextFilters) {
|
|
1352
|
+
updateSelectedField({ validationRules: [] });
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
updateSelectedField({
|
|
1356
|
+
validationRules: [{
|
|
1357
|
+
id: rule?.id ?? crypto.randomUUID().slice(0, 12),
|
|
1358
|
+
message: message || `${selectedField.label} is invalid`,
|
|
1359
|
+
expression: nextFilters,
|
|
1360
|
+
}],
|
|
1361
|
+
});
|
|
1362
|
+
};
|
|
1363
|
+
return (_jsx("div", { className: "space-y-4 p-3", children: selectedField ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `validation-message-${selectedField.id}`, children: "Failure message" }), _jsx(Input, { id: `validation-message-${selectedField.id}`, value: rule?.message ?? `${selectedField.label} is invalid`, onChange: (event) => {
|
|
1364
|
+
const nextMessage = event.target.value || `${selectedField.label} is invalid`;
|
|
1365
|
+
updateValidation((filters ?? createDefaultValidationRule(selectedField).expression), nextMessage);
|
|
1366
|
+
} })] }), _jsx("div", { className: "[&>div>div:first-child]:hidden", children: _jsx(FilterPanel, { filters: filters, onFiltersChange: (nextFilters) => updateValidation(nextFilters), fields: validationFields, operatorsByFieldType: CAPTURE_CONDITION_OPERATORS, constrainWorkspaceHeight: false, labels: {
|
|
1367
|
+
title: "Validation",
|
|
1368
|
+
helpText: "Add conditions that should fail this field when they match.",
|
|
1369
|
+
emptyText: "Add conditions that should fail this field when they match.",
|
|
1370
|
+
addFilter: "Add condition",
|
|
1371
|
+
addGroup: "Add group",
|
|
1372
|
+
addNestedFilter: "add condition",
|
|
1373
|
+
clearAllTitle: "Clear validation",
|
|
1374
|
+
} }) })] })) : _jsx(EmptyPanelText, { children: "Select a field to configure validation rules." }) }));
|
|
222
1375
|
}
|
|
223
|
-
return
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
function SubmissionViewDetailsPanel({ name, error, isExisting, isDirty, onNameChange, }) {
|
|
1379
|
+
return (_jsxs("div", { className: "space-y-5 p-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-submission-view-name", children: "View name" }), _jsx(Input, { id: "capture-submission-view-name", value: name, onChange: (event) => onNameChange(event.target.value), placeholder: "New submission view", "aria-invalid": Boolean(error) }), error ? (_jsx("p", { className: "text-xs font-medium text-destructive", children: error })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: isExisting ? "Rename this saved view or save current filter, sort, and column changes." : "Create a named saved view from the current filter, sort, and column setup." }))] }), _jsxs("div", { className: "rounded-lg border border-border bg-muted/30 p-3 text-xs text-muted-foreground", children: [_jsx("p", { className: "font-medium text-foreground", children: "Current view state" }), _jsx("p", { className: "mt-1", children: isDirty ? "This view has unsaved workbench changes." : "Current workbench settings match the saved view." })] })] }));
|
|
1380
|
+
}
|
|
1381
|
+
function BuilderSubmissionsView({ submissions, fields, columns, filters, sort, onFiltersChange: _onFiltersChange, onSortChange, onOpenColumns, isLoading, isError, onRetry, }) {
|
|
1382
|
+
const navigate = useNavigate();
|
|
1383
|
+
const sl = useScopeLink();
|
|
1384
|
+
const filteredSubmissions = useMemo(() => filterSubmissions(submissions, filters).sort((a, b) => compareSubmissions(a, b, sort)), [filters, sort, submissions]);
|
|
1385
|
+
const pagination = useWorkbenchOffsetPagination({
|
|
1386
|
+
pageSize: 25,
|
|
1387
|
+
total: filteredSubmissions.length,
|
|
1388
|
+
resetKeys: [filters, sort, columns],
|
|
1389
|
+
});
|
|
1390
|
+
const pageSubmissions = filteredSubmissions.slice(pagination.offset, pagination.offset + pagination.pageSize);
|
|
1391
|
+
const tableColumns = useMemo(() => [
|
|
1392
|
+
...columns,
|
|
1393
|
+
{ fieldKey: "__spacer", label: "", visible: true, position: Number.MAX_SAFE_INTEGER - 1, unavailable: true },
|
|
1394
|
+
{ fieldKey: "__manage", label: "", visible: true, position: Number.MAX_SAFE_INTEGER, unavailable: true },
|
|
1395
|
+
], [columns]);
|
|
1396
|
+
const renderCell = (submission, column) => renderSubmissionCell(submission, column);
|
|
1397
|
+
const renderHeader = (column) => column.fieldKey === "__spacer" ? null : column.fieldKey === "__manage" ? (_jsx("button", { type: "button", onClick: (event) => { event.stopPropagation(); onOpenColumns(); }, className: "flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", "aria-label": "Add columns", title: "Add columns", children: _jsx(Plus, { className: "h-4 w-4" }) })) : column.label;
|
|
1398
|
+
return (_jsx("section", { className: "min-h-0 flex-1 overflow-auto", children: isError ? (_jsx(QueryErrorState, { title: "Could not load form submissions", description: "Responses for this form could not be loaded. Try again to refresh the submissions list.", onRetry: onRetry })) : isLoading ? (_jsx(BuilderSubmissionsSkeleton, {})) : submissions.length === 0 ? (_jsx(BuilderSubmissionsEmptyState, {})) : (_jsx(WorkbenchTable, { data: pageSubmissions, columns: tableColumns, getRowId: (submission) => submission.id, renderCell: renderCell, renderHeader: renderHeader, isColumnSortable: (column) => !column.fieldKey.startsWith("__") && fields.some((field) => field.key === column.fieldKey && field.sortable !== false), getColumnClassName: (column) => column.fieldKey === "submittedAt" ? "w-[220px]" : column.fieldKey === "__spacer" ? "w-full px-0" : column.fieldKey === "__manage" ? "w-10 px-3" : "w-[180px]", onRowClick: (submission) => void navigate({ to: sl("/capture/submissions/$id"), params: { id: submission.id } }), sorts: [sort], onSortChange: (nextSorts) => {
|
|
1399
|
+
const next = nextSorts[0];
|
|
1400
|
+
if (next && !next.field.startsWith("__"))
|
|
1401
|
+
onSortChange({ field: next.field, direction: next.direction });
|
|
1402
|
+
}, total: filteredSubmissions.length, page: pagination.page, pageSize: pagination.pageSize, onPageChange: pagination.setPage, className: "h-full" })) }));
|
|
224
1403
|
}
|
|
225
|
-
function
|
|
1404
|
+
function BuilderSubmissionsSkeleton() {
|
|
1405
|
+
return (_jsxs("div", { className: "rounded-2xl border border-border bg-card shadow-sm", children: [_jsx("div", { className: "border-b border-border px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-4 w-4 rounded" }), _jsx(Skeleton, { className: "h-4 w-32" })] }) }), _jsx("div", { className: "divide-y divide-border", children: Array.from({ length: 4 }).map((_, index) => (_jsxs("div", { className: "p-4", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-3 w-36" })] }), _jsx("div", { className: "mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 3 }).map((__, cell) => (_jsx(Skeleton, { className: "h-12 rounded-lg" }, cell))) })] }, index))) })] }));
|
|
1406
|
+
}
|
|
1407
|
+
function BuilderSubmissionsEmptyState() {
|
|
1408
|
+
return (_jsxs("div", { className: "flex min-h-[320px] flex-col items-center justify-center rounded-2xl border border-dashed border-border bg-muted/25 p-8 text-center", children: [_jsx(Inbox, { className: "mb-3 h-10 w-10 text-muted-foreground/60" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "No submissions yet" }), _jsx("p", { className: "mt-1 max-w-md text-sm text-muted-foreground", children: "Once this form is published and respondents submit answers, they will appear here for review." })] }));
|
|
1409
|
+
}
|
|
1410
|
+
function BuilderShareView({ publicationId, publicationSlug, onCopy, }) {
|
|
1411
|
+
const publicPath = publicationSlug ? `/forms/${publicationSlug}` : null;
|
|
1412
|
+
const embedPath = publicationId ? `/forms/embed/${publicationId}` : null;
|
|
1413
|
+
const publicUrl = publicPath ? buildPublicUrl(publicPath) : null;
|
|
1414
|
+
const embedUrl = embedPath ? buildPublicUrl(embedPath) : null;
|
|
1415
|
+
const productionBaseUrl = "https://ydtb.app";
|
|
1416
|
+
const sdkInstallSnippet = "npm install @ydtb/public-sdk @ydtb/public-capture react";
|
|
1417
|
+
const sdkComponentSnippet = publicationId ? `import { CaptureForm } from "@ydtb/public-capture/react"
|
|
1418
|
+
|
|
1419
|
+
export function YdtbCaptureForm() {
|
|
1420
|
+
return (
|
|
1421
|
+
<CaptureForm
|
|
1422
|
+
apiBaseUrl="${productionBaseUrl}"
|
|
1423
|
+
lookup={{ publishId: "${publicationId}" }}
|
|
1424
|
+
source="external"
|
|
1425
|
+
/>
|
|
1426
|
+
)
|
|
1427
|
+
}` : null;
|
|
1428
|
+
const iframeSnippet = embedUrl ? `<iframe src="${embedUrl}" title="Capture form" style="width:100%;min-height:640px;border:0;"></iframe>` : null;
|
|
1429
|
+
if (!publicationSlug || !publicationId) {
|
|
1430
|
+
return (_jsx("section", { className: "min-h-0 flex-1 overflow-auto", children: _jsxs("div", { className: "flex min-h-[360px] flex-col items-center justify-center rounded-2xl border border-dashed border-border bg-muted/25 p-8 text-center", children: [_jsx(AlertCircle, { className: "mb-3 h-10 w-10 text-muted-foreground/60" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Publish to enable sharing" }), _jsx("p", { className: "mt-1 max-w-md text-sm text-muted-foreground", children: "Public links and embed snippets are created after this form is published. Use Publish in the page header when the form is ready." })] }) }));
|
|
1431
|
+
}
|
|
1432
|
+
return (_jsx("section", { className: "min-h-0 flex-1 overflow-auto", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs("div", { className: "rounded-xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Direct public link" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Share this respondent-safe public form route." })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [publicUrl && (_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void onCopy(publicUrl, "Public link"), children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy"] })), publicPath && (_jsx(Button, { variant: "outline", size: "icon", type: "button", asChild: true, "aria-label": "Open public form", children: _jsx("a", { href: publicPath, target: "_blank", rel: "noreferrer", children: _jsx(ExternalLink, { className: "h-4 w-4" }) }) }))] })] }), publicUrl && (_jsx("p", { className: "mt-3 break-all rounded-lg bg-muted/50 px-3 py-2 font-mono text-xs text-muted-foreground", children: publicUrl }))] }), _jsxs("div", { className: "rounded-xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Iframe embed" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Embed the public form in an external page with the current public embed route." })] }), iframeSnippet && (_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void onCopy(iframeSnippet, "Embed snippet"), children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy"] }))] }), iframeSnippet && (_jsx("pre", { className: "mt-3 overflow-auto rounded-lg bg-muted/50 p-3 text-xs text-muted-foreground", children: _jsx("code", { children: iframeSnippet }) }))] }), _jsxs("div", { className: "rounded-xl border border-border bg-card p-4 shadow-sm lg:col-span-2", children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "React SDK / external app" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Use this in Replit, Lovable, or a custom React site. No scope id is needed \u2014 the publish id resolves the form and owning scope on YDTB." })] }), _jsxs("div", { className: "flex shrink-0 flex-wrap gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void onCopy(sdkInstallSnippet, "SDK install command"), children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy install"] }), sdkComponentSnippet && (_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void onCopy(sdkComponentSnippet, "SDK component snippet"), children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy component"] }))] })] }), _jsxs("div", { className: "mt-3 grid gap-3 lg:grid-cols-[minmax(0,0.7fr)_minmax(0,1.3fr)]", children: [_jsxs("div", { className: "space-y-2 rounded-lg bg-muted/50 p-3 text-xs text-muted-foreground", children: [_jsxs("p", { children: [_jsx("span", { className: "font-semibold text-foreground", children: "Production API:" }), " ", _jsx("span", { className: "font-mono", children: productionBaseUrl })] }), _jsxs("p", { children: [_jsx("span", { className: "font-semibold text-foreground", children: "Publish ID:" }), " ", _jsx("span", { className: "font-mono", children: publicationId })] }), _jsx("p", { className: "leading-5", children: "Backend routing resolves the owning scope from this public publication. Do not expose or send internal scope ids from the external page." })] }), sdkComponentSnippet && (_jsx("pre", { className: "overflow-auto rounded-lg bg-muted/50 p-3 text-xs text-muted-foreground", children: _jsxs("code", { children: [sdkInstallSnippet, "\n\n", sdkComponentSnippet] }) }))] })] }), _jsxs("div", { className: "rounded-xl border border-dashed border-border bg-muted/25 p-4", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Embed constraints" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Allowed-origin controls and advanced completion behavior are not configured in this slice. Treat the iframe snippet as the current public embed mode." })] })] }) }));
|
|
1433
|
+
}
|
|
1434
|
+
function buildPublicUrl(path) {
|
|
1435
|
+
if (typeof window === "undefined")
|
|
1436
|
+
return path;
|
|
1437
|
+
return new URL(path, window.location.origin).toString();
|
|
1438
|
+
}
|
|
1439
|
+
const BUILT_IN_CAPTURE_ITEMS_WORKBENCH_VIEWS = [
|
|
1440
|
+
{ id: "all", name: "All items", icon: _jsx(Inbox, { className: "h-4 w-4" }) },
|
|
1441
|
+
{ id: "forms", name: "Forms", icon: _jsx(FileText, { className: "h-4 w-4" }) },
|
|
1442
|
+
{ id: "quizzes", name: "Quizzes", icon: _jsx(Sparkles, { className: "h-4 w-4" }) },
|
|
1443
|
+
{ id: "published", name: "Published", icon: _jsx(CheckCircle2, { className: "h-4 w-4" }) },
|
|
1444
|
+
{ id: "drafts", name: "Drafts", icon: _jsx(FileText, { className: "h-4 w-4" }) },
|
|
1445
|
+
];
|
|
1446
|
+
const CAPTURE_SUBMISSION_SYSTEM_FIELDS = [
|
|
1447
|
+
{ key: "submittedAt", label: "Submitted", type: "date", group: "Submission", sortable: true, filterable: true, system: true },
|
|
1448
|
+
{ key: "definitionName", label: "Form", type: "text", group: "Submission", sortable: true, filterable: true, system: true },
|
|
1449
|
+
{ key: "actionStatus", label: "Action Status", type: "dropdown", group: "Submission", sortable: true, filterable: true, system: true, options: [{ value: "pending", label: "Pending" }, { value: "running", label: "Running" }, { value: "success", label: "Success" }, { value: "failed", label: "Failed" }] },
|
|
1450
|
+
{ key: "contactId", label: "Contact", type: "text", group: "Submission", sortable: true, filterable: true, system: true },
|
|
1451
|
+
];
|
|
1452
|
+
const DEFAULT_CAPTURE_SUBMISSION_COLUMNS = persistWorkbenchColumns([
|
|
1453
|
+
{ fieldKey: "submittedAt", visible: true, position: 0 },
|
|
1454
|
+
{ fieldKey: "actionStatus", visible: true, position: 1 },
|
|
1455
|
+
]);
|
|
1456
|
+
const BUILT_IN_CAPTURE_SUBMISSION_WORKBENCH_VIEWS = [
|
|
1457
|
+
{ id: "all", name: "All submissions", icon: _jsx(Inbox, { className: "h-4 w-4" }) },
|
|
1458
|
+
{ id: "with-actions", name: "With actions", icon: _jsx(GitBranch, { className: "h-4 w-4" }) },
|
|
1459
|
+
];
|
|
1460
|
+
const CAPTURE_ITEMS_FIELDS = [
|
|
1461
|
+
{ key: "name", label: "Name", type: "text", group: "Capture Item", sortable: true, filterable: true, system: true },
|
|
1462
|
+
{ key: "surface", label: "Type", type: "dropdown", group: "Capture Item", sortable: true, filterable: true, system: true, options: [{ value: "form", label: "Form" }, { value: "quiz", label: "Quiz" }] },
|
|
1463
|
+
{ key: "status", label: "Status", type: "dropdown", group: "Capture Item", sortable: true, filterable: true, system: true, options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }] },
|
|
1464
|
+
{ key: "tags", label: "Tags", type: "labels", group: "Capture Item", sortable: true, filterable: true, system: true },
|
|
1465
|
+
{ key: "fieldCount", label: "Fields", type: "number", group: "Metrics", sortable: true, filterable: true, system: true },
|
|
1466
|
+
{ key: "submissionCount", label: "Submissions", type: "number", group: "Metrics", sortable: true, filterable: true, system: true },
|
|
1467
|
+
{ key: "updatedAt", label: "Last Modified", type: "date", group: "Timestamps", sortable: true, filterable: true, system: true },
|
|
1468
|
+
];
|
|
1469
|
+
const DEFAULT_CAPTURE_ITEMS_COLUMNS = persistWorkbenchColumns([
|
|
1470
|
+
{ fieldKey: "name", visible: true, position: 0 },
|
|
1471
|
+
{ fieldKey: "surface", visible: true, position: 1 },
|
|
1472
|
+
{ fieldKey: "status", visible: true, position: 2 },
|
|
1473
|
+
{ fieldKey: "tags", visible: true, position: 3 },
|
|
1474
|
+
{ fieldKey: "fieldCount", visible: true, position: 4 },
|
|
1475
|
+
{ fieldKey: "submissionCount", visible: true, position: 5 },
|
|
1476
|
+
{ fieldKey: "updatedAt", visible: true, position: 6 },
|
|
1477
|
+
]);
|
|
1478
|
+
const CAPTURE_ITEMS_FILTER_OPERATORS = {
|
|
1479
|
+
text: ["contains", "does_not_contain", "is", "is_not", "starts_with", "ends_with", "is_empty", "is_not_empty"],
|
|
1480
|
+
dropdown: ["is", "is_not", "is_empty", "is_not_empty"],
|
|
1481
|
+
labels: ["contains", "does_not_contain", "is_empty", "is_not_empty"],
|
|
1482
|
+
number: ["is", "is_not", "greater_than", "less_than", "greater_than_or_equal", "less_than_or_equal", "is_empty", "is_not_empty"],
|
|
1483
|
+
date: ["is_before", "is_after", "is_on_or_before", "is_on_or_after", "is_empty", "is_not_empty"],
|
|
1484
|
+
};
|
|
1485
|
+
const CAPTURE_SUBMISSION_FILTER_OPERATORS = CAPTURE_ITEMS_FILTER_OPERATORS;
|
|
1486
|
+
function createSubmissionActionStatusFilter() {
|
|
1487
|
+
return {
|
|
1488
|
+
id: "with-actions",
|
|
1489
|
+
logic: "and",
|
|
1490
|
+
conditions: [{ id: "action-status-present", fieldKey: "actionStatus", operator: "is_not_empty" }],
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function buildSubmissionWorkbenchFields(fields) {
|
|
1494
|
+
return [
|
|
1495
|
+
...CAPTURE_SUBMISSION_SYSTEM_FIELDS,
|
|
1496
|
+
...fields
|
|
1497
|
+
.filter((field) => field.type !== "honeypot")
|
|
1498
|
+
.map((field) => ({
|
|
1499
|
+
key: field.key,
|
|
1500
|
+
label: field.label || field.key,
|
|
1501
|
+
type: field.type === "rating" ? "number" : field.type === "select" ? "dropdown" : "text",
|
|
1502
|
+
group: "Answers",
|
|
1503
|
+
sortable: true,
|
|
1504
|
+
filterable: true,
|
|
1505
|
+
system: false,
|
|
1506
|
+
options: field.options?.map((option) => ({ value: option, label: option })),
|
|
1507
|
+
})),
|
|
1508
|
+
];
|
|
1509
|
+
}
|
|
1510
|
+
function getSubmissionFieldValue(submission, field) {
|
|
1511
|
+
if (field === "submittedAt")
|
|
1512
|
+
return new Date(submission.submittedAt).getTime();
|
|
1513
|
+
if (field === "definitionName")
|
|
1514
|
+
return submission.definitionName;
|
|
1515
|
+
if (field === "actionStatus")
|
|
1516
|
+
return submission.actionStatus ?? "";
|
|
1517
|
+
if (field === "contactId")
|
|
1518
|
+
return submission.contactId ?? "";
|
|
1519
|
+
const value = submission.answers[field];
|
|
1520
|
+
if (typeof value === "number")
|
|
1521
|
+
return value;
|
|
1522
|
+
if (Array.isArray(value))
|
|
1523
|
+
return value.join(", ");
|
|
1524
|
+
return String(value ?? "");
|
|
1525
|
+
}
|
|
1526
|
+
function compareSubmissions(a, b, sort) {
|
|
1527
|
+
const aValue = getSubmissionFieldValue(a, sort.field);
|
|
1528
|
+
const bValue = getSubmissionFieldValue(b, sort.field);
|
|
1529
|
+
const comparison = typeof aValue === "number" && typeof bValue === "number"
|
|
1530
|
+
? aValue - bValue
|
|
1531
|
+
: String(aValue).localeCompare(String(bValue), undefined, { sensitivity: "base", numeric: true });
|
|
1532
|
+
return sort.direction === "asc" ? comparison : -comparison;
|
|
1533
|
+
}
|
|
1534
|
+
function filterSubmissions(submissions, filters) {
|
|
1535
|
+
if (!filters || filters.conditions.length === 0)
|
|
1536
|
+
return submissions;
|
|
1537
|
+
return submissions.filter((submission) => evaluateWorkbenchFilter((fieldKey) => getSubmissionFieldValue(submission, fieldKey), filters));
|
|
1538
|
+
}
|
|
1539
|
+
function evaluateWorkbenchFilter(resolveValue, filter) {
|
|
1540
|
+
const evaluate = (node) => {
|
|
1541
|
+
if ("conditions" in node) {
|
|
1542
|
+
const results = node.conditions.map(evaluate);
|
|
1543
|
+
return node.logic === "and" ? results.every(Boolean) : results.some(Boolean);
|
|
1544
|
+
}
|
|
1545
|
+
const value = resolveValue(node.fieldKey);
|
|
1546
|
+
const raw = String(value ?? "");
|
|
1547
|
+
const expected = String(node.value ?? "");
|
|
1548
|
+
const numericValue = Number(value);
|
|
1549
|
+
const numericExpected = Number(expected);
|
|
1550
|
+
if (node.operator === "is_empty")
|
|
1551
|
+
return raw.length === 0;
|
|
1552
|
+
if (node.operator === "is_not_empty")
|
|
1553
|
+
return raw.length > 0;
|
|
1554
|
+
if (node.operator === "is")
|
|
1555
|
+
return raw.toLowerCase() === expected.toLowerCase();
|
|
1556
|
+
if (node.operator === "is_not")
|
|
1557
|
+
return raw.toLowerCase() !== expected.toLowerCase();
|
|
1558
|
+
if (node.operator === "contains")
|
|
1559
|
+
return raw.toLowerCase().includes(expected.toLowerCase());
|
|
1560
|
+
if (node.operator === "does_not_contain")
|
|
1561
|
+
return !raw.toLowerCase().includes(expected.toLowerCase());
|
|
1562
|
+
if (node.operator === "starts_with")
|
|
1563
|
+
return raw.toLowerCase().startsWith(expected.toLowerCase());
|
|
1564
|
+
if (node.operator === "ends_with")
|
|
1565
|
+
return raw.toLowerCase().endsWith(expected.toLowerCase());
|
|
1566
|
+
if (node.operator === "greater_than")
|
|
1567
|
+
return numericValue > numericExpected;
|
|
1568
|
+
if (node.operator === "less_than")
|
|
1569
|
+
return numericValue < numericExpected;
|
|
1570
|
+
if (node.operator === "greater_than_or_equal")
|
|
1571
|
+
return numericValue >= numericExpected;
|
|
1572
|
+
if (node.operator === "less_than_or_equal")
|
|
1573
|
+
return numericValue <= numericExpected;
|
|
1574
|
+
if (node.operator === "is_before")
|
|
1575
|
+
return numericValue < new Date(expected).getTime();
|
|
1576
|
+
if (node.operator === "is_after")
|
|
1577
|
+
return numericValue > new Date(expected).getTime();
|
|
1578
|
+
if (node.operator === "is_on_or_before")
|
|
1579
|
+
return numericValue <= new Date(expected).getTime();
|
|
1580
|
+
if (node.operator === "is_on_or_after")
|
|
1581
|
+
return numericValue >= new Date(expected).getTime();
|
|
1582
|
+
return true;
|
|
1583
|
+
};
|
|
1584
|
+
const results = filter.conditions.map(evaluate);
|
|
1585
|
+
return filter.logic === "and" ? results.every(Boolean) : results.some(Boolean);
|
|
1586
|
+
}
|
|
1587
|
+
function renderSubmissionCell(submission, column) {
|
|
1588
|
+
if (column.fieldKey.startsWith("__"))
|
|
1589
|
+
return null;
|
|
1590
|
+
if (column.fieldKey === "submittedAt")
|
|
1591
|
+
return _jsx("span", { className: "block text-sm whitespace-nowrap text-muted-foreground", children: new Date(submission.submittedAt).toLocaleString() });
|
|
1592
|
+
if (column.fieldKey === "definitionName")
|
|
1593
|
+
return _jsx("span", { className: "text-sm font-medium text-foreground", children: submission.definitionName });
|
|
1594
|
+
if (column.fieldKey === "actionStatus")
|
|
1595
|
+
return _jsx("span", { className: "text-sm capitalize text-muted-foreground", children: submission.actionStatus ?? "—" });
|
|
1596
|
+
const value = getSubmissionFieldValue(submission, column.fieldKey);
|
|
1597
|
+
return _jsx("span", { className: "text-sm text-muted-foreground", children: String(value || "—") });
|
|
1598
|
+
}
|
|
1599
|
+
function matchesCaptureItemsView(definition, viewId) {
|
|
1600
|
+
if (viewId === "forms")
|
|
1601
|
+
return definition.surface === "form";
|
|
1602
|
+
if (viewId === "quizzes")
|
|
1603
|
+
return definition.surface === "quiz";
|
|
1604
|
+
if (viewId === "published")
|
|
1605
|
+
return definition.status === "published";
|
|
1606
|
+
if (viewId === "drafts")
|
|
1607
|
+
return definition.status === "draft";
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
function getCaptureItemFieldValue(definition, field) {
|
|
1611
|
+
if (field === "tags")
|
|
1612
|
+
return (definition.tags ?? []).join(" ");
|
|
1613
|
+
if (field === "fieldCount")
|
|
1614
|
+
return definition.fieldCount;
|
|
1615
|
+
if (field === "submissionCount")
|
|
1616
|
+
return definition.submissionCount;
|
|
1617
|
+
if (field === "updatedAt")
|
|
1618
|
+
return new Date(definition.updatedAt ?? definition.createdAt ?? 0).getTime();
|
|
1619
|
+
return String(definition[field] ?? "");
|
|
1620
|
+
}
|
|
1621
|
+
function parseCaptureTagsValue(value) {
|
|
1622
|
+
if (!value)
|
|
1623
|
+
return [];
|
|
1624
|
+
try {
|
|
1625
|
+
const parsed = JSON.parse(value);
|
|
1626
|
+
if (Array.isArray(parsed))
|
|
1627
|
+
return normalizeCaptureTagValues(parsed.map(String));
|
|
1628
|
+
}
|
|
1629
|
+
catch {
|
|
1630
|
+
return normalizeCaptureTagValues(value.split(","));
|
|
1631
|
+
}
|
|
1632
|
+
return [];
|
|
1633
|
+
}
|
|
1634
|
+
function normalizeCaptureTagValues(tags) {
|
|
1635
|
+
const seen = new Set();
|
|
1636
|
+
const normalized = [];
|
|
1637
|
+
for (const tag of tags) {
|
|
1638
|
+
const value = tag.trim().replace(/\s+/g, " ").slice(0, 60);
|
|
1639
|
+
const key = value.toLowerCase();
|
|
1640
|
+
if (!value || seen.has(key))
|
|
1641
|
+
continue;
|
|
1642
|
+
seen.add(key);
|
|
1643
|
+
normalized.push(value);
|
|
1644
|
+
}
|
|
1645
|
+
return normalized;
|
|
1646
|
+
}
|
|
1647
|
+
function buildCaptureTagOptions(definitions) {
|
|
1648
|
+
const tags = normalizeCaptureTagValues(definitions.flatMap((definition) => definition.tags ?? []));
|
|
1649
|
+
return tags.map((tag) => ({ value: tag, label: tag, color: captureTagColor(tag) }));
|
|
1650
|
+
}
|
|
1651
|
+
function withCaptureTagOptions(fields, tagOptions) {
|
|
1652
|
+
return fields.map((field) => field.key === "tags" ? { ...field, options: tagOptions } : field);
|
|
1653
|
+
}
|
|
1654
|
+
function captureTagColor(tag) {
|
|
1655
|
+
const palette = ["#64748b", "#2563eb", "#7c3aed", "#db2777", "#dc2626", "#ea580c", "#16a34a", "#0891b2"];
|
|
1656
|
+
let hash = 0;
|
|
1657
|
+
for (const char of tag)
|
|
1658
|
+
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
|
1659
|
+
return palette[hash % palette.length];
|
|
1660
|
+
}
|
|
1661
|
+
function CaptureTagBadge({ tag }) {
|
|
1662
|
+
return (_jsx("span", { className: "inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium text-white", style: { backgroundColor: captureTagColor(tag) }, children: tag }));
|
|
1663
|
+
}
|
|
1664
|
+
function compareCaptureItems(a, b, sort) {
|
|
1665
|
+
const aValue = getCaptureItemFieldValue(a, sort.field);
|
|
1666
|
+
const bValue = getCaptureItemFieldValue(b, sort.field);
|
|
1667
|
+
const comparison = typeof aValue === "number" && typeof bValue === "number"
|
|
1668
|
+
? aValue - bValue
|
|
1669
|
+
: String(aValue).localeCompare(String(bValue), undefined, { sensitivity: "base", numeric: true });
|
|
1670
|
+
return sort.direction === "asc" ? comparison : -comparison;
|
|
1671
|
+
}
|
|
1672
|
+
function evaluateCaptureItemsFilter(definition, filter) {
|
|
1673
|
+
if (!filter || filter.conditions.length === 0)
|
|
1674
|
+
return true;
|
|
1675
|
+
const evaluate = (node) => {
|
|
1676
|
+
if ("conditions" in node) {
|
|
1677
|
+
const results = node.conditions.map(evaluate);
|
|
1678
|
+
return node.logic === "and" ? results.every(Boolean) : results.some(Boolean);
|
|
1679
|
+
}
|
|
1680
|
+
const value = getCaptureItemFieldValue(definition, node.fieldKey);
|
|
1681
|
+
const raw = String(value ?? "");
|
|
1682
|
+
const expected = String(node.value ?? "");
|
|
1683
|
+
const numericValue = Number(value);
|
|
1684
|
+
const numericExpected = Number(expected);
|
|
1685
|
+
if (node.operator === "is_empty")
|
|
1686
|
+
return raw.length === 0;
|
|
1687
|
+
if (node.operator === "is_not_empty")
|
|
1688
|
+
return raw.length > 0;
|
|
1689
|
+
if (node.operator === "is")
|
|
1690
|
+
return raw.toLowerCase() === expected.toLowerCase();
|
|
1691
|
+
if (node.operator === "is_not")
|
|
1692
|
+
return raw.toLowerCase() !== expected.toLowerCase();
|
|
1693
|
+
if (node.operator === "contains")
|
|
1694
|
+
return raw.toLowerCase().includes(expected.toLowerCase());
|
|
1695
|
+
if (node.operator === "does_not_contain")
|
|
1696
|
+
return !raw.toLowerCase().includes(expected.toLowerCase());
|
|
1697
|
+
if (node.operator === "starts_with")
|
|
1698
|
+
return raw.toLowerCase().startsWith(expected.toLowerCase());
|
|
1699
|
+
if (node.operator === "ends_with")
|
|
1700
|
+
return raw.toLowerCase().endsWith(expected.toLowerCase());
|
|
1701
|
+
if (node.operator === "greater_than")
|
|
1702
|
+
return numericValue > numericExpected;
|
|
1703
|
+
if (node.operator === "less_than")
|
|
1704
|
+
return numericValue < numericExpected;
|
|
1705
|
+
if (node.operator === "greater_than_or_equal")
|
|
1706
|
+
return numericValue >= numericExpected;
|
|
1707
|
+
if (node.operator === "less_than_or_equal")
|
|
1708
|
+
return numericValue <= numericExpected;
|
|
1709
|
+
if (node.operator === "is_before")
|
|
1710
|
+
return numericValue < new Date(expected).getTime();
|
|
1711
|
+
if (node.operator === "is_after")
|
|
1712
|
+
return numericValue > new Date(expected).getTime();
|
|
1713
|
+
if (node.operator === "is_on_or_before")
|
|
1714
|
+
return numericValue <= new Date(expected).getTime();
|
|
1715
|
+
if (node.operator === "is_on_or_after")
|
|
1716
|
+
return numericValue >= new Date(expected).getTime();
|
|
1717
|
+
return true;
|
|
1718
|
+
};
|
|
1719
|
+
const results = filter.conditions.map(evaluate);
|
|
1720
|
+
return filter.logic === "and" ? results.every(Boolean) : results.some(Boolean);
|
|
1721
|
+
}
|
|
1722
|
+
function StartPanel({ definitions, isLoading, isError, onRetryDefinitions, search, selectedDefinition, activeDetailTab, activeWorkbenchViewId, activeWorkbenchPanel, onActiveWorkbenchPanelChange, filters, onFiltersChange, columns, onColumnsChange, sort, onSortChange, onSelectDefinition, onCloseDetails, onDetailTabChange, onCreate, isCreating, onOpen, onTagsChange, workflows, workflowsLoading, workflowsError, onRetryWorkflows, contactCustomFields, contactCustomFieldsLoading, contactCustomFieldsError, onRetryContactCustomFields, onActionConfigChange, isActionConfigSaving, }) {
|
|
226
1723
|
const filteredDefinitions = useMemo(() => {
|
|
227
1724
|
const query = search.trim().toLowerCase();
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
1725
|
+
return definitions
|
|
1726
|
+
.filter((definition) => matchesCaptureItemsView(definition, activeWorkbenchViewId))
|
|
1727
|
+
.filter((definition) => evaluateCaptureItemsFilter(definition, filters))
|
|
1728
|
+
.filter((definition) => {
|
|
1729
|
+
if (!query)
|
|
1730
|
+
return true;
|
|
1731
|
+
return [definition.name, definition.surface, definition.status, definition.slug]
|
|
1732
|
+
.some((value) => value.toLowerCase().includes(query));
|
|
1733
|
+
})
|
|
1734
|
+
.sort((a, b) => compareCaptureItems(a, b, sort));
|
|
1735
|
+
}, [activeWorkbenchViewId, definitions, filters, search, sort]);
|
|
1736
|
+
const pagination = useWorkbenchOffsetPagination({
|
|
1737
|
+
pageSize: 25,
|
|
1738
|
+
total: filteredDefinitions.length,
|
|
1739
|
+
resetKeys: [activeWorkbenchViewId, filters, search, sort],
|
|
1740
|
+
});
|
|
1741
|
+
const pageDefinitions = filteredDefinitions.slice(pagination.offset, pagination.offset + pagination.pageSize);
|
|
1742
|
+
const tagOptions = useMemo(() => buildCaptureTagOptions(definitions), [definitions]);
|
|
1743
|
+
const tagField = useMemo(() => ({ id: "tags", name: "Tags", type: "labels", options: tagOptions }), [tagOptions]);
|
|
1744
|
+
const [actionDraftConfig, setActionDraftConfig] = useState(selectedDefinition?.actionConfig ?? {});
|
|
1745
|
+
useEffect(() => {
|
|
1746
|
+
setActionDraftConfig(selectedDefinition?.actionConfig ?? {});
|
|
1747
|
+
}, [selectedDefinition?.id, selectedDefinition?.actionConfig]);
|
|
1748
|
+
const actionTabs = useMemo(() => buildCaptureListPanelTabs(actionDraftConfig), [actionDraftConfig]);
|
|
1749
|
+
useEffect(() => {
|
|
1750
|
+
if (!selectedDefinition)
|
|
1751
|
+
return;
|
|
1752
|
+
if (!actionTabs.some((tab) => tab.id === activeDetailTab))
|
|
1753
|
+
onDetailTabChange("actions");
|
|
1754
|
+
}, [activeDetailTab, actionTabs, onDetailTabChange, selectedDefinition]);
|
|
1755
|
+
const toggleLeadAction = useCallback((kind, enabled) => {
|
|
1756
|
+
setActionDraftConfig((current) => {
|
|
1757
|
+
const actionConfig = current[kind] ?? defaultActionConfigFor(kind);
|
|
1758
|
+
return { ...current, [kind]: { ...actionConfig, enabled } };
|
|
1759
|
+
});
|
|
1760
|
+
if (enabled)
|
|
1761
|
+
onDetailTabChange(kind);
|
|
1762
|
+
}, [onDetailTabChange]);
|
|
1763
|
+
const showEmptyState = !isLoading && !isError && definitions.length === 0;
|
|
1764
|
+
const showNoResults = !isLoading && !isError && definitions.length > 0 && filteredDefinitions.length === 0;
|
|
1765
|
+
const renderCell = (definition, column) => {
|
|
1766
|
+
const fieldKey = column.fieldKey;
|
|
1767
|
+
if (fieldKey === "name") {
|
|
1768
|
+
const SurfaceIcon = definition.surface === "quiz" ? Sparkles : FileText;
|
|
1769
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10", children: _jsx(SurfaceIcon, { className: "h-4 w-4 text-primary" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-medium text-foreground", children: definition.name }), _jsxs("div", { className: "truncate text-xs text-muted-foreground", children: ["/", definition.slug] })] })] }));
|
|
1770
|
+
}
|
|
1771
|
+
if (fieldKey === "status")
|
|
1772
|
+
return _jsx(CaptureStatusBadge, { status: definition.status });
|
|
1773
|
+
if (fieldKey === "surface")
|
|
1774
|
+
return _jsx("span", { className: "text-sm capitalize text-muted-foreground", children: definition.surface });
|
|
1775
|
+
if (fieldKey === "tags") {
|
|
1776
|
+
return (_jsx("div", { className: "group/cell relative min-w-40", children: _jsx(InlineCellEditor, { field: tagField, value: JSON.stringify(definition.tags ?? []), onChange: (value) => onTagsChange(definition.id, parseCaptureTagsValue(value)), onCreateOption: (option) => onTagsChange(definition.id, [...(definition.tags ?? []), option.label]) }) }));
|
|
1777
|
+
}
|
|
1778
|
+
if (fieldKey === "fieldCount")
|
|
1779
|
+
return _jsx("span", { className: "block text-right text-sm tabular-nums text-muted-foreground", children: definition.fieldCount });
|
|
1780
|
+
if (fieldKey === "submissionCount")
|
|
1781
|
+
return _jsx("span", { className: "block text-right text-sm tabular-nums text-muted-foreground", children: definition.submissionCount });
|
|
1782
|
+
return _jsx("span", { className: "block text-right text-sm whitespace-nowrap text-muted-foreground", children: formatRelativeTime(definition.updatedAt ?? definition.createdAt ?? new Date()) });
|
|
1783
|
+
};
|
|
1784
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4", children: [isError && (_jsx(QueryErrorState, { title: "Could not load capture items", description: "Capture Items could not be loaded. Try again to refresh the list.", onRetry: onRetryDefinitions })), showEmptyState && (_jsxs("div", { className: "flex flex-col items-center justify-center rounded-2xl border border-border bg-card py-20 text-center shadow-sm", children: [_jsx(Inbox, { className: "mb-4 h-12 w-12 text-muted-foreground/40" }), _jsx("h3", { className: "mb-1 text-sm font-semibold text-foreground", children: "No capture items yet" }), _jsx("p", { className: "mb-4 text-sm text-muted-foreground", children: "Create your first form to get started." }), _jsxs(Button, { onClick: onCreate, disabled: isCreating, children: [_jsx(Plus, { className: "h-4 w-4" }), isCreating ? "Creating…" : "Create your first form"] })] })), showNoResults && (_jsxs("div", { className: "flex flex-col items-center justify-center rounded-2xl border border-border bg-card py-20 text-center shadow-sm", children: [_jsx(Search, { className: "mb-3 h-10 w-10 text-muted-foreground/40" }), _jsx("h3", { className: "mb-1 text-sm font-semibold text-foreground", children: "No capture items match your search" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Try adjusting your search." })] })), isLoading && _jsx(CaptureItemsSkeleton, {}), !isLoading && !isError && filteredDefinitions.length > 0 && (_jsxs("div", { className: "flex min-h-0 flex-1 items-stretch gap-4", children: [_jsxs("div", { className: "min-h-0 min-w-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "hidden h-full min-h-0 sm:block", children: _jsx(WorkbenchTable, { data: pageDefinitions, columns: columns, getRowId: (definition) => definition.id, renderCell: renderCell, getColumnClassName: (column) => ["fieldCount", "submissionCount", "updatedAt"].includes(column.fieldKey) ? "text-right" : undefined, onRowClick: onSelectDefinition, onRowDoubleClick: (definition) => onOpen(definition.id), sorts: [{ field: sort.field, direction: sort.direction }], onSortChange: (nextSorts) => {
|
|
1785
|
+
const next = nextSorts[0];
|
|
1786
|
+
if (next)
|
|
1787
|
+
onSortChange({ field: next.field, direction: next.direction });
|
|
1788
|
+
}, total: filteredDefinitions.length, page: pagination.page, pageSize: pagination.pageSize, onPageChange: pagination.setPage, className: "h-full" }) }), _jsx("div", { className: "min-h-0 flex-1 divide-y divide-border/50 overflow-auto rounded-xl border border-border bg-card shadow-sm sm:hidden", children: pageDefinitions.map((definition) => {
|
|
239
1789
|
const SurfaceIcon = definition.surface === "quiz" ? Sparkles : FileText;
|
|
240
|
-
return (_jsxs("button", { type: "button", onClick: () => onSelectDefinition(definition), className: "flex w-full items-center gap-3 px-4 py-3 text-left active:bg-muted/50", children: [_jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10", children: _jsx(SurfaceIcon, { className: "h-4 w-4 text-primary" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: definition.name }), _jsxs("span", { className: "block text-xs capitalize text-muted-foreground", children: [definition.surface, " \u00B7 ", definition.fieldCount, " fields \u00B7 ", definition.status] })] })] }, definition.id));
|
|
241
|
-
}) })] }), _jsx(TabbedPanel, { open: selectedDefinition != null, onClose: onCloseDetails, tabs:
|
|
1790
|
+
return (_jsxs("button", { type: "button", onClick: () => onSelectDefinition(definition), onDoubleClick: () => onOpen(definition.id), className: "flex w-full items-center gap-3 px-4 py-3 text-left active:bg-muted/50", children: [_jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10", children: _jsx(SurfaceIcon, { className: "h-4 w-4 text-primary" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: definition.name }), _jsxs("span", { className: "block text-xs capitalize text-muted-foreground", children: [definition.surface, " \u00B7 ", definition.fieldCount, " fields \u00B7 ", definition.submissionCount, " submissions \u00B7 ", definition.status] }), (definition.tags?.length ?? 0) > 0 && (_jsx("span", { className: "mt-1 flex flex-wrap gap-1", children: definition.tags?.slice(0, 3).map((tag) => _jsx(CaptureTagBadge, { tag: tag }, tag)) }))] })] }, definition.id));
|
|
1791
|
+
}) })] }), _jsx(TabbedPanel, { open: selectedDefinition != null, onClose: onCloseDetails, tabs: actionTabs, activeTab: activeDetailTab, onTabChange: onDetailTabChange, width: "456px", inline: true, flush: true, className: "self-stretch rounded-xl border shadow-sm", header: selectedDefinition ? (_jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "truncate text-sm font-semibold text-foreground", children: selectedDefinition.name }), _jsxs("p", { className: "mt-1 text-xs capitalize text-muted-foreground", children: [selectedDefinition.surface, " \u00B7 ", selectedDefinition.status] })] })) : undefined, footer: selectedDefinition ? (_jsx("div", { className: "flex items-center justify-end gap-2 border-t border-border px-4 py-3", children: _jsx(Button, { size: "sm", onClick: () => onOpen(selectedDefinition.id), children: "Open builder" }) })) : undefined, children: selectedDefinition ? (_jsx(CaptureListDetailPanel, { definition: selectedDefinition, activeTab: activeDetailTab, actionConfig: actionDraftConfig, persistedActionConfig: selectedDefinition.actionConfig ?? {}, workflows: workflows, workflowsLoading: workflowsLoading, workflowsError: workflowsError, onRetryWorkflows: onRetryWorkflows, contactCustomFields: contactCustomFields, contactCustomFieldsLoading: contactCustomFieldsLoading, contactCustomFieldsError: contactCustomFieldsError, onRetryContactCustomFields: onRetryContactCustomFields, onActionConfigChange: setActionDraftConfig, onToggleLeadAction: toggleLeadAction, onSaveActionConfig: (actionConfig) => onActionConfigChange(selectedDefinition.id, actionConfig), isActionConfigSaving: isActionConfigSaving })) : null }), _jsx(TabbedPanel, { open: activeWorkbenchPanel != null, onClose: () => onActiveWorkbenchPanelChange(null), tabs: [
|
|
1792
|
+
{ id: "filters", icon: Filter, label: "Filters" },
|
|
1793
|
+
{ id: "columns", icon: Columns3, label: "Columns" },
|
|
1794
|
+
], activeTab: activeWorkbenchPanel ?? "filters", onTabChange: (tabId) => onActiveWorkbenchPanelChange(tabId), width: "420px", header: (_jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: "Capture Items view" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Adjust filters and visible columns for this workbench." })] })), children: activeWorkbenchPanel === "columns" ? (_jsx(WorkbenchColumnPanel, { columns: columns, onColumnsChange: onColumnsChange })) : (_jsx(WorkbenchFilterPanel, { filters: filters, onFiltersChange: onFiltersChange, fields: fieldsToFilterFields(withCaptureTagOptions(CAPTURE_ITEMS_FIELDS, tagOptions)), operatorsByFieldType: CAPTURE_ITEMS_FILTER_OPERATORS, resultCount: filteredDefinitions.length })) })] }))] }));
|
|
242
1795
|
}
|
|
243
|
-
function CaptureListDetailPanel({ definition, activeTab, }) {
|
|
1796
|
+
function CaptureListDetailPanel({ definition, activeTab, actionConfig, persistedActionConfig, workflows, workflowsLoading, workflowsError, onRetryWorkflows, contactCustomFields, contactCustomFieldsLoading, contactCustomFieldsError, onRetryContactCustomFields, onActionConfigChange, onToggleLeadAction, onSaveActionConfig, isActionConfigSaving, }) {
|
|
244
1797
|
if (activeTab === "overview") {
|
|
245
|
-
return (_jsxs("div", { className: "space-y-4 p-4", children: [_jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Summary" }), _jsxs("dl", { className: "mt-3 grid gap-3 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Type" }), _jsx("dd", { className: "font-medium capitalize text-foreground", children: definition.surface })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Status" }), _jsx("dd", { children: _jsx(CaptureStatusBadge, { status: definition.status }) })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Fields" }), _jsx("dd", { className: "font-medium tabular-nums text-foreground", children: definition.fieldCount })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Last modified" }), _jsx("dd", { className: "font-medium text-foreground", children: formatRelativeTime(definition.updatedAt ?? definition.createdAt ?? new Date()) })] })] })] }), _jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Slug" }), _jsxs("p", { className: "mt-2 break-all rounded-lg bg-muted/50 px-2 py-1.5 font-mono text-xs text-muted-foreground", children: ["/", definition.slug] })] })] }));
|
|
1798
|
+
return (_jsxs("div", { className: "space-y-4 p-4", children: [_jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Summary" }), _jsxs("dl", { className: "mt-3 grid gap-3 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Type" }), _jsx("dd", { className: "font-medium capitalize text-foreground", children: definition.surface })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Status" }), _jsx("dd", { children: _jsx(CaptureStatusBadge, { status: definition.status }) })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Submissions" }), _jsx("dd", { className: "font-medium tabular-nums text-foreground", children: definition.submissionCount })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Fields" }), _jsx("dd", { className: "font-medium tabular-nums text-foreground", children: definition.fieldCount })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Last modified" }), _jsx("dd", { className: "font-medium text-foreground", children: formatRelativeTime(definition.updatedAt ?? definition.createdAt ?? new Date()) })] })] })] }), _jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Slug" }), _jsxs("p", { className: "mt-2 break-all rounded-lg bg-muted/50 px-2 py-1.5 font-mono text-xs text-muted-foreground", children: ["/", definition.slug] })] })] }));
|
|
246
1799
|
}
|
|
247
1800
|
if (activeTab === "publish") {
|
|
248
1801
|
return (_jsx("div", { className: "space-y-4 p-4", children: _jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Publication" }), definition.publicationSlug ? (_jsxs("p", { className: "mt-2 break-all text-sm text-foreground", children: ["/", definition.surface === "quiz" ? "quizzes" : "forms", "/", definition.publicationSlug] })) : (_jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "This capture item has not been published yet." }))] }) }));
|
|
@@ -250,19 +1803,54 @@ function CaptureListDetailPanel({ definition, activeTab, }) {
|
|
|
250
1803
|
if (activeTab === "submissions") {
|
|
251
1804
|
return (_jsx("div", { className: "p-4", children: _jsxs("div", { className: "rounded-xl border border-dashed border-border bg-muted/25 p-6 text-center", children: [_jsx(Inbox, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Submission rollup" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Submission counts and recent responses will live here once the list endpoint returns per-item metrics." })] }) }));
|
|
252
1805
|
}
|
|
253
|
-
|
|
1806
|
+
if (activeTab === "actions") {
|
|
1807
|
+
return (_jsx(LeadCaptureActionsSection, { config: actionConfig, persistedConfig: persistedActionConfig, onToggle: onToggleLeadAction, onReset: () => onActionConfigChange(persistedActionConfig), onSave: () => onSaveActionConfig(actionConfig), isSaving: isActionConfigSaving }));
|
|
1808
|
+
}
|
|
1809
|
+
if (isLeadActionKind(activeTab)) {
|
|
1810
|
+
return (_jsxs("div", { className: "space-y-4 p-4", children: [_jsx(ActionsPanel, { fields: definition.fields ?? [], config: actionConfig, workflows: workflows, workflowsLoading: workflowsLoading, workflowsError: workflowsError, onRetryWorkflows: onRetryWorkflows, contactCustomFields: contactCustomFields, contactCustomFieldsLoading: contactCustomFieldsLoading, contactCustomFieldsError: contactCustomFieldsError, onRetryContactCustomFields: onRetryContactCustomFields, onChange: onActionConfigChange, visibleActions: [activeTab], showSectionSwitches: false, compact: true }), _jsx(ActionConfigFooter, { isDirty: isActionConfigDirty(actionConfig, persistedActionConfig), isSaving: isActionConfigSaving, onReset: () => onActionConfigChange(persistedActionConfig), onSave: () => onSaveActionConfig(actionConfig) })] }));
|
|
1811
|
+
}
|
|
1812
|
+
return null;
|
|
1813
|
+
}
|
|
1814
|
+
function QueryErrorState({ title, description, onRetry, }) {
|
|
1815
|
+
return (_jsxs("div", { className: "flex min-h-[320px] flex-col items-center justify-center rounded-2xl border border-border bg-card p-8 text-center shadow-sm", children: [_jsx(AlertCircle, { className: "mb-3 h-10 w-10 text-destructive" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: title }), _jsx("p", { className: "mt-1 max-w-md text-sm text-muted-foreground", children: description }), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "mt-4", onClick: onRetry, children: "Try again" })] }));
|
|
1816
|
+
}
|
|
1817
|
+
function CaptureItemsSkeleton() {
|
|
1818
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 items-stretch gap-4", children: [_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-sm", children: [_jsx("div", { className: "border-b border-border px-5 py-3", children: _jsx("div", { className: "grid grid-cols-[minmax(0,1fr)_120px_120px_80px_140px] gap-4", children: Array.from({ length: 5 }).map((_, index) => _jsx(Skeleton, { className: "h-4" }, index)) }) }), _jsx("div", { className: "divide-y divide-border/50", children: Array.from({ length: 6 }).map((_, row) => (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_120px_120px_80px_140px] items-center gap-4 px-5 py-3.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { className: "h-8 w-8 rounded-lg" }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-2/3" }), _jsx(Skeleton, { className: "h-3 w-1/3" })] })] }), _jsx(Skeleton, { className: "h-4" }), _jsx(Skeleton, { className: "h-5 w-20 rounded-full" }), _jsx(Skeleton, { className: "h-4 w-8 justify-self-end" }), _jsx(Skeleton, { className: "h-4" })] }, row))) })] }), _jsx(Skeleton, { className: "hidden w-[380px] rounded-xl lg:block" })] }));
|
|
1819
|
+
}
|
|
1820
|
+
function BuilderWorkspaceSkeleton() {
|
|
1821
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4", children: [_jsxs("section", { className: "rounded-2xl border border-border bg-card p-4 shadow-sm", children: [_jsx(Skeleton, { className: "h-4 w-24" }), _jsx(Skeleton, { className: "mt-2 h-9 max-w-xl" })] }), _jsxs("div", { className: "flex min-h-0 flex-1 gap-4", children: [_jsxs("section", { className: "flex min-h-0 min-w-0 flex-1 flex-col rounded-2xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-40" }), _jsx(Skeleton, { className: "h-3 w-72" })] }), _jsx(Skeleton, { className: "h-6 w-16 rounded-full" })] }), _jsx("div", { className: "flex flex-1 flex-col gap-3 rounded-2xl border border-dashed border-border bg-muted/25 p-4", children: Array.from({ length: 4 }).map((_, index) => _jsx(Skeleton, { className: "h-20 rounded-xl" }, index)) })] }), _jsx(Skeleton, { className: "hidden w-[380px] rounded-2xl lg:block" })] })] }));
|
|
254
1822
|
}
|
|
255
1823
|
function CaptureStatusBadge({ status }) {
|
|
256
1824
|
const isPublished = status === "published";
|
|
257
1825
|
return (_jsx("span", { className: cn("inline-block rounded-full px-1.5 py-0.5 text-[10px] font-medium capitalize", isPublished ? "bg-success/10 text-success-foreground" : "bg-muted text-muted-foreground"), children: status }));
|
|
258
1826
|
}
|
|
259
|
-
function
|
|
1827
|
+
function isLeadActionKind(value) {
|
|
1828
|
+
return value === "contacts" || value === "email" || value === "workflow";
|
|
1829
|
+
}
|
|
1830
|
+
function isActionConfigDirty(current, saved) {
|
|
1831
|
+
return JSON.stringify(current) !== JSON.stringify(saved);
|
|
1832
|
+
}
|
|
1833
|
+
function LeadCaptureActionsSection({ config, persistedConfig, onToggle, onReset, onSave, isSaving, }) {
|
|
1834
|
+
return (_jsxs("div", { className: "space-y-4 p-4", children: [_jsxs("section", { className: "space-y-3 rounded-xl border border-border bg-background p-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Lead capture actions" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Toggle the actions this form should run after each submission. Active actions get their own configuration tabs." })] }), _jsx("div", { className: "space-y-2", children: CAPTURE_LEAD_ACTIONS.map((action) => {
|
|
1835
|
+
const Icon = action.icon;
|
|
1836
|
+
const enabled = Boolean(config[action.id]?.enabled);
|
|
1837
|
+
return (_jsxs("div", { className: "flex items-start justify-between gap-3 rounded-lg border border-border bg-muted/20 p-3", children: [_jsxs("div", { className: "flex min-w-0 gap-3", children: [_jsx("span", { className: "mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary", children: _jsx(Icon, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: action.label }), _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: action.description })] })] }), _jsx(Switch, { checked: enabled, onCheckedChange: (checked) => onToggle(action.id, checked), disabled: isSaving })] }, action.id));
|
|
1838
|
+
}) })] }), _jsx(ActionConfigFooter, { isDirty: isActionConfigDirty(config, persistedConfig), isSaving: isSaving, onReset: onReset, onSave: onSave })] }));
|
|
1839
|
+
}
|
|
1840
|
+
function ActionConfigFooter({ isDirty, isSaving, onReset, onSave, }) {
|
|
1841
|
+
return (_jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border pt-3", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onReset, disabled: !isDirty || isSaving, children: "Reset" }), _jsxs(Button, { type: "button", size: "sm", onClick: onSave, disabled: !isDirty || isSaving, children: [_jsx(Save, { className: "h-4 w-4" }), isSaving ? "Saving…" : "Save actions"] })] }));
|
|
1842
|
+
}
|
|
1843
|
+
function defaultActionConfigFor(kind) {
|
|
1844
|
+
if (kind === "contacts")
|
|
1845
|
+
return { enabled: false, customFieldMappings: {} };
|
|
1846
|
+
if (kind === "email")
|
|
1847
|
+
return { enabled: false, subjectTemplate: "New Capture submission", bodyTemplate: "A new Capture submission was received.\n\n{{answers}}" };
|
|
1848
|
+
return { enabled: false, execute: true };
|
|
1849
|
+
}
|
|
1850
|
+
function ActionsPanel({ fields, config, workflows, workflowsLoading, workflowsError, onRetryWorkflows, contactCustomFields, contactCustomFieldsLoading, contactCustomFieldsError, onRetryContactCustomFields, onChange, visibleActions, showSectionSwitches = true, compact = false, }) {
|
|
260
1851
|
const contacts = config.contacts ?? { enabled: false, customFieldMappings: {} };
|
|
261
1852
|
const email = config.email ?? { enabled: false, subjectTemplate: "New Capture submission", bodyTemplate: "A new Capture submission was received.\n\n{{answers}}" };
|
|
262
1853
|
const workflow = config.workflow ?? { enabled: false, execute: true };
|
|
263
|
-
const customMappingsText = Object.entries(contacts.customFieldMappings ?? {})
|
|
264
|
-
.map(([contactField, answerField]) => `${contactField}=${answerField}`)
|
|
265
|
-
.join("\n");
|
|
266
1854
|
const patchContacts = (patch) => {
|
|
267
1855
|
onChange({ ...config, contacts: { ...contacts, ...patch } });
|
|
268
1856
|
};
|
|
@@ -280,27 +1868,179 @@ function ActionsPanel({ fields, config, workflows, onChange, }) {
|
|
|
280
1868
|
answers: Object.fromEntries(fields.map((field) => [field.key, `<${field.label}>`])),
|
|
281
1869
|
},
|
|
282
1870
|
};
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1871
|
+
const visible = new Set(visibleActions ?? ["contacts", "email", "workflow"]);
|
|
1872
|
+
const sectionClassName = compact ? "rounded-xl border border-border bg-background p-3" : "rounded-2xl border border-border bg-card p-4 shadow-sm";
|
|
1873
|
+
const gridClassName = compact ? "mt-4 grid gap-4" : "mt-4 grid gap-4 md:grid-cols-3";
|
|
1874
|
+
const twoColumnGridClassName = compact ? "mt-4 grid gap-4" : "mt-4 grid gap-4 md:grid-cols-2";
|
|
1875
|
+
return (_jsxs("div", { className: "space-y-4", children: [visible.has("contacts") && _jsxs("section", { className: sectionClassName, children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Contacts create/update action" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "After a public submission is stored, map answers into Contacts. Failures are recorded as action runs without deleting the canonical submission." })] }), showSectionSwitches && _jsx(Switch, { checked: contacts.enabled, onCheckedChange: (enabled) => patchContacts({ enabled }) })] }), _jsxs("div", { className: gridClassName, children: [_jsx(FieldSelect, { label: "Name answer", value: contacts.nameFieldKey ?? "", fields: fields, onChange: (nameFieldKey) => patchContacts({ nameFieldKey }) }), _jsx(FieldSelect, { label: "Email answer", value: contacts.emailFieldKey ?? "", fields: fields, onChange: (emailFieldKey) => patchContacts({ emailFieldKey }) }), _jsx(FieldSelect, { label: "Phone answer", value: contacts.phoneFieldKey ?? "", fields: fields, onChange: (phoneFieldKey) => patchContacts({ phoneFieldKey }) })] }), _jsx(CustomFieldMappingsEditor, { fields: fields, contactFields: contactCustomFields, isLoading: contactCustomFieldsLoading, isError: contactCustomFieldsError, mappings: contacts.customFieldMappings ?? {}, onRetry: onRetryContactCustomFields, onChange: (customFieldMappings) => patchContacts({ customFieldMappings }) })] }), visible.has("email") && _jsxs("section", { className: sectionClassName, children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Email action" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Send an operator email, respondent email, or both after the canonical submission is stored. Templates can include outcomeLabel, outcomeBucket, and outcomeScore." })] }), showSectionSwitches && _jsx(Switch, { checked: email.enabled, onCheckedChange: (enabled) => patchEmail({ enabled }) })] }), _jsxs("div", { className: twoColumnGridClassName, children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-from", children: "From override" }), _jsx(Input, { id: "capture-email-from", placeholder: "Forms <forms@example.com>", value: email.from ?? "", onChange: (event) => patchEmail({ from: event.target.value || undefined }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-internal", children: "Operator recipient" }), _jsx(Input, { id: "capture-email-internal", type: "email", placeholder: "ops@example.com", value: email.internalRecipientEmail ?? "", onChange: (event) => patchEmail({ internalRecipientEmail: event.target.value || undefined }) })] }), _jsx(FieldSelect, { label: "Respondent email answer", value: email.respondentEmailFieldKey ?? "", fields: fields, onChange: (respondentEmailFieldKey) => patchEmail({ respondentEmailFieldKey }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-subject", children: "Subject template" }), _jsx(Input, { id: "capture-email-subject", value: email.subjectTemplate ?? "", onChange: (event) => patchEmail({ subjectTemplate: event.target.value || undefined }) })] })] }), _jsxs("div", { className: "mt-4 space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-body", children: "Body template" }), _jsx(Textarea, { id: "capture-email-body", value: email.bodyTemplate ?? "", onChange: (event) => patchEmail({ bodyTemplate: event.target.value || undefined }) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Template tokens can reference field keys like ", "{{email_ab12}}", "; ", "{{answers}}", " inserts all answers."] })] })] }), visible.has("workflow") && _jsxs("section", { className: sectionClassName, children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(GitBranch, { className: "h-4 w-4 text-primary" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Workflow trigger action" })] }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Start a current-scope workflow after submission. API trigger-compatible workflows receive answers plus quiz outcome summary." })] }), showSectionSwitches && _jsx(Switch, { checked: workflow.enabled, onCheckedChange: (enabled) => patchWorkflow({ enabled }) })] }), workflowsError ? (_jsx("div", { className: "mt-4 rounded-xl border border-destructive/30 bg-destructive/5 p-4", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx(AlertCircle, { className: "mt-0.5 h-4 w-4 shrink-0 text-destructive" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h4", { className: "text-sm font-semibold text-foreground", children: "Could not load workflows" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Workflow actions are optional. Retry to select a workflow trigger, or keep this action disabled and save other settings." }), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "mt-3", onClick: onRetryWorkflows, children: "Try again" })] })] }) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: compact ? "mt-4 grid gap-4" : "mt-4 grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Workflow definition" }), _jsxs("select", { className: "h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50", value: workflow.workflowDefinitionId ?? "", disabled: workflowsLoading || workflows.length === 0, onChange: (event) => {
|
|
1876
|
+
const next = workflows.find((candidate) => candidate.id === event.target.value);
|
|
1877
|
+
patchWorkflow({
|
|
1878
|
+
workflowDefinitionId: next?.id,
|
|
1879
|
+
triggerNodeId: next?.apiTriggerNodeId,
|
|
1880
|
+
});
|
|
1881
|
+
}, children: [_jsx("option", { value: "", children: workflowsLoading ? "Loading workflows…" : workflows.length === 0 ? "No compatible workflows available" : "Select workflow…" }), workflows.map((candidate) => (_jsxs("option", { value: candidate.id, children: [candidate.name, " \u00B7 ", candidate.compatible ? "API trigger" : "not API-ready"] }, candidate.id)))] }), !workflowsLoading && workflows.length === 0 && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Workflow triggering is optional. Create an API-trigger-compatible workflow to enable this action." }))] }), _jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border p-3", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Execute immediately" }), _jsx(Switch, { checked: workflow.execute ?? true, onCheckedChange: (execute) => patchWorkflow({ execute }), disabled: workflowsLoading || workflows.length === 0 })] })] }), _jsxs("div", { className: compact ? "mt-4 grid gap-4" : "mt-4 grid gap-4 lg:grid-cols-2", children: [_jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h4", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Compatibility" }), workflowsLoading ? (_jsxs("div", { className: "mt-2 space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-2/3" }), _jsx(Skeleton, { className: "h-3 w-1/2" })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: cn("mt-2 text-sm", selectedWorkflow?.compatible ? "text-foreground" : selectedWorkflow ? "text-destructive" : "text-muted-foreground"), children: selectedWorkflow?.summary ?? "Select a workflow to inspect trigger compatibility." }), selectedWorkflow && (_jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: ["Version ", selectedWorkflow.currentVersion, " \u00B7 Status ", selectedWorkflow.status, " \u00B7 Triggers ", selectedWorkflow.triggerTypes.join(", ") || "none"] }))] }))] }), _jsxs("div", { className: "rounded-xl border border-border bg-muted/30 p-3", children: [_jsx("h4", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Payload preview" }), _jsx("pre", { className: "mt-2 max-h-48 overflow-auto rounded-lg bg-background p-2 text-xs text-muted-foreground", children: JSON.stringify(workflowPreview, null, 2) })] })] })] }))] })] }));
|
|
1882
|
+
}
|
|
1883
|
+
function CustomFieldMappingsEditor({ fields, contactFields, mappings, isLoading, isError, onRetry, onChange, }) {
|
|
1884
|
+
const mappedKeys = Object.keys(mappings);
|
|
1885
|
+
const availableContactFields = contactFields.filter((field) => !mappedKeys.includes(field.key));
|
|
1886
|
+
const updateMapping = (contactFieldKey, answerFieldKey) => {
|
|
1887
|
+
const next = { ...mappings };
|
|
1888
|
+
if (answerFieldKey)
|
|
1889
|
+
next[contactFieldKey] = answerFieldKey;
|
|
1890
|
+
else
|
|
1891
|
+
delete next[contactFieldKey];
|
|
1892
|
+
onChange(next);
|
|
1893
|
+
};
|
|
1894
|
+
const renameMapping = (previousKey, nextKey) => {
|
|
1895
|
+
if (!nextKey || nextKey === previousKey)
|
|
1896
|
+
return;
|
|
1897
|
+
const next = { ...mappings };
|
|
1898
|
+
const answerFieldKey = next[previousKey];
|
|
1899
|
+
delete next[previousKey];
|
|
1900
|
+
if (answerFieldKey)
|
|
1901
|
+
next[nextKey] = answerFieldKey;
|
|
1902
|
+
onChange(next);
|
|
1903
|
+
};
|
|
1904
|
+
return (_jsxs("div", { className: "mt-4 space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Custom field mappings" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Map Contacts custom fields to Capture answers. Add as many mappings as this form needs." })] }), isError ? (_jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-3", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Could not load Contacts custom fields." }), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "mt-2", onClick: onRetry, children: "Try again" })] })) : isLoading ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-9 w-full" }), _jsx(Skeleton, { className: "h-9 w-full" })] })) : contactFields.length === 0 ? (_jsx("div", { className: "rounded-lg border border-dashed border-border bg-muted/25 p-3 text-sm text-muted-foreground", children: "No Contacts custom fields are available yet." })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "space-y-2", children: mappedKeys.length === 0 ? (_jsx("p", { className: "rounded-lg border border-dashed border-border bg-muted/25 p-3 text-sm text-muted-foreground", children: "No custom fields mapped." })) : mappedKeys.map((contactFieldKey) => {
|
|
1905
|
+
const selectedContactField = contactFields.find((field) => field.key === contactFieldKey);
|
|
1906
|
+
const selectableContactFields = [
|
|
1907
|
+
...(selectedContactField ? [selectedContactField] : [{ id: contactFieldKey, key: contactFieldKey, label: contactFieldKey, type: "text" }]),
|
|
1908
|
+
...availableContactFields,
|
|
1909
|
+
];
|
|
1910
|
+
return (_jsxs("div", { className: "grid gap-2 rounded-lg border border-border bg-muted/20 p-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Contact field" }), _jsx("select", { className: "h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", value: contactFieldKey, onChange: (event) => renameMapping(contactFieldKey, event.target.value), children: selectableContactFields.map((field) => _jsx("option", { value: field.key, children: field.label }, field.key)) })] }), _jsx(FieldSelect, { label: "Capture answer", value: mappings[contactFieldKey] ?? "", fields: fields, onChange: (answerFieldKey) => updateMapping(contactFieldKey, answerFieldKey) }), _jsx("div", { className: "flex items-end", children: _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", onClick: () => updateMapping(contactFieldKey, undefined), "aria-label": "Remove mapping", title: "Remove mapping", children: _jsx(Trash2, { className: "h-4 w-4" }) }) })] }, contactFieldKey));
|
|
1911
|
+
}) }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
1912
|
+
const nextField = availableContactFields[0];
|
|
1913
|
+
const firstAnswerField = fields[0];
|
|
1914
|
+
if (nextField && firstAnswerField)
|
|
1915
|
+
updateMapping(nextField.key, firstAnswerField.key);
|
|
1916
|
+
}, disabled: availableContactFields.length === 0 || fields.length === 0, children: [_jsx(Plus, { className: "h-4 w-4" }), "Add custom field mapping"] })] }))] }));
|
|
298
1917
|
}
|
|
299
1918
|
function FieldSelect({ label, value, fields, onChange, }) {
|
|
300
1919
|
return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: label }), _jsxs("select", { className: "h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", value: value, onChange: (event) => onChange(event.target.value || undefined), children: [_jsx("option", { value: "", children: "Not mapped" }), fields.map((field) => _jsxs("option", { value: field.key, children: [field.label, " (", field.key, ")"] }, field.id))] })] }));
|
|
301
1920
|
}
|
|
302
|
-
function
|
|
303
|
-
|
|
1921
|
+
function ImageBlockControls({ item, onChange, }) {
|
|
1922
|
+
const resolveStorageImageMutation = useMutation(captureApi.definitions.resolveStorageImageUrl.mutationOptions());
|
|
1923
|
+
const handleUpload = (event) => {
|
|
1924
|
+
const file = event.target.files?.[0];
|
|
1925
|
+
if (!file)
|
|
1926
|
+
return;
|
|
1927
|
+
const reader = new FileReader();
|
|
1928
|
+
reader.onloadend = () => {
|
|
1929
|
+
if (typeof reader.result === "string")
|
|
1930
|
+
onChange({ imageUrl: reader.result, source: { type: "upload" } });
|
|
1931
|
+
};
|
|
1932
|
+
reader.readAsDataURL(file);
|
|
1933
|
+
event.target.value = "";
|
|
1934
|
+
};
|
|
1935
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Image" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Upload a standalone image and choose its shape." })] }), _jsx("div", { className: "flex justify-center", children: item.imageUrl ? (_jsx("img", { src: item.imageUrl, alt: item.altText ?? "", className: getImageClassName(item) })) : (_jsx("div", { className: cn("flex items-center justify-center border border-dashed border-border bg-muted/30 text-sm text-muted-foreground", getImageClassName(item)), children: "No image" })) }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Image source" }), _jsx(AttachImageButton, { hasImage: !!item.imageUrl || item.source?.type === "storage", onUpload: handleUpload, onSelect: (picked) => {
|
|
1936
|
+
if (picked.kind === "external") {
|
|
1937
|
+
onChange({ imageUrl: picked.url, source: { type: "url" } });
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const patch = {
|
|
1941
|
+
altText: item.altText || picked.name,
|
|
1942
|
+
source: { type: "storage", storageItemId: picked.itemId },
|
|
1943
|
+
};
|
|
1944
|
+
onChange(patch);
|
|
1945
|
+
void resolveStorageImageMutation.mutateAsync({ itemId: picked.itemId })
|
|
1946
|
+
.then((result) => {
|
|
1947
|
+
onChange({ ...patch, imageUrl: result.url });
|
|
1948
|
+
toast.success("Storage image selected");
|
|
1949
|
+
})
|
|
1950
|
+
.catch(() => {
|
|
1951
|
+
toast.error("Image selected, but preview URL could not be loaded");
|
|
1952
|
+
});
|
|
1953
|
+
} })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "image-alt", children: "Alt text" }), _jsx(Input, { id: "image-alt", value: item.altText ?? "", placeholder: "Describe the image", onChange: (event) => onChange({ altText: event.target.value || undefined }) })] }), _jsx(SegmentedControl, { label: "Size", value: item.size ?? "medium", options: [["small", "Small"], ["medium", "Medium"], ["large", "Large"], ["full", "Full"]], onChange: (value) => onChange({ size: value }) }), _jsx(SegmentedControl, { label: "Fit", value: item.fit ?? "cover", options: [["cover", "Fill frame"], ["contain", "Fit image"]], onChange: (value) => onChange({ fit: value }) }), _jsx(SegmentedControl, { label: "Shape", value: item.shape ?? "square", options: [["square", "Square"], ["circle", "Circle"]], onChange: (value) => onChange({ shape: value }) }), _jsx(SegmentedControl, { label: "Alignment", value: item.align ?? "center", options: [["left", "Left"], ["center", "Center"], ["right", "Right"]], onChange: (value) => onChange({ align: value }) })] }));
|
|
1954
|
+
}
|
|
1955
|
+
function AttachImageButton({ hasImage, onSelect, onUpload, }) {
|
|
1956
|
+
const [open, setOpen] = useState(false);
|
|
1957
|
+
const inputId = useMemo(() => `capture-local-image-${Math.random().toString(36).slice(2)}`, []);
|
|
1958
|
+
let hasStoragePicker = true;
|
|
1959
|
+
try {
|
|
1960
|
+
useAssetPickerHooks();
|
|
1961
|
+
}
|
|
1962
|
+
catch {
|
|
1963
|
+
hasStoragePicker = false;
|
|
1964
|
+
}
|
|
1965
|
+
const buttonLabel = hasImage ? "Replace image" : "Attach image";
|
|
1966
|
+
if (!hasStoragePicker) {
|
|
1967
|
+
return (_jsxs("div", { children: [_jsx("input", { id: inputId, type: "file", accept: "image/*", className: "sr-only", onChange: onUpload }), _jsx(Button, { type: "button", variant: "outline", className: "w-full justify-start", asChild: true, children: _jsxs("label", { htmlFor: inputId, className: "cursor-pointer", children: [_jsx(ImageIcon, { className: "h-4 w-4" }), buttonLabel] }) }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Uses local upload when storage is not configured." })] }));
|
|
1968
|
+
}
|
|
1969
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-start", onClick: () => setOpen(true), children: [_jsx(ImageIcon, { className: "h-4 w-4" }), buttonLabel] }), _jsx(StorageAssetPicker, { open: open, onOpenChange: setOpen, mode: "upload-or-pick", initialDestination: ["capture-images"], ensureInitialDestination: true, mimePrefix: "image/", allowExternalUrl: true, onSelect: (item) => {
|
|
1970
|
+
onSelect(item);
|
|
1971
|
+
setOpen(false);
|
|
1972
|
+
} })] }));
|
|
1973
|
+
}
|
|
1974
|
+
function IconBlockControls({ item, onChange, }) {
|
|
1975
|
+
const [iconSearch, setIconSearch] = useState("");
|
|
1976
|
+
const pickerData = {
|
|
1977
|
+
icon: item.icon,
|
|
1978
|
+
iconType: "lucide",
|
|
1979
|
+
iconColor: item.iconColor ?? "indigo",
|
|
1980
|
+
backgroundColor: "slate",
|
|
1981
|
+
};
|
|
1982
|
+
const filteredIcons = ICON_LIST.filter((icon) => icon.name.toLowerCase().includes(iconSearch.toLowerCase()));
|
|
1983
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Icon" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Add a standalone visual marker between form items." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(AvatarPreview, { data: pickerData, size: "md" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm font-medium text-foreground", children: item.icon }), _jsx("p", { className: "text-xs text-muted-foreground", children: item.iconColor ?? "indigo" })] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Icon color" }), _jsx("div", { className: "grid grid-cols-10 gap-2", children: COLORS.map((color) => (_jsx("button", { type: "button", onClick: () => onChange({ iconColor: color.value }), style: { backgroundColor: getColorStyle(color.value).hex }, className: cn("h-6 w-6 rounded-full border border-border transition-all", (item.iconColor ?? "indigo") === color.value
|
|
1984
|
+
? "scale-110 ring-2 ring-muted-foreground ring-offset-2"
|
|
1985
|
+
: "opacity-90 hover:scale-110 hover:opacity-100"), title: color.name, "aria-label": color.name, "aria-pressed": (item.iconColor ?? "indigo") === color.value }, color.value))) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "icon-search", className: "text-sm font-medium text-foreground", children: "Choose icon" }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" }), _jsx(Input, { id: "icon-search", value: iconSearch, onChange: (event) => setIconSearch(event.target.value), placeholder: "Search icons...", className: "h-9 pl-9" })] }), _jsx("div", { className: "max-h-64 overflow-y-auto rounded-lg border border-border p-2", children: _jsxs("div", { className: "grid grid-cols-5 gap-2", children: [filteredIcons.map((icon) => {
|
|
1986
|
+
const IconComponent = icon.icon;
|
|
1987
|
+
const selected = item.icon === icon.name;
|
|
1988
|
+
return (_jsx("button", { type: "button", onClick: () => onChange({ icon: icon.name }), className: cn("flex aspect-square items-center justify-center rounded-lg border transition-colors", selected ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground"), title: icon.name, "aria-label": icon.name, "aria-pressed": selected, children: _jsx(IconComponent, { className: "h-5 w-5" }) }, icon.name));
|
|
1989
|
+
}), filteredIcons.length === 0 && _jsx("p", { className: "col-span-full py-4 text-center text-sm text-muted-foreground", children: "No icons found" })] }) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Alignment" }), _jsx("div", { className: "grid grid-cols-3 gap-1 rounded-md border border-input bg-muted/40 p-1", role: "group", "aria-label": "Icon alignment", children: [
|
|
1990
|
+
["left", "Left"],
|
|
1991
|
+
["center", "Center"],
|
|
1992
|
+
["right", "Right"],
|
|
1993
|
+
].map(([value, label]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", item.align === value
|
|
1994
|
+
? "bg-background text-foreground shadow-sm"
|
|
1995
|
+
: "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange({ align: value }), "aria-pressed": item.align === value, children: label }, value))) })] })] }));
|
|
1996
|
+
}
|
|
1997
|
+
function SegmentedControl({ label, value, options, onChange, }) {
|
|
1998
|
+
return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: label }), _jsx("div", { className: "grid gap-1 rounded-md border border-input bg-muted/40 p-1", style: { gridTemplateColumns: `repeat(${options.length}, minmax(0, 1fr))` }, role: "group", "aria-label": label, children: options.map(([optionValue, optionLabel]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", value === optionValue
|
|
1999
|
+
? "bg-background text-foreground shadow-sm"
|
|
2000
|
+
: "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange(optionValue), "aria-pressed": value === optionValue, children: optionLabel }, optionValue))) })] }));
|
|
2001
|
+
}
|
|
2002
|
+
function DeleteSelectedItemButton({ onDelete, label }) {
|
|
2003
|
+
return (_jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-start text-destructive hover:text-destructive", onClick: onDelete, children: [_jsx(Trash2, { className: "h-4 w-4" }), label] }));
|
|
2004
|
+
}
|
|
2005
|
+
function FieldKeyControl({ field, locked, duplicate, onChange, }) {
|
|
2006
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Label, { htmlFor: "field-key", children: "Key" }), _jsx("span", { className: "text-xs text-muted-foreground", children: locked ? "Locked after save" : duplicate ? "Must be unique" : "Unique in this form" })] }), _jsx(Input, { id: "field-key", value: field.key, disabled: locked, "aria-invalid": duplicate || undefined, onChange: (event) => onChange(event.target.value) }), duplicate && _jsx("p", { className: "text-xs text-destructive", children: "This key is already used by another field in this form." })] }));
|
|
2007
|
+
}
|
|
2008
|
+
function RatingFieldControls({ field, onChange, }) {
|
|
2009
|
+
const icon = getRatingIcon(field);
|
|
2010
|
+
const align = getRatingAlign(field);
|
|
2011
|
+
const color = getRatingColor(field);
|
|
2012
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Rating icon" }), _jsx("div", { className: "grid grid-cols-4 gap-1 rounded-md border border-input bg-muted/40 p-1", role: "group", "aria-label": "Rating icon", children: [
|
|
2013
|
+
["star", "Star"],
|
|
2014
|
+
["circle", "Circle"],
|
|
2015
|
+
["square", "Square"],
|
|
2016
|
+
["heart", "Heart"],
|
|
2017
|
+
].map(([value, label]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", icon === value
|
|
2018
|
+
? "bg-background text-foreground shadow-sm"
|
|
2019
|
+
: "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange({ ratingIcon: value }), "aria-pressed": icon === value, children: label }, value))) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Color" }), _jsx("div", { className: "grid grid-cols-9 gap-2", role: "group", "aria-label": "Rating color", children: CAPTURE_RATING_COLORS.map((option) => (_jsx("button", { type: "button", onClick: () => onChange({ ratingColor: option.value }), style: { backgroundColor: option.hex }, className: cn("h-6 w-6 rounded-full border border-border transition-all", color === option.value
|
|
2020
|
+
? "scale-110 ring-2 ring-muted-foreground ring-offset-2"
|
|
2021
|
+
: "opacity-90 hover:scale-110 hover:opacity-100"), title: option.name, "aria-label": option.name, "aria-pressed": color === option.value }, option.value))) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Alignment" }), _jsx("div", { className: "grid grid-cols-3 gap-1 rounded-md border border-input bg-muted/40 p-1", role: "group", "aria-label": "Rating alignment", children: [
|
|
2022
|
+
["left", "Left"],
|
|
2023
|
+
["center", "Center"],
|
|
2024
|
+
["right", "Right"],
|
|
2025
|
+
].map(([value, label]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", align === value
|
|
2026
|
+
? "bg-background text-foreground shadow-sm"
|
|
2027
|
+
: "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange({ ratingAlign: value }), "aria-pressed": align === value, children: label }, value))) })] })] }));
|
|
2028
|
+
}
|
|
2029
|
+
function FieldLayoutControls({ field, onChange, }) {
|
|
2030
|
+
const layout = getFieldLayout(field);
|
|
2031
|
+
return (_jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Width" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Wrap" }), _jsx(Switch, { checked: layout.wrapAfter, onCheckedChange: (checked) => onChange({ wrapAfter: checked }) })] })] }), _jsx("div", { className: "grid grid-cols-3 gap-1 rounded-md border border-input bg-muted/40 p-1", role: "group", "aria-label": "Field width", children: [
|
|
2032
|
+
["1/3", "⅓"],
|
|
2033
|
+
["1/2", "½"],
|
|
2034
|
+
["full", "Full"],
|
|
2035
|
+
].map(([value, label]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", layout.width === value
|
|
2036
|
+
? "bg-background text-foreground shadow-sm"
|
|
2037
|
+
: "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange({ width: value }), "aria-pressed": layout.width === value, children: label }, value))) })] }) }));
|
|
2038
|
+
}
|
|
2039
|
+
function supportsPlaceholder(field) {
|
|
2040
|
+
return field.type === "text" || field.type === "email" || field.type === "phone" || field.type === "textarea";
|
|
2041
|
+
}
|
|
2042
|
+
function StaticVisibilityControl({ hidden, onHiddenChange, }) {
|
|
2043
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Visible" }), _jsx(Switch, { checked: !hidden, onCheckedChange: (checked) => onHiddenChange(!checked) })] }));
|
|
304
2044
|
}
|
|
305
2045
|
function EmptyPanelText({ children }) {
|
|
306
2046
|
return _jsx("p", { className: "rounded-xl bg-muted/40 p-3 text-sm text-muted-foreground", children: children });
|