astro-tractstack 2.0.0-rc.63 → 2.0.0-rc.64
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/package.json +2 -2
- package/templates/src/client/belief-events.js +29 -10
- package/templates/src/components/Menu.tsx +22 -26
- package/templates/src/components/edit/pane/PanePanel_path.tsx +0 -2
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +18 -9
- package/templates/src/components/form/ActionBuilderField.tsx +20 -3
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-tractstack",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.64",
|
|
4
4
|
"description": "Astro integration for TractStack - redeeming the web from boring experiences",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@ark-ui/react": "^5.21.0",
|
|
56
56
|
"@astrojs/react": "^3.6.3",
|
|
57
57
|
"@heroicons/react": "^2.1.1",
|
|
58
|
-
"@internationalized/date": "3.
|
|
58
|
+
"@internationalized/date": "^3.9.0",
|
|
59
59
|
"@mhsdesign/jit-browser-tailwindcss": "^0.4.2",
|
|
60
60
|
"@nanostores/persistent": "^1.1.0",
|
|
61
61
|
"@nanostores/react": "^1.0.0",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const VERBOSE = false;
|
|
2
2
|
|
|
3
|
-
// This function now contains all essential one-time and recurring HTMX setup.
|
|
4
3
|
function configureHtmx() {
|
|
5
|
-
if (!window.htmx)
|
|
4
|
+
if (!window.htmx || window.HTMX_LISTENER_ATTACHED) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
window.HTMX_LISTENER_ATTACHED = true;
|
|
6
8
|
|
|
7
9
|
if (!window.HTMX_CONFIGURED) {
|
|
8
10
|
window.htmx.config.selfRequestsOnly = false;
|
|
@@ -11,13 +13,12 @@ function configureHtmx() {
|
|
|
11
13
|
|
|
12
14
|
window.htmx.on(document.body, 'htmx:configRequest', function (evt) {
|
|
13
15
|
const config = window.TRACTSTACK_CONFIG;
|
|
14
|
-
if (!config || !config.sessionId) return;
|
|
16
|
+
if (!config || !config.sessionId) return;
|
|
15
17
|
|
|
16
18
|
if (evt.detail.path && evt.detail.path.startsWith('/api/v1/')) {
|
|
17
19
|
evt.detail.path = config.backendUrl + evt.detail.path;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
// MODIFIED: Use session ID from the global config object
|
|
21
22
|
const sessionId = config.sessionId;
|
|
22
23
|
evt.detail.headers['X-Tenant-ID'] = config.tenantId;
|
|
23
24
|
evt.detail.headers['X-StoryFragment-ID'] = config.storyfragmentId;
|
|
@@ -25,6 +26,27 @@ function configureHtmx() {
|
|
|
25
26
|
evt.detail.headers['X-TractStack-Session-ID'] = sessionId;
|
|
26
27
|
}
|
|
27
28
|
});
|
|
29
|
+
|
|
30
|
+
window.htmx.on(document.body, 'htmx:beforeRequest', async function (evt) {
|
|
31
|
+
const params = evt.detail.requestConfig.parameters;
|
|
32
|
+
if (params && params.beliefVerb === 'IDENTIFY_AS') {
|
|
33
|
+
evt.preventDefault();
|
|
34
|
+
|
|
35
|
+
const originalPayload = params;
|
|
36
|
+
const unsetPayload = {
|
|
37
|
+
unsetBeliefIds: originalPayload.beliefId,
|
|
38
|
+
paneId: originalPayload.paneId || '',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await sendBeliefUpdate(unsetPayload);
|
|
43
|
+
await sendBeliefUpdate(originalPayload);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (VERBOSE)
|
|
46
|
+
console.error('🔴 BELIEF: Two-step identifyAs update failed', error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
28
50
|
}
|
|
29
51
|
|
|
30
52
|
const pageBeliefs = {};
|
|
@@ -32,7 +54,6 @@ let activeStoryfragmentId = null;
|
|
|
32
54
|
|
|
33
55
|
function waitForSessionReady() {
|
|
34
56
|
return new Promise((resolve) => {
|
|
35
|
-
// This event is fired by sse.ts after the handshake is complete.
|
|
36
57
|
if (window.TRACTSTACK_CONFIG?.session?.isReady) {
|
|
37
58
|
resolve();
|
|
38
59
|
} else {
|
|
@@ -54,7 +75,7 @@ function initializeBeliefs() {
|
|
|
54
75
|
'🔧 BELIEF: First-time initialization of belief handlers and HTMX config.'
|
|
55
76
|
);
|
|
56
77
|
|
|
57
|
-
configureHtmx();
|
|
78
|
+
configureHtmx();
|
|
58
79
|
|
|
59
80
|
document.addEventListener('change', function (event) {
|
|
60
81
|
const target = event.target;
|
|
@@ -96,7 +117,6 @@ async function handleBeliefChange(element) {
|
|
|
96
117
|
|
|
97
118
|
trackBeliefState(beliefId, beliefValue);
|
|
98
119
|
|
|
99
|
-
// Pass the current page's beliefs to the backend
|
|
100
120
|
await sendBeliefUpdate({
|
|
101
121
|
beliefId,
|
|
102
122
|
beliefType,
|
|
@@ -110,7 +130,7 @@ async function sendBeliefUpdate(data) {
|
|
|
110
130
|
|
|
111
131
|
try {
|
|
112
132
|
const config = window.TRACTSTACK_CONFIG;
|
|
113
|
-
if (!config || !config.sessionId) return;
|
|
133
|
+
if (!config || !config.sessionId) return;
|
|
114
134
|
|
|
115
135
|
if (VERBOSE)
|
|
116
136
|
console.log('🚨 FRONTEND DEBUG: Sending belief update with headers:', {
|
|
@@ -170,7 +190,7 @@ function setActiveStoryFragment() {
|
|
|
170
190
|
console.log(
|
|
171
191
|
`📖 BELIEF: Active story fragment set to ${activeStoryfragmentId}`
|
|
172
192
|
);
|
|
173
|
-
|
|
193
|
+
|
|
174
194
|
if (!pageBeliefs[activeStoryfragmentId]) {
|
|
175
195
|
pageBeliefs[activeStoryfragmentId] = {};
|
|
176
196
|
}
|
|
@@ -184,7 +204,6 @@ document.addEventListener('astro:page-load', () => {
|
|
|
184
204
|
setActiveStoryFragment();
|
|
185
205
|
});
|
|
186
206
|
|
|
187
|
-
// Also set the active story on the very first page load.
|
|
188
207
|
document.addEventListener('DOMContentLoaded', setActiveStoryFragment);
|
|
189
208
|
|
|
190
209
|
if (VERBOSE)
|
|
@@ -64,7 +64,6 @@ const MenuComponent = (props: MenuProps) => {
|
|
|
64
64
|
const { payload, slug, isContext, brandConfig } = props;
|
|
65
65
|
const thisPayload = payload.optionsPayload;
|
|
66
66
|
|
|
67
|
-
// Helper function to process menu links - MODIFIED to build the correct hx-vals payload
|
|
68
67
|
function processMenuLink(e: MenuLink): ProcessedMenuLinkDatum {
|
|
69
68
|
const item = { ...e } as ProcessedMenuLinkDatum;
|
|
70
69
|
const actionLisp = item.actionLisp?.trim();
|
|
@@ -83,35 +82,36 @@ const MenuComponent = (props: MenuProps) => {
|
|
|
83
82
|
return item;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const tokens =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
const [lispTokens] = lispLexer(actionLisp);
|
|
86
|
+
|
|
87
|
+
if (lispTokens && lispTokens.length > 0) {
|
|
88
|
+
// Deconstruct the nested structure: e.g., ['declare', ['HotLead', 'BELIEVES_YES']]
|
|
89
|
+
const tokens = lispTokens[0] as LispToken[];
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
(tokens[0] === 'declare' || tokens[0] === 'identifyAs') &&
|
|
93
|
+
Array.isArray(tokens[1]) &&
|
|
94
|
+
tokens[1].length >= 2
|
|
95
|
+
) {
|
|
96
|
+
const command = tokens[0] as string;
|
|
97
|
+
const params = tokens[1] as (string | number)[];
|
|
98
|
+
const beliefId = params[0] as string;
|
|
99
|
+
const value = params[1] as string;
|
|
98
100
|
|
|
99
|
-
if (command && beliefId !== undefined && value !== undefined) {
|
|
100
101
|
let hxValsMap: { [key: string]: string } = {};
|
|
101
102
|
|
|
102
|
-
// CORRECTED: Build the hx-vals payload to match server expectations.
|
|
103
103
|
if (command === 'declare') {
|
|
104
104
|
hxValsMap = {
|
|
105
|
-
beliefId:
|
|
106
|
-
beliefType: 'Belief',
|
|
107
|
-
beliefValue:
|
|
105
|
+
beliefId: beliefId,
|
|
106
|
+
beliefType: 'Belief',
|
|
107
|
+
beliefValue: value,
|
|
108
108
|
};
|
|
109
109
|
} else if (command === 'identifyAs') {
|
|
110
110
|
hxValsMap = {
|
|
111
|
-
beliefId:
|
|
112
|
-
beliefType: 'Belief',
|
|
113
|
-
beliefVerb: 'IDENTIFY_AS',
|
|
114
|
-
beliefObject:
|
|
111
|
+
beliefId: beliefId,
|
|
112
|
+
beliefType: 'Belief',
|
|
113
|
+
beliefVerb: 'IDENTIFY_AS',
|
|
114
|
+
beliefObject: value,
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -129,12 +129,10 @@ const MenuComponent = (props: MenuProps) => {
|
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
// Fallback for unknown commands or parsing failures
|
|
133
132
|
item.renderAs = 'span';
|
|
134
133
|
return item;
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
// Process featured and additional links using the modified helper
|
|
138
136
|
const featuredLinks = thisPayload
|
|
139
137
|
.filter((e: MenuLink) => e.featured)
|
|
140
138
|
.map(processMenuLink);
|
|
@@ -142,7 +140,6 @@ const MenuComponent = (props: MenuProps) => {
|
|
|
142
140
|
.filter((e: MenuLink) => !e.featured)
|
|
143
141
|
.map(processMenuLink);
|
|
144
142
|
|
|
145
|
-
// Helper component to render either a link or a button, avoiding repetition.
|
|
146
143
|
const InteractiveMenuItem = ({ item }: { item: ProcessedMenuLinkDatum }) => {
|
|
147
144
|
if (item.renderAs === 'button') {
|
|
148
145
|
return (
|
|
@@ -173,7 +170,6 @@ const MenuComponent = (props: MenuProps) => {
|
|
|
173
170
|
);
|
|
174
171
|
}
|
|
175
172
|
|
|
176
|
-
// Fallback for 'span'
|
|
177
173
|
return (
|
|
178
174
|
<span
|
|
179
175
|
className="text-mydarkgrey block text-2xl font-bold leading-6 opacity-50"
|
|
@@ -60,7 +60,6 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
|
|
|
60
60
|
if (!idsResponse.ok) throw new Error('Failed to fetch belief IDs');
|
|
61
61
|
|
|
62
62
|
const idsResult = await idsResponse.json();
|
|
63
|
-
// CORRECTED: The key from the backend is "beliefIds", not "beliefs"
|
|
64
63
|
if (!idsResult.beliefIds || idsResult.beliefIds.length === 0) {
|
|
65
64
|
setAvailableBeliefs([]);
|
|
66
65
|
setIsLoading(false);
|
|
@@ -74,7 +73,6 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
|
|
|
74
73
|
'Content-Type': 'application/json',
|
|
75
74
|
'X-Tenant-ID': tenantId,
|
|
76
75
|
},
|
|
77
|
-
// CORRECTED: Pass the array from the correct key "beliefIds"
|
|
78
76
|
body: JSON.stringify({ beliefIds: idsResult.beliefIds }),
|
|
79
77
|
});
|
|
80
78
|
|
|
@@ -50,6 +50,13 @@ interface InteractiveDisclosureWidgetProps {
|
|
|
50
50
|
|
|
51
51
|
const generateId = (): string => Math.random().toString(36).substring(2, 9);
|
|
52
52
|
|
|
53
|
+
const quoteIfNecessary = (command: string, value: string): string => {
|
|
54
|
+
if (command === 'identifyAs' && value.includes(' ')) {
|
|
55
|
+
return `"${value}"`;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
};
|
|
59
|
+
|
|
53
60
|
const IconSelector = ({
|
|
54
61
|
value,
|
|
55
62
|
onChange,
|
|
@@ -136,7 +143,9 @@ const DisclosureItemEditor = ({
|
|
|
136
143
|
}) => {
|
|
137
144
|
return (
|
|
138
145
|
<div
|
|
139
|
-
className={`space-y-4 rounded-lg border bg-white p-4 shadow-sm transition-opacity ${
|
|
146
|
+
className={`space-y-4 rounded-lg border bg-white p-4 shadow-sm transition-opacity ${
|
|
147
|
+
item.isDisabled ? 'border-gray-100 opacity-40' : 'border-gray-200'
|
|
148
|
+
}`}
|
|
140
149
|
>
|
|
141
150
|
<div className="flex items-center justify-between">
|
|
142
151
|
<div className="flex items-center gap-2">
|
|
@@ -168,7 +177,9 @@ const DisclosureItemEditor = ({
|
|
|
168
177
|
<button
|
|
169
178
|
type="button"
|
|
170
179
|
onClick={onToggle}
|
|
171
|
-
className={`rounded p-1 hover:bg-gray-100 ${
|
|
180
|
+
className={`rounded p-1 hover:bg-gray-100 ${
|
|
181
|
+
item.isDisabled ? 'text-blue-600' : 'text-red-600'
|
|
182
|
+
}`}
|
|
172
183
|
>
|
|
173
184
|
{item.isDisabled ? (
|
|
174
185
|
<ArrowUturnLeftIcon className="h-4 w-4" />
|
|
@@ -274,7 +285,6 @@ export default function InteractiveDisclosureWidget({
|
|
|
274
285
|
|
|
275
286
|
const actionCommand =
|
|
276
287
|
currentBelief.scale === 'custom' ? 'identifyAs' : 'declare';
|
|
277
|
-
|
|
278
288
|
const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
|
|
279
289
|
(loadedItem) => {
|
|
280
290
|
const isFromScale = scaleKeys.some(
|
|
@@ -285,13 +295,12 @@ export default function InteractiveDisclosureWidget({
|
|
|
285
295
|
id: generateId(),
|
|
286
296
|
isCustom: !isFromScale,
|
|
287
297
|
actionLisp: isFromScale
|
|
288
|
-
? `(${actionCommand} ${beliefTag} ${loadedItem.beliefValue})`
|
|
298
|
+
? `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, loadedItem.beliefValue)})`
|
|
289
299
|
: loadedItem.actionLisp,
|
|
290
300
|
isDisabled: false,
|
|
291
301
|
};
|
|
292
302
|
}
|
|
293
303
|
);
|
|
294
|
-
|
|
295
304
|
scaleKeys.forEach(({ slug, name }) => {
|
|
296
305
|
if (!finalDisclosures.some((d) => d.beliefValue === slug)) {
|
|
297
306
|
finalDisclosures.push({
|
|
@@ -300,13 +309,12 @@ export default function InteractiveDisclosureWidget({
|
|
|
300
309
|
title: name,
|
|
301
310
|
description: '',
|
|
302
311
|
icon: 'app',
|
|
303
|
-
actionLisp: `(${actionCommand} ${beliefTag} ${slug})`,
|
|
312
|
+
actionLisp: `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, slug)})`,
|
|
304
313
|
isCustom: false,
|
|
305
314
|
isDisabled: true,
|
|
306
315
|
});
|
|
307
316
|
}
|
|
308
317
|
});
|
|
309
|
-
|
|
310
318
|
setDisclosures(finalDisclosures);
|
|
311
319
|
} catch (e) {
|
|
312
320
|
console.error('Error parsing disclosure payload:', e);
|
|
@@ -345,7 +353,6 @@ export default function InteractiveDisclosureWidget({
|
|
|
345
353
|
const disclosuresToStore: StoredDisclosureItem[] = disclosures
|
|
346
354
|
.filter((d) => !d.isDisabled)
|
|
347
355
|
.map(({ id, isCustom, isDisabled, ...rest }) => rest);
|
|
348
|
-
|
|
349
356
|
const payload = { styles: widgetStyles, disclosures: disclosuresToStore };
|
|
350
357
|
onUpdate([selectedBeliefTag, JSON.stringify(payload)]);
|
|
351
358
|
};
|
|
@@ -369,7 +376,7 @@ export default function InteractiveDisclosureWidget({
|
|
|
369
376
|
title: name,
|
|
370
377
|
description: '',
|
|
371
378
|
icon: 'app',
|
|
372
|
-
actionLisp: `(${actionCommand} ${tag} ${slug})`,
|
|
379
|
+
actionLisp: `(${actionCommand} ${tag} ${quoteIfNecessary(actionCommand, slug)})`,
|
|
373
380
|
isCustom: false,
|
|
374
381
|
isDisabled: false,
|
|
375
382
|
}));
|
|
@@ -407,8 +414,10 @@ export default function InteractiveDisclosureWidget({
|
|
|
407
414
|
setDisclosures(
|
|
408
415
|
disclosures.map((d) => (d.id === id ? { ...d, ...updates } : d))
|
|
409
416
|
);
|
|
417
|
+
|
|
410
418
|
const updateWidgetStyles = (updates: Partial<WidgetStyles>) =>
|
|
411
419
|
setWidgetStyles((prev) => ({ ...prev, ...updates }));
|
|
420
|
+
|
|
412
421
|
const toggleDisclosure = (id: string) =>
|
|
413
422
|
setDisclosures(
|
|
414
423
|
disclosures.map((d) =>
|
|
@@ -80,10 +80,27 @@ export default function ActionBuilderField({
|
|
|
80
80
|
|
|
81
81
|
const handleParamChange = (newParams: string) => {
|
|
82
82
|
setParams(newParams);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
const trimmedParams = newParams.trim();
|
|
84
|
+
|
|
85
|
+
if (!trimmedParams || trimmedParams === '()') {
|
|
86
86
|
onChange('');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (command === 'identifyAs') {
|
|
91
|
+
const firstSpaceIndex = trimmedParams.indexOf(' ');
|
|
92
|
+
if (firstSpaceIndex === -1) {
|
|
93
|
+
// Handle case with only beliefId and no value
|
|
94
|
+
onChange(`(${command} ${trimmedParams})`);
|
|
95
|
+
} else {
|
|
96
|
+
const beliefId = trimmedParams.substring(0, firstSpaceIndex);
|
|
97
|
+
const value = trimmedParams.substring(firstSpaceIndex + 1);
|
|
98
|
+
const finalValue = value.includes(' ') ? `"${value}"` : value;
|
|
99
|
+
onChange(`(${command} ${beliefId} ${finalValue})`);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
// Original behavior for all other commands
|
|
103
|
+
onChange(`(${command} ${trimmedParams})`);
|
|
87
104
|
}
|
|
88
105
|
};
|
|
89
106
|
|
|
@@ -37,16 +37,14 @@ export default function BeliefForm({
|
|
|
37
37
|
onClose,
|
|
38
38
|
}: BeliefFormProps) {
|
|
39
39
|
const [customValue, setCustomValue] = useState('');
|
|
40
|
+
const [customValueError, setCustomValueError] = useState<string | null>(null);
|
|
40
41
|
|
|
41
|
-
// Subscribe to orphan analysis store
|
|
42
42
|
const orphanState = useStore(orphanAnalysisStore);
|
|
43
43
|
|
|
44
|
-
// Load orphan analysis on component mount
|
|
45
44
|
useEffect(() => {
|
|
46
45
|
loadOrphanAnalysis();
|
|
47
46
|
}, []);
|
|
48
47
|
|
|
49
|
-
// Get usage information for this belief
|
|
50
48
|
const getBeliefUsage = (): string[] => {
|
|
51
49
|
if (!belief?.id || !orphanState.data || !orphanState.data.beliefs) {
|
|
52
50
|
return [];
|
|
@@ -54,7 +52,6 @@ export default function BeliefForm({
|
|
|
54
52
|
return orphanState.data.beliefs[belief.id] || [];
|
|
55
53
|
};
|
|
56
54
|
|
|
57
|
-
// Check if belief is in use
|
|
58
55
|
const isBeliefInUse = (): boolean => {
|
|
59
56
|
if (isCreate || !belief?.id) return false;
|
|
60
57
|
return getBeliefUsage().length > 0;
|
|
@@ -63,7 +60,6 @@ export default function BeliefForm({
|
|
|
63
60
|
const beliefInUse = isBeliefInUse();
|
|
64
61
|
const usageCount = getBeliefUsage().length;
|
|
65
62
|
|
|
66
|
-
// Initialize form state
|
|
67
63
|
const initialState: BeliefNodeState = belief
|
|
68
64
|
? convertToLocalState(belief)
|
|
69
65
|
: {
|
|
@@ -85,7 +81,6 @@ export default function BeliefForm({
|
|
|
85
81
|
data
|
|
86
82
|
);
|
|
87
83
|
|
|
88
|
-
// Call success callback after save (original pattern)
|
|
89
84
|
setTimeout(() => {
|
|
90
85
|
onClose?.(true);
|
|
91
86
|
}, 1000);
|
|
@@ -102,23 +97,30 @@ export default function BeliefForm({
|
|
|
102
97
|
},
|
|
103
98
|
});
|
|
104
99
|
|
|
105
|
-
const
|
|
106
|
-
|
|
100
|
+
const handleCustomValueChange = (value: string) => {
|
|
101
|
+
setCustomValue(value);
|
|
102
|
+
const valueRegex = /^[a-zA-Z]([a-zA-Z0-9?!]| (?=[a-zA-Z0-9?!]))*$/;
|
|
103
|
+
if (value && !valueRegex.test(value)) {
|
|
104
|
+
setCustomValueError(
|
|
105
|
+
'Must start with a letter. No double or trailing spaces.'
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
setCustomValueError(null);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
107
111
|
|
|
112
|
+
const handleAddCustomValue = () => {
|
|
113
|
+
if (!customValue.trim() || customValueError) return;
|
|
108
114
|
const newState = addCustomValue(formState.state, customValue);
|
|
109
115
|
formState.updateField('customValues', newState.customValues);
|
|
110
116
|
setCustomValue('');
|
|
111
117
|
};
|
|
112
118
|
|
|
113
119
|
const handleRemoveCustomValue = (index: number) => {
|
|
114
|
-
// Check if this is a newly added value (not saved yet)
|
|
115
120
|
const currentValue = formState.state.customValues[index];
|
|
116
121
|
const originalValues = formState.originalState.customValues || [];
|
|
117
122
|
const isNewValue = !originalValues.includes(currentValue);
|
|
118
123
|
|
|
119
|
-
// Allow removal if:
|
|
120
|
-
// 1. Belief is not in use, OR
|
|
121
|
-
// 2. This is a new value that hasn't been saved yet
|
|
122
124
|
if (!beliefInUse || isNewValue) {
|
|
123
125
|
const newState = removeCustomValue(formState.state, index);
|
|
124
126
|
formState.updateField('customValues', newState.customValues);
|
|
@@ -188,7 +190,6 @@ export default function BeliefForm({
|
|
|
188
190
|
|
|
189
191
|
return (
|
|
190
192
|
<div className="space-y-8">
|
|
191
|
-
{/* Header */}
|
|
192
193
|
<div className="border-b border-gray-200 pb-4">
|
|
193
194
|
<h2 className="text-2xl font-bold text-gray-900">
|
|
194
195
|
{isCreate ? 'Create Belief' : 'Edit Belief'}
|
|
@@ -200,10 +201,8 @@ export default function BeliefForm({
|
|
|
200
201
|
</p>
|
|
201
202
|
</div>
|
|
202
203
|
|
|
203
|
-
{/* Usage Warning */}
|
|
204
204
|
{renderUsageWarning()}
|
|
205
205
|
|
|
206
|
-
{/* Info Box */}
|
|
207
206
|
<div className="rounded-md bg-blue-50 p-4">
|
|
208
207
|
<div className="text-sm text-blue-700">
|
|
209
208
|
<p className="font-bold">What are Beliefs?</p>
|
|
@@ -225,7 +224,6 @@ export default function BeliefForm({
|
|
|
225
224
|
</div>
|
|
226
225
|
</div>
|
|
227
226
|
|
|
228
|
-
{/* Basic Fields */}
|
|
229
227
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
230
228
|
<StringInput
|
|
231
229
|
value={formState.state.title}
|
|
@@ -254,7 +252,6 @@ export default function BeliefForm({
|
|
|
254
252
|
</div>
|
|
255
253
|
</div>
|
|
256
254
|
|
|
257
|
-
{/* Scale Selection */}
|
|
258
255
|
<div className="space-y-4">
|
|
259
256
|
<div className="relative">
|
|
260
257
|
<EnumSelect
|
|
@@ -276,7 +273,6 @@ export default function BeliefForm({
|
|
|
276
273
|
{renderScalePreview()}
|
|
277
274
|
</div>
|
|
278
275
|
|
|
279
|
-
{/* Custom Values Section */}
|
|
280
276
|
{formState.state.scale === 'custom' && (
|
|
281
277
|
<div className="space-y-4">
|
|
282
278
|
<div>
|
|
@@ -286,31 +282,41 @@ export default function BeliefForm({
|
|
|
286
282
|
</p>
|
|
287
283
|
</div>
|
|
288
284
|
|
|
289
|
-
{/* Add Custom Value */}
|
|
290
285
|
<div className="flex gap-2">
|
|
291
286
|
<div className="flex-1">
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
287
|
+
<input
|
|
288
|
+
type="text"
|
|
289
|
+
value={customValue}
|
|
290
|
+
onChange={(e) => handleCustomValueChange(e.target.value)}
|
|
291
|
+
onKeyDown={handleKeyDown}
|
|
292
|
+
placeholder="Enter custom value"
|
|
293
|
+
className={`block w-full rounded-md border-0 px-3 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset placeholder:text-gray-400 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 ${
|
|
294
|
+
customValueError
|
|
295
|
+
? 'ring-red-500 focus:ring-red-600'
|
|
296
|
+
: 'ring-gray-300 focus:ring-cyan-600'
|
|
297
|
+
}`}
|
|
298
|
+
/>
|
|
302
299
|
</div>
|
|
303
300
|
<button
|
|
304
301
|
type="button"
|
|
305
302
|
onClick={handleAddCustomValue}
|
|
306
|
-
disabled={!customValue.trim()}
|
|
303
|
+
disabled={!customValue.trim() || !!customValueError}
|
|
307
304
|
className="inline-flex items-center rounded-md bg-cyan-600 px-3 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600 disabled:cursor-not-allowed disabled:opacity-50"
|
|
308
305
|
>
|
|
309
306
|
<PlusIcon className="h-4 w-4" />
|
|
310
307
|
</button>
|
|
311
308
|
</div>
|
|
312
309
|
|
|
313
|
-
{
|
|
310
|
+
{customValueError && (
|
|
311
|
+
<p className="mt-1 text-sm text-red-600">{customValueError}</p>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{formState.errors.customValues && (
|
|
315
|
+
<p className="text-sm text-red-600">
|
|
316
|
+
{formState.errors.customValues}
|
|
317
|
+
</p>
|
|
318
|
+
)}
|
|
319
|
+
|
|
314
320
|
{formState.state.customValues.length > 0 && (
|
|
315
321
|
<div className="space-y-2">
|
|
316
322
|
{formState.state.customValues.map((value, index) => {
|
|
@@ -355,7 +361,6 @@ export default function BeliefForm({
|
|
|
355
361
|
</div>
|
|
356
362
|
)}
|
|
357
363
|
|
|
358
|
-
{/* Save/Cancel Bar */}
|
|
359
364
|
<UnsavedChangesBar
|
|
360
365
|
formState={formState}
|
|
361
366
|
message="You have unsaved belief changes"
|
|
@@ -363,7 +368,6 @@ export default function BeliefForm({
|
|
|
363
368
|
cancelLabel="Discard Changes"
|
|
364
369
|
/>
|
|
365
370
|
|
|
366
|
-
{/* Cancel Navigation Button */}
|
|
367
371
|
<div className="flex justify-start">
|
|
368
372
|
<button
|
|
369
373
|
type="button"
|
|
@@ -4,9 +4,6 @@ import type {
|
|
|
4
4
|
FieldErrors,
|
|
5
5
|
} from '@/types/tractstack';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Convert backend BeliefNode to frontend BeliefNodeState
|
|
9
|
-
*/
|
|
10
7
|
export function convertToLocalState(beliefNode: BeliefNode): BeliefNodeState {
|
|
11
8
|
return {
|
|
12
9
|
id: beliefNode.id,
|
|
@@ -17,9 +14,6 @@ export function convertToLocalState(beliefNode: BeliefNode): BeliefNodeState {
|
|
|
17
14
|
};
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
/**
|
|
21
|
-
* Convert frontend BeliefNodeState to backend BeliefNode format
|
|
22
|
-
*/
|
|
23
17
|
export function convertToBackendFormat(state: BeliefNodeState): BeliefNode {
|
|
24
18
|
return {
|
|
25
19
|
id: state.id,
|
|
@@ -31,47 +25,45 @@ export function convertToBackendFormat(state: BeliefNodeState): BeliefNode {
|
|
|
31
25
|
};
|
|
32
26
|
}
|
|
33
27
|
|
|
34
|
-
/**
|
|
35
|
-
* Validate belief node state
|
|
36
|
-
*/
|
|
37
28
|
export function validateBeliefNode(state: BeliefNodeState): FieldErrors {
|
|
38
29
|
const errors: FieldErrors = {};
|
|
39
30
|
|
|
40
|
-
// Validate title
|
|
41
31
|
if (!state.title?.trim()) {
|
|
42
32
|
errors.title = 'Title is required';
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
// Validate slug
|
|
46
35
|
if (!state.slug?.trim()) {
|
|
47
36
|
errors.slug = 'Slug is required';
|
|
37
|
+
} else {
|
|
38
|
+
const slugRegex = /^[a-zA-Z]+$/;
|
|
39
|
+
if (!slugRegex.test(state.slug)) {
|
|
40
|
+
errors.slug = 'Slug must contain only letters (a-z, A-Z)';
|
|
41
|
+
}
|
|
48
42
|
}
|
|
49
43
|
|
|
50
|
-
// Validate scale
|
|
51
44
|
if (!state.scale?.trim()) {
|
|
52
45
|
errors.scale = 'Scale is required';
|
|
53
46
|
}
|
|
54
47
|
|
|
55
|
-
// Validate custom values if scale is custom
|
|
56
48
|
if (state.scale === 'custom') {
|
|
57
49
|
if (!state.customValues || state.customValues.length === 0) {
|
|
58
50
|
errors.customValues =
|
|
59
51
|
'At least one custom value is required for custom scale';
|
|
60
52
|
} else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
const valueRegex = /^[a-zA-Z]([a-zA-Z0-9?!]| (?=[a-zA-Z0-9?!]))*$/;
|
|
54
|
+
for (const value of state.customValues) {
|
|
55
|
+
if (value.trim() && !valueRegex.test(value)) {
|
|
56
|
+
errors.customValues =
|
|
57
|
+
'Values must start with a letter, have no double or trailing spaces, and use valid characters.';
|
|
58
|
+
break;
|
|
64
59
|
}
|
|
65
|
-
}
|
|
60
|
+
}
|
|
66
61
|
}
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
return errors;
|
|
70
65
|
}
|
|
71
66
|
|
|
72
|
-
/**
|
|
73
|
-
* State interceptor for form state management
|
|
74
|
-
*/
|
|
75
67
|
export function beliefStateIntercept(
|
|
76
68
|
state: BeliefNodeState,
|
|
77
69
|
field: keyof BeliefNodeState,
|
|
@@ -88,7 +80,6 @@ export function beliefStateIntercept(
|
|
|
88
80
|
break;
|
|
89
81
|
case 'scale':
|
|
90
82
|
newState.scale = value || '';
|
|
91
|
-
// Clear custom values when scale changes away from custom
|
|
92
83
|
if (value !== 'custom') {
|
|
93
84
|
newState.customValues = [];
|
|
94
85
|
}
|
|
@@ -103,9 +94,6 @@ export function beliefStateIntercept(
|
|
|
103
94
|
return newState;
|
|
104
95
|
}
|
|
105
96
|
|
|
106
|
-
/**
|
|
107
|
-
* Add a new custom value to the state
|
|
108
|
-
*/
|
|
109
97
|
export function addCustomValue(
|
|
110
98
|
state: BeliefNodeState,
|
|
111
99
|
value: string
|
|
@@ -118,9 +106,6 @@ export function addCustomValue(
|
|
|
118
106
|
};
|
|
119
107
|
}
|
|
120
108
|
|
|
121
|
-
/**
|
|
122
|
-
* Remove a custom value from the state
|
|
123
|
-
*/
|
|
124
109
|
export function removeCustomValue(
|
|
125
110
|
state: BeliefNodeState,
|
|
126
111
|
index: number
|
|
@@ -131,9 +116,6 @@ export function removeCustomValue(
|
|
|
131
116
|
};
|
|
132
117
|
}
|
|
133
118
|
|
|
134
|
-
/**
|
|
135
|
-
* Update a specific custom value in the state
|
|
136
|
-
*/
|
|
137
119
|
export function updateCustomValue(
|
|
138
120
|
state: BeliefNodeState,
|
|
139
121
|
index: number,
|
|
@@ -148,9 +130,6 @@ export function updateCustomValue(
|
|
|
148
130
|
};
|
|
149
131
|
}
|
|
150
132
|
|
|
151
|
-
/**
|
|
152
|
-
* Scale options for the belief form
|
|
153
|
-
*/
|
|
154
133
|
export const SCALE_OPTIONS = [
|
|
155
134
|
{ value: 'likert', label: 'Likert Scale (1-5)' },
|
|
156
135
|
{ value: 'agreement', label: 'Agreement (Agree/Disagree)' },
|
|
@@ -160,9 +139,6 @@ export const SCALE_OPTIONS = [
|
|
|
160
139
|
{ value: 'custom', label: 'Custom Values' },
|
|
161
140
|
];
|
|
162
141
|
|
|
163
|
-
/**
|
|
164
|
-
* Get scale preview data for displaying scale options
|
|
165
|
-
*/
|
|
166
142
|
export function getScalePreview(scale: string) {
|
|
167
143
|
const scalePreviewData: {
|
|
168
144
|
[key: string]: Array<{ id: number; name: string; color: string }>;
|