astro-tractstack 2.0.0-rc.33 → 2.0.0-rc.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +0 -4
- package/package.json +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +16 -27
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/state/SaveModal.tsx +87 -93
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +4 -3
- package/utils/inject-files.ts +0 -4
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
package/dist/index.js
CHANGED
|
@@ -478,10 +478,6 @@ async function w(t, e, c) {
|
|
|
478
478
|
src: t("../templates/src/components/edit/pane/PanePanel_path.tsx"),
|
|
479
479
|
dest: "src/components/edit/pane/PanePanel_path.tsx"
|
|
480
480
|
},
|
|
481
|
-
{
|
|
482
|
-
src: t("../templates/src/components/edit/pane/PanePanel_slug.tsx"),
|
|
483
|
-
dest: "src/components/edit/pane/PanePanel_slug.tsx"
|
|
484
|
-
},
|
|
485
481
|
{
|
|
486
482
|
src: t("../templates/src/components/edit/pane/PanePanel_title.tsx"),
|
|
487
483
|
dest: "src/components/edit/pane/PanePanel_title.tsx"
|
package/package.json
CHANGED
|
@@ -9,10 +9,13 @@ import {
|
|
|
9
9
|
isContextPaneNode,
|
|
10
10
|
hasBeliefPayload,
|
|
11
11
|
} from '@/utils/compositor/typeGuards';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
settingsPanelStore,
|
|
14
|
+
viewportKeyStore,
|
|
15
|
+
fullContentMapStore,
|
|
16
|
+
} from '@/stores/storykeep';
|
|
13
17
|
import { getCtx } from '@/stores/nodes';
|
|
14
18
|
import PaneTitlePanel from './PanePanel_title';
|
|
15
|
-
import PaneSlugPanel from './PanePanel_slug';
|
|
16
19
|
import PaneMagicPathPanel from './PanePanel_path';
|
|
17
20
|
import PaneImpressionPanel from './PanePanel_impression';
|
|
18
21
|
import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
|
|
@@ -30,7 +33,7 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
|
|
|
30
33
|
const reorderMode = toolMode.value === `move`;
|
|
31
34
|
const isActiveMode =
|
|
32
35
|
activePaneMode.panel === 'settings' && activePaneMode.paneId === nodeId;
|
|
33
|
-
|
|
36
|
+
const $contentMap = useStore(fullContentMapStore);
|
|
34
37
|
const $viewportKey = useStore(viewportKeyStore);
|
|
35
38
|
const isMobile = $viewportKey.value === `mobile`;
|
|
36
39
|
|
|
@@ -104,8 +107,6 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
|
|
|
104
107
|
|
|
105
108
|
if (mode === PaneConfigMode.TITLE) {
|
|
106
109
|
return <PaneTitlePanel nodeId={nodeId} setMode={setSaveMode} />;
|
|
107
|
-
} else if (mode === PaneConfigMode.SLUG) {
|
|
108
|
-
return <PaneSlugPanel nodeId={nodeId} setMode={setSaveMode} />;
|
|
109
110
|
} else if (mode === PaneConfigMode.PATH) {
|
|
110
111
|
return <PaneMagicPathPanel nodeId={nodeId} setMode={setSaveMode} />;
|
|
111
112
|
} else if (mode === PaneConfigMode.IMPRESSION) {
|
|
@@ -137,28 +138,16 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
|
|
|
137
138
|
<>
|
|
138
139
|
{!isTemplate && (
|
|
139
140
|
<>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
</button>
|
|
151
|
-
<button
|
|
152
|
-
onClick={() => setSaveMode(PaneConfigMode.SLUG)}
|
|
153
|
-
className={buttonClass}
|
|
154
|
-
>
|
|
155
|
-
Slug
|
|
156
|
-
{!isMobile && (
|
|
157
|
-
<>
|
|
158
|
-
: <strong>{paneNode.slug}</strong>
|
|
159
|
-
</>
|
|
160
|
-
)}
|
|
161
|
-
</button>
|
|
141
|
+
{$contentMap.some(
|
|
142
|
+
(item) => item.type === 'Pane' && item.id === nodeId
|
|
143
|
+
) && (
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => setSaveMode(PaneConfigMode.TITLE)}
|
|
146
|
+
className={buttonClass}
|
|
147
|
+
>
|
|
148
|
+
ID
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
162
151
|
<button
|
|
163
152
|
onClick={() => setSaveMode(PaneConfigMode.IMPRESSION)}
|
|
164
153
|
className={buttonClass}
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
type SetStateAction,
|
|
6
6
|
type ChangeEvent,
|
|
7
7
|
} from 'react';
|
|
8
|
+
import { useStore } from '@nanostores/react';
|
|
8
9
|
import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
|
|
9
10
|
import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
|
|
11
|
+
import { fullContentMapStore } from '@/stores/storykeep';
|
|
10
12
|
import { getCtx } from '@/stores/nodes';
|
|
11
|
-
import { cloneDeep } from '@/utils/helpers';
|
|
13
|
+
import { cloneDeep, findUniqueSlug, titleToSlug } from '@/utils/helpers';
|
|
12
14
|
import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
|
|
13
15
|
|
|
14
16
|
interface PaneTitlePanelProps {
|
|
@@ -18,36 +20,141 @@ interface PaneTitlePanelProps {
|
|
|
18
20
|
|
|
19
21
|
const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
|
|
20
22
|
const [title, setTitle] = useState('');
|
|
21
|
-
const [
|
|
22
|
-
const [
|
|
23
|
-
const [
|
|
23
|
+
const [slug, setSlug] = useState('');
|
|
24
|
+
const [isValidTitle, setIsValidTitle] = useState(false);
|
|
25
|
+
const [isValidSlug, setIsValidSlug] = useState(false);
|
|
26
|
+
const [warningTitle, setWarningTitle] = useState(false);
|
|
27
|
+
const [warningSlug, setWarningSlug] = useState(false);
|
|
28
|
+
const [titleCharCount, setTitleCharCount] = useState(0);
|
|
29
|
+
const [slugCharCount, setSlugCharCount] = useState(0);
|
|
30
|
+
const [slugValidationError, setSlugValidationError] = useState<string | null>(
|
|
31
|
+
null
|
|
32
|
+
);
|
|
33
|
+
const [canSaveSlug, setCanSaveSlug] = useState(false);
|
|
24
34
|
|
|
35
|
+
const $contentMap = useStore(fullContentMapStore);
|
|
25
36
|
const ctx = getCtx();
|
|
26
37
|
const allNodes = ctx.allNodes.get();
|
|
27
38
|
const paneNode = allNodes.get(nodeId) as PaneNode;
|
|
28
39
|
if (!paneNode) return null;
|
|
29
40
|
|
|
41
|
+
const existingSlugs = $contentMap
|
|
42
|
+
.filter(
|
|
43
|
+
(item) =>
|
|
44
|
+
['Pane', 'StoryFragment'].includes(item.type) && item.id !== nodeId
|
|
45
|
+
)
|
|
46
|
+
.map((item) => item.slug);
|
|
47
|
+
|
|
30
48
|
useEffect(() => {
|
|
31
49
|
setTitle(paneNode.title);
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
setSlug(paneNode.slug);
|
|
51
|
+
setTitleCharCount(paneNode.title.length);
|
|
52
|
+
setSlugCharCount(paneNode.slug.length);
|
|
53
|
+
checkSlugLiveValidity(paneNode.slug);
|
|
54
|
+
}, [paneNode.title, paneNode.slug]);
|
|
34
55
|
|
|
35
56
|
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
36
57
|
const newTitle = e.target.value;
|
|
37
58
|
if (newTitle.length <= 50) {
|
|
38
|
-
// Prevent more than 70 chars
|
|
39
59
|
setTitle(newTitle);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
setTitleCharCount(newTitle.length);
|
|
61
|
+
setIsValidTitle(newTitle.length >= 5 && newTitle.length <= 35);
|
|
62
|
+
setWarningTitle(newTitle.length > 35 && newTitle.length <= 50);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleSlugChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
67
|
+
const newSlug = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
68
|
+
|
|
69
|
+
if (newSlug.length <= 50) {
|
|
70
|
+
setSlug(newSlug);
|
|
71
|
+
checkSlugLiveValidity(newSlug);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const checkSlugLiveValidity = (value: string) => {
|
|
76
|
+
const length = value.length;
|
|
77
|
+
setSlugCharCount(length);
|
|
78
|
+
|
|
79
|
+
// Basic format check for allowed characters
|
|
80
|
+
if (!/^[a-z0-9-]*$/.test(value)) {
|
|
81
|
+
setSlugValidationError(
|
|
82
|
+
'Only lowercase letters, numbers, and hyphens allowed'
|
|
83
|
+
);
|
|
84
|
+
setIsValidSlug(false);
|
|
85
|
+
setCanSaveSlug(false);
|
|
86
|
+
return false;
|
|
43
87
|
}
|
|
88
|
+
|
|
89
|
+
// Length checks
|
|
90
|
+
setIsValidSlug(length >= 3 && length <= 40);
|
|
91
|
+
setWarningSlug(length > 40 && length <= 50);
|
|
92
|
+
setSlugValidationError(null);
|
|
93
|
+
|
|
94
|
+
// Check if we can save
|
|
95
|
+
if (length >= 3) {
|
|
96
|
+
const saveValidation = checkSlugSaveValidity(value);
|
|
97
|
+
setCanSaveSlug(saveValidation.isValid);
|
|
98
|
+
if (!saveValidation.isValid) {
|
|
99
|
+
setSlugValidationError(saveValidation.error || null);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
setCanSaveSlug(false);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const checkSlugSaveValidity = (
|
|
109
|
+
value: string
|
|
110
|
+
): { isValid: boolean; error?: string } => {
|
|
111
|
+
// Strict pattern that prevents leading/trailing hyphens and multiple consecutive hyphens
|
|
112
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
113
|
+
return {
|
|
114
|
+
isValid: false,
|
|
115
|
+
error:
|
|
116
|
+
'Slug must start and end with letters or numbers, and no consecutive hyphens',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check duplicates
|
|
121
|
+
if (existingSlugs.includes(value)) {
|
|
122
|
+
return {
|
|
123
|
+
isValid: false,
|
|
124
|
+
error: 'This slug is already in use',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { isValid: true };
|
|
44
129
|
};
|
|
45
130
|
|
|
46
131
|
const handleTitleBlur = () => {
|
|
47
132
|
if (title.length >= 5) {
|
|
48
|
-
//
|
|
133
|
+
// Auto-generate slug if slug is empty or still system-generated
|
|
134
|
+
let updatedSlug = slug;
|
|
135
|
+
if (!slug || slug === paneNode.slug) {
|
|
136
|
+
const generatedSlug = titleToSlug(title);
|
|
137
|
+
const uniqueSlug = findUniqueSlug(generatedSlug, existingSlugs);
|
|
138
|
+
updatedSlug = uniqueSlug;
|
|
139
|
+
setSlug(uniqueSlug);
|
|
140
|
+
checkSlugLiveValidity(uniqueSlug);
|
|
141
|
+
}
|
|
142
|
+
|
|
49
143
|
const ctx = getCtx();
|
|
50
|
-
const updatedNode = {
|
|
144
|
+
const updatedNode = {
|
|
145
|
+
...cloneDeep(paneNode),
|
|
146
|
+
title,
|
|
147
|
+
slug: updatedSlug,
|
|
148
|
+
isChanged: true,
|
|
149
|
+
};
|
|
150
|
+
ctx.modifyNodes([updatedNode]);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleSlugBlur = () => {
|
|
155
|
+
if (canSaveSlug) {
|
|
156
|
+
const ctx = getCtx();
|
|
157
|
+
const updatedNode = { ...cloneDeep(paneNode), slug, isChanged: true };
|
|
51
158
|
ctx.modifyNodes([updatedNode]);
|
|
52
159
|
}
|
|
53
160
|
};
|
|
@@ -56,7 +163,7 @@ const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
|
|
|
56
163
|
<div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6 shadow-inner">
|
|
57
164
|
<div className="px-3.5">
|
|
58
165
|
<div className="mb-4 flex justify-between">
|
|
59
|
-
<h3 className="text-lg font-bold">Pane Title</h3>
|
|
166
|
+
<h3 className="text-lg font-bold">Pane Title & Slug</h3>
|
|
60
167
|
<button
|
|
61
168
|
onClick={() => setMode(PaneConfigMode.DEFAULT)}
|
|
62
169
|
className="text-myblue hover:text-black"
|
|
@@ -65,73 +172,133 @@ const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
|
|
|
65
172
|
</button>
|
|
66
173
|
</div>
|
|
67
174
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
93
|
-
) : isValid ? (
|
|
94
|
-
<CheckIcon className="h-5 w-5 text-green-500" />
|
|
95
|
-
) : warning ? (
|
|
96
|
-
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
97
|
-
) : null}
|
|
98
|
-
<span
|
|
99
|
-
className={`text-sm ${
|
|
100
|
-
charCount < 5
|
|
101
|
-
? 'text-red-500'
|
|
102
|
-
: isValid
|
|
103
|
-
? 'text-green-500'
|
|
104
|
-
: warning
|
|
105
|
-
? 'text-yellow-500'
|
|
106
|
-
: 'text-gray-500'
|
|
175
|
+
{/* Title Input */}
|
|
176
|
+
<div className="mb-6">
|
|
177
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
178
|
+
Title
|
|
179
|
+
</label>
|
|
180
|
+
<div className="relative">
|
|
181
|
+
<input
|
|
182
|
+
type="text"
|
|
183
|
+
value={title}
|
|
184
|
+
onChange={handleTitleChange}
|
|
185
|
+
onBlur={handleTitleBlur}
|
|
186
|
+
onKeyDown={(e) => {
|
|
187
|
+
if (e.key === 'Enter') {
|
|
188
|
+
e.currentTarget.blur();
|
|
189
|
+
}
|
|
190
|
+
}}
|
|
191
|
+
className={`w-full rounded-md border px-2 py-1 pr-16 ${
|
|
192
|
+
titleCharCount < 5
|
|
193
|
+
? 'border-red-500 bg-red-50'
|
|
194
|
+
: isValidTitle
|
|
195
|
+
? 'border-green-500 bg-green-50'
|
|
196
|
+
: warningTitle
|
|
197
|
+
? 'border-yellow-500 bg-yellow-50'
|
|
198
|
+
: 'border-gray-300'
|
|
107
199
|
}`}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
200
|
+
placeholder="Enter pane title (5-35 characters recommended)"
|
|
201
|
+
/>
|
|
202
|
+
<div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
|
|
203
|
+
{titleCharCount < 5 ? (
|
|
204
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
205
|
+
) : isValidTitle ? (
|
|
206
|
+
<CheckIcon className="h-5 w-5 text-green-500" />
|
|
207
|
+
) : warningTitle ? (
|
|
208
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
209
|
+
) : null}
|
|
210
|
+
<span
|
|
211
|
+
className={`text-sm ${
|
|
212
|
+
titleCharCount < 5
|
|
213
|
+
? 'text-red-500'
|
|
214
|
+
: isValidTitle
|
|
215
|
+
? 'text-green-500'
|
|
216
|
+
: warningTitle
|
|
217
|
+
? 'text-yellow-500'
|
|
218
|
+
: 'text-gray-500'
|
|
219
|
+
}`}
|
|
220
|
+
>
|
|
221
|
+
{titleCharCount}/50
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
111
224
|
</div>
|
|
112
225
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
226
|
+
|
|
227
|
+
{/* Slug Input */}
|
|
228
|
+
<div className="mb-4">
|
|
229
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
230
|
+
Slug (URL)
|
|
231
|
+
</label>
|
|
232
|
+
<div className="relative">
|
|
233
|
+
<input
|
|
234
|
+
type="text"
|
|
235
|
+
value={slug}
|
|
236
|
+
onChange={handleSlugChange}
|
|
237
|
+
onBlur={handleSlugBlur}
|
|
238
|
+
onKeyDown={(e) => {
|
|
239
|
+
if (e.key === 'Enter') {
|
|
240
|
+
e.currentTarget.blur();
|
|
241
|
+
}
|
|
242
|
+
}}
|
|
243
|
+
className={`w-full rounded-md border px-2 py-1 pr-16 ${
|
|
244
|
+
slugValidationError || slugCharCount < 3
|
|
245
|
+
? 'border-red-500 bg-red-50'
|
|
246
|
+
: isValidSlug && canSaveSlug
|
|
247
|
+
? 'border-green-500 bg-green-50'
|
|
248
|
+
: warningSlug
|
|
249
|
+
? 'border-yellow-500 bg-yellow-50'
|
|
250
|
+
: 'border-gray-300'
|
|
251
|
+
}`}
|
|
252
|
+
placeholder="Enter pane slug (3-40 characters recommended)"
|
|
253
|
+
/>
|
|
254
|
+
<div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
|
|
255
|
+
{slugValidationError || slugCharCount < 3 ? (
|
|
256
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
257
|
+
) : isValidSlug && canSaveSlug ? (
|
|
258
|
+
<CheckIcon className="h-5 w-5 text-green-500" />
|
|
259
|
+
) : warningSlug ? (
|
|
260
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
261
|
+
) : null}
|
|
262
|
+
<span
|
|
263
|
+
className={`text-sm ${
|
|
264
|
+
slugValidationError || slugCharCount < 3
|
|
265
|
+
? 'text-red-500'
|
|
266
|
+
: isValidSlug && canSaveSlug
|
|
267
|
+
? 'text-green-500'
|
|
268
|
+
: warningSlug
|
|
269
|
+
? 'text-yellow-500'
|
|
270
|
+
: 'text-gray-500'
|
|
271
|
+
}`}
|
|
272
|
+
>
|
|
273
|
+
{slugCharCount}/50
|
|
127
274
|
</span>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
{slugValidationError && (
|
|
278
|
+
<div className="mt-2 text-sm text-red-600">
|
|
279
|
+
<ExclamationTriangleIcon className="mr-1 inline h-4 w-4" />
|
|
280
|
+
{slugValidationError}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{/* Help Text */}
|
|
286
|
+
<div className="space-y-4 text-sm text-gray-600">
|
|
287
|
+
<div>
|
|
288
|
+
<h4 className="font-semibold">Title Guidelines:</h4>
|
|
289
|
+
<ul className="ml-4 mt-1 list-disc">
|
|
290
|
+
<li>5-35 characters recommended for optimal display</li>
|
|
291
|
+
<li>Clear, descriptive title for the pane content</li>
|
|
292
|
+
</ul>
|
|
293
|
+
</div>
|
|
294
|
+
<div>
|
|
295
|
+
<h4 className="font-semibold">Slug Guidelines:</h4>
|
|
296
|
+
<ul className="ml-4 mt-1 list-disc">
|
|
297
|
+
<li>Used for analytics tracking</li>
|
|
298
|
+
<li>Only lowercase letters, numbers, and hyphens</li>
|
|
299
|
+
<li>Must start and end with letter or number</li>
|
|
300
|
+
<li>No consecutive hyphens</li>
|
|
301
|
+
</ul>
|
|
135
302
|
</div>
|
|
136
303
|
</div>
|
|
137
304
|
</div>
|
|
@@ -73,7 +73,6 @@ export default function SaveModal({
|
|
|
73
73
|
const isSaving = useRef(false);
|
|
74
74
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
75
75
|
const isCreateMode = slug === 'create';
|
|
76
|
-
const contentMap = fullContentMapStore.get();
|
|
77
76
|
const pendingHomePageSlug = pendingHomePageSlugStore.get();
|
|
78
77
|
const goBackend =
|
|
79
78
|
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
@@ -334,113 +333,108 @@ export default function SaveModal({
|
|
|
334
333
|
currentStep: 0,
|
|
335
334
|
totalSteps: dirtyPanes.length,
|
|
336
335
|
});
|
|
337
|
-
for (let i = 0; i < dirtyPanes.length; i++) {
|
|
338
|
-
const paneNode = dirtyPanes[i];
|
|
339
336
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
paneNode.id,
|
|
344
|
-
isContext
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
payload.optionsPayload.nodes.forEach((transformedNode) => {
|
|
348
|
-
const liveNode = ctx.allNodes.get().get(transformedNode.id);
|
|
349
|
-
if (!liveNode) return;
|
|
350
|
-
|
|
351
|
-
let needsUpdate = false;
|
|
352
|
-
let updatedNode: BaseNode = { ...liveNode };
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
transformedNode.nodeType === 'TagElement' &&
|
|
356
|
-
transformedNode.elementCss
|
|
357
|
-
) {
|
|
358
|
-
const flatNode = liveNode as FlatNode;
|
|
359
|
-
if (flatNode.elementCss !== transformedNode.elementCss) {
|
|
360
|
-
(updatedNode as FlatNode).elementCss =
|
|
361
|
-
transformedNode.elementCss;
|
|
362
|
-
needsUpdate = true;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
337
|
+
const bulkPayload = dirtyPanes.map((paneNode) =>
|
|
338
|
+
transformLivePaneForSave(ctx, paneNode.id, isContext)
|
|
339
|
+
);
|
|
365
340
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
newParentCss;
|
|
384
|
-
needsUpdate = true;
|
|
385
|
-
}
|
|
341
|
+
bulkPayload.forEach((payload) => {
|
|
342
|
+
payload.optionsPayload.nodes.forEach((transformedNode) => {
|
|
343
|
+
const liveNode = ctx.allNodes.get().get(transformedNode.id);
|
|
344
|
+
if (!liveNode) return;
|
|
345
|
+
|
|
346
|
+
let needsUpdate = false;
|
|
347
|
+
let updatedNode: BaseNode = { ...liveNode };
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
transformedNode.nodeType === 'TagElement' &&
|
|
351
|
+
transformedNode.elementCss
|
|
352
|
+
) {
|
|
353
|
+
const flatNode = liveNode as FlatNode;
|
|
354
|
+
if (flatNode.elementCss !== transformedNode.elementCss) {
|
|
355
|
+
(updatedNode as FlatNode).elementCss =
|
|
356
|
+
transformedNode.elementCss;
|
|
357
|
+
needsUpdate = true;
|
|
386
358
|
}
|
|
359
|
+
}
|
|
387
360
|
|
|
388
|
-
|
|
389
|
-
|
|
361
|
+
if (
|
|
362
|
+
transformedNode.nodeType === 'Markdown' &&
|
|
363
|
+
transformedNode.parentCss
|
|
364
|
+
) {
|
|
365
|
+
const markdownNode = liveNode as MarkdownPaneFragmentNode;
|
|
366
|
+
const currentParentCss = markdownNode.parentCss;
|
|
367
|
+
const newParentCss = transformedNode.parentCss as string[];
|
|
368
|
+
|
|
369
|
+
const isDifferent =
|
|
370
|
+
!currentParentCss ||
|
|
371
|
+
currentParentCss.length !== newParentCss.length ||
|
|
372
|
+
currentParentCss.some(
|
|
373
|
+
(css, index) => css !== newParentCss[index]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (isDifferent) {
|
|
377
|
+
(updatedNode as MarkdownPaneFragmentNode).parentCss =
|
|
378
|
+
newParentCss;
|
|
379
|
+
needsUpdate = true;
|
|
390
380
|
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const paneExistsInBackend = contentMap.some(
|
|
394
|
-
(item) => item.type === 'Pane' && item.id === paneNode.id
|
|
395
|
-
);
|
|
396
|
-
const isCreatePaneMode = !paneExistsInBackend;
|
|
397
|
-
const endpoint = isCreatePaneMode
|
|
398
|
-
? `${goBackend}/api/v1/nodes/panes/create`
|
|
399
|
-
: `${goBackend}/api/v1/nodes/panes/${payload.id}`;
|
|
400
|
-
const method = isCreatePaneMode ? 'POST' : 'PUT';
|
|
381
|
+
}
|
|
401
382
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
383
|
+
if (needsUpdate) {
|
|
384
|
+
ctx.allNodes.get().set(transformedNode.id, updatedNode);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
});
|
|
407
388
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
'X-Tenant-ID': tenantId,
|
|
413
|
-
},
|
|
414
|
-
credentials: 'include',
|
|
415
|
-
body: JSON.stringify(payload),
|
|
416
|
-
});
|
|
389
|
+
const endpoint = `${goBackend}/api/v1/nodes/panes/bulk`;
|
|
390
|
+
addDebugMessage(
|
|
391
|
+
`Processing ${dirtyPanes.length} panes via -> POST ${endpoint}`
|
|
392
|
+
);
|
|
417
393
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
394
|
+
try {
|
|
395
|
+
const response = await fetch(endpoint, {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: {
|
|
398
|
+
'Content-Type': 'application/json',
|
|
399
|
+
'X-Tenant-ID': tenantId,
|
|
400
|
+
},
|
|
401
|
+
credentials: 'include',
|
|
402
|
+
body: JSON.stringify({ panes: bulkPayload }),
|
|
403
|
+
});
|
|
421
404
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
} catch (etlError) {
|
|
425
|
-
const errorMsg =
|
|
426
|
-
etlError instanceof Error ? etlError.message : 'Unknown error';
|
|
427
|
-
addDebugMessage(`Pane ${paneNode.id} ETL failed: ${errorMsg}`);
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const errorText = await response.text();
|
|
428
407
|
throw new Error(
|
|
429
|
-
`
|
|
408
|
+
`HTTP error! status: ${response.status} - ${errorText}`
|
|
430
409
|
);
|
|
431
410
|
}
|
|
432
411
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
(completedProcessingSteps / totalProcessingSteps) *
|
|
437
|
-
PROGRESS_PHASES.PROCESSING;
|
|
438
|
-
setProgress(
|
|
439
|
-
PROGRESS_PHASES.PREPARATION +
|
|
440
|
-
PROGRESS_PHASES.UPLOADS +
|
|
441
|
-
processingProgress
|
|
412
|
+
await response.json();
|
|
413
|
+
addDebugMessage(
|
|
414
|
+
`${dirtyPanes.length} panes saved successfully via bulk endpoint.`
|
|
442
415
|
);
|
|
416
|
+
} catch (bulkError) {
|
|
417
|
+
const errorMsg =
|
|
418
|
+
bulkError instanceof Error
|
|
419
|
+
? bulkError.message
|
|
420
|
+
: 'Unknown bulk save error';
|
|
421
|
+
addDebugMessage(`Bulk pane save failed: ${errorMsg}`);
|
|
422
|
+
throw new Error(`Failed to save panes in bulk: ${errorMsg}`);
|
|
443
423
|
}
|
|
424
|
+
|
|
425
|
+
setStageProgress({
|
|
426
|
+
currentStep: dirtyPanes.length,
|
|
427
|
+
totalSteps: dirtyPanes.length,
|
|
428
|
+
});
|
|
429
|
+
completedProcessingSteps += dirtyPanes.length;
|
|
430
|
+
const processingProgress =
|
|
431
|
+
(completedProcessingSteps / totalProcessingSteps) *
|
|
432
|
+
PROGRESS_PHASES.PROCESSING;
|
|
433
|
+
setProgress(
|
|
434
|
+
PROGRESS_PHASES.PREPARATION +
|
|
435
|
+
PROGRESS_PHASES.UPLOADS +
|
|
436
|
+
processingProgress
|
|
437
|
+
);
|
|
444
438
|
}
|
|
445
439
|
|
|
446
440
|
if (!isContext && dirtyStoryFragments.length > 0) {
|
|
@@ -58,14 +58,15 @@ const logoPositions = generatePositions();
|
|
|
58
58
|
{
|
|
59
59
|
assetUrl && (
|
|
60
60
|
<div
|
|
61
|
-
class="pointer-events-none absolute overflow-hidden rounded-2xl p-1.5 md:p-3.5"
|
|
61
|
+
class="pointer-events-none absolute mr-6 overflow-hidden rounded-2xl p-1.5 md:p-3.5"
|
|
62
62
|
style={{
|
|
63
63
|
top: '2rem',
|
|
64
64
|
left: '64rem',
|
|
65
65
|
right: '0',
|
|
66
66
|
bottom: '3.5rem',
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
'mix-blend-mode': 'multiply',
|
|
68
|
+
opacity: '0.07',
|
|
69
|
+
border: '2px dashed rgba(0, 0, 0, 1)',
|
|
69
70
|
}}
|
|
70
71
|
>
|
|
71
72
|
{logoPositions.map((position) => (
|
package/utils/inject-files.ts
CHANGED
|
@@ -479,10 +479,6 @@ export async function injectTemplateFiles(
|
|
|
479
479
|
src: resolve('../templates/src/components/edit/pane/PanePanel_path.tsx'),
|
|
480
480
|
dest: 'src/components/edit/pane/PanePanel_path.tsx',
|
|
481
481
|
},
|
|
482
|
-
{
|
|
483
|
-
src: resolve('../templates/src/components/edit/pane/PanePanel_slug.tsx'),
|
|
484
|
-
dest: 'src/components/edit/pane/PanePanel_slug.tsx',
|
|
485
|
-
},
|
|
486
482
|
{
|
|
487
483
|
src: resolve('../templates/src/components/edit/pane/PanePanel_title.tsx'),
|
|
488
484
|
dest: 'src/components/edit/pane/PanePanel_title.tsx',
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, type Dispatch, type SetStateAction } from 'react';
|
|
2
|
-
import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
|
|
3
|
-
import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
|
|
4
|
-
import { getCtx } from '@/stores/nodes';
|
|
5
|
-
import { cloneDeep } from '@/utils/helpers';
|
|
6
|
-
import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
|
|
7
|
-
|
|
8
|
-
interface PaneSlugPanelProps {
|
|
9
|
-
nodeId: string;
|
|
10
|
-
setMode: Dispatch<SetStateAction<PaneConfigMode>>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const PaneSlugPanel = ({ nodeId, setMode }: PaneSlugPanelProps) => {
|
|
14
|
-
const [slug, setSlug] = useState('');
|
|
15
|
-
const [isValid, setIsValid] = useState(false);
|
|
16
|
-
const [warning, setWarning] = useState(false);
|
|
17
|
-
const [charCount, setCharCount] = useState(0);
|
|
18
|
-
const [validationError, setValidationError] = useState<string | null>(null);
|
|
19
|
-
const [canSave, setCanSave] = useState(false);
|
|
20
|
-
|
|
21
|
-
const ctx = getCtx();
|
|
22
|
-
const allNodes = ctx.allNodes.get();
|
|
23
|
-
const paneNode = allNodes.get(nodeId) as PaneNode;
|
|
24
|
-
if (!paneNode) return null;
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
setSlug(paneNode.slug);
|
|
28
|
-
setCharCount(paneNode.slug.length);
|
|
29
|
-
checkLiveValidity(paneNode.slug);
|
|
30
|
-
}, [paneNode.slug]);
|
|
31
|
-
|
|
32
|
-
// More permissive validation for typing
|
|
33
|
-
const checkLiveValidity = (value: string) => {
|
|
34
|
-
const length = value.length;
|
|
35
|
-
setCharCount(length);
|
|
36
|
-
|
|
37
|
-
// Basic format check for allowed characters
|
|
38
|
-
if (!/^[a-z0-9-]*$/.test(value)) {
|
|
39
|
-
setValidationError(
|
|
40
|
-
'Only lowercase letters, numbers, and hyphens allowed'
|
|
41
|
-
);
|
|
42
|
-
setIsValid(false);
|
|
43
|
-
setCanSave(false);
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Length checks
|
|
48
|
-
setIsValid(length >= 3 && length <= 40);
|
|
49
|
-
setWarning(length > 40 && length <= 50);
|
|
50
|
-
setValidationError(null);
|
|
51
|
-
|
|
52
|
-
// Check if we can save
|
|
53
|
-
if (length >= 3) {
|
|
54
|
-
const saveValidation = checkSaveValidity(value);
|
|
55
|
-
setCanSave(saveValidation.isValid);
|
|
56
|
-
if (!saveValidation.isValid) {
|
|
57
|
-
setValidationError(saveValidation.error || null);
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
setCanSave(false);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return true;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Strict validation for saving
|
|
67
|
-
const checkSaveValidity = (
|
|
68
|
-
value: string
|
|
69
|
-
): { isValid: boolean; error?: string } => {
|
|
70
|
-
// Strict pattern that prevents leading/trailing hyphens and multiple consecutive hyphens
|
|
71
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
72
|
-
return {
|
|
73
|
-
isValid: false,
|
|
74
|
-
error:
|
|
75
|
-
'Slug must start and end with letters or numbers, and no consecutive hyphens',
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Check duplicates and reserved slugs
|
|
80
|
-
return ctx.isSlugValid(value, nodeId);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const handleSlugChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
84
|
-
const newSlug = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
85
|
-
|
|
86
|
-
if (newSlug.length <= 50) {
|
|
87
|
-
setSlug(newSlug);
|
|
88
|
-
checkLiveValidity(newSlug);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const handleSlugBlur = () => {
|
|
93
|
-
if (canSave) {
|
|
94
|
-
const ctx = getCtx();
|
|
95
|
-
const updatedNode = { ...cloneDeep(paneNode), slug, isChanged: true };
|
|
96
|
-
ctx.modifyNodes([updatedNode]);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6 shadow-inner">
|
|
102
|
-
<div className="px-3.5">
|
|
103
|
-
<div className="mb-4 flex justify-between">
|
|
104
|
-
<h3 className="text-lg font-bold">Pane Slug</h3>
|
|
105
|
-
<button
|
|
106
|
-
onClick={() => setMode(PaneConfigMode.DEFAULT)}
|
|
107
|
-
className="text-myblue hover:text-black"
|
|
108
|
-
>
|
|
109
|
-
← Go Back
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<div className="relative">
|
|
114
|
-
<input
|
|
115
|
-
type="text"
|
|
116
|
-
value={slug}
|
|
117
|
-
onChange={handleSlugChange}
|
|
118
|
-
onBlur={handleSlugBlur}
|
|
119
|
-
onKeyDown={(e) => {
|
|
120
|
-
if (e.key === 'Enter') {
|
|
121
|
-
e.currentTarget.blur();
|
|
122
|
-
}
|
|
123
|
-
}}
|
|
124
|
-
className={`w-full rounded-md border px-2 py-1 pr-16 ${
|
|
125
|
-
validationError || charCount < 3
|
|
126
|
-
? 'border-red-500 bg-red-50'
|
|
127
|
-
: isValid && canSave
|
|
128
|
-
? 'border-green-500 bg-green-50'
|
|
129
|
-
: warning
|
|
130
|
-
? 'border-yellow-500 bg-yellow-50'
|
|
131
|
-
: 'border-gray-300'
|
|
132
|
-
}`}
|
|
133
|
-
placeholder="Enter pane slug (3-40 characters recommended)"
|
|
134
|
-
/>
|
|
135
|
-
<div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
|
|
136
|
-
{validationError || charCount < 3 ? (
|
|
137
|
-
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
138
|
-
) : isValid && canSave ? (
|
|
139
|
-
<CheckIcon className="h-5 w-5 text-green-500" />
|
|
140
|
-
) : warning ? (
|
|
141
|
-
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
142
|
-
) : null}
|
|
143
|
-
<span
|
|
144
|
-
className={`text-sm ${
|
|
145
|
-
validationError || charCount < 3
|
|
146
|
-
? 'text-red-500'
|
|
147
|
-
: isValid && canSave
|
|
148
|
-
? 'text-green-500'
|
|
149
|
-
: warning
|
|
150
|
-
? 'text-yellow-500'
|
|
151
|
-
: 'text-gray-500'
|
|
152
|
-
}`}
|
|
153
|
-
>
|
|
154
|
-
{charCount}/50
|
|
155
|
-
</span>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
{validationError && (
|
|
159
|
-
<div className="mt-2 text-sm text-red-600">
|
|
160
|
-
<ExclamationTriangleIcon className="mr-1 inline h-4 w-4" />
|
|
161
|
-
{validationError}
|
|
162
|
-
</div>
|
|
163
|
-
)}
|
|
164
|
-
<div className="mt-4 space-y-4 text-lg">
|
|
165
|
-
<div className="text-gray-600">
|
|
166
|
-
This is solely used for analytics!
|
|
167
|
-
<ul className="ml-4 mt-1">
|
|
168
|
-
<li>
|
|
169
|
-
<CheckIcon className="inline h-4 w-4" /> Keep it concise and
|
|
170
|
-
descriptive
|
|
171
|
-
</li>
|
|
172
|
-
<li>
|
|
173
|
-
<CheckIcon className="inline h-4 w-4" /> Use lowercase letters
|
|
174
|
-
and numbers
|
|
175
|
-
</li>
|
|
176
|
-
<li>
|
|
177
|
-
<CheckIcon className="inline h-4 w-4" /> Use hyphens between
|
|
178
|
-
words
|
|
179
|
-
</li>
|
|
180
|
-
<li>
|
|
181
|
-
<CheckIcon className="inline h-4 w-4" /> Must start and end with
|
|
182
|
-
a letter or number
|
|
183
|
-
</li>
|
|
184
|
-
</ul>
|
|
185
|
-
</div>
|
|
186
|
-
<div className="py-4">
|
|
187
|
-
{charCount < 3 && (
|
|
188
|
-
<span className="text-red-500">
|
|
189
|
-
Slug must be at least 3 characters
|
|
190
|
-
</span>
|
|
191
|
-
)}
|
|
192
|
-
{charCount >= 3 && charCount < 5 && !validationError && (
|
|
193
|
-
<span className="text-gray-500">
|
|
194
|
-
Consider adding more characters for better description
|
|
195
|
-
</span>
|
|
196
|
-
)}
|
|
197
|
-
{warning && !validationError && (
|
|
198
|
-
<span className="text-yellow-500">
|
|
199
|
-
Slug is getting long - consider shortening it
|
|
200
|
-
</span>
|
|
201
|
-
)}
|
|
202
|
-
{isValid && canSave && charCount >= 5 && !validationError && (
|
|
203
|
-
<span className="text-green-500">
|
|
204
|
-
Good slug length and format!
|
|
205
|
-
</span>
|
|
206
|
-
)}
|
|
207
|
-
{isValid && !canSave && !validationError && (
|
|
208
|
-
<span className="text-gray-500">
|
|
209
|
-
Valid characters but needs proper formatting to save
|
|
210
|
-
</span>
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
export default PaneSlugPanel;
|