astro-tractstack 2.1.3 → 2.2.1
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/README.md +54 -266
- package/bin/create-tractstack.js +9 -6
- package/dist/index.js +109 -71
- package/package.json +4 -2
- package/templates/css/custom.css +5 -0
- package/templates/icons/code.svg +18 -0
- package/templates/icons/li.svg +4 -0
- package/templates/icons/link.svg +22 -0
- package/templates/icons/p.svg +3 -0
- package/templates/src/client/app.js +80 -1
- package/templates/src/components/Footer.astro +1 -1
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +6 -6
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +3 -3
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
- package/templates/src/components/codehooks/ListContentSetup.tsx +2 -2
- package/templates/src/components/codehooks/ProductCardSetup.tsx +1 -1
- package/templates/src/components/codehooks/ProductGridSetup.tsx +2 -2
- package/templates/src/components/codehooks/SandboxRegisterForm.tsx +3 -3
- package/templates/src/components/compositor/Compositor.tsx +25 -9
- package/templates/src/components/compositor/Node.tsx +168 -496
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +1 -0
- package/templates/src/components/compositor/elements/SignUp.tsx +1 -1
- package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +2 -0
- package/templates/src/components/compositor/nodes/CreativePane.tsx +262 -0
- package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +4 -6
- package/templates/src/components/compositor/nodes/GridLayout.tsx +4 -2
- package/templates/src/components/compositor/nodes/Markdown.tsx +18 -3
- package/templates/src/components/compositor/nodes/Pane.tsx +11 -5
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +1 -1
- package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +5 -5
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +90 -42
- package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +2 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +27 -1
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +10 -8
- package/templates/src/components/compositor/tools/NodeOverlay.tsx +224 -0
- package/templates/src/components/compositor/tools/PaneOverlay.tsx +122 -0
- package/templates/src/components/edit/Header.tsx +68 -9
- package/templates/src/components/edit/PanelSwitch.tsx +42 -4
- package/templates/src/components/edit/SettingsPanel.tsx +2 -3
- package/templates/src/components/edit/ToolMode.tsx +1 -31
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +1 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +193 -659
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +15 -82
- package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +95 -45
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +137 -49
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -1
- package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +375 -0
- package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +1 -23
- package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +327 -0
- package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +267 -0
- package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +371 -0
- package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +201 -76
- package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +141 -0
- package/templates/src/components/edit/panels/CreativeImagePanel.tsx +435 -0
- package/templates/src/components/edit/panels/CreativeLinkPanel.tsx +110 -0
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +1 -1
- package/templates/src/components/edit/panels/StyleParentPanel.tsx +118 -126
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +3 -2
- package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +1 -0
- package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +3 -1
- package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +3 -1
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +1 -1
- package/templates/src/components/edit/state/SaveModal.tsx +19 -787
- package/templates/src/components/edit/state/SaveToLibraryModal.tsx +2 -2
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +1 -1
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +5 -5
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +1 -1
- package/templates/src/components/edit/widgets/SignupWidget.tsx +1 -1
- package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +1 -1
- package/templates/src/components/fields/ArtpackImage.tsx +11 -3
- package/templates/src/components/fields/BackgroundImage.tsx +8 -0
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +15 -9
- package/templates/src/components/fields/ImageUpload.tsx +6 -0
- package/templates/src/components/form/ActionBuilderField.tsx +15 -5
- package/templates/src/components/form/ActionBuilderSlugSelector.tsx +1 -1
- package/templates/src/components/form/ColorPicker.tsx +1 -1
- package/templates/src/components/form/EnumSelect.tsx +1 -1
- package/templates/src/components/form/NumberInput.tsx +1 -1
- package/templates/src/components/form/StringArrayInput.tsx +1 -1
- package/templates/src/components/form/StringInput.tsx +1 -1
- package/templates/src/components/form/UnsavedChangesBar.tsx +1 -1
- package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -2
- package/templates/src/components/form/advanced/AuthConfigSection.tsx +2 -2
- package/templates/src/components/profile/ProfileCreate.tsx +1 -1
- package/templates/src/components/profile/ProfileEdit.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +2 -2
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +2 -2
- package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +6 -6
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/PaneTable.tsx +358 -0
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -1
- package/templates/src/constants/prompts.json +18 -10
- package/templates/src/constants.ts +3 -0
- package/templates/src/hooks/usePaneFragments.ts +60 -0
- package/templates/src/lib/session.ts +71 -16
- package/templates/src/pages/[...slug].astro +4 -46
- package/templates/src/pages/api/css.ts +149 -0
- package/templates/src/pages/maint.astro +1 -1
- package/templates/src/pages/storykeep/login.astro +2 -2
- package/templates/src/stores/nodes.ts +162 -49
- package/templates/src/stores/orphanAnalysis.ts +6 -30
- package/templates/src/stores/previews.ts +7 -0
- package/templates/src/stores/storykeep.ts +0 -8
- package/templates/src/types/compositorTypes.ts +53 -10
- package/templates/src/utils/compositor/aiGeneration.ts +93 -0
- package/templates/src/utils/compositor/allowInsert.ts +2 -0
- package/templates/src/utils/compositor/htmlAst.ts +704 -0
- package/templates/src/utils/compositor/nodesHelper.ts +281 -102
- package/templates/src/utils/compositor/savePipeline.ts +893 -0
- package/templates/src/utils/etl/index.ts +3 -0
- package/templates/src/utils/etl/transformer.ts +10 -0
- package/templates/src/utils/helpers.ts +101 -0
- package/utils/inject-files.ts +100 -62
- package/templates/icons/text.svg +0 -6
- package/templates/src/components/compositor/NodeWithGuid.tsx +0 -69
- package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +0 -33
- package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +0 -56
- package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +0 -269
- package/templates/src/components/compositor/nodes/Pane_eraser.tsx +0 -186
- package/templates/src/components/compositor/nodes/Pane_layout.tsx +0 -79
- package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +0 -26
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +0 -61
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +0 -120
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +0 -62
- package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +0 -26
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPendingImageOperation,
|
|
3
|
+
clearPendingImageOperation,
|
|
4
|
+
pendingHomePageSlugStore,
|
|
5
|
+
} from '@/stores/storykeep';
|
|
6
|
+
import { cloneDeep } from '@/utils/helpers';
|
|
7
|
+
import {
|
|
8
|
+
transformLivePaneForSave,
|
|
9
|
+
transformStoryFragmentForSave,
|
|
10
|
+
} from '@/utils/etl/index';
|
|
11
|
+
import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
|
|
12
|
+
import { scanAndReplaceBase64 } from '@/utils/compositor/htmlAst';
|
|
13
|
+
import type { NodesContext } from '@/stores/nodes';
|
|
14
|
+
import type {
|
|
15
|
+
BaseNode,
|
|
16
|
+
PaneNode,
|
|
17
|
+
StoryFragmentNode,
|
|
18
|
+
FlatNode,
|
|
19
|
+
MarkdownPaneFragmentNode,
|
|
20
|
+
GridLayoutNode,
|
|
21
|
+
} from '@/types/compositorTypes';
|
|
22
|
+
|
|
23
|
+
type PendingFileNode = BaseNode & {
|
|
24
|
+
base64Data?: string;
|
|
25
|
+
fileId?: string;
|
|
26
|
+
src?: string;
|
|
27
|
+
srcSet?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type SaveStage =
|
|
31
|
+
| 'PREPARING'
|
|
32
|
+
| 'SAVING_PENDING_FILES'
|
|
33
|
+
| 'PROCESSING_OG_IMAGES'
|
|
34
|
+
| 'COOKING_NODES'
|
|
35
|
+
| 'PROCESSING_STYLES'
|
|
36
|
+
| 'SAVING_PANES'
|
|
37
|
+
| 'SAVING_STORY_FRAGMENTS'
|
|
38
|
+
| 'LINKING_FILES'
|
|
39
|
+
| 'UPDATING_HOME_PAGE'
|
|
40
|
+
| 'COMPLETED'
|
|
41
|
+
| 'ERROR';
|
|
42
|
+
|
|
43
|
+
export interface SaveStageProgress {
|
|
44
|
+
currentStep: number;
|
|
45
|
+
totalSteps: number;
|
|
46
|
+
currentFileName?: string;
|
|
47
|
+
isUploading?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SavePipelineConfig {
|
|
51
|
+
slug: string;
|
|
52
|
+
isContext: boolean;
|
|
53
|
+
isCreateMode: boolean;
|
|
54
|
+
hydrate: boolean;
|
|
55
|
+
tenantId: string;
|
|
56
|
+
backendUrl: string;
|
|
57
|
+
pendingHomePageSlug: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SavePipelineCallbacks {
|
|
61
|
+
setStage: (stage: SaveStage) => void;
|
|
62
|
+
setProgress: (percentage: number) => void;
|
|
63
|
+
setStageProgress: (
|
|
64
|
+
update: SaveStageProgress | ((prev: SaveStageProgress) => SaveStageProgress)
|
|
65
|
+
) => void;
|
|
66
|
+
logDebug: (message: string) => void;
|
|
67
|
+
setIsIndeterminateStage: (isIndeterminate: boolean) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const PROGRESS_PHASES = {
|
|
71
|
+
PREPARATION: 5,
|
|
72
|
+
UPLOADS: 60,
|
|
73
|
+
PROCESSING: 25,
|
|
74
|
+
FINALIZATION: 10,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export async function executeSavePipeline(
|
|
78
|
+
ctx: NodesContext,
|
|
79
|
+
config: SavePipelineConfig,
|
|
80
|
+
callbacks: SavePipelineCallbacks
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const {
|
|
83
|
+
slug,
|
|
84
|
+
isContext,
|
|
85
|
+
isCreateMode,
|
|
86
|
+
hydrate,
|
|
87
|
+
tenantId,
|
|
88
|
+
backendUrl,
|
|
89
|
+
pendingHomePageSlug,
|
|
90
|
+
} = config;
|
|
91
|
+
const {
|
|
92
|
+
setStage,
|
|
93
|
+
setProgress,
|
|
94
|
+
setStageProgress,
|
|
95
|
+
logDebug,
|
|
96
|
+
setIsIndeterminateStage,
|
|
97
|
+
} = callbacks;
|
|
98
|
+
|
|
99
|
+
const allDirtyNodes = ctx.getDirtyNodes();
|
|
100
|
+
|
|
101
|
+
setStage('PREPARING');
|
|
102
|
+
setProgress(PROGRESS_PHASES.PREPARATION);
|
|
103
|
+
logDebug(
|
|
104
|
+
`Starting save process... (${isContext ? 'Context' : 'StoryFragment'} mode, ${isCreateMode ? 'CREATE' : 'UPDATE'})`
|
|
105
|
+
);
|
|
106
|
+
logDebug(
|
|
107
|
+
`Config: slug=${slug}, tenantId=${tenantId}, backendUrl=${backendUrl}`
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
let dirtyPanes = allDirtyNodes.filter(
|
|
111
|
+
(node) => node.nodeType === 'Pane'
|
|
112
|
+
) as PaneNode[];
|
|
113
|
+
let dirtyStoryFragments = allDirtyNodes.filter(
|
|
114
|
+
(node) => node.nodeType === 'StoryFragment'
|
|
115
|
+
) as StoryFragmentNode[];
|
|
116
|
+
|
|
117
|
+
if (isContext) {
|
|
118
|
+
dirtyStoryFragments = [];
|
|
119
|
+
logDebug('Context mode: Ignoring StoryFragment nodes');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const nodesWithPendingFiles = allDirtyNodes.filter(
|
|
123
|
+
(node): node is BaseNode & { base64Data?: string } =>
|
|
124
|
+
'base64Data' in node && !!node.base64Data
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const storyFragmentsWithPendingImages = dirtyStoryFragments.filter(
|
|
128
|
+
(fragment) => {
|
|
129
|
+
const pendingOp = getPendingImageOperation(fragment.id);
|
|
130
|
+
return pendingOp && pendingOp.type === 'upload';
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const totalFileBytes = nodesWithPendingFiles.reduce(
|
|
135
|
+
(sum, node) => sum + (node.base64Data?.length || 0),
|
|
136
|
+
0
|
|
137
|
+
);
|
|
138
|
+
const totalOgBytes = storyFragmentsWithPendingImages.reduce(
|
|
139
|
+
(sum, fragment) => {
|
|
140
|
+
const pendingOp = getPendingImageOperation(fragment.id);
|
|
141
|
+
return sum + (pendingOp?.data?.length || 0);
|
|
142
|
+
},
|
|
143
|
+
0
|
|
144
|
+
);
|
|
145
|
+
const totalUploadBytes = totalFileBytes + totalOgBytes;
|
|
146
|
+
let completedUploadBytes = 0;
|
|
147
|
+
|
|
148
|
+
const relevantNodeCount = dirtyPanes.length + dirtyStoryFragments.length;
|
|
149
|
+
logDebug(
|
|
150
|
+
`Found ${relevantNodeCount} relevant dirty nodes to save (${dirtyPanes.length} Panes, ${dirtyStoryFragments.length} StoryFragments)`
|
|
151
|
+
);
|
|
152
|
+
logDebug(
|
|
153
|
+
`Pending uploads: ${nodesWithPendingFiles.length} standard files, ${storyFragmentsWithPendingImages.length} OG images`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
relevantNodeCount === 0 &&
|
|
158
|
+
nodesWithPendingFiles.length === 0 &&
|
|
159
|
+
storyFragmentsWithPendingImages.length === 0 &&
|
|
160
|
+
!pendingHomePageSlug
|
|
161
|
+
) {
|
|
162
|
+
logDebug('No changes to save');
|
|
163
|
+
setStage('COMPLETED');
|
|
164
|
+
setProgress(100);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const uploadedOGPaths: Record<string, string> = {};
|
|
169
|
+
|
|
170
|
+
if (nodesWithPendingFiles.length > 0) {
|
|
171
|
+
setStage('SAVING_PENDING_FILES');
|
|
172
|
+
logDebug(
|
|
173
|
+
`Starting processing of ${nodesWithPendingFiles.length} pending files...`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < nodesWithPendingFiles.length; i++) {
|
|
177
|
+
const fileNode = nodesWithPendingFiles[i];
|
|
178
|
+
const fileBytes = fileNode.base64Data?.length || 0;
|
|
179
|
+
const endpoint = `${backendUrl}/api/v1/nodes/files/create`;
|
|
180
|
+
|
|
181
|
+
setStageProgress({
|
|
182
|
+
currentStep: i + 1,
|
|
183
|
+
totalSteps: nodesWithPendingFiles.length,
|
|
184
|
+
currentFileName: `${fileNode.id}.jpg`,
|
|
185
|
+
isUploading: true,
|
|
186
|
+
});
|
|
187
|
+
logDebug(
|
|
188
|
+
`Processing file ${i + 1}/${nodesWithPendingFiles.length}: ${fileNode.id} -> POST ${endpoint}`
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(endpoint, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: {
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
'X-Tenant-ID': tenantId,
|
|
197
|
+
},
|
|
198
|
+
credentials: 'include',
|
|
199
|
+
body: JSON.stringify({ base64Data: fileNode.base64Data }),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await response.json();
|
|
207
|
+
|
|
208
|
+
const updatedNode = cloneDeep(fileNode) as PendingFileNode;
|
|
209
|
+
delete updatedNode.base64Data;
|
|
210
|
+
|
|
211
|
+
updatedNode.fileId = result.fileId;
|
|
212
|
+
updatedNode.src = result.src;
|
|
213
|
+
if (result.srcSet) updatedNode.srcSet = result.srcSet;
|
|
214
|
+
updatedNode.isChanged = true;
|
|
215
|
+
ctx.modifyNodes([updatedNode]);
|
|
216
|
+
|
|
217
|
+
const localRef = fileNode as PendingFileNode;
|
|
218
|
+
delete localRef.base64Data;
|
|
219
|
+
localRef.fileId = result.fileId;
|
|
220
|
+
localRef.src = result.src;
|
|
221
|
+
if (result.srcSet) localRef.srcSet = result.srcSet;
|
|
222
|
+
logDebug(
|
|
223
|
+
`File ${fileNode.id} uploaded successfully - got fileId: ${result.fileId}`
|
|
224
|
+
);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const errorMsg =
|
|
227
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
228
|
+
logDebug(`File ${fileNode.id} upload failed: ${errorMsg}`);
|
|
229
|
+
throw new Error(`Failed to upload file ${fileNode.id}: ${errorMsg}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
completedUploadBytes += fileBytes;
|
|
233
|
+
const uploadProgress =
|
|
234
|
+
totalUploadBytes > 0
|
|
235
|
+
? (completedUploadBytes / totalUploadBytes) * PROGRESS_PHASES.UPLOADS
|
|
236
|
+
: 0;
|
|
237
|
+
setProgress(PROGRESS_PHASES.PREPARATION + uploadProgress);
|
|
238
|
+
setStageProgress((prev: SaveStageProgress) => ({
|
|
239
|
+
...prev,
|
|
240
|
+
isUploading: false,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const creativePanes = dirtyPanes.filter(
|
|
246
|
+
(p) => p.htmlAst && p.htmlAst.editableElements
|
|
247
|
+
);
|
|
248
|
+
if (creativePanes.length > 0) {
|
|
249
|
+
logDebug(
|
|
250
|
+
`Scanning ${creativePanes.length} Creative Panes for embedded uploads...`
|
|
251
|
+
);
|
|
252
|
+
setIsIndeterminateStage(true);
|
|
253
|
+
|
|
254
|
+
for (const pane of creativePanes) {
|
|
255
|
+
if (!pane.htmlAst) continue;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
logDebug(`Scanning pane ${pane.id} for base64 assets...`);
|
|
259
|
+
const cleanAst = await scanAndReplaceBase64(
|
|
260
|
+
pane.htmlAst,
|
|
261
|
+
async (base64Data) => {
|
|
262
|
+
logDebug(
|
|
263
|
+
`Uploading embedded asset for pane ${pane.id} (${base64Data.length} bytes)...`
|
|
264
|
+
);
|
|
265
|
+
const endpoint = `${backendUrl}/api/v1/nodes/files/create`;
|
|
266
|
+
|
|
267
|
+
const response = await fetch(endpoint, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: {
|
|
270
|
+
'Content-Type': 'application/json',
|
|
271
|
+
'X-Tenant-ID': tenantId,
|
|
272
|
+
},
|
|
273
|
+
credentials: 'include',
|
|
274
|
+
body: JSON.stringify({ base64Data }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
const txt = await response.text();
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Creative upload failed: ${response.status} - ${txt}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const res = await response.json();
|
|
284
|
+
logDebug(`Asset uploaded. New fileId: ${res.fileId}`);
|
|
285
|
+
return res;
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
ctx.modifyNodes([
|
|
290
|
+
{ ...pane, htmlAst: cleanAst, isChanged: true } as PaneNode,
|
|
291
|
+
]);
|
|
292
|
+
logDebug(`Creative Pane ${pane.id} assets processed and node updated.`);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error('Creative Pane Asset Upload Error:', err);
|
|
295
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
296
|
+
logDebug(
|
|
297
|
+
`Failed to process assets for Creative Pane ${pane.id}: ${errMsg}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
setIsIndeterminateStage(false);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (storyFragmentsWithPendingImages.length > 0) {
|
|
305
|
+
setStage('PROCESSING_OG_IMAGES');
|
|
306
|
+
logDebug(
|
|
307
|
+
`Processing ${storyFragmentsWithPendingImages.length} OG images...`
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
for (let i = 0; i < storyFragmentsWithPendingImages.length; i++) {
|
|
311
|
+
const fragment = storyFragmentsWithPendingImages[i];
|
|
312
|
+
const pendingOp = getPendingImageOperation(fragment.id);
|
|
313
|
+
const imageBytes = pendingOp?.data?.length || 0;
|
|
314
|
+
|
|
315
|
+
if (pendingOp && pendingOp.type === 'upload' && pendingOp.data) {
|
|
316
|
+
const ogUploadEndpoint = `${backendUrl}/api/v1/nodes/images/og`;
|
|
317
|
+
logDebug(
|
|
318
|
+
`Processing OG image ${i + 1}/${storyFragmentsWithPendingImages.length}: ${fragment.id} -> POST ${ogUploadEndpoint}`
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
setStageProgress({
|
|
322
|
+
currentStep: i + 1,
|
|
323
|
+
totalSteps: storyFragmentsWithPendingImages.length,
|
|
324
|
+
currentFileName: pendingOp?.filename || `${fragment.id}-og.png`,
|
|
325
|
+
isUploading: true,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const uploadPayload = {
|
|
329
|
+
data: pendingOp.data,
|
|
330
|
+
filename: pendingOp.filename || `${fragment.id}-${Date.now()}.png`,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const response = await fetch(ogUploadEndpoint, {
|
|
335
|
+
method: 'POST',
|
|
336
|
+
headers: {
|
|
337
|
+
'Content-Type': 'application/json',
|
|
338
|
+
'X-Tenant-ID': tenantId,
|
|
339
|
+
},
|
|
340
|
+
credentials: 'include',
|
|
341
|
+
body: JSON.stringify(uploadPayload),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = await response.json();
|
|
349
|
+
uploadedOGPaths[fragment.id] = result.path;
|
|
350
|
+
logDebug(`OG image uploaded successfully: ${result.path}`);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const errorMsg =
|
|
353
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
354
|
+
logDebug(`OG image upload failed for ${fragment.id}: ${errorMsg}`);
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Failed to upload OG image for ${fragment.id}: ${errorMsg}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
completedUploadBytes += imageBytes;
|
|
360
|
+
const uploadProgress =
|
|
361
|
+
totalUploadBytes > 0
|
|
362
|
+
? (completedUploadBytes / totalUploadBytes) *
|
|
363
|
+
PROGRESS_PHASES.UPLOADS
|
|
364
|
+
: 0;
|
|
365
|
+
setProgress(PROGRESS_PHASES.PREPARATION + uploadProgress);
|
|
366
|
+
setStageProgress((prev: SaveStageProgress) => ({
|
|
367
|
+
...prev,
|
|
368
|
+
isUploading: false,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (totalUploadBytes > 0) {
|
|
375
|
+
setProgress(PROGRESS_PHASES.PREPARATION + PROGRESS_PHASES.UPLOADS);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const totalProcessingSteps = dirtyPanes.length + dirtyStoryFragments.length;
|
|
379
|
+
let completedProcessingSteps = 0;
|
|
380
|
+
|
|
381
|
+
if (allDirtyNodes.length > 0) {
|
|
382
|
+
setStage('COOKING_NODES');
|
|
383
|
+
setIsIndeterminateStage(true);
|
|
384
|
+
logDebug(
|
|
385
|
+
`Cooking ${allDirtyNodes.length} nodes for whitelist extraction...`
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const cookingUpdates: BaseNode[] = [];
|
|
389
|
+
const nodesToCookMap = new Map<string, BaseNode>();
|
|
390
|
+
|
|
391
|
+
dirtyPanes.forEach((pane) => {
|
|
392
|
+
const subtree = ctx.getNodesRecursively(pane);
|
|
393
|
+
subtree.forEach((node) => {
|
|
394
|
+
nodesToCookMap.set(node.id, node);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
nodesToCookMap.forEach((liveNode) => {
|
|
399
|
+
try {
|
|
400
|
+
let updatedNode: BaseNode | null = null;
|
|
401
|
+
|
|
402
|
+
if (liveNode.nodeType === 'TagElement') {
|
|
403
|
+
const flatNode = liveNode as FlatNode;
|
|
404
|
+
const computedCSS = ctx.getNodeClasses(flatNode.id, 'auto', 0);
|
|
405
|
+
if (flatNode.elementCss !== computedCSS) {
|
|
406
|
+
updatedNode = {
|
|
407
|
+
...liveNode,
|
|
408
|
+
elementCss: computedCSS,
|
|
409
|
+
} as FlatNode;
|
|
410
|
+
}
|
|
411
|
+
} else if (liveNode.nodeType === 'Markdown') {
|
|
412
|
+
const markdownNode = liveNode as MarkdownPaneFragmentNode;
|
|
413
|
+
let needsUpdate = false;
|
|
414
|
+
const nextNode = { ...markdownNode };
|
|
415
|
+
|
|
416
|
+
if (markdownNode.parentClasses) {
|
|
417
|
+
const computedParentCss = markdownNode.parentClasses.map(
|
|
418
|
+
(_: any, index: number) =>
|
|
419
|
+
ctx.getNodeClasses(liveNode.id, 'auto', index)
|
|
420
|
+
);
|
|
421
|
+
if (
|
|
422
|
+
JSON.stringify(markdownNode.parentCss) !==
|
|
423
|
+
JSON.stringify(computedParentCss)
|
|
424
|
+
) {
|
|
425
|
+
nextNode.parentCss = computedParentCss;
|
|
426
|
+
needsUpdate = true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (markdownNode.gridClasses) {
|
|
431
|
+
const [allClasses] = processClassesForViewports(
|
|
432
|
+
markdownNode.gridClasses,
|
|
433
|
+
{},
|
|
434
|
+
1
|
|
435
|
+
);
|
|
436
|
+
if (allClasses && allClasses.length > 0) {
|
|
437
|
+
const computedGridCss = allClasses[0];
|
|
438
|
+
if (markdownNode.gridCss !== computedGridCss) {
|
|
439
|
+
nextNode.gridCss = computedGridCss;
|
|
440
|
+
needsUpdate = true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (needsUpdate) updatedNode = nextNode;
|
|
446
|
+
} else if (liveNode.nodeType === 'GridLayoutNode') {
|
|
447
|
+
const gridNode = liveNode as GridLayoutNode;
|
|
448
|
+
let needsUpdate = false;
|
|
449
|
+
const nextNode = { ...gridNode };
|
|
450
|
+
|
|
451
|
+
if (gridNode.parentClasses) {
|
|
452
|
+
const computedParentCss = gridNode.parentClasses.map(
|
|
453
|
+
(_: any, index: number) =>
|
|
454
|
+
ctx.getNodeClasses(liveNode.id, 'auto', index)
|
|
455
|
+
);
|
|
456
|
+
if (
|
|
457
|
+
JSON.stringify(gridNode.parentCss) !==
|
|
458
|
+
JSON.stringify(computedParentCss)
|
|
459
|
+
) {
|
|
460
|
+
nextNode.parentCss = computedParentCss.join(` `);
|
|
461
|
+
needsUpdate = true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (gridNode.gridColumns) {
|
|
466
|
+
const { mobile, tablet, desktop } = gridNode.gridColumns;
|
|
467
|
+
let computedGridCss = `grid grid-cols-${mobile}`;
|
|
468
|
+
if (tablet !== mobile) computedGridCss += ` md:grid-cols-${tablet}`;
|
|
469
|
+
if (desktop !== tablet)
|
|
470
|
+
computedGridCss += ` xl:grid-cols-${desktop}`;
|
|
471
|
+
|
|
472
|
+
if (gridNode.gridCss !== computedGridCss) {
|
|
473
|
+
nextNode.gridCss = computedGridCss;
|
|
474
|
+
needsUpdate = true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (needsUpdate) updatedNode = nextNode;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (updatedNode) {
|
|
482
|
+
cookingUpdates.push(updatedNode);
|
|
483
|
+
}
|
|
484
|
+
} catch (e) {
|
|
485
|
+
console.warn(`Failed to cook node ${liveNode.id}`, e);
|
|
486
|
+
logDebug(`Failed to cook node ${liveNode.id}: ${e}`);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (cookingUpdates.length > 0) {
|
|
491
|
+
ctx.modifyNodes(cookingUpdates, {
|
|
492
|
+
notify: false,
|
|
493
|
+
recordHistory: false,
|
|
494
|
+
});
|
|
495
|
+
logDebug(`Cooked ${cookingUpdates.length} nodes successfully.`);
|
|
496
|
+
} else {
|
|
497
|
+
logDebug('No nodes needed cooking.');
|
|
498
|
+
}
|
|
499
|
+
setIsIndeterminateStage(false);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
setStage('PROCESSING_STYLES');
|
|
503
|
+
setIsIndeterminateStage(true);
|
|
504
|
+
const baseFinalizationProgress =
|
|
505
|
+
PROGRESS_PHASES.PREPARATION +
|
|
506
|
+
PROGRESS_PHASES.UPLOADS +
|
|
507
|
+
PROGRESS_PHASES.PROCESSING;
|
|
508
|
+
setProgress(baseFinalizationProgress + PROGRESS_PHASES.FINALIZATION / 2);
|
|
509
|
+
logDebug(`Processing styles... gathering dirty classes.`);
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const { dirtyPaneIds, classes: dirtyClasses } =
|
|
513
|
+
ctx.getDirtyNodesClassData();
|
|
514
|
+
logDebug(
|
|
515
|
+
`Found ${dirtyClasses.length} distinct classes across ${dirtyPaneIds.length} panes.`
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const astroEndpoint = `/api/tailwind`;
|
|
519
|
+
const astroPayload = { dirtyPaneIds, dirtyClasses };
|
|
520
|
+
logDebug(
|
|
521
|
+
`POST ${astroEndpoint} - payload size: ${JSON.stringify(astroPayload).length}`
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const astroResponse = await fetch(astroEndpoint, {
|
|
525
|
+
method: 'POST',
|
|
526
|
+
headers: {
|
|
527
|
+
'Content-Type': 'application/json',
|
|
528
|
+
'X-Tenant-ID': tenantId,
|
|
529
|
+
},
|
|
530
|
+
credentials: 'include',
|
|
531
|
+
body: JSON.stringify(astroPayload),
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
if (!astroResponse.ok) {
|
|
535
|
+
throw new Error(`CSS generation failed! status: ${astroResponse.status}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const astroResult = await astroResponse.json();
|
|
539
|
+
|
|
540
|
+
if (!astroResult.success || !astroResult.generatedCss) {
|
|
541
|
+
throw new Error('CSS generation failed: no CSS returned');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
logDebug(`CSS generated: ${astroResult.generatedCss.length} bytes.`);
|
|
545
|
+
|
|
546
|
+
const goEndpoint = `${backendUrl}/api/v1/tailwind/update`;
|
|
547
|
+
const goPayload = { frontendCss: astroResult.generatedCss };
|
|
548
|
+
logDebug(`POST ${goEndpoint} - saving frontend CSS`);
|
|
549
|
+
|
|
550
|
+
const goResponse = await fetch(goEndpoint, {
|
|
551
|
+
method: 'POST',
|
|
552
|
+
headers: {
|
|
553
|
+
'Content-Type': 'application/json',
|
|
554
|
+
'X-Tenant-ID': tenantId,
|
|
555
|
+
},
|
|
556
|
+
credentials: 'include',
|
|
557
|
+
body: JSON.stringify(goPayload),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (!goResponse.ok) {
|
|
561
|
+
throw new Error(`CSS save failed! status: ${goResponse.status}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const goResult = await goResponse.json();
|
|
565
|
+
logDebug(`CSS saved successfully: stylesVer ${goResult.stylesVer}`);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
568
|
+
logDebug(`Styles processing failed: ${errorMsg}`);
|
|
569
|
+
throw new Error(`Failed to process styles: ${errorMsg}`);
|
|
570
|
+
} finally {
|
|
571
|
+
setIsIndeterminateStage(false);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (dirtyPanes.length > 0) {
|
|
575
|
+
setStage('SAVING_PANES');
|
|
576
|
+
setIsIndeterminateStage(true);
|
|
577
|
+
setStageProgress({
|
|
578
|
+
currentStep: 0,
|
|
579
|
+
totalSteps: dirtyPanes.length,
|
|
580
|
+
});
|
|
581
|
+
logDebug(`Preparing to save ${dirtyPanes.length} panes...`);
|
|
582
|
+
|
|
583
|
+
const bulkPayload = dirtyPanes.map((paneNode) =>
|
|
584
|
+
transformLivePaneForSave(ctx, paneNode.id, isContext)
|
|
585
|
+
);
|
|
586
|
+
logDebug(`Bulk payload constructed. Count: ${bulkPayload.length}`);
|
|
587
|
+
|
|
588
|
+
bulkPayload.forEach((payload) => {
|
|
589
|
+
payload.optionsPayload.nodes.forEach((transformedNode) => {
|
|
590
|
+
const liveNode = ctx.allNodes.get().get(transformedNode.id);
|
|
591
|
+
if (!liveNode) return;
|
|
592
|
+
|
|
593
|
+
let needsUpdate = false;
|
|
594
|
+
let updatedNode: BaseNode = { ...liveNode };
|
|
595
|
+
|
|
596
|
+
if (
|
|
597
|
+
transformedNode.nodeType === 'TagElement' &&
|
|
598
|
+
transformedNode.elementCss
|
|
599
|
+
) {
|
|
600
|
+
const flatNode = liveNode as FlatNode;
|
|
601
|
+
if (flatNode.elementCss !== transformedNode.elementCss) {
|
|
602
|
+
(updatedNode as FlatNode).elementCss = transformedNode.elementCss;
|
|
603
|
+
needsUpdate = true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (
|
|
608
|
+
transformedNode.nodeType === 'Markdown' &&
|
|
609
|
+
transformedNode.parentCss
|
|
610
|
+
) {
|
|
611
|
+
const markdownNode = liveNode as MarkdownPaneFragmentNode;
|
|
612
|
+
const currentParentCss = markdownNode.parentCss;
|
|
613
|
+
const newParentCss = transformedNode.parentCss as string[];
|
|
614
|
+
|
|
615
|
+
const isDifferent =
|
|
616
|
+
!currentParentCss ||
|
|
617
|
+
currentParentCss.length !== newParentCss.length ||
|
|
618
|
+
currentParentCss.some((css, index) => css !== newParentCss[index]);
|
|
619
|
+
|
|
620
|
+
if (isDifferent) {
|
|
621
|
+
(updatedNode as MarkdownPaneFragmentNode).parentCss = newParentCss;
|
|
622
|
+
needsUpdate = true;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (needsUpdate) {
|
|
627
|
+
ctx.allNodes.get().set(transformedNode.id, updatedNode);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const endpoint = `${backendUrl}/api/v1/nodes/panes/bulk`;
|
|
633
|
+
logDebug(`Processing ${dirtyPanes.length} panes via -> POST ${endpoint}`);
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
const response = await fetch(endpoint, {
|
|
637
|
+
method: 'POST',
|
|
638
|
+
headers: {
|
|
639
|
+
'Content-Type': 'application/json',
|
|
640
|
+
'X-Tenant-ID': tenantId,
|
|
641
|
+
},
|
|
642
|
+
credentials: 'include',
|
|
643
|
+
body: JSON.stringify({ panes: bulkPayload }),
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
if (!response.ok) {
|
|
647
|
+
const errorText = await response.text();
|
|
648
|
+
throw new Error(
|
|
649
|
+
`HTTP error! status: ${response.status} - ${errorText}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
await response.json();
|
|
654
|
+
logDebug(
|
|
655
|
+
`${dirtyPanes.length} panes saved successfully via bulk endpoint.`
|
|
656
|
+
);
|
|
657
|
+
} catch (bulkError) {
|
|
658
|
+
const errorMsg =
|
|
659
|
+
bulkError instanceof Error
|
|
660
|
+
? bulkError.message
|
|
661
|
+
: 'Unknown bulk save error';
|
|
662
|
+
logDebug(`Bulk pane save failed: ${errorMsg}`);
|
|
663
|
+
throw new Error(`Failed to save panes in bulk: ${errorMsg}`);
|
|
664
|
+
} finally {
|
|
665
|
+
setIsIndeterminateStage(false);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
setStageProgress({
|
|
669
|
+
currentStep: dirtyPanes.length,
|
|
670
|
+
totalSteps: dirtyPanes.length,
|
|
671
|
+
});
|
|
672
|
+
completedProcessingSteps += dirtyPanes.length;
|
|
673
|
+
const processingProgress =
|
|
674
|
+
(completedProcessingSteps / totalProcessingSteps) *
|
|
675
|
+
PROGRESS_PHASES.PROCESSING;
|
|
676
|
+
setProgress(
|
|
677
|
+
PROGRESS_PHASES.PREPARATION + PROGRESS_PHASES.UPLOADS + processingProgress
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!isContext && dirtyStoryFragments.length > 0) {
|
|
682
|
+
setStage('SAVING_STORY_FRAGMENTS');
|
|
683
|
+
setStageProgress({
|
|
684
|
+
currentStep: 0,
|
|
685
|
+
totalSteps: dirtyStoryFragments.length,
|
|
686
|
+
});
|
|
687
|
+
logDebug(`Saving ${dirtyStoryFragments.length} story fragments...`);
|
|
688
|
+
|
|
689
|
+
for (let i = 0; i < dirtyStoryFragments.length; i++) {
|
|
690
|
+
const fragment = dirtyStoryFragments[i];
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
const payload = await transformStoryFragmentForSave(
|
|
694
|
+
ctx,
|
|
695
|
+
fragment.id,
|
|
696
|
+
tenantId
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
if (uploadedOGPaths[fragment.id]) {
|
|
700
|
+
payload.socialImagePath = uploadedOGPaths[fragment.id];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const endpoint = isCreateMode
|
|
704
|
+
? `${backendUrl}/api/v1/nodes/storyfragments/create`
|
|
705
|
+
: `${backendUrl}/api/v1/nodes/storyfragments/${payload.id}/complete`;
|
|
706
|
+
const method = isCreateMode ? 'POST' : 'PUT';
|
|
707
|
+
|
|
708
|
+
logDebug(
|
|
709
|
+
`Processing story fragment ${i + 1}/${dirtyStoryFragments.length}: ${fragment.id} -> ${method} ${endpoint}`
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
const response = await fetch(endpoint, {
|
|
713
|
+
method,
|
|
714
|
+
headers: {
|
|
715
|
+
'Content-Type': 'application/json',
|
|
716
|
+
'X-Tenant-ID': tenantId,
|
|
717
|
+
},
|
|
718
|
+
credentials: 'include',
|
|
719
|
+
body: JSON.stringify(payload),
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
await response.json();
|
|
727
|
+
logDebug(`StoryFragment ${fragment.id} saved successfully`);
|
|
728
|
+
|
|
729
|
+
if (uploadedOGPaths[fragment.id]) {
|
|
730
|
+
clearPendingImageOperation(fragment.id);
|
|
731
|
+
logDebug(`Cleared pending image operation for ${fragment.id}`);
|
|
732
|
+
}
|
|
733
|
+
} catch (etlError) {
|
|
734
|
+
const errorMsg =
|
|
735
|
+
etlError instanceof Error ? etlError.message : 'Unknown error';
|
|
736
|
+
logDebug(`StoryFragment ${fragment.id} ETL failed: ${errorMsg}`);
|
|
737
|
+
throw new Error(
|
|
738
|
+
`Failed to save story fragment ${fragment.id}: ${errorMsg}`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
setStageProgress((prev: SaveStageProgress) => ({
|
|
743
|
+
...prev,
|
|
744
|
+
currentStep: i + 1,
|
|
745
|
+
}));
|
|
746
|
+
completedProcessingSteps++;
|
|
747
|
+
const processingProgress =
|
|
748
|
+
(completedProcessingSteps / totalProcessingSteps) *
|
|
749
|
+
PROGRESS_PHASES.PROCESSING;
|
|
750
|
+
setProgress(
|
|
751
|
+
PROGRESS_PHASES.PREPARATION +
|
|
752
|
+
PROGRESS_PHASES.UPLOADS +
|
|
753
|
+
processingProgress
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (dirtyPanes.length > 0) {
|
|
759
|
+
setStage('LINKING_FILES');
|
|
760
|
+
setIsIndeterminateStage(true);
|
|
761
|
+
setProgress(baseFinalizationProgress);
|
|
762
|
+
logDebug('Starting file-pane relationship linking...');
|
|
763
|
+
|
|
764
|
+
const relationships = [];
|
|
765
|
+
for (const paneNode of dirtyPanes) {
|
|
766
|
+
const fileIds = ctx.getPaneImageFileIds(paneNode.id);
|
|
767
|
+
if (fileIds.length > 0) {
|
|
768
|
+
logDebug(`Pane ${paneNode.id} has files: ${fileIds.join(', ')}`);
|
|
769
|
+
}
|
|
770
|
+
relationships.push({
|
|
771
|
+
paneId: paneNode.id,
|
|
772
|
+
fileIds: fileIds,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (relationships.some((rel) => rel.fileIds.length > 0)) {
|
|
777
|
+
try {
|
|
778
|
+
const bulkEndpoint = `${backendUrl}/api/v1/nodes/panes/files/bulk`;
|
|
779
|
+
const activeRels = relationships.filter((r) => r.fileIds.length > 0);
|
|
780
|
+
logDebug(`Linking relationships: ${JSON.stringify(activeRels)}`);
|
|
781
|
+
|
|
782
|
+
const response = await fetch(bulkEndpoint, {
|
|
783
|
+
method: 'POST',
|
|
784
|
+
headers: {
|
|
785
|
+
'Content-Type': 'application/json',
|
|
786
|
+
'X-Tenant-ID': tenantId,
|
|
787
|
+
},
|
|
788
|
+
credentials: 'include',
|
|
789
|
+
body: JSON.stringify({ relationships }),
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
if (!response.ok) {
|
|
793
|
+
const txt = await response.text();
|
|
794
|
+
throw new Error(`HTTP error! status: ${response.status} - ${txt}`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const result = await response.json();
|
|
798
|
+
logDebug(
|
|
799
|
+
`File-pane relationships linked successfully: ${result.message}`
|
|
800
|
+
);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
const errorMsg =
|
|
803
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
804
|
+
logDebug(`Failed to link file-pane relationships: ${errorMsg}`);
|
|
805
|
+
throw new Error(`Failed to link file-pane relationships: ${errorMsg}`);
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
logDebug('No file relationships to link');
|
|
809
|
+
}
|
|
810
|
+
setIsIndeterminateStage(false);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (pendingHomePageSlug) {
|
|
814
|
+
setStage('UPDATING_HOME_PAGE');
|
|
815
|
+
setIsIndeterminateStage(true);
|
|
816
|
+
setProgress(baseFinalizationProgress + (PROGRESS_PHASES.FINALIZATION - 2));
|
|
817
|
+
logDebug(`Updating home page to: ${pendingHomePageSlug}`);
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const response = await fetch(`${backendUrl}/api/v1/config/brand`, {
|
|
821
|
+
method: 'GET',
|
|
822
|
+
headers: {
|
|
823
|
+
'Content-Type': 'application/json',
|
|
824
|
+
'X-Tenant-ID': tenantId,
|
|
825
|
+
},
|
|
826
|
+
credentials: 'include',
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`Failed to get current brand config: ${response.status}`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const currentBrandConfig = await response.json();
|
|
836
|
+
|
|
837
|
+
const updatedBrandConfig = {
|
|
838
|
+
...currentBrandConfig,
|
|
839
|
+
HOME_SLUG: pendingHomePageSlug,
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const updateResponse = await fetch(`${backendUrl}/api/v1/config/brand`, {
|
|
843
|
+
method: 'PUT',
|
|
844
|
+
headers: {
|
|
845
|
+
'Content-Type': 'application/json',
|
|
846
|
+
'X-Tenant-ID': tenantId,
|
|
847
|
+
},
|
|
848
|
+
credentials: 'include',
|
|
849
|
+
body: JSON.stringify(updatedBrandConfig),
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
if (!updateResponse.ok) {
|
|
853
|
+
throw new Error(`Failed to update home page: ${updateResponse.status}`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
pendingHomePageSlugStore.set(null);
|
|
857
|
+
logDebug('Home page updated successfully');
|
|
858
|
+
} catch (error) {
|
|
859
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
860
|
+
logDebug(`Home page update failed: ${errorMsg}`);
|
|
861
|
+
throw new Error(`Failed to update home page: ${errorMsg}`);
|
|
862
|
+
} finally {
|
|
863
|
+
setIsIndeterminateStage(false);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (hydrate) {
|
|
868
|
+
logDebug('Finalizing setup (Kill Switch)...');
|
|
869
|
+
try {
|
|
870
|
+
const response = await fetch(`${backendUrl}/api/v1/setup/complete`, {
|
|
871
|
+
method: 'POST',
|
|
872
|
+
headers: {
|
|
873
|
+
'Content-Type': 'application/json',
|
|
874
|
+
'X-Tenant-ID': tenantId,
|
|
875
|
+
},
|
|
876
|
+
credentials: 'include',
|
|
877
|
+
body: JSON.stringify({}),
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
if (!response.ok) {
|
|
881
|
+
throw new Error(`Kill Switch failed: ${response.status}`);
|
|
882
|
+
}
|
|
883
|
+
logDebug('Hydration token cleared.');
|
|
884
|
+
} catch (e) {
|
|
885
|
+
console.error('Kill switch error:', e);
|
|
886
|
+
logDebug('Warning: Failed to clear hydration token.');
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
setStage('COMPLETED');
|
|
891
|
+
setProgress(100);
|
|
892
|
+
logDebug('Save process completed successfully!');
|
|
893
|
+
}
|