@braedonsaunders/appkit-forms 0.2.2
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/LICENSE +661 -0
- package/README.md +74 -0
- package/canvas-editor.d.ts +24 -0
- package/canvas-editor.d.ts.map +1 -0
- package/canvas-editor.js +115 -0
- package/canvas-editor.js.map +1 -0
- package/canvas-runtime.d.ts +12 -0
- package/canvas-runtime.d.ts.map +1 -0
- package/canvas-runtime.js +54 -0
- package/canvas-runtime.js.map +1 -0
- package/element-preview.d.ts +9 -0
- package/element-preview.d.ts.map +1 -0
- package/element-preview.js +167 -0
- package/element-preview.js.map +1 -0
- package/form-designer.d.ts +29 -0
- package/form-designer.d.ts.map +1 -0
- package/form-designer.js +214 -0
- package/form-designer.js.map +1 -0
- package/form-flow-validation.d.ts +5 -0
- package/form-flow-validation.d.ts.map +1 -0
- package/form-flow-validation.js +154 -0
- package/form-flow-validation.js.map +1 -0
- package/form-renderer.d.ts +37 -0
- package/form-renderer.d.ts.map +1 -0
- package/form-renderer.js +282 -0
- package/form-renderer.js.map +1 -0
- package/formula-builder.d.ts +25 -0
- package/formula-builder.d.ts.map +1 -0
- package/formula-builder.js +310 -0
- package/formula-builder.js.map +1 -0
- package/generated-copy.d.ts +17 -0
- package/generated-copy.d.ts.map +1 -0
- package/generated-copy.js +396 -0
- package/generated-copy.js.map +1 -0
- package/index.d.ts +27 -0
- package/index.d.ts.map +1 -0
- package/index.js +25 -0
- package/index.js.map +1 -0
- package/logic-builder.d.ts +27 -0
- package/logic-builder.d.ts.map +1 -0
- package/logic-builder.js +106 -0
- package/logic-builder.js.map +1 -0
- package/package.json +54 -0
- package/photo-field-state.d.ts +9 -0
- package/photo-field-state.d.ts.map +1 -0
- package/photo-field-state.js +11 -0
- package/photo-field-state.js.map +1 -0
- package/production-designer-adapter.d.ts +100 -0
- package/production-designer-adapter.d.ts.map +1 -0
- package/production-designer-adapter.js +2 -0
- package/production-designer-adapter.js.map +1 -0
- package/production-file-upload.d.ts +27 -0
- package/production-file-upload.d.ts.map +1 -0
- package/production-file-upload.js +105 -0
- package/production-file-upload.js.map +1 -0
- package/production-form-designer.d.ts +3 -0
- package/production-form-designer.d.ts.map +1 -0
- package/production-form-designer.js +1124 -0
- package/production-form-designer.js.map +1 -0
- package/production-form-renderer.d.ts +16 -0
- package/production-form-renderer.d.ts.map +1 -0
- package/production-form-renderer.js +2134 -0
- package/production-form-renderer.js.map +1 -0
- package/production-runtime-adapter.d.ts +187 -0
- package/production-runtime-adapter.d.ts.map +1 -0
- package/production-runtime-adapter.js +2 -0
- package/production-runtime-adapter.js.map +1 -0
- package/production-runtime-context.d.ts +29 -0
- package/production-runtime-context.d.ts.map +1 -0
- package/production-runtime-context.js +35 -0
- package/production-runtime-context.js.map +1 -0
- package/production-runtime-layout.d.ts +21 -0
- package/production-runtime-layout.d.ts.map +1 -0
- package/production-runtime-layout.js +33 -0
- package/production-runtime-layout.js.map +1 -0
- package/properties.d.ts +36 -0
- package/properties.d.ts.map +1 -0
- package/properties.js +133 -0
- package/properties.js.map +1 -0
- package/record-actions-panel.d.ts +28 -0
- package/record-actions-panel.d.ts.map +1 -0
- package/record-actions-panel.js +136 -0
- package/record-actions-panel.js.map +1 -0
- package/record-behavior-panel.d.ts +15 -0
- package/record-behavior-panel.d.ts.map +1 -0
- package/record-behavior-panel.js +59 -0
- package/record-behavior-panel.js.map +1 -0
- package/record-config.d.ts +44 -0
- package/record-config.d.ts.map +1 -0
- package/record-config.js +2 -0
- package/record-config.js.map +1 -0
- package/record-list-panel.d.ts +16 -0
- package/record-list-panel.d.ts.map +1 -0
- package/record-list-panel.js +86 -0
- package/record-list-panel.js.map +1 -0
- package/risk-matrix.d.ts +93 -0
- package/risk-matrix.d.ts.map +1 -0
- package/risk-matrix.js +239 -0
- package/risk-matrix.js.map +1 -0
- package/runtime-controls.d.ts +19 -0
- package/runtime-controls.d.ts.map +1 -0
- package/runtime-controls.js +61 -0
- package/runtime-controls.js.map +1 -0
- package/sketch-pad-client.d.ts +4 -0
- package/sketch-pad-client.d.ts.map +1 -0
- package/sketch-pad-client.js +54 -0
- package/sketch-pad-client.js.map +1 -0
- package/sketch-pad.d.ts +13 -0
- package/sketch-pad.d.ts.map +1 -0
- package/sketch-pad.js +17 -0
- package/sketch-pad.js.map +1 -0
- package/styles.css +7 -0
- package/text.d.ts +5 -0
- package/text.d.ts.map +1 -0
- package/text.js +10 -0
- package/text.js.map +1 -0
- package/workflow-properties.d.ts +16 -0
- package/workflow-properties.d.ts.map +1 -0
- package/workflow-properties.js +31 -0
- package/workflow-properties.js.map +1 -0
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { GeneratedText, useGeneratedTranslations, GeneratedValue, useGeneratedValueTranslations, } from './generated-copy.js';
|
|
4
|
+
// Form designer.
|
|
5
|
+
//
|
|
6
|
+
// Three-pane layout:
|
|
7
|
+
// - Left: field palette (categorized, with icons), section list + workflow step editor
|
|
8
|
+
// - Middle: canvas — sections and fields, palette drops, drag reorder, click-to-select
|
|
9
|
+
// - Right: properties panel for the current selection (form / section / field) with
|
|
10
|
+
// tabs for Basic / Validation / Logic / Default / Calc
|
|
11
|
+
//
|
|
12
|
+
// All edits mutate a local copy of FormSchemaV1. Publishing writes a new
|
|
13
|
+
// immutable version through the host adapter.
|
|
14
|
+
import { useEffect, useRef, useState, useTransition, useMemo } from 'react';
|
|
15
|
+
import { AlignLeft, ArrowDown, ArrowUp, Calculator, Calendar, Check, CheckCircle2, CheckSquare, ChevronDown, ClipboardCheck, Eye, FileText, Globe2, GripVertical, Hash, Image as ImageIcon, ListChecks, Mail, MapPin, Mic, Minus, Pencil, Pin, PinOff, Phone, Plus, Radio, Save, ShieldCheck, Signal, Sliders, Star, ToggleLeft, Table2, Trash2, Type, Upload, User, Users, Video, } from 'lucide-react';
|
|
16
|
+
import { Reorder, useDragControls } from 'framer-motion';
|
|
17
|
+
import { Alert, AlertDescription, AlertTitle, Badge, Button, Card, CardContent, CardHeader, CardTitle, Drawer, Input, Label, Select, Textarea, toast, } from '@braedonsaunders/appkit-ui';
|
|
18
|
+
import { FIELD_TYPES, entityKindForPicker, storesResponseValue, } from '@braedonsaunders/appkit-forms-core';
|
|
19
|
+
import { localizeText } from '@braedonsaunders/appkit-i18n';
|
|
20
|
+
import { RecordBehaviorPanel } from './record-behavior-panel.js';
|
|
21
|
+
import { RecordActionsPanel } from './record-actions-panel.js';
|
|
22
|
+
import { RecordListPanel } from './record-list-panel.js';
|
|
23
|
+
import { readText, writeText } from './text.js';
|
|
24
|
+
import { LogicBuilder } from './logic-builder.js';
|
|
25
|
+
import { FormulaBuilder } from './formula-builder.js';
|
|
26
|
+
import { CanvasEditor, defaultBox } from './canvas-editor.js';
|
|
27
|
+
import { BarChart3, Bold, Briefcase, Building2, Database, Info, LayoutGrid, ListOrdered, Map as MapIcon, MapPinned, MousePointerClick, PanelLeft, PenTool, RefreshCw, ScanLine, Send, SlidersHorizontal, Sparkles, Workflow as WorkflowIcon, } from 'lucide-react';
|
|
28
|
+
// --- Palette ---------------------------------------------------------------
|
|
29
|
+
// Field-type icon registry. Falls back to a generic Type icon for unknowns.
|
|
30
|
+
const FIELD_ICONS = {
|
|
31
|
+
text: Type,
|
|
32
|
+
long_text: AlignLeft,
|
|
33
|
+
number: Hash,
|
|
34
|
+
currency: Calculator,
|
|
35
|
+
percentage: Hash,
|
|
36
|
+
slider: Sliders,
|
|
37
|
+
gps: MapPin,
|
|
38
|
+
matrix: LayoutGrid,
|
|
39
|
+
date: Calendar,
|
|
40
|
+
datetime: Calendar,
|
|
41
|
+
time: Calendar,
|
|
42
|
+
email: Mail,
|
|
43
|
+
phone: Phone,
|
|
44
|
+
url: FileText,
|
|
45
|
+
radio: Radio,
|
|
46
|
+
checkbox_group: CheckSquare,
|
|
47
|
+
select: ChevronDown,
|
|
48
|
+
multi_select: ListChecks,
|
|
49
|
+
pass_fail_na: ShieldCheck,
|
|
50
|
+
rating: Star,
|
|
51
|
+
yes_no_comment: ToggleLeft,
|
|
52
|
+
traffic_light: Signal,
|
|
53
|
+
person_picker: User,
|
|
54
|
+
multi_person_picker: Users,
|
|
55
|
+
customer_picker: Building2,
|
|
56
|
+
project_picker: Briefcase,
|
|
57
|
+
site_picker: MapPin,
|
|
58
|
+
area_picker: MapIcon,
|
|
59
|
+
gl_account: Database,
|
|
60
|
+
party: Building2,
|
|
61
|
+
photo: ImageIcon,
|
|
62
|
+
photo_upload: ImageIcon,
|
|
63
|
+
photo_ai: Sparkles,
|
|
64
|
+
photo_annotated: MapPinned,
|
|
65
|
+
file: Upload,
|
|
66
|
+
video: Video,
|
|
67
|
+
audio: Mic,
|
|
68
|
+
sketch: PenTool,
|
|
69
|
+
signature: Pencil,
|
|
70
|
+
typed_attestation: CheckCircle2,
|
|
71
|
+
formula: Calculator,
|
|
72
|
+
risk_matrix: Sliders,
|
|
73
|
+
heading: Type,
|
|
74
|
+
paragraph: AlignLeft,
|
|
75
|
+
divider: Minus,
|
|
76
|
+
table: Table2,
|
|
77
|
+
lookup: Database,
|
|
78
|
+
data_table: Table2,
|
|
79
|
+
metric: BarChart3,
|
|
80
|
+
qr_scanner: ScanLine,
|
|
81
|
+
ranking: ListOrdered,
|
|
82
|
+
rich_text: Bold,
|
|
83
|
+
address: MapPin,
|
|
84
|
+
};
|
|
85
|
+
// One element per concept — no duplicates across groups.
|
|
86
|
+
const PALETTE_PRIMARY = [
|
|
87
|
+
{
|
|
88
|
+
label: 'Common',
|
|
89
|
+
types: [
|
|
90
|
+
'text',
|
|
91
|
+
'long_text',
|
|
92
|
+
'number',
|
|
93
|
+
'currency',
|
|
94
|
+
'percentage',
|
|
95
|
+
'slider',
|
|
96
|
+
'date',
|
|
97
|
+
'table',
|
|
98
|
+
'select',
|
|
99
|
+
'checkbox_group',
|
|
100
|
+
'pass_fail_na',
|
|
101
|
+
'signature',
|
|
102
|
+
'photo',
|
|
103
|
+
'file',
|
|
104
|
+
'person_picker',
|
|
105
|
+
'formula',
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
const PALETTE_MORE = [
|
|
110
|
+
{
|
|
111
|
+
label: 'Date & contact',
|
|
112
|
+
types: ['datetime', 'time', 'gps', 'address', 'email', 'phone', 'url', 'qr_scanner'],
|
|
113
|
+
},
|
|
114
|
+
{ label: 'Choice', types: ['radio', 'multi_select', 'ranking', 'matrix'] },
|
|
115
|
+
{ label: 'Scoring', types: ['rating', 'yes_no_comment', 'traffic_light'] },
|
|
116
|
+
{
|
|
117
|
+
label: 'Pickers',
|
|
118
|
+
types: [
|
|
119
|
+
'multi_person_picker',
|
|
120
|
+
'customer_picker',
|
|
121
|
+
'project_picker',
|
|
122
|
+
'site_picker',
|
|
123
|
+
'area_picker',
|
|
124
|
+
'gl_account',
|
|
125
|
+
'party',
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{ label: 'Media', types: ['photo_ai', 'photo_annotated', 'sketch', 'video', 'audio'] },
|
|
129
|
+
{ label: 'Computed', types: ['risk_matrix', 'typed_attestation'] },
|
|
130
|
+
{ label: 'Data', types: ['lookup', 'data_table', 'metric'] },
|
|
131
|
+
{ label: 'Display', types: ['heading', 'paragraph', 'rich_text', 'divider'] },
|
|
132
|
+
];
|
|
133
|
+
function newId(prefix) {
|
|
134
|
+
return `${prefix}_${globalThis.crypto.randomUUID()}`;
|
|
135
|
+
}
|
|
136
|
+
function emptyField(type) {
|
|
137
|
+
return {
|
|
138
|
+
id: newId('f'),
|
|
139
|
+
type,
|
|
140
|
+
label: { en: FIELD_TYPES[type].label },
|
|
141
|
+
required: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function emptyStep() {
|
|
145
|
+
return {
|
|
146
|
+
key: newId('step'),
|
|
147
|
+
title: { en: 'New step' },
|
|
148
|
+
assignee: { type: 'expression', expr: '$submitter' },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const KIND_META = {
|
|
152
|
+
form: { label: 'Form', cls: 'bg-primary-subtle text-primary' },
|
|
153
|
+
wizard: {
|
|
154
|
+
label: 'Wizard',
|
|
155
|
+
cls: 'bg-primary-subtle text-primary',
|
|
156
|
+
},
|
|
157
|
+
checklist: {
|
|
158
|
+
label: 'Checklist',
|
|
159
|
+
cls: 'bg-success-subtle text-success',
|
|
160
|
+
},
|
|
161
|
+
register: {
|
|
162
|
+
label: 'Register',
|
|
163
|
+
cls: 'bg-warning-subtle text-warning',
|
|
164
|
+
},
|
|
165
|
+
mini_app: {
|
|
166
|
+
label: 'Mini-app',
|
|
167
|
+
cls: 'bg-primary-subtle text-primary',
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
function normalizeDesignerSchema(schema) {
|
|
171
|
+
return {
|
|
172
|
+
...structuredClone(schema),
|
|
173
|
+
workflow: schema.workflow ?? {
|
|
174
|
+
steps: [{ key: 'submit', title: 'Submit', assignee: { type: 'expression', expr: '$submitter' } }],
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function ProductionFormDesigner({ adapter, templateId, templateName, templateKind = 'form', initialSchema, currentVersion, initialSurface = 'build', overview, recordConfig, allowedRoles = [], roles = [], flows = [], recordActionAdapter, dataSources = [], dataSourcesLoading = false, onRefreshDataSources, renderFlows, renderAssistant, backHref, recordsHref, assignmentCreateHref, assignmentsHref, dataSourcesHref, publishedHref, canPin = false, pinned = false, onSchemaChange, locale, defaultLocale, enabledLocales, localeLabels, className, }) {
|
|
179
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
180
|
+
const tGenerated = useGeneratedTranslations();
|
|
181
|
+
const languages = (value) => localeLabels?.[value] ?? value.toUpperCase();
|
|
182
|
+
const [schema, setSchema] = useState(() => normalizeDesignerSchema(initialSchema));
|
|
183
|
+
const [appName, setAppName] = useState(templateName);
|
|
184
|
+
const [recordConfiguration, setRecordConfiguration] = useState(recordConfig ?? {});
|
|
185
|
+
const [isPinned, setIsPinned] = useState(pinned);
|
|
186
|
+
// Unified editor: left rail tab + right surface.
|
|
187
|
+
const [leftTab, setLeftTab] = useState('build');
|
|
188
|
+
const [surface, setSurface] = useState(initialSurface === 'flows' && renderFlows ? 'flows' : 'build');
|
|
189
|
+
const [designerTab, setDesignerTab] = useState(() => initialSchema.tabs?.[0]?.id ?? '');
|
|
190
|
+
const [showAiAssistant, setShowAiAssistant] = useState(false);
|
|
191
|
+
// The element type currently being dragged from the left palette — read by the
|
|
192
|
+
// canvas on drop. (HTML5 dataTransfer is also set, for the browser's drag UX.)
|
|
193
|
+
const dragElementRef = useRef(null);
|
|
194
|
+
const [dropSectionId, setDropSectionId] = useState(null);
|
|
195
|
+
const [selection, setSelection] = useState({ kind: 'form' });
|
|
196
|
+
// Which right-hand flyout is open. Preview + properties used to be permanent
|
|
197
|
+
// columns (too cramped); now they slide in on demand.
|
|
198
|
+
const [rightPanel, setRightPanel] = useState('none');
|
|
199
|
+
const [showPublish, setShowPublish] = useState(false);
|
|
200
|
+
const [pending, start] = useTransition();
|
|
201
|
+
const [error, setError] = useState(null);
|
|
202
|
+
const [changelog, setChangelog] = useState('');
|
|
203
|
+
const [contentLocale, setContentLocale] = useState(locale);
|
|
204
|
+
useEffect(() => onSchemaChange?.(schema), [onSchemaChange, schema]);
|
|
205
|
+
// Selectable fields for the records-list "List" tab — real answer fields only
|
|
206
|
+
// (content-only display elements carry no data to show in a column).
|
|
207
|
+
const listFields = useMemo(() => {
|
|
208
|
+
const skip = new Set(['heading', 'paragraph', 'divider', 'metric']);
|
|
209
|
+
const out = [];
|
|
210
|
+
for (const sec of schema.sections)
|
|
211
|
+
for (const f of sec.fields) {
|
|
212
|
+
if (skip.has(f.type))
|
|
213
|
+
continue;
|
|
214
|
+
out.push({
|
|
215
|
+
id: f.id,
|
|
216
|
+
label: localizeText(f.label, contentLocale, f.id, defaultLocale),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}, [contentLocale, defaultLocale, schema]);
|
|
221
|
+
// Current records-list config lives under recordConfig.list (the page passes
|
|
222
|
+
// the full recordConfig jsonb; its type omits `list`, but it's there at runtime).
|
|
223
|
+
const listConfig = recordConfiguration.list;
|
|
224
|
+
let selectedField = null;
|
|
225
|
+
if (selection.kind === 'field') {
|
|
226
|
+
for (const sec of schema.sections) {
|
|
227
|
+
const f = sec.fields.find((x) => x.id === selection.fieldId);
|
|
228
|
+
if (f) {
|
|
229
|
+
selectedField = { section: sec, field: f };
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const selectedSection = useMemo(() => {
|
|
235
|
+
if (selection.kind !== 'section')
|
|
236
|
+
return null;
|
|
237
|
+
return schema.sections.find((s) => s.id === selection.sectionId) ?? null;
|
|
238
|
+
}, [schema, selection]);
|
|
239
|
+
function update(fn) {
|
|
240
|
+
setSchema((s) => fn(structuredClone(s)));
|
|
241
|
+
}
|
|
242
|
+
// Selecting something opens the properties flyout; workflow is edited in the
|
|
243
|
+
// canvas so it just switches the canvas view.
|
|
244
|
+
function selectField(sectionId, fieldId) {
|
|
245
|
+
setSelection({ kind: 'field', sectionId, fieldId });
|
|
246
|
+
setRightPanel('props');
|
|
247
|
+
}
|
|
248
|
+
function selectSection(sectionId) {
|
|
249
|
+
setSelection({ kind: 'section', sectionId });
|
|
250
|
+
setRightPanel('props');
|
|
251
|
+
}
|
|
252
|
+
function openWorkflow() {
|
|
253
|
+
setSelection({ kind: 'workflow' });
|
|
254
|
+
setRightPanel('none');
|
|
255
|
+
}
|
|
256
|
+
function addSection() {
|
|
257
|
+
const id = newId('sec');
|
|
258
|
+
update((draft) => {
|
|
259
|
+
draft.sections.push({
|
|
260
|
+
id,
|
|
261
|
+
title: { en: 'New section' },
|
|
262
|
+
fields: [],
|
|
263
|
+
// New sections land in the active designer tab (if the app uses tabs).
|
|
264
|
+
...(draft.tabs?.length ? { tabId: designerTab || draft.tabs[0].id } : {}),
|
|
265
|
+
});
|
|
266
|
+
return draft;
|
|
267
|
+
});
|
|
268
|
+
selectSection(id);
|
|
269
|
+
}
|
|
270
|
+
// --- Tabs (presentational app pages) -------------------------------------
|
|
271
|
+
function addTab() {
|
|
272
|
+
const id = newId('tab');
|
|
273
|
+
update((draft) => {
|
|
274
|
+
const tabs = draft.tabs ?? [];
|
|
275
|
+
const title = { en: `Tab ${tabs.length + 1}` };
|
|
276
|
+
if (tabs.length === 0) {
|
|
277
|
+
// First tab: pull every existing section into it so nothing is orphaned.
|
|
278
|
+
draft.tabs = [{ id, title }];
|
|
279
|
+
for (const s of draft.sections)
|
|
280
|
+
s.tabId = id;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
draft.tabs = [...tabs, { id, title }];
|
|
284
|
+
}
|
|
285
|
+
return draft;
|
|
286
|
+
});
|
|
287
|
+
setDesignerTab(id);
|
|
288
|
+
}
|
|
289
|
+
function renameTab(id, title) {
|
|
290
|
+
update((draft) => {
|
|
291
|
+
const t = draft.tabs?.find((x) => x.id === id);
|
|
292
|
+
if (t)
|
|
293
|
+
t.title = writeText(t.title, title, contentLocale);
|
|
294
|
+
return draft;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function deleteTab(id) {
|
|
298
|
+
update((draft) => {
|
|
299
|
+
const remaining = (draft.tabs ?? []).filter((t) => t.id !== id);
|
|
300
|
+
const fallback = remaining[0]?.id;
|
|
301
|
+
for (const s of draft.sections)
|
|
302
|
+
if (s.tabId === id)
|
|
303
|
+
s.tabId = fallback;
|
|
304
|
+
draft.tabs = remaining.length ? remaining : undefined;
|
|
305
|
+
return draft;
|
|
306
|
+
});
|
|
307
|
+
setDesignerTab((cur) => (cur === id ? (schema.tabs?.find((t) => t.id !== id)?.id ?? '') : cur));
|
|
308
|
+
}
|
|
309
|
+
function addField(sectionId, type) {
|
|
310
|
+
const f = emptyField(type);
|
|
311
|
+
update((draft) => {
|
|
312
|
+
const sec = draft.sections.find((s) => s.id === sectionId);
|
|
313
|
+
if (!sec)
|
|
314
|
+
return draft;
|
|
315
|
+
sec.fields.push(f);
|
|
316
|
+
// If the section is in canvas mode, also place the new element on the grid
|
|
317
|
+
// (append below everything) — otherwise a clicked element would be invisible.
|
|
318
|
+
if (sec.canvas) {
|
|
319
|
+
const box = defaultBox(type);
|
|
320
|
+
const bottomY = sec.canvas.items.reduce((m, it) => Math.max(m, it.y + it.h), 0);
|
|
321
|
+
sec.canvas = {
|
|
322
|
+
...sec.canvas,
|
|
323
|
+
items: [
|
|
324
|
+
...sec.canvas.items,
|
|
325
|
+
{ i: f.id, x: 0, y: bottomY, w: Math.min(box.w * 2, 12), h: box.h },
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return draft;
|
|
330
|
+
});
|
|
331
|
+
selectField(sectionId, f.id);
|
|
332
|
+
}
|
|
333
|
+
function deleteField(sectionId, fieldId) {
|
|
334
|
+
update((draft) => {
|
|
335
|
+
const sec = draft.sections.find((s) => s.id === sectionId);
|
|
336
|
+
if (!sec)
|
|
337
|
+
return draft;
|
|
338
|
+
sec.fields = sec.fields.filter((f) => f.id !== fieldId);
|
|
339
|
+
// Keep the canvas layout in sync — drop the deleted field's box.
|
|
340
|
+
if (sec.canvas)
|
|
341
|
+
sec.canvas = { ...sec.canvas, items: sec.canvas.items.filter((it) => it.i !== fieldId) };
|
|
342
|
+
return draft;
|
|
343
|
+
});
|
|
344
|
+
if (selection.kind === 'field' && selection.fieldId === fieldId) {
|
|
345
|
+
setSelection({ kind: 'section', sectionId });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function deleteSection(sectionId) {
|
|
349
|
+
update((draft) => {
|
|
350
|
+
draft.sections = draft.sections.filter((s) => s.id !== sectionId);
|
|
351
|
+
return draft;
|
|
352
|
+
});
|
|
353
|
+
setSelection({ kind: 'form' });
|
|
354
|
+
}
|
|
355
|
+
function moveField(sectionId, fieldId, delta) {
|
|
356
|
+
update((draft) => {
|
|
357
|
+
const sec = draft.sections.find((s) => s.id === sectionId);
|
|
358
|
+
if (!sec)
|
|
359
|
+
return draft;
|
|
360
|
+
const i = sec.fields.findIndex((f) => f.id === fieldId);
|
|
361
|
+
const j = i + delta;
|
|
362
|
+
if (i < 0 || j < 0 || j >= sec.fields.length)
|
|
363
|
+
return draft;
|
|
364
|
+
[sec.fields[i], sec.fields[j]] = [sec.fields[j], sec.fields[i]];
|
|
365
|
+
return draft;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
// Drag-to-reorder fields within a section (framer-motion Reorder). Replaces
|
|
369
|
+
// the schema's field order with the dragged order; the up/down arrows remain
|
|
370
|
+
// as a keyboard-accessible fallback.
|
|
371
|
+
function reorderFields(sectionId, fields) {
|
|
372
|
+
setSchema((s) => ({
|
|
373
|
+
...s,
|
|
374
|
+
sections: s.sections.map((sec) => (sec.id === sectionId ? { ...sec, fields } : sec)),
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
function moveSection(sectionId, delta) {
|
|
378
|
+
update((draft) => {
|
|
379
|
+
const i = draft.sections.findIndex((s) => s.id === sectionId);
|
|
380
|
+
const j = i + delta;
|
|
381
|
+
if (i < 0 || j < 0 || j >= draft.sections.length)
|
|
382
|
+
return draft;
|
|
383
|
+
[draft.sections[i], draft.sections[j]] = [draft.sections[j], draft.sections[i]];
|
|
384
|
+
return draft;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function updateField(sectionId, fieldId, patch) {
|
|
388
|
+
update((draft) => {
|
|
389
|
+
const sec = draft.sections.find((s) => s.id === sectionId);
|
|
390
|
+
if (!sec)
|
|
391
|
+
return draft;
|
|
392
|
+
const idx = sec.fields.findIndex((f) => f.id === fieldId);
|
|
393
|
+
if (idx < 0)
|
|
394
|
+
return draft;
|
|
395
|
+
sec.fields[idx] = { ...sec.fields[idx], ...patch };
|
|
396
|
+
return draft;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function updateSection(sectionId, patch) {
|
|
400
|
+
update((draft) => {
|
|
401
|
+
const i = draft.sections.findIndex((s) => s.id === sectionId);
|
|
402
|
+
if (i < 0)
|
|
403
|
+
return draft;
|
|
404
|
+
draft.sections[i] = { ...draft.sections[i], ...patch };
|
|
405
|
+
return draft;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
// --- Canvas (free-form positioned layout) --------------------------------
|
|
409
|
+
function canvasBoxFor(type) {
|
|
410
|
+
return defaultBox(type);
|
|
411
|
+
}
|
|
412
|
+
// Free-form CANVAS layout is a GLOBAL, app-level mode (advanced) — not a
|
|
413
|
+
// per-section toggle. Turning it on auto-places every section's fields in a
|
|
414
|
+
// column so nothing is lost; turning it off drops the positioning back to a
|
|
415
|
+
// stacked layout.
|
|
416
|
+
function setAllCanvas(on) {
|
|
417
|
+
update((draft) => {
|
|
418
|
+
for (const sec of draft.sections) {
|
|
419
|
+
if (on && !sec.canvas) {
|
|
420
|
+
let y = 0;
|
|
421
|
+
sec.canvas = {
|
|
422
|
+
cols: 12,
|
|
423
|
+
rowHeight: 40,
|
|
424
|
+
items: sec.fields.map((f) => {
|
|
425
|
+
const box = canvasBoxFor(f.type);
|
|
426
|
+
const item = { i: f.id, x: 0, y, w: Math.min(box.w * 2, 12), h: box.h };
|
|
427
|
+
y += box.h;
|
|
428
|
+
return item;
|
|
429
|
+
}),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
else if (!on && sec.canvas) {
|
|
433
|
+
delete sec.canvas;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return draft;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const canvasMode = schema.sections.length > 0 && schema.sections.every((s) => !!s.canvas);
|
|
440
|
+
// When the app uses tabs, the build surface shows one tab's sections at a time.
|
|
441
|
+
const appTabs = schema.tabs ?? [];
|
|
442
|
+
const visibleSections = appTabs.length
|
|
443
|
+
? schema.sections.filter((s) => (s.tabId ?? appTabs[0].id) === designerTab)
|
|
444
|
+
: schema.sections;
|
|
445
|
+
function setCanvasItems(sectionId, items) {
|
|
446
|
+
update((draft) => {
|
|
447
|
+
const sec = draft.sections.find((s) => s.id === sectionId);
|
|
448
|
+
if (!sec || !sec.canvas)
|
|
449
|
+
return draft;
|
|
450
|
+
sec.canvas = { ...sec.canvas, items };
|
|
451
|
+
return draft;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function addWidgetToCanvas(sectionId, type, box) {
|
|
455
|
+
const f = emptyField(type);
|
|
456
|
+
update((draft) => {
|
|
457
|
+
const sec = draft.sections.find((s) => s.id === sectionId);
|
|
458
|
+
if (!sec)
|
|
459
|
+
return draft;
|
|
460
|
+
sec.fields.push(f);
|
|
461
|
+
const canvas = sec.canvas ?? { cols: 12, rowHeight: 40, items: [] };
|
|
462
|
+
sec.canvas = {
|
|
463
|
+
...canvas,
|
|
464
|
+
items: [...canvas.items, { i: f.id, x: box.x, y: box.y, w: box.w, h: box.h }],
|
|
465
|
+
};
|
|
466
|
+
return draft;
|
|
467
|
+
});
|
|
468
|
+
selectField(sectionId, f.id);
|
|
469
|
+
}
|
|
470
|
+
function publish() {
|
|
471
|
+
setError(tGeneratedValue(null));
|
|
472
|
+
start(async () => {
|
|
473
|
+
const result = await adapter.publish({
|
|
474
|
+
templateId,
|
|
475
|
+
schema,
|
|
476
|
+
changelog: changelog.trim(),
|
|
477
|
+
});
|
|
478
|
+
if (!result.ok) {
|
|
479
|
+
setError(tGeneratedValue(result.error ?? tGenerated('m_16c73b6230c543')));
|
|
480
|
+
toast.error(tGeneratedValue(result.error ?? tGenerated('m_16c73b6230c543')));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
toast.success(tGenerated('m_165fbf2bc8254b', { value0: result.version }));
|
|
484
|
+
setShowPublish(false);
|
|
485
|
+
if (publishedHref)
|
|
486
|
+
adapter.navigate?.(publishedHref);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
// Sections grouped by their workflow step assignment, used for the step
|
|
490
|
+
// chips in the canvas header.
|
|
491
|
+
const stepsCount = schema.workflow.steps.length;
|
|
492
|
+
return (_jsxs("div", { className: `flex h-full min-h-0 flex-col ${className ?? ''}`, children: [_jsxs("header", { className: "flex shrink-0 items-center justify-between border-b border-border bg-surface px-4 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("a", { href: backHref, className: "shrink-0 text-sm text-primary hover:underline", children: _jsx(GeneratedText, { id: "m_07f039d1ce81ec" }) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "truncate text-sm font-semibold", children: _jsx(GeneratedValue, { value: appName }) }), _jsx("span", { className: `rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide uppercase ${KIND_META[templateKind].cls}`, children: _jsx(GeneratedValue, { value: KIND_META[templateKind].label }) })] }), _jsxs("div", { className: "text-xs text-fg-muted", children: [_jsx(GeneratedText, { id: "m_1d76a1713fe585" }), _jsx(GeneratedValue, { value: currentVersion }), " \u00B7", ' ', _jsx(GeneratedValue, { value: schema.sections.length }), ' ', _jsx(GeneratedText, { id: "m_02f67a0e8ba5ce" }), _jsx(GeneratedValue, { value: schema.sections.length === 1 ? '' : _jsx(GeneratedText, { id: "m_00ded356f0f424" }) })] })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("label", { className: "flex items-center gap-1.5 text-xs text-fg-muted", children: [_jsx(Globe2, { size: 14 }), _jsx(Select, { value: contentLocale, onChange: (event) => setContentLocale(event.target.value), className: "h-8 min-w-28 text-xs", "aria-label": tGenerated('m_104e7b02f49d5e'), children: enabledLocales.map((enabledLocale) => (_jsx("option", { value: enabledLocale, children: languages(enabledLocale) }, enabledLocale))) })] }), _jsx(GeneratedValue, { value: renderAssistant ? (_jsxs(Button, { size: "sm", onClick: () => setShowAiAssistant(true), className: "bg-primary text-primary-fg hover:bg-primary-hover", children: [_jsx(Sparkles, { size: 14 }), " ", _jsx(GeneratedText, { id: "m_1e0a86199c09df" })] })) : null }), _jsx("a", { href: recordsHref, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(ClipboardCheck, { size: 14 }), " ", _jsx(GeneratedText, { id: "m_0d06d42b0344eb" })] }) }), _jsxs(Button, { variant: rightPanel === 'preview' ? 'secondary' : 'outline', size: "sm", onClick: () => setRightPanel((p) => (p === 'preview' ? 'none' : 'preview')), children: [_jsx(Eye, { size: 14 }), _jsx(GeneratedText, { id: "m_11d37007232de5" })] }), _jsxs(Button, { size: "sm", onClick: () => setShowPublish(true), disabled: pending, children: [_jsx(Save, { size: 14 }), _jsx(GeneratedText, { id: "m_01b66d9dba6889" }), _jsx(GeneratedValue, { value: currentVersion + 1 })] })] })] }), _jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsxs("aside", { className: "flex w-1/3 min-w-72 max-w-[30rem] shrink-0 flex-col border-r border-border bg-surface", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-0.5 border-b border-border px-2 py-1.5", children: [_jsx(LeftTab, { active: leftTab === 'overview', onClick: () => setLeftTab('overview'), icon: _jsx(Info, { size: 16 }), label: tGenerated('m_102c6abe56e4d5') }), _jsx(LeftTab, { active: leftTab === 'build', onClick: () => setLeftTab('build'), icon: _jsx(PanelLeft, { size: 16 }), label: tGenerated('m_0adae4a94c7be3') }), _jsx(LeftTab, { active: leftTab === 'record', onClick: () => setLeftTab('record'), icon: _jsx(SlidersHorizontal, { size: 16 }), label: tGenerated('m_1c1250c8c10a42') }), _jsx(LeftTab, { active: leftTab === 'list', onClick: () => setLeftTab('list'), icon: _jsx(Table2, { size: 16 }), label: tGenerated('m_02ec964c87acae') }), _jsx(LeftTab, { active: leftTab === 'actions', onClick: () => setLeftTab('actions'), icon: _jsx(MousePointerClick, { size: 16 }), label: tGenerated('m_13f79ee70f138d') }), _jsx(LeftTab, { active: leftTab === 'assignments', onClick: () => setLeftTab('assignments'), icon: _jsx(Send, { size: 16 }), label: tGenerated('m_0444d7da46d928') }), _jsx(LeftTab, { active: leftTab === 'permissions', onClick: () => setLeftTab('permissions'), icon: _jsx(ShieldCheck, { size: 16 }), label: tGenerated('m_128e83250b7fc0') })] }), _jsx("div", { className: "app-scroll min-h-0 flex-1 overflow-y-auto p-3", children: _jsx(GeneratedValue, { value: leftTab === 'overview' ? (_jsx(OverviewPanel, { name: appName, overview: overview, onSaved: setAppName, onSave: (input) => adapter.saveOverview({ templateId, ...input }), canPin: canPin, pinned: isPinned, onPin: adapter.setPinned ? async (next) => {
|
|
493
|
+
const result = await adapter.setPinned({ templateId, pinned: next });
|
|
494
|
+
if (result.ok)
|
|
495
|
+
setIsPinned(next);
|
|
496
|
+
return result;
|
|
497
|
+
} : undefined })) : leftTab === 'record' ? (_jsx(RecordBehaviorPanel, { value: recordConfiguration, roles: roles, onChange: setRecordConfiguration, onSave: async (next) => {
|
|
498
|
+
const result = await adapter.saveRecordConfig({ templateId, recordConfig: next });
|
|
499
|
+
if (!result.ok)
|
|
500
|
+
throw new Error(result.error ?? 'Record behavior could not be saved.');
|
|
501
|
+
} })) : leftTab === 'list' ? (_jsx(RecordListPanel, { value: listConfig, fields: listFields, onChange: (list) => setRecordConfiguration((current) => ({ ...current, list })), onSave: async (list) => {
|
|
502
|
+
const result = await adapter.saveListConfig({ templateId, listConfig: list });
|
|
503
|
+
if (!result.ok)
|
|
504
|
+
throw new Error(result.error ?? 'Record list could not be saved.');
|
|
505
|
+
} })) : leftTab === 'actions' ? (_jsx(RecordActionsPanel, { flows: flows, adapter: recordActionAdapter })) : leftTab === 'assignments' ? (_jsx(AssignmentsPanel, { createHref: assignmentCreateHref, listHref: assignmentsHref })) : leftTab === 'permissions' ? (_jsx(PermissionsPanel, { roles: roles, initial: allowedRoles, onSave: (next) => adapter.savePermissions({ templateId, allowedRoles: next }) })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: openWorkflow, title: tGenerated('m_037b62ee03b335'), className: `mb-3 block w-full rounded px-2 py-1 text-left text-xs font-semibold ${selection.kind === 'workflow'
|
|
506
|
+
? 'bg-primary-subtle text-primary'
|
|
507
|
+
: 'text-fg hover:bg-surface-hover'}`, children: [_jsx(GeneratedText, { id: "m_001f1bfc6e0cd8" }), _jsx(GeneratedValue, { value: stepsCount }), ")"] }), _jsx("h3", { className: "mb-1 text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_1a19ebd22b2247" }) }), _jsx("p", { className: "mb-2 text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_02043182fe57f9" }) }), _jsx(GeneratedValue, { value: [...PALETTE_PRIMARY, ...PALETTE_MORE].map((group) => (_jsx(FieldPaletteGroup, { group: group, onDragType: (t) => {
|
|
508
|
+
dragElementRef.current = t;
|
|
509
|
+
}, onDragEnd: () => {
|
|
510
|
+
dragElementRef.current = null;
|
|
511
|
+
setDropSectionId(null);
|
|
512
|
+
}, onAdd: (t) => {
|
|
513
|
+
const targetSection = selection.kind === 'section' || selection.kind === 'field'
|
|
514
|
+
? selection.sectionId
|
|
515
|
+
: schema.sections[schema.sections.length - 1]?.id;
|
|
516
|
+
if (targetSection)
|
|
517
|
+
addField(targetSection, t);
|
|
518
|
+
} }, group.label))) })] })) }) })] }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col bg-bg-subtle bg-bg", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-1 border-b border-border bg-surface px-3 py-1.5", children: [_jsx(SurfaceTab, { active: surface === 'build', onClick: () => setSurface('build'), icon: _jsx(LayoutGrid, { size: 13 }), label: tGenerated('m_0b120bda8434d5') }), renderFlows ? (_jsx(SurfaceTab, { active: surface === 'flows', onClick: () => setSurface('flows'), icon: _jsx(WorkflowIcon, { size: 13 }), label: tGenerated('m_1a4786daa752b1') })) : null, _jsx(GeneratedValue, { value: surface === 'build' ? (_jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsx("span", { className: "hidden text-[10px] font-semibold tracking-wider text-fg-muted uppercase sm:block", title: tGenerated('m_0342c212db3a34'), children: _jsx(GeneratedText, { id: "m_174bcc6db92052" }) }), _jsxs("div", { className: "flex items-center rounded-md border border-border p-0.5", children: [_jsx("button", { type: "button", onClick: () => setAllCanvas(false), className: `rounded px-2 py-0.5 text-xs font-medium transition ${!canvasMode
|
|
519
|
+
? 'bg-fg text-bg'
|
|
520
|
+
: 'text-fg-muted hover:bg-surface-hover'}`, children: _jsx(GeneratedText, { id: "m_1e888eaf9e8eed" }) }), _jsxs("button", { type: "button", onClick: () => setAllCanvas(true), className: `flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium transition ${canvasMode
|
|
521
|
+
? 'bg-primary text-primary-fg'
|
|
522
|
+
: 'text-fg-muted hover:bg-surface-hover'}`, children: [_jsx(LayoutGrid, { size: 12 }), " ", _jsx(GeneratedText, { id: "m_0627e4bbe74939" })] })] })] })) : null })] }), _jsx(GeneratedValue, { value: surface === 'flows' ? (_jsx("div", { className: "min-h-0 flex-1", children: renderFlows?.({ templateId, name: appName, schema, flows }) })) : (_jsx("div", { className: "app-scroll min-h-0 flex-1 overflow-y-auto p-4 lg:p-6", children: _jsx("div", { className: "w-full", children: _jsxs("div", { className: "space-y-3", children: [_jsx(GeneratedValue, { value: selection.kind === 'workflow' ? (_jsx(WorkflowEditor, { schema: schema, locale: contentLocale, defaultLocale: defaultLocale, onChange: (steps) => setSchema((s) => ({ ...s, workflow: { steps } })) })) : null }), _jsxs("div", { className: "flex flex-wrap items-center gap-1 rounded-lg border border-border bg-surface p-1.5", children: [_jsx(GeneratedValue, { value: appTabs.map((t) => (_jsxs("button", { type: "button", onClick: () => setDesignerTab(t.id), onDoubleClick: () => {
|
|
523
|
+
const next = window.prompt('Rename tab', localizeText(t.title, contentLocale, '', defaultLocale));
|
|
524
|
+
if (next != null)
|
|
525
|
+
renameTab(t.id, next.trim() || 'Tab');
|
|
526
|
+
}, title: tGenerated('m_0d31ecacb5b2ee'), className: `group flex items-center gap-1 rounded-md px-3 py-1 text-xs font-medium transition ${designerTab === t.id
|
|
527
|
+
? 'bg-primary text-primary-fg'
|
|
528
|
+
: 'text-fg-muted hover:bg-surface-hover'}`, children: [_jsx(GeneratedValue, { value: localizeText(t.title, contentLocale, 'Tab', defaultLocale) }), _jsx(GeneratedValue, { value: appTabs.length > 1 ? (_jsx(Trash2, { size: 11, className: designerTab === t.id
|
|
529
|
+
? 'text-primary-fg/70 hover:text-primary-fg'
|
|
530
|
+
: 'text-fg-subtle hover:text-danger', onClick: (e) => {
|
|
531
|
+
e.stopPropagation();
|
|
532
|
+
deleteTab(t.id);
|
|
533
|
+
} })) : null })] }, t.id))) }), _jsxs("button", { type: "button", onClick: addTab, className: "flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium text-primary hover:bg-primary-subtle hover:bg-primary-subtle/40", title: tGeneratedValue(appTabs.length === 0
|
|
534
|
+
? tGenerated('m_1b7cb35c4f57dd')
|
|
535
|
+
: tGenerated('m_1e7fd6a7c790fe')), children: [_jsx(Plus, { size: 12 }), ' ', _jsx(GeneratedValue, { value: appTabs.length === 0 ? (_jsx(GeneratedText, { id: "m_0cfeced513f8d1" })) : (_jsx(GeneratedText, { id: "m_1457c1cce0b63c" })) })] })] }), _jsx(GeneratedValue, { value: visibleSections.map((sec, i) => {
|
|
536
|
+
const active = selection.kind === 'section' && selection.sectionId === sec.id;
|
|
537
|
+
return (_jsxs(Card, { onDragEnter: sec.canvas
|
|
538
|
+
? undefined
|
|
539
|
+
: (event) => {
|
|
540
|
+
if (!dragElementRef.current)
|
|
541
|
+
return;
|
|
542
|
+
event.preventDefault();
|
|
543
|
+
setDropSectionId(sec.id);
|
|
544
|
+
}, onDragOver: sec.canvas
|
|
545
|
+
? undefined
|
|
546
|
+
: (event) => {
|
|
547
|
+
if (!dragElementRef.current)
|
|
548
|
+
return;
|
|
549
|
+
event.preventDefault();
|
|
550
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
551
|
+
}, onDrop: sec.canvas
|
|
552
|
+
? undefined
|
|
553
|
+
: (event) => {
|
|
554
|
+
event.preventDefault();
|
|
555
|
+
event.stopPropagation();
|
|
556
|
+
const type = dragElementRef.current;
|
|
557
|
+
dragElementRef.current = null;
|
|
558
|
+
setDropSectionId(null);
|
|
559
|
+
if (type)
|
|
560
|
+
addField(sec.id, type);
|
|
561
|
+
}, className: `border transition-colors ${dropSectionId === sec.id
|
|
562
|
+
? 'border-primary bg-primary-subtle/70 ring-2 ring-primary/40'
|
|
563
|
+
: active
|
|
564
|
+
? 'border-primary ring-1 ring-primary'
|
|
565
|
+
: 'border-border'}`, children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between gap-2 pb-2", children: [_jsx("button", { type: "button", onClick: () => selectSection(sec.id), className: "flex-1 text-left", children: _jsxs(CardTitle, { className: "text-base", children: [_jsx(GeneratedValue, { value: localizeText(sec.title, contentLocale, 'Untitled section', defaultLocale) }), _jsx(GeneratedValue, { value: ' ' }), _jsx(GeneratedValue, { value: sec.repeating ? (_jsx(Badge, { variant: "secondary", children: _jsx(GeneratedText, { id: "m_06e1ca227aca9b" }) })) : null }), _jsx(GeneratedValue, { value: sec.showIf ? (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: _jsx(GeneratedText, { id: "m_02529c1532db7a" }) })) : null }), _jsx(GeneratedValue, { value: sec.step ? (_jsxs(Badge, { variant: "outline", className: "text-[10px]", children: [_jsx(GeneratedText, { id: "m_11933adf482cac" }), ' ', _jsx(GeneratedValue, { value: stepLabel(schema, sec.step, contentLocale, defaultLocale) })] })) : null })] }) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(IconButton, { title: tGenerated('m_1ec1460770eaa0'), onClick: () => moveSection(sec.id, -1), disabled: i === 0, children: _jsx(ArrowUp, { size: 14 }) }), _jsx(IconButton, { title: tGenerated('m_14ab8cefda3cf9'), onClick: () => moveSection(sec.id, 1), disabled: i === visibleSections.length - 1, children: _jsx(ArrowDown, { size: 14 }) }), _jsx(IconButton, { title: tGenerated('m_1e97527af4024b'), onClick: () => deleteSection(sec.id), children: _jsx(Trash2, { size: 14, className: "text-danger" }) })] })] }), _jsx(CardContent, { className: "pt-0", children: _jsx(GeneratedValue, { value: sec.canvas ? (_jsx(CanvasEditor, { section: sec, locale: contentLocale, defaultLocale: defaultLocale, selectedFieldId: selection.kind === 'field' &&
|
|
566
|
+
selection.sectionId === sec.id
|
|
567
|
+
? selection.fieldId
|
|
568
|
+
: null, dragTypeRef: dragElementRef, onLayout: (items) => setCanvasItems(sec.id, items), onAddWidget: (type, box) => addWidgetToCanvas(sec.id, type, box), onSelect: (fieldId) => selectField(sec.id, fieldId), onDelete: (fieldId) => deleteField(sec.id, fieldId) })) : sec.fields.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed border-border-strong p-4 text-center text-xs text-fg-muted border-border", children: _jsx(GeneratedText, { id: "m_0d6735f83b77a3" }) })) : (_jsx(Reorder.Group, { axis: "y", values: sec.fields, onReorder: (fields) => reorderFields(sec.id, fields), as: "ul", className: "divide-y divide-border-subtle", children: _jsx(GeneratedValue, { value: sec.fields.map((f, j) => (_jsx(FieldRow, { field: f, locale: contentLocale, defaultLocale: defaultLocale, isSelected: selection.kind === 'field' &&
|
|
569
|
+
selection.fieldId === f.id, typeLabel: FIELD_TYPES[f.type]?.label ?? f.type, onSelect: () => selectField(sec.id, f.id), onMoveUp: () => moveField(sec.id, f.id, -1), onMoveDown: () => moveField(sec.id, f.id, 1), onDelete: () => deleteField(sec.id, f.id), canUp: j > 0, canDown: j < sec.fields.length - 1 }, f.id))) }) })) }) })] }, sec.id));
|
|
570
|
+
}) }), _jsxs(Button, { variant: "outline", onClick: addSection, className: "w-full", children: [_jsx(Plus, { size: 14 }), _jsx(GeneratedText, { id: "m_0cfd5e4e441158" })] })] }) }) })) })] })] }), _jsx(Drawer, { open: rightPanel === 'props', onClose: () => setRightPanel('none'), title: tGeneratedValue(selection.kind === 'field'
|
|
571
|
+
? tGenerated('m_0478595ea3a2da')
|
|
572
|
+
: tGenerated('m_0d513924d97753')), size: "sm", children: _jsx(GeneratedValue, { value: selection.kind === 'field' && selectedField ? (_jsx(FieldProperties, { sectionId: selectedField.section.id, field: selectedField.field, schema: schema, locale: contentLocale, defaultLocale: defaultLocale, dataSources: dataSources, dataSourcesLoading: dataSourcesLoading, onRefreshDataSources: onRefreshDataSources, dataSourcesHref: dataSourcesHref, onChange: (patch) => updateField(selectedField.section.id, selectedField.field.id, patch) }, selectedField.field.id)) : selection.kind === 'section' && selectedSection ? (_jsx(SectionProperties, { section: selectedSection, schema: schema, locale: contentLocale, defaultLocale: defaultLocale, onChange: (patch) => updateSection(selectedSection.id, patch) }, selectedSection.id)) : null }) }), _jsx(Drawer, { open: rightPanel === 'preview', onClose: () => setRightPanel('none'), title: tGenerated('m_11d37007232de5'), description: tGenerated('m_1e28aaa8e490af'), size: "lg", children: _jsx(Preview, { schema: schema, locale: contentLocale, defaultLocale: defaultLocale }) }), _jsx(Drawer, { open: showPublish, onClose: () => setShowPublish(false), title: tGenerated('m_04073463647168', { value0: currentVersion + 1 }), description: tGenerated('m_0eced645981830'), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", onClick: () => setShowPublish(false), children: _jsx(GeneratedText, { id: "m_112e2e8ecda428" }) }), _jsxs(Button, { onClick: publish, disabled: pending, children: [_jsx(Check, { size: 14 }), _jsx(GeneratedValue, { value: pending ? (_jsx(GeneratedText, { id: "m_12b32266eec3e2" })) : (_jsx(GeneratedText, { id: "m_0c072fb8baf115" })) })] })] }), children: _jsxs("div", { className: "space-y-3", children: [_jsxs(Alert, { variant: "info", children: [_jsx(AlertTitle, { children: _jsx(GeneratedText, { id: "m_1c2ad488291a89" }) }), _jsxs(AlertDescription, { children: [_jsx(GeneratedText, { id: "m_16d04782fa275d" }), _jsx(GeneratedValue, { value: currentVersion + 1 }), _jsx(GeneratedText, { id: "m_025b2e6dff2c41" })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: _jsx(GeneratedText, { id: "m_155fd3bdb6e009" }) }), _jsx(Textarea, { rows: 3, value: changelog, placeholder: tGenerated('m_061739b5f07deb'), onChange: (e) => setChangelog(e.target.value) })] }), _jsx(GeneratedValue, { value: error ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: _jsx(GeneratedValue, { value: error }) }) })) : null })] }) }), renderAssistant?.({
|
|
573
|
+
open: showAiAssistant,
|
|
574
|
+
onClose: () => setShowAiAssistant(false),
|
|
575
|
+
templateId,
|
|
576
|
+
schema,
|
|
577
|
+
apply: (next) => {
|
|
578
|
+
const normalized = normalizeDesignerSchema(next);
|
|
579
|
+
setSchema(normalized);
|
|
580
|
+
if (normalized.tabs?.length)
|
|
581
|
+
setDesignerTab(normalized.tabs[0].id);
|
|
582
|
+
toast.success(tGenerated('m_180fea01504577'));
|
|
583
|
+
},
|
|
584
|
+
})] }));
|
|
585
|
+
}
|
|
586
|
+
function stepLabel(schema, stepKey, locale, defaultLocale) {
|
|
587
|
+
const step = schema.workflow?.steps.find((s) => s.key === stepKey);
|
|
588
|
+
return localizeText(step?.title, locale, stepKey, defaultLocale);
|
|
589
|
+
}
|
|
590
|
+
function FieldPaletteGroup({ group, onAdd, onDragType, onDragEnd, }) {
|
|
591
|
+
const tGenerated = useGeneratedTranslations();
|
|
592
|
+
return (_jsxs("div", { className: "mb-3", children: [_jsx("div", { className: "px-1 pb-1 text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedValue, { value: group.label }) }), _jsx("div", { className: "grid grid-cols-1 gap-1", children: _jsx(GeneratedValue, { value: group.types.map((t) => {
|
|
593
|
+
const Icon = FIELD_ICONS[t] ?? Type;
|
|
594
|
+
return (_jsxs("button", { type: "button", draggable: true, onDragStart: (e) => {
|
|
595
|
+
onDragType?.(t);
|
|
596
|
+
e.dataTransfer.setData('text/plain', t);
|
|
597
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
598
|
+
}, onDragEnd: onDragEnd, onClick: () => onAdd(t), className: "flex cursor-grab items-center gap-2 rounded border border-border px-2 py-1 text-left text-xs hover:border-primary hover:bg-primary-subtle active:cursor-grabbing hover:bg-primary-subtle/40", title: tGenerated('m_1b1fa20684f949'), children: [_jsx(Icon, { size: 12 }), _jsx(GeneratedValue, { value: FIELD_TYPES[t].label })] }, t));
|
|
599
|
+
}) }) })] }));
|
|
600
|
+
}
|
|
601
|
+
function IconButton({ title, onClick, disabled, children, }) {
|
|
602
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
603
|
+
return (_jsx("button", { type: "button", title: tGeneratedValue(title), onClick: onClick, disabled: disabled, className: "rounded p-1 text-fg-muted hover:bg-surface-hover hover:text-fg disabled:cursor-not-allowed disabled:opacity-40", children: _jsx(GeneratedValue, { value: children }) }));
|
|
604
|
+
}
|
|
605
|
+
// One draggable field row in the designer canvas. Drag is handle-only (via
|
|
606
|
+
// framer-motion dragControls) so clicking the row still selects the field; the
|
|
607
|
+
// up/down arrows remain as a keyboard-accessible fallback.
|
|
608
|
+
function FieldRow({ field, locale, defaultLocale, isSelected, typeLabel, onSelect, onMoveUp, onMoveDown, onDelete, canUp, canDown, }) {
|
|
609
|
+
const tGenerated = useGeneratedTranslations();
|
|
610
|
+
const controls = useDragControls();
|
|
611
|
+
const Icon = FIELD_ICONS[field.type] ?? Type;
|
|
612
|
+
return (_jsxs(Reorder.Item, { value: field, dragListener: false, dragControls: controls, as: "li", className: `flex items-center justify-between gap-2 rounded px-1 py-2 ${isSelected ? 'bg-primary-subtle' : 'bg-surface'}`, children: [_jsx("button", { type: "button", "aria-label": tGenerated('m_0b04b904ce4f9a'), onPointerDown: (e) => controls.start(e), className: "cursor-grab touch-none rounded p-0.5 text-fg-subtle hover:text-fg-muted active:cursor-grabbing", children: _jsx(GripVertical, { size: 14 }) }), _jsxs("button", { type: "button", onClick: onSelect, className: "flex flex-1 items-center gap-2 text-left", children: [_jsx(Icon, { size: 14 }), _jsx("span", { className: "w-24 truncate text-xs text-fg-muted", children: _jsx(GeneratedValue, { value: typeLabel }) }), _jsxs("span", { className: "text-sm font-medium", children: [_jsx(GeneratedValue, { value: localizeText(field.label, locale, field.id, defaultLocale) }), _jsx(GeneratedValue, { value: field.required || field.validation?.required ? (_jsx("span", { className: "text-danger", children: " *" })) : null })] }), _jsx(GeneratedValue, { value: field.showIf ? (_jsx(Badge, { variant: "secondary", className: "text-[10px]", children: _jsx(GeneratedText, { id: "m_02529c1532db7a" }) })) : null }), _jsx(GeneratedValue, { value: field.formula ? (_jsx(Badge, { variant: "secondary", className: "text-[10px]", children: _jsx(GeneratedText, { id: "m_194bb13d821775" }) })) : null })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(IconButton, { title: tGenerated('m_1ec1460770eaa0'), onClick: onMoveUp, disabled: !canUp, children: _jsx(ArrowUp, { size: 12 }) }), _jsx(IconButton, { title: tGenerated('m_14ab8cefda3cf9'), onClick: onMoveDown, disabled: !canDown, children: _jsx(ArrowDown, { size: 12 }) }), _jsx(IconButton, { title: tGenerated('m_11773f3c3f7558'), onClick: onDelete, children: _jsx(Trash2, { size: 12, className: "text-danger" }) })] })] }));
|
|
613
|
+
}
|
|
614
|
+
// --- Form-level properties -------------------------------------------------
|
|
615
|
+
// --- Workflow editor -------------------------------------------------------
|
|
616
|
+
function WorkflowEditor({ schema, locale, defaultLocale, onChange, }) {
|
|
617
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
618
|
+
const tGenerated = useGeneratedTranslations();
|
|
619
|
+
const steps = schema.workflow?.steps ?? [];
|
|
620
|
+
function setStep(i, patch) {
|
|
621
|
+
onChange(steps.map((s, j) => (j === i ? { ...s, ...patch } : s)));
|
|
622
|
+
}
|
|
623
|
+
function add() {
|
|
624
|
+
onChange([...steps, emptyStep()]);
|
|
625
|
+
}
|
|
626
|
+
function remove(i) {
|
|
627
|
+
if (steps.length === 1)
|
|
628
|
+
return;
|
|
629
|
+
onChange(steps.filter((_, j) => j !== i));
|
|
630
|
+
}
|
|
631
|
+
function move(i, delta) {
|
|
632
|
+
const j = i + delta;
|
|
633
|
+
if (j < 0 || j >= steps.length)
|
|
634
|
+
return;
|
|
635
|
+
const next = [...steps];
|
|
636
|
+
[next[i], next[j]] = [next[j], next[i]];
|
|
637
|
+
onChange(next);
|
|
638
|
+
}
|
|
639
|
+
return (_jsxs(Card, { className: "border-2 border-primary/40", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: _jsx(GeneratedText, { id: "m_1c75ba367ef317" }) }) }), _jsxs(CardContent, { className: "space-y-2", children: [_jsxs("p", { className: "text-xs text-fg-muted", children: [_jsx(GeneratedText, { id: "m_1a11f78c7d99d8" }), ' ', _jsx("em", { children: _jsx(GeneratedText, { id: "m_02b0a83932da4a" }) }), ' ', "\u2192", _jsx(GeneratedValue, { value: ' ' }), _jsx("em", { children: _jsx(GeneratedText, { id: "m_14efdd0bff7d70" }) }), ' ', "\u2192", ' ', _jsx("em", { children: _jsx(GeneratedText, { id: "m_1cc0241831296d" }) }), _jsx(GeneratedText, { id: "m_17833f54b99d38" }), _jsx(GeneratedValue, { value: ' ' }), _jsx("strong", { children: _jsx(GeneratedText, { id: "m_1c8658eb9347b4" }) }), ' ', _jsx(GeneratedText, { id: "m_0bf548b271dbc1" }), ' ', _jsx("strong", { children: _jsx(GeneratedText, { id: "m_1a4786daa752b1" }) }), ' ', _jsx(GeneratedText, { id: "m_003ae75549476b" })] }), _jsx("ul", { className: "space-y-2", children: _jsx(GeneratedValue, { value: steps.map((s, i) => (_jsx("li", { className: "rounded-md border border-border bg-surface p-2", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "mt-1 inline-flex h-6 w-6 items-center justify-center rounded-full bg-primary-subtle text-xs font-semibold text-primary bg-primary-subtle/40", children: _jsx(GeneratedValue, { value: i + 1 }) }), _jsxs("div", { className: "flex-1 space-y-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-[10px]", children: _jsx(GeneratedText, { id: "m_0decefd558c355" }) }), _jsx(Input, { className: "h-7 text-xs", value: readText(s.title, locale), placeholder: tGeneratedValue(localizeText(s.title, locale, s.key, defaultLocale)), onChange: (e) => setStep(i, {
|
|
640
|
+
title: writeText(s.title, e.target.value, locale),
|
|
641
|
+
}) })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-[10px]", children: _jsx(GeneratedText, { id: "m_147b61c2aef29b" }) }), _jsx(Input, { className: "h-7 font-mono text-xs", value: s.key, disabled: true })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-[10px]", children: _jsx(GeneratedText, { id: "m_160787e531bffd" }) }), _jsxs(Select, { className: "h-7 text-xs", value: s.assignee.type, onChange: (e) => {
|
|
642
|
+
const t = e.target.value;
|
|
643
|
+
const assignee = t === 'literal'
|
|
644
|
+
? { type: 'literal', userId: '' }
|
|
645
|
+
: t === 'role'
|
|
646
|
+
? { type: 'role', role: '' }
|
|
647
|
+
: { type: 'expression', expr: '$submitter' };
|
|
648
|
+
setStep(i, { assignee });
|
|
649
|
+
}, children: [_jsx("option", { value: "expression", children: 'Expression' }), _jsx("option", { value: "role", children: 'Role' }), _jsx("option", { value: "literal", children: 'Specific user' })] })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-[10px]", children: _jsx(GeneratedText, { id: "m_078d7e637546f9" }) }), _jsx(Input, { className: "h-7 text-xs", value: s.assignee.type === 'literal'
|
|
650
|
+
? s.assignee.userId
|
|
651
|
+
: s.assignee.type === 'role'
|
|
652
|
+
? s.assignee.role
|
|
653
|
+
: s.assignee.expr, onChange: (e) => {
|
|
654
|
+
const v = e.target.value;
|
|
655
|
+
if (s.assignee.type === 'literal')
|
|
656
|
+
setStep(i, { assignee: { type: 'literal', userId: v } });
|
|
657
|
+
else if (s.assignee.type === 'role')
|
|
658
|
+
setStep(i, { assignee: { type: 'role', role: v } });
|
|
659
|
+
else
|
|
660
|
+
setStep(i, { assignee: { type: 'expression', expr: v } });
|
|
661
|
+
} })] })] }), _jsxs("label", { className: "flex items-center gap-2 text-xs", children: [_jsx("input", { type: "checkbox", checked: s.signatureRequired ?? false, onChange: (e) => setStep(i, { signatureRequired: e.target.checked }) }), _jsx(GeneratedText, { id: "m_02ffe91f500dc8" })] })] }), _jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx(IconButton, { title: tGenerated('m_1ec1460770eaa0'), onClick: () => move(i, -1), disabled: i === 0, children: _jsx(ArrowUp, { size: 12 }) }), _jsx(IconButton, { title: tGenerated('m_14ab8cefda3cf9'), onClick: () => move(i, 1), disabled: i === steps.length - 1, children: _jsx(ArrowDown, { size: 12 }) }), _jsx(IconButton, { title: tGenerated('m_09aebdb43a47c7'), onClick: () => remove(i), disabled: steps.length === 1, children: _jsx(Trash2, { size: 12, className: "text-danger" }) })] })] }) }, s.key))) }) }), _jsxs(Button, { variant: "outline", onClick: add, className: "w-full", children: [_jsx(Plus, { size: 14 }), " ", _jsx(GeneratedText, { id: "m_0ce705b8fa979c" })] })] })] }));
|
|
662
|
+
}
|
|
663
|
+
function FieldProperties({ sectionId, field, schema, locale, defaultLocale, dataSources, dataSourcesLoading, onRefreshDataSources, dataSourcesHref, onChange, }) {
|
|
664
|
+
const [tab, setTab] = useState('basic');
|
|
665
|
+
const isCalcField = field.type === 'formula';
|
|
666
|
+
const storesValue = storesResponseValue(field);
|
|
667
|
+
const ownerSection = schema.sections.find((section) => section.id === sectionId);
|
|
668
|
+
const otherFields = schema.sections
|
|
669
|
+
.flatMap((section) => {
|
|
670
|
+
if (section.repeating && section.id !== sectionId)
|
|
671
|
+
return [];
|
|
672
|
+
if (!ownerSection?.repeating && section.repeating)
|
|
673
|
+
return [];
|
|
674
|
+
return section.fields;
|
|
675
|
+
})
|
|
676
|
+
.filter((candidate) => candidate.id !== field.id && storesResponseValue(candidate))
|
|
677
|
+
.map((f) => ({ id: f.id, label: localizeText(f.label, locale, f.id, defaultLocale) }));
|
|
678
|
+
const repeatingSections = schema.sections
|
|
679
|
+
.filter((s) => s.repeating)
|
|
680
|
+
.map((s) => ({
|
|
681
|
+
id: s.id,
|
|
682
|
+
label: localizeText(s.title, locale, s.id, defaultLocale),
|
|
683
|
+
fields: s.fields
|
|
684
|
+
.filter((candidate) => storesResponseValue(candidate) ||
|
|
685
|
+
(!ownerSection?.repeating &&
|
|
686
|
+
candidate.type === 'formula' &&
|
|
687
|
+
candidate.formula !== undefined))
|
|
688
|
+
.map((f) => ({
|
|
689
|
+
id: f.id,
|
|
690
|
+
label: localizeText(f.label, locale, f.id, defaultLocale),
|
|
691
|
+
})),
|
|
692
|
+
}));
|
|
693
|
+
// Single-entity picker fields the formula builder's entity_attr operator
|
|
694
|
+
// can target. Multi-pickers are excluded because entity_attr resolves one
|
|
695
|
+
// entity per picker, not a list.
|
|
696
|
+
const pickerFields = schema.sections
|
|
697
|
+
.filter((s) => !s.repeating)
|
|
698
|
+
.flatMap((s) => s.fields)
|
|
699
|
+
.filter((f) => entityKindForPicker(f.type) !== null)
|
|
700
|
+
.map((f) => ({
|
|
701
|
+
id: f.id,
|
|
702
|
+
label: localizeText(f.label, locale, f.id, defaultLocale),
|
|
703
|
+
kind: entityKindForPicker(f.type),
|
|
704
|
+
}));
|
|
705
|
+
const tabs = [
|
|
706
|
+
{ value: 'basic', label: 'Basic', show: true },
|
|
707
|
+
{ value: 'validation', label: 'Validation', show: storesValue },
|
|
708
|
+
{ value: 'logic', label: 'Logic', show: true },
|
|
709
|
+
{ value: 'default', label: 'Default', show: storesValue },
|
|
710
|
+
{ value: 'formula', label: 'Formula', show: isCalcField },
|
|
711
|
+
];
|
|
712
|
+
return (_jsxs("div", { className: "space-y-3 text-sm", children: [_jsxs("h3", { className: "text-sm font-semibold text-fg", children: [_jsx(GeneratedText, { id: "m_0bff3b64e15427" }), ' ', _jsx(GeneratedValue, { value: FIELD_TYPES[field.type]?.label ?? field.type })] }), _jsx("div", { className: "flex gap-1 border-b border-border", children: _jsx(GeneratedValue, { value: tabs
|
|
713
|
+
.filter((t) => t.show)
|
|
714
|
+
.map((t) => (_jsx("button", { type: "button", onClick: () => setTab(t.value), className: `-mb-px border-b-2 px-2 py-1 text-xs ${tab === t.value
|
|
715
|
+
? 'border-primary font-semibold text-primary'
|
|
716
|
+
: 'border-transparent text-fg-muted hover:text-fg'}`, children: _jsx(GeneratedValue, { value: t.label }) }, t.value))) }) }), _jsx(GeneratedValue, { value: tab === 'basic' ? (_jsx(FieldBasicTab, { sectionId: sectionId, field: field, schema: schema, locale: locale, defaultLocale: defaultLocale, dataSources: dataSources, dataSourcesLoading: dataSourcesLoading, onRefreshDataSources: onRefreshDataSources, dataSourcesHref: dataSourcesHref, onChange: onChange })) : null }), _jsx(GeneratedValue, { value: tab === 'validation' && storesValue ? (_jsx(FieldValidationTab, { field: field, onChange: onChange })) : null }), _jsx(GeneratedValue, { value: tab === 'logic' ? (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_024d5ac76425dc" }) }), _jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_0db2c879ad4429" }) }), _jsx(LogicBuilder, { rule: field.showIf, availableFields: otherFields, onChange: (rule) => onChange({ showIf: rule }) })] })) : null }), _jsx(GeneratedValue, { value: tab === 'default' && storesValue ? (_jsx(FieldDefaultTab, { field: field, onChange: onChange })) : null }), _jsx(GeneratedValue, { value: tab === 'formula' && isCalcField ? (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1f15a1cd1a64c4" }) }), _jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_0cd91fbbd3fca0" }) }), _jsx(FormulaBuilder, { value: field.formula, allFields: otherFields, repeatingSections: repeatingSections, pickerFields: pickerFields, onChange: (next) => onChange({ formula: next }) })] })) : null })] }));
|
|
717
|
+
}
|
|
718
|
+
function FieldBasicTab({ sectionId, field, schema, locale, defaultLocale, dataSources, dataSourcesLoading, onRefreshDataSources, dataSourcesHref, onChange, }) {
|
|
719
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
720
|
+
// Other fields in the app — targets for cascade filters + lookup auto-fill.
|
|
721
|
+
const ownerSection = schema.sections.find((section) => section.id === sectionId);
|
|
722
|
+
const otherFields = schema.sections
|
|
723
|
+
.flatMap((section) => {
|
|
724
|
+
if (section.repeating && section.id !== sectionId)
|
|
725
|
+
return [];
|
|
726
|
+
if (!ownerSection?.repeating && section.repeating)
|
|
727
|
+
return [];
|
|
728
|
+
return section.fields;
|
|
729
|
+
})
|
|
730
|
+
.filter((candidate) => candidate.id !== field.id && storesResponseValue(candidate))
|
|
731
|
+
.map((f) => ({ id: f.id, label: localizeText(f.label, locale, f.id, defaultLocale) }));
|
|
732
|
+
const autofillFields = (ownerSection?.repeating
|
|
733
|
+
? ownerSection.fields
|
|
734
|
+
: schema.sections.filter((section) => !section.repeating).flatMap((section) => section.fields))
|
|
735
|
+
.filter((candidate) => candidate.id !== field.id && storesResponseValue(candidate))
|
|
736
|
+
.map((candidate) => ({
|
|
737
|
+
id: candidate.id,
|
|
738
|
+
label: localizeText(candidate.label, locale, candidate.id, defaultLocale),
|
|
739
|
+
}));
|
|
740
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_13d7101bd1f531" }) }), _jsx(Input, { value: field.id, disabled: true, className: "font-mono text-xs" })] }), _jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "text-xs", children: [_jsx(GeneratedText, { id: "m_1d4f77715c6f3d" }), _jsx(GeneratedValue, { value: locale.toUpperCase() }), ")"] }), _jsx(Input, { value: readText(field.label, locale), placeholder: tGeneratedValue(localizeText(field.label, locale, field.id, defaultLocale)), onChange: (e) => onChange({ label: writeText(field.label, e.target.value, locale) }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_0d04877b1a742b" }) }), _jsx(Textarea, { rows: 2, value: readText(field.helpText, locale), placeholder: tGeneratedValue(localizeText(field.helpText, locale, '', defaultLocale)), onChange: (e) => {
|
|
741
|
+
onChange({
|
|
742
|
+
helpText: e.target.value
|
|
743
|
+
? writeText(field.helpText, e.target.value, locale)
|
|
744
|
+
: undefined,
|
|
745
|
+
});
|
|
746
|
+
} })] }), _jsx(GeneratedValue, { value: storesResponseValue(field) ? (_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: field.required ?? false, onChange: (e) => onChange({ required: e.target.checked }) }), _jsx(GeneratedText, { id: "m_12fe2fe7a9ddad" })] })) : null }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_14ea5fb193bdaf" }) }), _jsxs(Select, { className: "h-8 text-xs", value: String(field.colSpan ?? ''), onChange: (e) => onChange({ colSpan: e.target.value ? Number(e.target.value) : undefined }), children: [_jsx("option", { value: "", children: 'Full width' }), _jsx("option", { value: "1", children: '1 column' }), _jsx("option", { value: "2", children: '2 columns' }), _jsx("option", { value: "3", children: '3 columns' }), _jsx("option", { value: "4", children: '4 columns' })] }), _jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_0f334abe43e80f" }) })] }), _jsx(GeneratedValue, { value: field.type === 'select' ||
|
|
747
|
+
field.type === 'radio' ||
|
|
748
|
+
field.type === 'multi_select' ||
|
|
749
|
+
field.type === 'checkbox_group' ||
|
|
750
|
+
field.type === 'ranking' ? (_jsx(ChoiceOptionsEditor, { field: field, locale: locale, onChange: onChange })) : null }), _jsx(GeneratedValue, { value: field.type === 'table' ? _jsx(TableConfigEditor, { field: field, onChange: onChange }) : null }), _jsx(GeneratedValue, { value: field.type === 'slider' ? _jsx(SliderConfigEditor, { field: field, onChange: onChange }) : null }), _jsx(GeneratedValue, { value: field.type === 'matrix' ? _jsx(MatrixConfigEditor, { field: field, onChange: onChange }) : null }), _jsx(GeneratedValue, { value: field.type === 'lookup' || field.type === 'data_table' || field.type === 'metric' ? (_jsx(DataBindingEditor, { field: field, otherFields: otherFields, autofillFields: autofillFields, sources: dataSources, loading: dataSourcesLoading, onRefresh: onRefreshDataSources, dataSourcesHref: dataSourcesHref, onChange: onChange })) : null })] }));
|
|
751
|
+
}
|
|
752
|
+
function SliderConfigEditor({ field, onChange, }) {
|
|
753
|
+
const tGenerated = useGeneratedTranslations();
|
|
754
|
+
const c = (field.config ?? {});
|
|
755
|
+
const set = (patch) => onChange({ config: { ...field.config, ...patch } });
|
|
756
|
+
return (_jsxs("div", { className: "space-y-2 rounded-md border border-border p-2", children: [_jsx("div", { className: "text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_03363edf51ed99" }) }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_100639ca393959" }) }), _jsx(Input, { type: "number", value: c.min ?? 0, onChange: (e) => set({ min: Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1929e34b445f83" }) }), _jsx(Input, { type: "number", value: c.max ?? 10, onChange: (e) => set({ max: Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_0cff7e37da2b3f" }) }), _jsx(Input, { type: "number", value: c.step ?? 1, onChange: (e) => set({ step: Number(e.target.value) }) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_19241615edae92" }) }), _jsx(Input, { value: c.unit ?? '', placeholder: tGenerated('m_075f330b972a3b'), onChange: (e) => set({ unit: e.target.value }) })] })] }));
|
|
757
|
+
}
|
|
758
|
+
function MatrixConfigEditor({ field, onChange, }) {
|
|
759
|
+
const tGenerated = useGeneratedTranslations();
|
|
760
|
+
const c = (field.config ?? {});
|
|
761
|
+
const rows = c.rows ?? [];
|
|
762
|
+
const scale = c.scale ?? [];
|
|
763
|
+
const setRows = (next) => onChange({ config: { ...field.config, rows: next } });
|
|
764
|
+
const setScale = (next) => onChange({ config: { ...field.config, scale: next } });
|
|
765
|
+
return (_jsxs("div", { className: "space-y-3 rounded-md border border-border p-2", children: [_jsxs("div", { children: [_jsxs("div", { className: "mb-1 flex items-center justify-between", children: [_jsx("span", { className: "text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_03be2202673df4" }) }), _jsx("button", { type: "button", className: "text-xs text-primary hover:underline", onClick: () => setRows([...rows, { key: newId('row'), label: `Row ${rows.length + 1}` }]), children: _jsx(GeneratedText, { id: "m_0d067cf371aad1" }) })] }), _jsx("div", { className: "space-y-1", children: _jsx(GeneratedValue, { value: rows.map((r, i) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Input, { value: r.label, onChange: (e) => setRows(rows.map((x, j) => (j === i ? { ...x, label: e.target.value } : x))) }), _jsx(IconButton, { title: tGenerated('m_12b310a027b08a'), onClick: () => setRows(rows.filter((_, j) => j !== i)), children: _jsx(Trash2, { size: 13, className: "text-danger" }) })] }, r.key))) }) })] }), _jsxs("div", { children: [_jsxs("div", { className: "mb-1 flex items-center justify-between", children: [_jsx("span", { className: "text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_1bd1bb813826dc" }) }), _jsx("button", { type: "button", className: "text-xs text-primary hover:underline", onClick: () => setScale([
|
|
766
|
+
...scale,
|
|
767
|
+
{ value: String(scale.length + 1), label: String(scale.length + 1) },
|
|
768
|
+
]), children: _jsx(GeneratedText, { id: "m_0f92f0f3b0d515" }) })] }), _jsx("div", { className: "space-y-1", children: _jsx(GeneratedValue, { value: scale.map((s, i) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Input, { className: "w-16 font-mono text-xs", value: s.value, onChange: (e) => setScale(scale.map((x, j) => (j === i ? { ...x, value: e.target.value } : x))) }), _jsx(Input, { value: s.label, onChange: (e) => setScale(scale.map((x, j) => (j === i ? { ...x, label: e.target.value } : x))) }), _jsx(IconButton, { title: tGenerated('m_095b70fae90c39'), onClick: () => setScale(scale.filter((_, j) => j !== i)), children: _jsx(Trash2, { size: 13, className: "text-danger" }) })] }, i))) }) })] })] }));
|
|
769
|
+
}
|
|
770
|
+
function DataBindingEditor({ field, otherFields, autofillFields, sources, loading, onRefresh, dataSourcesHref, onChange, }) {
|
|
771
|
+
const b = field.binding;
|
|
772
|
+
const source = sources.find((s) => s.key === b?.sourceKey);
|
|
773
|
+
const cols = source?.columns ?? [];
|
|
774
|
+
// Persist a binding patch — but drop the binding entirely if no source is
|
|
775
|
+
// chosen, so the schema never carries an invalid empty sourceKey.
|
|
776
|
+
const patch = (p) => {
|
|
777
|
+
const next = { ...(b ?? {}), ...p };
|
|
778
|
+
if (!next.sourceKey) {
|
|
779
|
+
onChange({ binding: undefined });
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
onChange({
|
|
783
|
+
binding: next,
|
|
784
|
+
...(field.type === 'data_table' && p.selectable === 'none'
|
|
785
|
+
? { required: undefined, validation: undefined, defaultValue: undefined }
|
|
786
|
+
: {}),
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
return (_jsxs("div", { className: "space-y-2.5 rounded-md border border-primary bg-primary-subtle/30 p-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-[10px] font-semibold tracking-wider text-primary uppercase", children: _jsx(GeneratedText, { id: "m_0d523c82eb812b" }) }), dataSourcesHref ? (_jsx("a", { href: dataSourcesHref, target: "_blank", rel: "noreferrer", className: "text-[11px] text-primary hover:underline", children: _jsx(GeneratedText, { id: "m_1154a02825ffe0" }) })) : null, onRefresh ? (_jsxs("button", { type: "button", onClick: () => void onRefresh(), className: "inline-flex items-center gap-1 text-[11px] text-primary hover:underline", children: [_jsx(RefreshCw, { size: 11 }), _jsx(GeneratedText, { id: "m_16f11d7bc7b7b4" })] })) : null] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1e51e17fccd721" }) }), _jsxs(Select, { className: "h-8 text-xs", value: b?.sourceKey ?? '', onChange: (e) => onChange({
|
|
790
|
+
binding: e.target.value ? { sourceKey: e.target.value } : undefined,
|
|
791
|
+
...(field.type === 'data_table'
|
|
792
|
+
? { required: undefined, validation: undefined, defaultValue: undefined }
|
|
793
|
+
: {}),
|
|
794
|
+
}), children: [_jsx("option", { value: "", children: loading ? 'Loading…' : '— pick a data source —' }), sources.map((s) => (_jsx("option", { value: s.key, children: s.name }, s.id)))] }), _jsx(GeneratedValue, { value: !loading && sources.length === 0 ? (_jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_19589d12bcbbf9" }) })) : null })] }), _jsx(GeneratedValue, { value: source ? (_jsxs(_Fragment, { children: [_jsx(GeneratedValue, { value: field.type === 'lookup' ? (_jsx(LookupBindingFields, { b: b, cols: cols, otherFields: otherFields, autofillFields: autofillFields, patch: patch })) : null }), _jsx(GeneratedValue, { value: field.type === 'data_table' ? (_jsx(DataTableBindingFields, { b: b, cols: cols, otherFields: otherFields, patch: patch })) : null }), _jsx(GeneratedValue, { value: field.type === 'metric' ? (_jsx(MetricBindingFields, { b: b, cols: cols, otherFields: otherFields, patch: patch })) : null })] })) : null })] }));
|
|
795
|
+
}
|
|
796
|
+
function LookupBindingFields({ b, cols, otherFields, autofillFields, patch, }) {
|
|
797
|
+
const tGenerated = useGeneratedTranslations();
|
|
798
|
+
const autofill = b?.autofill ?? [];
|
|
799
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_16ba74a70fdc2f" }) }), _jsxs(Select, { className: "h-8 text-xs", value: b?.labelColumn ?? '', onChange: (e) => patch({ labelColumn: e.target.value || undefined }), children: [_jsx("option", { value: "", children: 'First column' }), cols.map((c) => (_jsx("option", { value: c.key, children: c.label }, c.key)))] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_01c681f83ca98e" }) }), _jsxs(Select, { className: "h-8 text-xs", value: b?.valueColumn ?? '', onChange: (e) => patch({ valueColumn: e.target.value || undefined }), children: [_jsx("option", { value: "", children: 'Row id' }), cols.map((c) => (_jsx("option", { value: c.key, children: c.label }, c.key)))] })] })] }), _jsx(CascadeBindingFields, { b: b, cols: cols, otherFields: otherFields, patch: patch }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_0d55cb5819a8ab" }) }), _jsx(Input, { type: "number", min: 1, max: 1000, className: "h-8 text-xs", value: b?.limit ?? '', placeholder: "50", onChange: (e) => patch({ limit: e.target.value ? Number(e.target.value) : undefined }) }), _jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_1c39dfb38ad051" }) })] }), _jsxs("div", { className: "space-y-1.5 rounded border border-border bg-surface p-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_1aa941e2f78dc5" }) }), _jsx("button", { type: "button", className: "text-xs text-primary hover:underline", onClick: () => patch({
|
|
800
|
+
autofill: [
|
|
801
|
+
...autofill,
|
|
802
|
+
{ column: cols[0]?.key ?? '', targetFieldId: autofillFields[0]?.id ?? '' },
|
|
803
|
+
],
|
|
804
|
+
}), children: _jsx(GeneratedText, { id: "m_0e2a97913eb84b" }) })] }), _jsx(GeneratedValue, { value: autofill.length === 0 ? (_jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_125586069a681f" }) })) : (autofill.map((m, i) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Select, { className: "h-8 text-xs", value: m.column, onChange: (e) => patch({
|
|
805
|
+
autofill: autofill.map((x, j) => j === i ? { ...x, column: e.target.value } : x),
|
|
806
|
+
}), children: cols.map((c) => (_jsx("option", { value: c.key, children: c.label }, c.key))) }), _jsx("span", { className: "text-fg-muted", children: "\u2192" }), _jsx(Select, { className: "h-8 text-xs", value: m.targetFieldId, onChange: (e) => patch({
|
|
807
|
+
autofill: autofill.map((x, j) => j === i ? { ...x, targetFieldId: e.target.value } : x),
|
|
808
|
+
}), children: autofillFields.map((f) => (_jsx("option", { value: f.id, children: f.label }, f.id))) }), _jsx(IconButton, { title: tGenerated('m_1d82f43c88df4c'), onClick: () => patch({ autofill: autofill.filter((_, j) => j !== i) }), children: _jsx(Trash2, { size: 13, className: "text-danger" }) })] }, i)))) })] })] }));
|
|
809
|
+
}
|
|
810
|
+
function CascadeBindingFields({ b, cols, otherFields, patch, }) {
|
|
811
|
+
return (_jsxs("div", { className: "space-y-1.5 rounded border border-border bg-surface p-2", children: [_jsx("div", { className: "text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_171519729cccb4" }) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_0bb347c23433ea" }) }), _jsxs(Select, { className: "h-8 text-xs", value: b?.filterByField ?? '', onChange: (e) => patch(e.target.value
|
|
812
|
+
? { filterByField: e.target.value }
|
|
813
|
+
: { filterByField: undefined, filterColumn: undefined }), children: [_jsx("option", { value: "", children: '— none —' }), otherFields.map((f) => (_jsx("option", { value: f.id, children: f.label }, f.id)))] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_0fdf1d8380c89b" }) }), _jsxs(Select, { className: "h-8 text-xs", value: b?.filterColumn ?? '', disabled: !b?.filterByField, onChange: (e) => patch({ filterColumn: e.target.value || undefined }), children: [_jsx("option", { value: "", children: "\u2014" }), cols.map((c) => (_jsx("option", { value: c.key, children: c.label }, c.key)))] })] })] }), _jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_03f068cc4e9384" }) })] }));
|
|
814
|
+
}
|
|
815
|
+
function DataTableBindingFields({ b, cols, otherFields, patch, }) {
|
|
816
|
+
const allShown = !b?.columns;
|
|
817
|
+
const shown = b?.columns ?? cols.map((c) => c.key);
|
|
818
|
+
const toggle = (key) => {
|
|
819
|
+
const base = b?.columns ?? cols.map((c) => c.key);
|
|
820
|
+
const next = base.includes(key) ? base.filter((k) => k !== key) : [...base, key];
|
|
821
|
+
patch({ columns: next });
|
|
822
|
+
};
|
|
823
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(CascadeBindingFields, { b: b, cols: cols, otherFields: otherFields, patch: patch }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_16f13907f8a65a" }) }), _jsx("div", { className: "flex flex-wrap gap-1.5 rounded border border-border bg-surface p-1.5", children: _jsx(GeneratedValue, { value: cols.map((c) => {
|
|
824
|
+
const on = allShown || shown.includes(c.key);
|
|
825
|
+
return (_jsx("button", { type: "button", onClick: () => toggle(c.key), className: `rounded px-1.5 py-0.5 text-[11px] ${on ? 'bg-primary-subtle text-primary' : 'bg-surface-hover text-fg-muted line-through'}`, children: _jsx(GeneratedValue, { value: c.label }) }, c.key));
|
|
826
|
+
}) }) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_14b18c80af7bd4" }) }), _jsxs(Select, { className: "h-8 text-xs", value: b?.selectable ?? 'none', onChange: (e) => patch({ selectable: e.target.value }), children: [_jsx("option", { value: "none", children: 'Display only' }), _jsx("option", { value: "single", children: 'Pick one row' }), _jsx("option", { value: "multi", children: 'Pick many rows' })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1ab6807a4e8f9e" }) }), _jsx(Input, { type: "number", min: 1, max: 1000, className: "h-8 text-xs", value: b?.limit ?? '', placeholder: "25", onChange: (e) => patch({ limit: e.target.value ? Number(e.target.value) : undefined }) })] })] })] }));
|
|
827
|
+
}
|
|
828
|
+
function MetricBindingFields({ b, cols, otherFields, patch, }) {
|
|
829
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
830
|
+
const agg = b?.aggregate ?? { fn: 'count' };
|
|
831
|
+
const setAgg = (p) => patch({ aggregate: { ...agg, ...p } });
|
|
832
|
+
const needsColumn = agg.fn !== 'count';
|
|
833
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(CascadeBindingFields, { b: b, cols: cols, otherFields: otherFields, patch: patch }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_102bef82722058" }) }), _jsxs(Select, { className: "h-8 text-xs", value: agg.fn, onChange: (e) => {
|
|
834
|
+
const fn = e.target.value;
|
|
835
|
+
setAgg(fn === 'count' ? { fn, column: undefined } : { fn });
|
|
836
|
+
}, children: [_jsx("option", { value: "count", children: 'Count' }), _jsx("option", { value: "sum", children: 'Sum' }), _jsx("option", { value: "avg", children: 'Average' }), _jsx("option", { value: "min", children: 'Min' }), _jsx("option", { value: "max", children: 'Max' })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_166da8c01a8107" }) }), _jsxs(Select, { className: "h-8 text-xs", value: agg.column ?? '', disabled: !needsColumn, onChange: (e) => setAgg({ column: e.target.value || undefined }), children: [_jsx("option", { value: "", children: needsColumn ? '— pick —' : 'n/a' }), cols.map((c) => (_jsx("option", { value: c.key, children: c.label }, c.key)))] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_175ee59112fb66" }) }), _jsxs(Select, { className: "h-8 text-xs", value: agg.groupBy ?? '', onChange: (e) => {
|
|
837
|
+
const groupBy = e.target.value || undefined;
|
|
838
|
+
patch({
|
|
839
|
+
aggregate: { ...agg, groupBy },
|
|
840
|
+
display: groupBy ? (b?.display === 'pie' ? 'pie' : 'bar') : 'number',
|
|
841
|
+
});
|
|
842
|
+
}, children: [_jsx("option", { value: "", children: '— no grouping —' }), cols.map((c) => (_jsx("option", { value: c.key, children: c.label }, c.key)))] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_06b9949f5fa1bc" }) }), _jsx(Select, { className: "h-8 text-xs", value: b?.display ?? (agg.groupBy ? 'bar' : 'number'), onChange: (e) => patch({ display: e.target.value }), children: agg.groupBy ? (_jsxs(_Fragment, { children: [_jsx("option", { value: "bar", children: 'Bar chart' }), _jsx("option", { value: "pie", children: 'Pie chart' })] })) : (_jsx("option", { value: "number", children: 'Number' })) })] })] }), _jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_067480a82419f2" }) }), _jsx(GeneratedValue, { value: agg.groupBy ? (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_147c46b5f494d3" }) }), _jsx(Input, { type: "number", min: 1, max: 1000, className: "h-8 text-xs", value: b?.limit ?? '', placeholder: tGeneratedValue(b?.display === 'pie' ? '8' : '12'), onChange: (e) => patch({ limit: e.target.value ? Number(e.target.value) : undefined }) }), _jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_018f962bfee661" }) })] })) : null })] }));
|
|
843
|
+
}
|
|
844
|
+
function FieldValidationTab({ field, onChange, }) {
|
|
845
|
+
const tGenerated = useGeneratedTranslations();
|
|
846
|
+
const v = field.validation ?? {};
|
|
847
|
+
function set(patch) {
|
|
848
|
+
const next = { ...v, ...patch };
|
|
849
|
+
// Strip undefineds so the JSON stays tight.
|
|
850
|
+
for (const k of Object.keys(next)) {
|
|
851
|
+
if (next[k] === undefined || next[k] === '')
|
|
852
|
+
delete next[k];
|
|
853
|
+
}
|
|
854
|
+
onChange({ validation: Object.keys(next).length ? next : undefined });
|
|
855
|
+
}
|
|
856
|
+
const isNumeric = field.type === 'number' || field.type === 'rating';
|
|
857
|
+
const isText = field.type === 'text' ||
|
|
858
|
+
field.type === 'long_text' ||
|
|
859
|
+
field.type === 'email' ||
|
|
860
|
+
field.type === 'phone' ||
|
|
861
|
+
field.type === 'url';
|
|
862
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: v.required ?? field.required ?? false, onChange: (e) => set({ required: e.target.checked }) }), _jsx(GeneratedText, { id: "m_0316664a18ded7" })] }), _jsx(GeneratedValue, { value: isNumeric ? (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_100639ca393959" }) }), _jsx(Input, { type: "number", value: v.min ?? '', onChange: (e) => set({ min: e.target.value === '' ? undefined : Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1929e34b445f83" }) }), _jsx(Input, { type: "number", value: v.max ?? '', onChange: (e) => set({ max: e.target.value === '' ? undefined : Number(e.target.value) }) })] })] })) : null }), _jsx(GeneratedValue, { value: isText ? (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_141aeed76d15ff" }) }), _jsx(Input, { type: "number", value: v.minLength ?? '', onChange: (e) => set({ minLength: e.target.value === '' ? undefined : Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_0806911c39f2f6" }) }), _jsx(Input, { type: "number", value: v.maxLength ?? '', onChange: (e) => set({ maxLength: e.target.value === '' ? undefined : Number(e.target.value) }) })] })] })) : null }), _jsx(GeneratedValue, { value: isText ? (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1357113b889d93" }) }), _jsx(Input, { value: v.pattern ?? '', onChange: (e) => set({ pattern: e.target.value || undefined }), placeholder: tGenerated('m_16907e8a555a15') }), _jsxs("p", { className: "text-[11px] text-fg-muted", children: [_jsx(GeneratedText, { id: "m_1653ad0a199171" }), ' ', _jsx(GeneratedValue, { value: _jsx(GeneratedText, { id: "m_1a3262f89c2c42" }) }), ' ', _jsx(GeneratedText, { id: "m_00b3d8649525da" })] })] })) : null }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1a014cc338576c" }) }), _jsx(Input, { value: v.message ?? '', onChange: (e) => set({ message: e.target.value || undefined }), placeholder: tGenerated('m_1737e921e5c6c6') })] })] }));
|
|
863
|
+
}
|
|
864
|
+
function FieldDefaultTab({ field, onChange, }) {
|
|
865
|
+
const tGenerated = useGeneratedTranslations();
|
|
866
|
+
const d = field.defaultValue;
|
|
867
|
+
const kind = d?.kind ?? '';
|
|
868
|
+
function setKind(next) {
|
|
869
|
+
if (!next)
|
|
870
|
+
return onChange({ defaultValue: undefined });
|
|
871
|
+
switch (next) {
|
|
872
|
+
case 'literal':
|
|
873
|
+
onChange({ defaultValue: { kind: 'literal', value: '' } });
|
|
874
|
+
break;
|
|
875
|
+
case 'today':
|
|
876
|
+
onChange({ defaultValue: { kind: 'today' } });
|
|
877
|
+
break;
|
|
878
|
+
case 'now':
|
|
879
|
+
onChange({ defaultValue: { kind: 'now' } });
|
|
880
|
+
break;
|
|
881
|
+
case 'current_user_person_id':
|
|
882
|
+
onChange({ defaultValue: { kind: 'current_user_person_id' } });
|
|
883
|
+
break;
|
|
884
|
+
case 'current_user_name':
|
|
885
|
+
onChange({ defaultValue: { kind: 'current_user_name' } });
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_0de21bd4fd99e9" }) }), _jsxs(Select, { className: "h-8 text-xs", value: kind, onChange: (e) => setKind(e.target.value), children: [_jsx("option", { value: "", children: '— No default —' }), _jsx("option", { value: "literal", children: 'Literal value' }), _jsx("option", { value: "today", children: "Today's date" }), _jsx("option", { value: "now", children: 'Right now' }), _jsx("option", { value: "current_user_person_id", children: "Current user's person id" }), _jsx("option", { value: "current_user_name", children: "Current user's name" })] }), _jsx(GeneratedValue, { value: d?.kind === 'literal' ? (_jsx(Input, { value: String(d.value ?? ''), onChange: (e) => onChange({ defaultValue: { kind: 'literal', value: e.target.value } }), placeholder: tGenerated('m_0776e7f56fd55a') })) : null })] }));
|
|
890
|
+
}
|
|
891
|
+
function ChoiceOptionsEditor({ field, locale, onChange, }) {
|
|
892
|
+
const tGenerated = useGeneratedTranslations();
|
|
893
|
+
const options = field.validation?.options ?? [];
|
|
894
|
+
const update = (next) => onChange({ validation: { ...field.validation, options: next } });
|
|
895
|
+
return (_jsxs("div", { className: "space-y-2 rounded-md border border-border bg-bg-subtle/50 p-2", children: [_jsx("div", { className: "text-xs font-semibold text-fg", children: _jsx(GeneratedText, { id: "m_0e69ebb67d27c2" }) }), _jsx(GeneratedValue, { value: options.length === 0 ? (_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_06a6b6820d522d" }) })) : (_jsx("ul", { className: "space-y-1", children: _jsx(GeneratedValue, { value: options.map((opt, i) => (_jsxs("li", { className: "flex items-center gap-1", children: [_jsx(Input, { className: "h-8 flex-1 text-xs", value: opt.value, placeholder: tGenerated('m_1ed2cc27e05841'), onChange: (e) => {
|
|
896
|
+
const next = [...options];
|
|
897
|
+
next[i] = { ...opt, value: e.target.value };
|
|
898
|
+
update(next);
|
|
899
|
+
} }), _jsx(Input, { className: "h-8 flex-1 text-xs", value: readText(opt.label, locale), placeholder: tGenerated('m_02a6c587f3d3fb'), onChange: (e) => {
|
|
900
|
+
const next = [...options];
|
|
901
|
+
next[i] = {
|
|
902
|
+
...opt,
|
|
903
|
+
label: writeText(opt.label, e.target.value, locale),
|
|
904
|
+
};
|
|
905
|
+
update(next);
|
|
906
|
+
} }), _jsx("button", { onClick: () => update(options.filter((_, j) => j !== i)), className: "text-fg-muted hover:text-danger", children: _jsx(Trash2, { size: 12 }) })] }, i))) }) })) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => update([
|
|
907
|
+
...options,
|
|
908
|
+
{
|
|
909
|
+
value: `opt_${options.length + 1}`,
|
|
910
|
+
label: { [locale]: 'New option' },
|
|
911
|
+
},
|
|
912
|
+
]), children: [_jsx(Plus, { size: 12 }), _jsx(GeneratedText, { id: "m_157bc1fc2157b9" })] })] }));
|
|
913
|
+
}
|
|
914
|
+
// --- Table config (column + row editor for `table` fields) ------------------
|
|
915
|
+
function TableConfigEditor({ field, onChange, }) {
|
|
916
|
+
const tGenerated = useGeneratedTranslations();
|
|
917
|
+
const config = (field.config ?? {});
|
|
918
|
+
const columns = (config.columns ?? []);
|
|
919
|
+
const rowMode = config.rowMode === 'fixed' ? 'fixed' : 'addable';
|
|
920
|
+
const fixedRows = config.rows ?? [];
|
|
921
|
+
function setConfig(patch) {
|
|
922
|
+
onChange({ config: { ...config, ...patch } });
|
|
923
|
+
}
|
|
924
|
+
const setColumns = (next) => setConfig({ columns: next });
|
|
925
|
+
const setRows = (next) => setConfig({ rows: next });
|
|
926
|
+
return (_jsxs("div", { className: "space-y-3 rounded-md border border-border bg-bg-subtle/50 p-2", children: [_jsx("div", { className: "text-xs font-semibold text-fg", children: _jsx(GeneratedText, { id: "m_0d25e53152dc0a" }) }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_03be2202673df4" }) }), _jsxs(Select, { className: "h-8 text-xs", value: rowMode, onChange: (e) => {
|
|
927
|
+
const nextMode = e.target.value;
|
|
928
|
+
setConfig(nextMode === 'fixed'
|
|
929
|
+
? { rowMode: nextMode, minRows: undefined, maxRows: undefined }
|
|
930
|
+
: { rowMode: nextMode, rows: undefined });
|
|
931
|
+
}, children: [_jsx("option", { value: "addable", children: 'Addable — user adds / removes rows' }), _jsx("option", { value: "fixed", children: 'Predefined — fixed list of rows' })] })] }), _jsx(GeneratedValue, { value: rowMode === 'addable' ? (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_00c5b5a40a72f6" }) }), _jsx(Input, { type: "number", className: "h-8 text-xs", value: config.minRows ?? '', onChange: (e) => setConfig({
|
|
932
|
+
minRows: e.target.value === '' ? undefined : Number(e.target.value),
|
|
933
|
+
}) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_199de632ae64e0" }) }), _jsx(Input, { type: "number", className: "h-8 text-xs", value: config.maxRows ?? '', onChange: (e) => setConfig({
|
|
934
|
+
maxRows: e.target.value === '' ? undefined : Number(e.target.value),
|
|
935
|
+
}) })] })] })) : null }), _jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_04eacfda3069db" }) }), _jsx(GeneratedValue, { value: columns.length === 0 ? (_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_09750de58b1490" }) })) : (_jsx("ul", { className: "space-y-2", children: _jsx(GeneratedValue, { value: columns.map((c, i) => (_jsxs("li", { className: "space-y-1 rounded border border-border bg-surface p-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Input, { className: "h-7 flex-1 text-xs", value: c.label, placeholder: tGenerated('m_1e2be77312c5b8'), onChange: (e) => setColumns(columns.map((x, idx) => idx === i ? { ...x, label: e.target.value } : x)) }), _jsxs(Select, { className: "h-7 w-24 text-xs", value: c.type, onChange: (e) => {
|
|
936
|
+
const type = e.target.value;
|
|
937
|
+
setColumns(columns.map((x, idx) => idx === i
|
|
938
|
+
? {
|
|
939
|
+
...x,
|
|
940
|
+
type,
|
|
941
|
+
options: type === 'select' ? (x.options ?? []) : undefined,
|
|
942
|
+
}
|
|
943
|
+
: x));
|
|
944
|
+
}, children: [_jsx("option", { value: "text", children: 'Text' }), _jsx("option", { value: "number", children: 'Number' }), _jsx("option", { value: "select", children: 'Dropdown' }), _jsx("option", { value: "checkbox", children: 'Checkbox' }), _jsx("option", { value: "date", children: 'Date' })] }), _jsx("button", { type: "button", onClick: () => setColumns(columns.filter((_, idx) => idx !== i)), className: "rounded p-1 text-fg-muted hover:text-danger", title: tGenerated('m_0605fd789eea1e'), children: _jsx(Trash2, { size: 12 }) })] }), _jsx(Input, { className: "h-7 font-mono text-[11px]", value: c.key, placeholder: tGenerated('m_1d19a02fd2872f'), onChange: (e) => setColumns(columns.map((x, idx) => idx === i ? { ...x, key: e.target.value.replace(/\s+/g, '_') } : x)) }), _jsx(GeneratedValue, { value: c.type === 'select' ? (_jsx(TableColumnOptions, { options: c.options ?? [], onChange: (opts) => setColumns(columns.map((x, idx) => idx === i ? { ...x, options: opts } : x)) })) : null })] }, i))) }) })) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
|
|
945
|
+
const n = columns.length + 1;
|
|
946
|
+
setColumns([...columns, { key: `col_${n}`, label: `Column ${n}`, type: 'text' }]);
|
|
947
|
+
}, children: [_jsx(Plus, { size: 12 }), " ", _jsx(GeneratedText, { id: "m_059cd549852b55" })] })] }), _jsx(GeneratedValue, { value: rowMode === 'fixed' ? (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_159e1e93b7e19d" }) }), _jsx(GeneratedValue, { value: fixedRows.length === 0 ? (_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_126f736e7419f7" }) })) : (_jsx("ul", { className: "space-y-1", children: _jsx(GeneratedValue, { value: fixedRows.map((r, i) => (_jsxs("li", { className: "flex items-center gap-1", children: [_jsx(Input, { className: "h-7 flex-1 text-xs", value: r.label, placeholder: tGenerated('m_0f24da1a05270a'), onChange: (e) => setRows(fixedRows.map((x, idx) => idx === i ? { label: e.target.value } : x)) }), _jsx("button", { type: "button", onClick: () => setRows(fixedRows.filter((_, idx) => idx !== i)), className: "rounded p-1 text-fg-muted hover:text-danger", title: tGenerated('m_12b310a027b08a'), children: _jsx(Trash2, { size: 12 }) })] }, i))) }) })) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setRows([...fixedRows, { label: `Row ${fixedRows.length + 1}` }]), children: [_jsx(Plus, { size: 12 }), " ", _jsx(GeneratedText, { id: "m_1eabd71bbc0199" })] })] })) : null })] }));
|
|
948
|
+
}
|
|
949
|
+
function TableColumnOptions({ options, onChange, }) {
|
|
950
|
+
const tGenerated = useGeneratedTranslations();
|
|
951
|
+
return (_jsxs("div", { className: "space-y-1 rounded border border-border bg-bg-subtle p-1.5", children: [_jsx("div", { className: "text-[10px] font-semibold tracking-wider text-fg-muted uppercase", children: _jsx(GeneratedText, { id: "m_0e69ebb67d27c2" }) }), _jsx(GeneratedValue, { value: options.map((o, i) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Input, { className: "h-6 flex-1 text-[11px]", value: o.label, placeholder: tGenerated('m_044a5abd7d3ad4'), onChange: (e) => onChange(options.map((x, idx) => idx === i
|
|
952
|
+
? {
|
|
953
|
+
value: x.value || e.target.value.toLowerCase().replace(/\s+/g, '_'),
|
|
954
|
+
label: e.target.value,
|
|
955
|
+
}
|
|
956
|
+
: x)) }), _jsx("button", { type: "button", onClick: () => onChange(options.filter((_, idx) => idx !== i)), className: "rounded p-0.5 text-fg-muted hover:text-danger", children: _jsx(Trash2, { size: 11 }) })] }, i))) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => onChange([
|
|
957
|
+
...options,
|
|
958
|
+
{ value: `opt_${options.length + 1}`, label: `Option ${options.length + 1}` },
|
|
959
|
+
]), children: [_jsx(Plus, { size: 11 }), " ", _jsx(GeneratedText, { id: "m_00f78a864ff266" })] })] }));
|
|
960
|
+
}
|
|
961
|
+
// --- Section properties ----------------------------------------------------
|
|
962
|
+
function SectionProperties({ section, schema, locale, defaultLocale, onChange, }) {
|
|
963
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
964
|
+
const tGenerated = useGeneratedTranslations();
|
|
965
|
+
const allFields = schema.sections
|
|
966
|
+
.filter((candidate) => !candidate.repeating)
|
|
967
|
+
.flatMap((candidate) => candidate.fields)
|
|
968
|
+
.filter(storesResponseValue)
|
|
969
|
+
.map((field) => ({
|
|
970
|
+
id: field.id,
|
|
971
|
+
label: localizeText(field.label, locale, field.id, defaultLocale),
|
|
972
|
+
}));
|
|
973
|
+
return (_jsxs("div", { className: "space-y-3 text-sm", children: [_jsx("h3", { className: "text-sm font-semibold text-fg", children: _jsx(GeneratedText, { id: "m_0d513924d97753" }) }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_089e71eb771aca" }) }), _jsx(Input, { value: section.id, disabled: true, className: "font-mono text-xs" })] }), _jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "text-xs", children: [_jsx(GeneratedText, { id: "m_1ea646f0dcd3d5" }), _jsx(GeneratedValue, { value: locale.toUpperCase() }), ")"] }), _jsx(Input, { value: readText(section.title, locale), placeholder: tGeneratedValue(localizeText(section.title, locale, section.id, defaultLocale)), onChange: (e) => onChange({ title: writeText(section.title, e.target.value, locale) }) })] }), _jsx(GeneratedValue, { value: schema.tabs?.length ? (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1457c1cce0b63c" }) }), _jsx(Select, { value: section.tabId ?? schema.tabs[0].id, onChange: (e) => onChange({ tabId: e.target.value }), children: schema.tabs.map((t) => (_jsx("option", { value: t.id, children: localizeText(t.title, locale, t.id, defaultLocale) }, t.id))) })] })) : null }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_14d923495cf14c" }) }), _jsx(Textarea, { rows: 2, value: readText(section.description, locale), placeholder: tGeneratedValue(localizeText(section.description, locale, '', defaultLocale)), onChange: (e) => {
|
|
974
|
+
onChange({
|
|
975
|
+
description: e.target.value
|
|
976
|
+
? writeText(section.description, e.target.value, locale)
|
|
977
|
+
: undefined,
|
|
978
|
+
});
|
|
979
|
+
} })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_0664f38791aed8" }) }), _jsxs(Select, { className: "h-8 text-xs", value: section.step ?? '', onChange: (e) => onChange({ step: e.target.value || undefined }), children: [_jsx("option", { value: "", children: '— first step (default) —' }), (schema.workflow?.steps ?? []).map((s) => (_jsx("option", { value: s.key, children: localizeText(s.title, locale, s.key, defaultLocale) }, s.key)))] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1ee65e14915cf1" }) }), _jsxs(Select, { className: "h-8 text-xs", value: String(section.layout?.columns ?? 1), onChange: (e) => {
|
|
980
|
+
const n = Number(e.target.value);
|
|
981
|
+
onChange({ layout: n > 1 ? { columns: n, gap: section.layout?.gap } : undefined });
|
|
982
|
+
}, children: [_jsx("option", { value: "1", children: '1 column' }), _jsx("option", { value: "2", children: '2 columns' }), _jsx("option", { value: "3", children: '3 columns' }), _jsx("option", { value: "4", children: '4 columns' })] }), _jsx("p", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_1e0a540071af99" }) })] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: section.repeating ?? false, onChange: (e) => onChange({ repeating: e.target.checked }) }), _jsx(GeneratedText, { id: "m_10a741ad57253f" })] }), _jsx(GeneratedValue, { value: section.repeating ? (_jsxs("div", { className: "space-y-2 rounded border border-border bg-bg-subtle/50 p-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_00c5b5a40a72f6" }) }), _jsx(Input, { type: "number", value: section.minRows ?? '', onChange: (e) => onChange({
|
|
983
|
+
minRows: e.target.value === '' ? undefined : Number(e.target.value),
|
|
984
|
+
}) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_199de632ae64e0" }) }), _jsx(Input, { type: "number", value: section.maxRows ?? '', onChange: (e) => onChange({
|
|
985
|
+
maxRows: e.target.value === '' ? undefined : Number(e.target.value),
|
|
986
|
+
}) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_1c4a359175c7fa" }) }), _jsx(Input, { value: section.rowLabelTemplate ?? '', placeholder: tGenerated('m_02890419bf69c5'), onChange: (e) => onChange({ rowLabelTemplate: e.target.value || undefined }) }), _jsxs("p", { className: "text-[10px] text-fg-muted", children: [_jsx(GeneratedText, { id: "m_0ea6ba70f4dcd9" }), ' ', _jsx(GeneratedValue, { value: _jsx(GeneratedText, { id: "m_126c7eab8ac9fd" }) }), ",", ' ', _jsx(GeneratedValue, { value: _jsx(GeneratedText, { id: "m_1ed8de977a458a" }) }), _jsx(GeneratedText, { id: "m_1e018b65c95ffd" }), ' ', _jsx(GeneratedValue, { value: _jsx(GeneratedText, { id: "m_1fad56c24b22ed" }) }), ' ', _jsx(GeneratedText, { id: "m_02193054995990" })] })] })] })) : null }), _jsxs("div", { className: "space-y-1 pt-2", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedText, { id: "m_024d5ac76425dc" }) }), _jsx(LogicBuilder, { rule: section.showIf, availableFields: allFields, onChange: (rule) => onChange({ showIf: rule }) })] })] }));
|
|
987
|
+
}
|
|
988
|
+
// --- Preview pane ----------------------------------------------------------
|
|
989
|
+
function Preview({ schema, locale, defaultLocale, }) {
|
|
990
|
+
const sections = schema.sections;
|
|
991
|
+
const groupedByStep = useMemo(() => {
|
|
992
|
+
const out = new Map();
|
|
993
|
+
const defaultStep = schema.workflow?.steps[0]?.key ?? '';
|
|
994
|
+
for (const sec of sections) {
|
|
995
|
+
const k = sec.step ?? defaultStep;
|
|
996
|
+
const list = out.get(k) ?? [];
|
|
997
|
+
list.push(sec);
|
|
998
|
+
out.set(k, list);
|
|
999
|
+
}
|
|
1000
|
+
return out;
|
|
1001
|
+
}, [sections, schema.workflow?.steps]);
|
|
1002
|
+
return (_jsxs(Card, { className: "border-2 border-dashed border-border-strong bg-surface border-border", children: [_jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "text-base", children: [_jsx(GeneratedText, { id: "m_07c67e89962d16" }), ' ', _jsx(GeneratedValue, { value: localizeText(schema.title, locale, 'Form', defaultLocale) })] }), _jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_045d47c21e1cdf" }) })] }), _jsx(CardContent, { className: "space-y-4 text-sm", children: _jsx(GeneratedValue, { value: (schema.workflow?.steps ?? []).map((step) => {
|
|
1003
|
+
const stepSections = groupedByStep.get(step.key) ?? [];
|
|
1004
|
+
if (stepSections.length === 0)
|
|
1005
|
+
return null;
|
|
1006
|
+
return (_jsxs("div", { className: "rounded-md border border-border", children: [_jsxs("div", { className: "border-b border-border bg-bg-subtle px-3 py-1.5 text-xs font-semibold text-fg bg-bg-subtle/40", children: [_jsx(GeneratedText, { id: "m_1acffe9b7aa3dd" }), ' ', _jsx(GeneratedValue, { value: localizeText(step.title, locale, step.key, defaultLocale) })] }), _jsx("div", { className: "space-y-3 p-3", children: _jsx(GeneratedValue, { value: stepSections.map((sec) => (_jsxs("div", { children: [_jsxs("h3", { className: "mb-1 text-sm font-semibold text-fg", children: [_jsx(GeneratedValue, { value: localizeText(sec.title, locale, sec.id, defaultLocale) }), _jsx(GeneratedValue, { value: ' ' }), _jsx(GeneratedValue, { value: sec.repeating ? (_jsx(Badge, { variant: "secondary", className: "text-[10px]", children: _jsx(GeneratedText, { id: "m_06e1ca227aca9b" }) })) : null })] }), _jsx(GeneratedValue, { value: localizeText(sec.description, locale, '', defaultLocale) ? (_jsx("p", { className: "mb-1 text-xs text-fg-muted", children: _jsx(GeneratedValue, { value: localizeText(sec.description, locale, '', defaultLocale) }) })) : null }), _jsx("div", { className: "space-y-2", children: _jsx(GeneratedValue, { value: sec.fields.map((f) => (_jsxs("div", { children: [_jsxs("label", { className: "block text-xs font-medium text-fg", children: [_jsx(GeneratedValue, { value: localizeText(f.label, locale, f.id, defaultLocale) }), _jsx(GeneratedValue, { value: ' ' }), _jsx(GeneratedValue, { value: f.required || f.validation?.required ? (_jsx("span", { className: "text-danger", children: "*" })) : null }), _jsx(GeneratedValue, { value: f.showIf ? (_jsx(Badge, { variant: "outline", className: "ml-1 text-[10px]", children: _jsx(GeneratedText, { id: "m_02529c1532db7a" }) })) : null })] }), _jsx(PreviewField, { type: f.type })] }, f.id))) }) })] }, sec.id))) }) })] }, step.key));
|
|
1007
|
+
}) }) })] }));
|
|
1008
|
+
}
|
|
1009
|
+
function PreviewField({ type }) {
|
|
1010
|
+
const tGenerated = useGeneratedTranslations();
|
|
1011
|
+
switch (type) {
|
|
1012
|
+
case 'long_text':
|
|
1013
|
+
return _jsx(Textarea, { rows: 2, disabled: true, placeholder: tGenerated('m_11d37007232de5') });
|
|
1014
|
+
case 'select':
|
|
1015
|
+
case 'radio':
|
|
1016
|
+
return (_jsx(Select, { disabled: true, children: _jsx("option", { children: "\u2014" }) }));
|
|
1017
|
+
case 'checkbox_group':
|
|
1018
|
+
case 'multi_select':
|
|
1019
|
+
return (_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_0a31fefe8144a2" }) }));
|
|
1020
|
+
case 'signature':
|
|
1021
|
+
return _jsx("div", { className: "h-16 rounded border border-dashed border-border-strong bg-bg-subtle" });
|
|
1022
|
+
case 'photo':
|
|
1023
|
+
case 'photo_upload':
|
|
1024
|
+
case 'file':
|
|
1025
|
+
case 'video':
|
|
1026
|
+
case 'audio':
|
|
1027
|
+
return (_jsx("div", { className: "rounded border border-dashed border-border-strong p-3 text-center text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_1411910c3561c0" }) }));
|
|
1028
|
+
case 'heading':
|
|
1029
|
+
return (_jsx("h4", { className: "text-base font-semibold", children: _jsx(GeneratedText, { id: "m_0f8b37ea44adb6" }) }));
|
|
1030
|
+
case 'paragraph':
|
|
1031
|
+
return (_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_0131c1cfaf2d5c" }) }));
|
|
1032
|
+
case 'divider':
|
|
1033
|
+
return _jsx("hr", { className: "border-border" });
|
|
1034
|
+
case 'formula':
|
|
1035
|
+
return _jsx(Input, { disabled: true, value: "(computed)" });
|
|
1036
|
+
case 'pass_fail_na':
|
|
1037
|
+
return (_jsx("div", { className: "flex gap-1", children: _jsx(GeneratedValue, { value: ['PASS', 'FAIL', 'N/A'].map((v) => (_jsx("span", { className: "rounded border border-border px-2 py-1 text-[10px]", children: _jsx(GeneratedValue, { value: v }) }, v))) }) }));
|
|
1038
|
+
case 'traffic_light':
|
|
1039
|
+
return (_jsx("div", { className: "flex gap-1", children: _jsx(GeneratedValue, { value: [
|
|
1040
|
+
['bg-success', 'Green'],
|
|
1041
|
+
['bg-warning', 'Yellow'],
|
|
1042
|
+
['bg-danger', 'Red'],
|
|
1043
|
+
].map(([tone, label]) => (_jsxs("span", { className: "flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px]", children: [_jsx("span", { className: `inline-block h-2 w-2 rounded-full ${tone}` }), _jsx(GeneratedValue, { value: label })] }, label))) }) }));
|
|
1044
|
+
default:
|
|
1045
|
+
return _jsx(Input, { disabled: true, placeholder: tGenerated('m_11d37007232de5') });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// --- Unified editor: rail tabs + left-pane panels ---------------------------
|
|
1049
|
+
function LabeledField({ label, children }) {
|
|
1050
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs", children: _jsx(GeneratedValue, { value: label }) }), _jsx(GeneratedValue, { value: children })] }));
|
|
1051
|
+
}
|
|
1052
|
+
function LeftTab({ active, onClick, icon, label, }) {
|
|
1053
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
1054
|
+
return (_jsx("button", { type: "button", onClick: onClick, title: tGeneratedValue(label), "aria-label": tGeneratedValue(label), className: `flex flex-1 items-center justify-center rounded-md px-1.5 py-2 transition ${active
|
|
1055
|
+
? 'bg-primary-subtle text-primary'
|
|
1056
|
+
: 'text-fg-muted hover:bg-surface-hover hover:text-fg'}`, children: _jsx(GeneratedValue, { value: icon }) }));
|
|
1057
|
+
}
|
|
1058
|
+
function SurfaceTab({ active, onClick, icon, label, }) {
|
|
1059
|
+
return (_jsxs("button", { type: "button", onClick: onClick, className: `flex items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition ${active
|
|
1060
|
+
? 'bg-fg text-bg'
|
|
1061
|
+
: 'text-fg-muted hover:bg-surface-hover'}`, children: [_jsx(GeneratedValue, { value: icon }), " ", _jsx(GeneratedValue, { value: label })] }));
|
|
1062
|
+
}
|
|
1063
|
+
function OverviewPanel({ name, overview, onSaved, onSave, canPin = false, pinned = false, onPin, }) {
|
|
1064
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
1065
|
+
const tGenerated = useGeneratedTranslations();
|
|
1066
|
+
const [n, setN] = useState(name);
|
|
1067
|
+
const [description, setDescription] = useState(overview?.description ?? '');
|
|
1068
|
+
const [surfaceAsTool, setSurfaceAsTool] = useState(overview?.surfaceAsTool ?? false);
|
|
1069
|
+
const [pending, start] = useTransition();
|
|
1070
|
+
const save = () => {
|
|
1071
|
+
if (n.trim().length < 2) {
|
|
1072
|
+
toast.error(tGenerated('m_0aa48682b14c95'));
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
start(async () => {
|
|
1076
|
+
const res = await onSave({
|
|
1077
|
+
name: n.trim(),
|
|
1078
|
+
description,
|
|
1079
|
+
surfaceAsTool,
|
|
1080
|
+
});
|
|
1081
|
+
if (!res.ok) {
|
|
1082
|
+
toast.error(tGeneratedValue(res.error ?? tGenerated('m_0af1983403d12e')));
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
onSaved(n.trim());
|
|
1086
|
+
toast.success(tGenerated('m_0964f6e2cc4375'));
|
|
1087
|
+
});
|
|
1088
|
+
};
|
|
1089
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_08114bb9803f7d" }) }), _jsx(LabeledField, { label: tGenerated('m_18a30dc84d2a64'), children: _jsx(Input, { value: n, onChange: (e) => setN(e.target.value) }) }), _jsx(LabeledField, { label: tGenerated('m_14d923495cf14c'), children: _jsx(Textarea, { rows: 4, value: description, placeholder: tGenerated('m_074251897a5932'), onChange: (e) => setDescription(e.target.value) }) }), _jsxs("label", { className: "flex items-start gap-2 rounded-md border border-border p-3 text-sm", children: [_jsx("input", { type: "checkbox", checked: surfaceAsTool, onChange: (e) => setSurfaceAsTool(e.target.checked), className: "mt-0.5 h-4 w-4" }), _jsxs("span", { children: [_jsx("span", { className: "font-medium text-fg", children: _jsx(GeneratedText, { id: "m_1f008c56f09179" }) }), _jsxs("span", { className: "block text-xs text-fg-muted", children: [_jsx(GeneratedText, { id: "m_160f2c28b65229" }), ' ', _jsx("span", { className: "font-mono", children: _jsx(GeneratedText, { id: "m_1ddad1532de2e3" }) }), ' ', _jsx(GeneratedText, { id: "m_186d665b9052f3" })] })] })] }), _jsx(Button, { onClick: save, disabled: pending, className: "w-full", children: _jsx(GeneratedValue, { value: pending ? (_jsx(GeneratedText, { id: "m_106811f2aac664" })) : (_jsx(GeneratedText, { id: "m_16f4ae27da8dbb" })) }) }), _jsx(GeneratedValue, { value: canPin ? (_jsxs("div", { className: "space-y-2 rounded-md border border-border p-3", children: [_jsx("div", { className: "text-sm font-medium text-fg", children: _jsx(GeneratedText, { id: "m_0cc4443595b87e" }) }), _jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_1dadff0d2c2898" }) }), onPin ? (_jsxs(Button, { variant: pinned ? 'secondary' : 'outline', size: "sm", disabled: pending, onClick: () => start(async () => {
|
|
1090
|
+
const result = await onPin(!pinned);
|
|
1091
|
+
if (!result.ok)
|
|
1092
|
+
toast.error(tGeneratedValue(result.error ?? tGenerated('m_09249e0e6a801e')));
|
|
1093
|
+
}), children: [pinned ? _jsx(PinOff, { size: 14 }) : _jsx(Pin, { size: 14 }), pinned ? _jsx(GeneratedText, { id: "m_09eea65fa736d3" }) : _jsx(GeneratedText, { id: "m_16b0d6e1ba7d5e" })] })) : null] })) : null })] }));
|
|
1094
|
+
}
|
|
1095
|
+
function AssignmentsPanel({ createHref, listHref }) {
|
|
1096
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("p", { className: "text-xs text-fg-muted", children: [_jsx(GeneratedText, { id: "m_0976c514f311fb" }), ' ', _jsx("span", { className: "font-medium text-fg", children: _jsx(GeneratedText, { id: "m_096d47f60747b3" }) }), ' ', _jsx(GeneratedText, { id: "m_1bac81ba078d96" })] }), _jsx("a", { href: createHref, className: "block", children: _jsxs(Button, { className: "w-full", children: [_jsx(Send, { size: 14 }), " ", _jsx(GeneratedText, { id: "m_0b60657b166b0e" })] }) }), _jsx("a", { href: listHref, className: "block", children: _jsx(Button, { variant: "outline", className: "w-full", children: _jsx(GeneratedText, { id: "m_0c528a939be441" }) }) }), _jsx("p", { className: "text-[11px] text-fg-muted", children: _jsx(GeneratedText, { id: "m_0ad38d3d51d13e" }) })] }));
|
|
1097
|
+
}
|
|
1098
|
+
function PermissionsPanel({ roles, initial, onSave, }) {
|
|
1099
|
+
const tGeneratedValue = useGeneratedValueTranslations();
|
|
1100
|
+
const tGenerated = useGeneratedTranslations();
|
|
1101
|
+
const [sel, setSel] = useState(new Set(initial));
|
|
1102
|
+
const [pending, start] = useTransition();
|
|
1103
|
+
const toggle = (key) => setSel((s) => {
|
|
1104
|
+
const n = new Set(s);
|
|
1105
|
+
if (n.has(key))
|
|
1106
|
+
n.delete(key);
|
|
1107
|
+
else
|
|
1108
|
+
n.add(key);
|
|
1109
|
+
return n;
|
|
1110
|
+
});
|
|
1111
|
+
const save = () => start(async () => {
|
|
1112
|
+
const res = await onSave(Array.from(sel));
|
|
1113
|
+
if (!res.ok) {
|
|
1114
|
+
toast.error(tGeneratedValue(res.error ?? tGenerated('m_0af1983403d12e')));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
toast.success(tGenerated('m_1e42ea374d7063'));
|
|
1118
|
+
});
|
|
1119
|
+
const restricted = sel.size > 0;
|
|
1120
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_14c557ff352d60" }) }), _jsx("div", { className: `rounded-md border px-3 py-2 text-xs ${restricted
|
|
1121
|
+
? 'border-warning bg-warning-subtle text-warning'
|
|
1122
|
+
: 'border-success bg-success-subtle text-success'}`, children: _jsx(GeneratedValue, { value: restricted ? (_jsx(GeneratedText, { id: "m_1f688129a274f7", values: { value0: sel.size, value1: sel.size === 1 ? '' : 's' } })) : (_jsx(GeneratedText, { id: "m_1452f24ba8370a" })) }) }), _jsx(GeneratedValue, { value: roles.length === 0 ? (_jsx("p", { className: "text-xs text-fg-muted", children: _jsx(GeneratedText, { id: "m_008f5a3d7812ab" }) })) : (_jsx("ul", { className: "space-y-1", children: _jsx(GeneratedValue, { value: roles.map((r) => (_jsx("li", { children: _jsxs("label", { className: "flex cursor-pointer items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm hover:bg-bg-subtle", children: [_jsx("input", { type: "checkbox", checked: sel.has(r.key), onChange: () => toggle(r.key) }), _jsx("span", { className: "flex-1", children: _jsx(GeneratedValue, { value: r.name }) }), _jsx("span", { className: "text-[10px] text-fg-muted", children: _jsx(GeneratedValue, { value: r.key }) })] }) }, r.key))) }) })) }), _jsx(Button, { onClick: save, disabled: pending, className: "w-full", children: _jsx(GeneratedValue, { value: pending ? (_jsx(GeneratedText, { id: "m_106811f2aac664" })) : (_jsx(GeneratedText, { id: "m_044b4d57bb99ce" })) }) })] }));
|
|
1123
|
+
}
|
|
1124
|
+
//# sourceMappingURL=production-form-designer.js.map
|