astro-tractstack 2.0.41 → 2.0.42
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 +8 -2
- package/package.json +1 -1
- package/templates/src/components/Header.astro +1 -0
- package/templates/src/components/compositor/Node.tsx +4 -1
- package/templates/src/components/compositor/preview/PanesPreviewGenerator.tsx +5 -1
- package/templates/src/components/edit/SettingsPanel.tsx +1 -3
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -10
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +6 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +4 -3
- package/templates/src/components/edit/panels/StyleParentPanel.tsx +0 -2
- package/templates/src/components/edit/state/SaveModal.tsx +250 -79
- package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +27 -16
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +5 -7
- package/templates/src/components/edit/widgets/BeliefWidget.tsx +4 -1
- package/templates/src/components/edit/widgets/IdentifyAsWidget.tsx +5 -1
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +5 -1
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +4 -1
- package/templates/src/components/fields/ImageUpload.tsx +4 -1
- package/templates/src/components/form/ActionBuilderField.tsx +5 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +4 -2
- package/templates/src/components/storykeep/state/BrandingWrapper.tsx +13 -1
- package/templates/src/components/storykeep/widgets/HydrateWizard.tsx +84 -0
- package/templates/src/components/storykeep/widgets/{SetupWizard.tsx → InitWizard.tsx} +4 -3
- package/templates/src/components/widgets/Impression.tsx +3 -1
- package/templates/src/hooks/useSearch.ts +5 -3
- package/templates/src/layouts/Layout.astro +1 -23
- package/templates/src/pages/[...slug]/edit.astro +0 -1
- package/templates/src/pages/api/auth/decode.ts +2 -4
- package/templates/src/pages/api/auth/login.ts +4 -5
- package/templates/src/pages/api/auth/logout.ts +22 -7
- package/templates/src/pages/api/auth/profile.ts +4 -2
- package/templates/src/pages/api/sandbox.ts +3 -5
- package/templates/src/pages/api/tailwind.ts +6 -9
- package/templates/src/pages/storykeep/branding.astro +18 -1
- package/templates/src/pages/storykeep/init.astro +2 -2
- package/templates/src/stores/analytics.ts +5 -14
- package/templates/src/stores/nodes.ts +1 -6
- package/templates/src/stores/orphanAnalysis.ts +5 -40
- package/templates/src/types/compositorTypes.ts +1 -1
- package/templates/src/types/tractstack.ts +2 -0
- package/templates/src/utils/actions/actionButton.ts +3 -1
- package/templates/src/utils/api/brandHelpers.ts +1 -0
- package/templates/src/utils/api/setupHelpers.ts +177 -20
- package/templates/src/utils/api.ts +14 -26
- package/templates/src/utils/compositor/nodesHelper.ts +5 -1
- package/templates/src/utils/tenantResolver.ts +1 -1
- package/utils/inject-files.ts +8 -2
|
@@ -6,6 +6,7 @@ import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
|
|
|
6
6
|
import { getFullContentMap } from '@/stores/analytics';
|
|
7
7
|
import { getBrandConfig } from '@/utils/api/brandConfig';
|
|
8
8
|
import { preHealthCheck } from '@/utils/backend';
|
|
9
|
+
import type { LoadData } from '@/types/compositorTypes';
|
|
9
10
|
|
|
10
11
|
const tenantId =
|
|
11
12
|
Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
@@ -23,10 +24,25 @@ if (authCheck) {
|
|
|
23
24
|
const userIsAuthenticated = isAuthenticated(Astro);
|
|
24
25
|
const userIsAdmin = isAdmin(Astro);
|
|
25
26
|
const role = userIsAdmin ? `admin` : userIsAuthenticated ? `editor` : null;
|
|
26
|
-
|
|
27
27
|
const brandConfig = await getBrandConfig(tenantId);
|
|
28
28
|
const initializing = !brandConfig.SITE_INIT;
|
|
29
29
|
|
|
30
|
+
let initialSuitcase: LoadData | null = null;
|
|
31
|
+
if (initializing) {
|
|
32
|
+
const goBackend =
|
|
33
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`${goBackend}/api/v1/setup/suitcase`, {
|
|
36
|
+
headers: { 'X-Tenant-Id': tenantId },
|
|
37
|
+
});
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
initialSuitcase = await response.json();
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.warn('[Branding] Failed to probe suitcase:', e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
const title = 'Branding | StoryKeep';
|
|
31
47
|
|
|
32
48
|
let fullContentMap;
|
|
@@ -52,6 +68,7 @@ try {
|
|
|
52
68
|
role={role}
|
|
53
69
|
initializing={initializing}
|
|
54
70
|
initialBrandConfig={brandConfig}
|
|
71
|
+
initialSuitcase={initialSuitcase}
|
|
55
72
|
/>
|
|
56
73
|
</div>
|
|
57
74
|
</main>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { freshInstallStore } from '@/stores/backend';
|
|
3
3
|
import { preHealthCheck } from '@/utils/backend';
|
|
4
|
-
import
|
|
4
|
+
import InitWizard from '@/components/storykeep/widgets/InitWizard';
|
|
5
5
|
|
|
6
6
|
const isMultiTenant = import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
|
|
7
7
|
|
|
@@ -41,7 +41,7 @@ const mainStylesUrl = isDev
|
|
|
41
41
|
</div>
|
|
42
42
|
) : (
|
|
43
43
|
<div class="max-w-5xl p-3.5 md:p-8">
|
|
44
|
-
<
|
|
44
|
+
<InitWizard client:load />
|
|
45
45
|
</div>
|
|
46
46
|
)
|
|
47
47
|
}
|
|
@@ -11,7 +11,6 @@ export interface AppliedFilter {
|
|
|
11
11
|
value: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// Internal tenant-keyed storage
|
|
15
14
|
const tenantEpinetCustomFilters = atom<
|
|
16
15
|
Record<
|
|
17
16
|
string,
|
|
@@ -50,13 +49,13 @@ const tenantFullContentMaps = atom<
|
|
|
50
49
|
|
|
51
50
|
// Helper to get current tenant ID
|
|
52
51
|
function getCurrentTenantId(): string {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
const resolvedTenantId =
|
|
53
|
+
(typeof window !== 'undefined' && window.TRACTSTACK_CONFIG?.tenantId) ||
|
|
54
|
+
import.meta.env.PUBLIC_TENANTID ||
|
|
55
|
+
'default';
|
|
56
|
+
return resolvedTenantId;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// Default filter state
|
|
60
59
|
const defaultEpinetFilters = {
|
|
61
60
|
enabled: false,
|
|
62
61
|
visitorType: 'all' as 'all' | 'anonymous' | 'known',
|
|
@@ -69,7 +68,6 @@ const defaultEpinetFilters = {
|
|
|
69
68
|
appliedFilters: [],
|
|
70
69
|
};
|
|
71
70
|
|
|
72
|
-
// Create tenant-aware atoms that work with useStore
|
|
73
71
|
const createEpinetFiltersStore = () => {
|
|
74
72
|
const store = {
|
|
75
73
|
get: () => {
|
|
@@ -95,8 +93,6 @@ const createEpinetFiltersStore = () => {
|
|
|
95
93
|
callback(filters[tenantId] || defaultEpinetFilters);
|
|
96
94
|
});
|
|
97
95
|
},
|
|
98
|
-
|
|
99
|
-
// Required nanostore properties for useStore
|
|
100
96
|
lc: 0,
|
|
101
97
|
listen: function (callback: any) {
|
|
102
98
|
return this.subscribe(callback);
|
|
@@ -131,8 +127,6 @@ const createFullContentMapStore = () => {
|
|
|
131
127
|
callback(maps[tenantId] || null);
|
|
132
128
|
});
|
|
133
129
|
},
|
|
134
|
-
|
|
135
|
-
// Required nanostore properties for useStore
|
|
136
130
|
lc: 0,
|
|
137
131
|
listen: function (callback: any) {
|
|
138
132
|
return this.subscribe(callback);
|
|
@@ -152,15 +146,12 @@ export const fullContentMapStore = createFullContentMapStore();
|
|
|
152
146
|
|
|
153
147
|
export async function getFullContentMap(tenantId: string): Promise<any[]> {
|
|
154
148
|
const api = new TractStackAPI(tenantId);
|
|
155
|
-
|
|
156
|
-
// Check tenant-specific cache
|
|
157
149
|
const cached = tenantFullContentMaps.get()[tenantId];
|
|
158
150
|
|
|
159
151
|
try {
|
|
160
152
|
const response = await api.getContentMapWithTimestamp(cached?.lastUpdated);
|
|
161
153
|
|
|
162
154
|
if (response.success && response.data) {
|
|
163
|
-
// Update tenant-specific cache
|
|
164
155
|
const newData = {
|
|
165
156
|
data: response.data.data,
|
|
166
157
|
lastUpdated: response.data.lastUpdated,
|
|
@@ -609,7 +609,7 @@ export class NodesContext {
|
|
|
609
609
|
return { left: originalNode, right: null };
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
// Handle split at the beginning of the string
|
|
612
|
+
// Handle split at the beginning of the string
|
|
613
613
|
if (offset === 0) {
|
|
614
614
|
if (VERBOSE)
|
|
615
615
|
console.log(
|
|
@@ -1992,8 +1992,6 @@ export class NodesContext {
|
|
|
1992
1992
|
|
|
1993
1993
|
let autoCreatedMarkdownNode: MarkdownPaneFragmentNode | null = null;
|
|
1994
1994
|
|
|
1995
|
-
//console.log(`--- [TRAP - TEMPLATE BEFORE] ---`, cloneDeep(node));
|
|
1996
|
-
// 3. HANDLE EMPTY PANE BY AUTO-CREATING A MARKDOWN NODE
|
|
1997
1995
|
if (targetNode.nodeType === 'Pane') {
|
|
1998
1996
|
// Create a minimal markdown node to act as the container
|
|
1999
1997
|
const newMarkdownNode: MarkdownPaneFragmentNode = {
|
|
@@ -2070,8 +2068,6 @@ export class NodesContext {
|
|
|
2070
2068
|
);
|
|
2071
2069
|
}
|
|
2072
2070
|
|
|
2073
|
-
//console.log(`--- [TRAP - FLATTENED AFTER] ---`, cloneDeep(flattenedNodes));
|
|
2074
|
-
|
|
2075
2071
|
// 5. PERFORM REMAINING STATE MUTATIONS
|
|
2076
2072
|
if (originalPaneNode) {
|
|
2077
2073
|
this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
|
|
@@ -2838,7 +2834,6 @@ export class NodesContext {
|
|
|
2838
2834
|
|
|
2839
2835
|
getDirtyNodesClassData(): { dirtyPaneIds: string[]; classes: string[] } {
|
|
2840
2836
|
const dirtyNodes = this.getDirtyNodes();
|
|
2841
|
-
|
|
2842
2837
|
const dirtyPaneIds = dirtyNodes
|
|
2843
2838
|
.filter((node) => node.nodeType === 'Pane')
|
|
2844
2839
|
.map((node) => node.id);
|
|
@@ -24,10 +24,11 @@ const tenantOrphanAnalysis = atom<Record<string, OrphanAnalysisState>>({});
|
|
|
24
24
|
|
|
25
25
|
// Helper to get current tenant ID
|
|
26
26
|
function getCurrentTenantId(): string {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const resolvedTenantId =
|
|
28
|
+
(typeof window !== 'undefined' && window.TRACTSTACK_CONFIG?.tenantId) ||
|
|
29
|
+
import.meta.env.PUBLIC_TENANTID ||
|
|
30
|
+
'default';
|
|
31
|
+
return resolvedTenantId;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// Default state
|
|
@@ -38,7 +39,6 @@ const defaultOrphanState: OrphanAnalysisState = {
|
|
|
38
39
|
lastFetched: null,
|
|
39
40
|
};
|
|
40
41
|
|
|
41
|
-
// Create tenant-aware store that works with useStore
|
|
42
42
|
const createOrphanAnalysisStore = () => {
|
|
43
43
|
const store = {
|
|
44
44
|
get: () => {
|
|
@@ -52,8 +52,6 @@ const createOrphanAnalysisStore = () => {
|
|
|
52
52
|
callback(analysis[tenantId] || defaultOrphanState);
|
|
53
53
|
});
|
|
54
54
|
},
|
|
55
|
-
|
|
56
|
-
// Required nanostore properties for useStore
|
|
57
55
|
lc: 0,
|
|
58
56
|
listen: function (callback: any) {
|
|
59
57
|
return this.subscribe(callback);
|
|
@@ -71,7 +69,6 @@ const createOrphanAnalysisStore = () => {
|
|
|
71
69
|
|
|
72
70
|
export const orphanAnalysisStore = createOrphanAnalysisStore();
|
|
73
71
|
|
|
74
|
-
// Helper to update state for specific tenant
|
|
75
72
|
function updateTenantState(
|
|
76
73
|
tenantId: string,
|
|
77
74
|
updates: Partial<OrphanAnalysisState>
|
|
@@ -88,7 +85,6 @@ function updateTenantState(
|
|
|
88
85
|
});
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
// Helper function to count orphans from the analysis data
|
|
92
88
|
export function countOrphans(data: OrphanAnalysisData | null): number {
|
|
93
89
|
if (!data) return 0;
|
|
94
90
|
|
|
@@ -374,34 +370,3 @@ function stopPolling(tenantId: string): void {
|
|
|
374
370
|
// Clean up polling state
|
|
375
371
|
pollingState.delete(tenantId);
|
|
376
372
|
}
|
|
377
|
-
|
|
378
|
-
export function clearOrphanAnalysis(): void {
|
|
379
|
-
const tenantId = getCurrentTenantId();
|
|
380
|
-
stopPolling(tenantId);
|
|
381
|
-
fetchingStates.set(tenantId, false);
|
|
382
|
-
|
|
383
|
-
updateTenantState(tenantId, defaultOrphanState);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Enhanced utility function to get polling status for debugging
|
|
387
|
-
export function getPollingStatus(tenantId?: string): Record<string, any> {
|
|
388
|
-
const targetTenantId = tenantId || getCurrentTenantId();
|
|
389
|
-
const state = pollingState.get(targetTenantId);
|
|
390
|
-
const isActive = pollingIntervals.has(targetTenantId);
|
|
391
|
-
|
|
392
|
-
if (!state && !isActive) {
|
|
393
|
-
return { status: 'inactive', tenantId: targetTenantId };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
status: isActive ? 'active' : 'stopped',
|
|
398
|
-
tenantId: targetTenantId,
|
|
399
|
-
attempts: state?.attempts || 0,
|
|
400
|
-
maxAttempts: MAX_POLLING_ATTEMPTS,
|
|
401
|
-
consecutiveErrors: state?.consecutiveErrors || 0,
|
|
402
|
-
startTime: state?.startTime,
|
|
403
|
-
lastAttemptTime: state?.lastAttemptTime,
|
|
404
|
-
elapsed: state?.startTime ? Date.now() - state.startTime : 0,
|
|
405
|
-
maxDuration: MAX_POLLING_DURATION,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
@@ -239,7 +239,7 @@ export interface PaneNode extends BaseNode {
|
|
|
239
239
|
export interface StoryFragmentNode extends BaseNode {
|
|
240
240
|
title: string;
|
|
241
241
|
slug: string;
|
|
242
|
-
tractStackId
|
|
242
|
+
tractStackId: string;
|
|
243
243
|
paneIds: string[];
|
|
244
244
|
menuId?: string;
|
|
245
245
|
tailwindBgColour?: string;
|
|
@@ -184,6 +184,7 @@ export interface BrandConfig {
|
|
|
184
184
|
KNOWN_RESOURCES?: KnownResourcesConfig;
|
|
185
185
|
DESIGN_LIBRARY?: DesignLibraryConfig;
|
|
186
186
|
HAS_AAI?: boolean;
|
|
187
|
+
HAS_HYDRATION_TOKEN?: boolean;
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
export interface BrandConfigState {
|
|
@@ -217,6 +218,7 @@ export interface BrandConfigState {
|
|
|
217
218
|
knownResources: KnownResourcesConfig;
|
|
218
219
|
designLibrary?: DesignLibraryConfig;
|
|
219
220
|
hasAAI: boolean;
|
|
221
|
+
hasHydrationToken: boolean;
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
// Form validation types
|
|
@@ -18,6 +18,8 @@ async function sendAnalyticsEvent(event: {
|
|
|
18
18
|
try {
|
|
19
19
|
const config = window.TRACTSTACK_CONFIG;
|
|
20
20
|
if (!config || !config.sessionId) return;
|
|
21
|
+
const backendUrl =
|
|
22
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
21
23
|
|
|
22
24
|
const sessionId = config.sessionId;
|
|
23
25
|
const formData: { [key: string]: string } = {
|
|
@@ -31,7 +33,7 @@ async function sendAnalyticsEvent(event: {
|
|
|
31
33
|
formData.duration = event.duration.toString();
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
await fetch(`${
|
|
36
|
+
await fetch(`${backendUrl}/api/v1/state`, {
|
|
35
37
|
method: 'POST',
|
|
36
38
|
headers: {
|
|
37
39
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -40,6 +40,7 @@ export function convertToLocalState(
|
|
|
40
40
|
knownResources: brandConfig.KNOWN_RESOURCES ?? {},
|
|
41
41
|
designLibrary: brandConfig.DESIGN_LIBRARY ?? undefined,
|
|
42
42
|
hasAAI: brandConfig.HAS_AAI ?? false,
|
|
43
|
+
hasHydrationToken: brandConfig.HAS_HYDRATION_TOKEN ?? false,
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { getCtx } from '@/stores/nodes';
|
|
1
2
|
import { TractStackAPI } from '@/utils/api';
|
|
3
|
+
import { pendingHomePageSlugStore } from '@/stores/storykeep';
|
|
4
|
+
import type { FullContentMapItem } from '@/types/tractstack';
|
|
5
|
+
import type { LoadData, TractStackNode } from '@/types/compositorTypes';
|
|
6
|
+
|
|
7
|
+
const VERBOSE = false;
|
|
2
8
|
|
|
3
9
|
export interface SetupWizardState {
|
|
4
10
|
email: string;
|
|
@@ -18,43 +24,57 @@ export const initialSetupState: SetupWizardState = {
|
|
|
18
24
|
tursoAuthToken: '',
|
|
19
25
|
};
|
|
20
26
|
|
|
21
|
-
/**
|
|
22
|
-
* State interceptor (Preserving existing UI patterns)
|
|
23
|
-
*/
|
|
24
27
|
export function setupStateIntercept(
|
|
25
28
|
newState: SetupWizardState,
|
|
26
29
|
field: keyof SetupWizardState,
|
|
27
30
|
value: any
|
|
28
31
|
): SetupWizardState {
|
|
29
|
-
|
|
32
|
+
if (VERBOSE)
|
|
33
|
+
console.log(
|
|
34
|
+
`[setupStateIntercept] Intercepting field: ${String(field)}, Value:`,
|
|
35
|
+
value
|
|
36
|
+
);
|
|
37
|
+
|
|
30
38
|
if (field === 'tursoEnabled' && !value) {
|
|
31
|
-
|
|
39
|
+
const result = {
|
|
32
40
|
...newState,
|
|
33
41
|
tursoEnabled: false,
|
|
34
42
|
tursoDatabaseURL: '',
|
|
35
43
|
tursoAuthToken: '',
|
|
36
44
|
};
|
|
45
|
+
if (VERBOSE)
|
|
46
|
+
console.log(
|
|
47
|
+
'[setupStateIntercept] Turso disabled. Resetting Turso fields.',
|
|
48
|
+
result
|
|
49
|
+
);
|
|
50
|
+
return result;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
|
-
// Pattern: Clear confirmation password when main password changes
|
|
40
53
|
if (field === 'adminPassword') {
|
|
41
|
-
|
|
54
|
+
const result = {
|
|
42
55
|
...newState,
|
|
43
56
|
adminPassword: value,
|
|
44
57
|
confirmPassword: '',
|
|
45
58
|
};
|
|
59
|
+
if (VERBOSE)
|
|
60
|
+
console.log(
|
|
61
|
+
'[setupStateIntercept] Admin password changed. Clearing confirm password.'
|
|
62
|
+
);
|
|
63
|
+
return result;
|
|
46
64
|
}
|
|
47
65
|
|
|
66
|
+
if (VERBOSE)
|
|
67
|
+
console.log(
|
|
68
|
+
'[setupStateIntercept] No special handling required. Returning new state.'
|
|
69
|
+
);
|
|
48
70
|
return newState;
|
|
49
71
|
}
|
|
50
72
|
|
|
51
|
-
/**
|
|
52
|
-
* Validation Logic (Preserving existing Regex and Rules)
|
|
53
|
-
*/
|
|
54
73
|
export function validateSetup(state: SetupWizardState): Record<string, string> {
|
|
74
|
+
if (VERBOSE)
|
|
75
|
+
console.log('[validateSetup] Starting validation for state:', state);
|
|
55
76
|
const errors: Record<string, string> = {};
|
|
56
77
|
|
|
57
|
-
// Email Validation pattern
|
|
58
78
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
59
79
|
if (!state.email.trim()) {
|
|
60
80
|
errors.email = 'Email is required';
|
|
@@ -62,19 +82,16 @@ export function validateSetup(state: SetupWizardState): Record<string, string> {
|
|
|
62
82
|
errors.email = 'Please enter a valid email address';
|
|
63
83
|
}
|
|
64
84
|
|
|
65
|
-
// Password Validation pattern
|
|
66
85
|
if (!state.adminPassword.trim()) {
|
|
67
86
|
errors.adminPassword = 'Admin password is required';
|
|
68
87
|
} else if (state.adminPassword.length < 8) {
|
|
69
88
|
errors.adminPassword = 'Admin password must be at least 8 characters long';
|
|
70
89
|
}
|
|
71
90
|
|
|
72
|
-
// Confirmation Pattern
|
|
73
91
|
if (!errors.adminPassword && state.adminPassword !== state.confirmPassword) {
|
|
74
92
|
errors.confirmPassword = 'Passwords do not match';
|
|
75
93
|
}
|
|
76
94
|
|
|
77
|
-
// Turso Validation Pattern
|
|
78
95
|
if (state.tursoEnabled) {
|
|
79
96
|
if (!state.tursoDatabaseURL.trim()) {
|
|
80
97
|
errors.tursoDatabaseURL = 'Turso Database URL is required';
|
|
@@ -88,14 +105,26 @@ export function validateSetup(state: SetupWizardState): Record<string, string> {
|
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
|
|
108
|
+
if (Object.keys(errors).length > 0) {
|
|
109
|
+
if (VERBOSE)
|
|
110
|
+
console.error('[validateSetup] Validation failed with errors:', errors);
|
|
111
|
+
} else {
|
|
112
|
+
if (VERBOSE) console.log('[validateSetup] Validation successful.');
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
return errors;
|
|
92
116
|
}
|
|
93
117
|
|
|
94
|
-
/**
|
|
95
|
-
* API Call (Preserving Payload Structure)
|
|
96
|
-
*/
|
|
97
118
|
export async function initializeSystem(state: SetupWizardState): Promise<void> {
|
|
98
|
-
|
|
119
|
+
if (VERBOSE)
|
|
120
|
+
console.log('[initializeSystem] Starting system initialization.');
|
|
121
|
+
|
|
122
|
+
const tenantId =
|
|
123
|
+
window.TRACTSTACK_CONFIG?.tenantId ||
|
|
124
|
+
import.meta.env.PUBLIC_TENANTID ||
|
|
125
|
+
'default';
|
|
126
|
+
|
|
127
|
+
const api = new TractStackAPI(tenantId);
|
|
99
128
|
|
|
100
129
|
const payload = {
|
|
101
130
|
adminEmail: state.email.trim(),
|
|
@@ -106,9 +135,137 @@ export async function initializeSystem(state: SetupWizardState): Promise<void> {
|
|
|
106
135
|
}),
|
|
107
136
|
};
|
|
108
137
|
|
|
109
|
-
const
|
|
138
|
+
const endpoint = '/api/v1/setup/initialize';
|
|
139
|
+
if (VERBOSE)
|
|
140
|
+
console.log(
|
|
141
|
+
`[initializeSystem] POSTing to ${endpoint}. Payload (passwords masked):`,
|
|
142
|
+
{
|
|
143
|
+
...payload,
|
|
144
|
+
adminPassword: '***',
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const response = await api.post(endpoint, payload);
|
|
149
|
+
if (VERBOSE)
|
|
150
|
+
console.log('[initializeSystem] API Response received:', response);
|
|
110
151
|
|
|
111
152
|
if (!response.success) {
|
|
112
|
-
|
|
153
|
+
const errorMessage = response.error || 'Setup failed';
|
|
154
|
+
if (VERBOSE)
|
|
155
|
+
console.error('[initializeSystem] Setup failed. Throwing error.');
|
|
156
|
+
throw new Error(errorMessage);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (VERBOSE)
|
|
160
|
+
console.log('[initializeSystem] System initialization successful.');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function forceMarkAllDirty(ctx: any) {
|
|
164
|
+
if (VERBOSE)
|
|
165
|
+
console.log('[forceMarkAllDirty] Flagging all nodes for SaveModal...');
|
|
166
|
+
const allNodes = Array.from(ctx.allNodes.get().values());
|
|
167
|
+
const dirtyUpdates = allNodes.map((n: any) => ({ ...n, isChanged: true }));
|
|
168
|
+
ctx.modifyNodes(dirtyUpdates);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function prepareHydrationContext(
|
|
172
|
+
data: LoadData,
|
|
173
|
+
fullContentMap?: FullContentMapItem[]
|
|
174
|
+
): void {
|
|
175
|
+
if (VERBOSE)
|
|
176
|
+
console.log(
|
|
177
|
+
'[prepareHydrationContext] Preparing context for suitcase hydration.'
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const ctx = getCtx();
|
|
181
|
+
|
|
182
|
+
if (!fullContentMap) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
'Content map is required for suitcase installation to perform strict takeover.'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const tractStackNodeItem = fullContentMap.find(
|
|
189
|
+
(n) => n.type === 'TractStack'
|
|
190
|
+
);
|
|
191
|
+
if (!tractStackNodeItem) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
'Missing TractStack node in content map. Cannot link content.'
|
|
194
|
+
);
|
|
113
195
|
}
|
|
196
|
+
|
|
197
|
+
const helloFragment = fullContentMap.find(
|
|
198
|
+
(n) =>
|
|
199
|
+
(n.type === 'StoryFragment' || n.type === 'StoryFragment') &&
|
|
200
|
+
n.slug === 'hello'
|
|
201
|
+
);
|
|
202
|
+
if (!helloFragment) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"Missing 'hello' StoryFragment in content map. Cannot perform takeover."
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!data.storyfragmentNodes || data.storyfragmentNodes.length !== 1) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
'Suitcase must contain exactly one StoryFragment for takeover hydration.'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const incomingFragment = data.storyfragmentNodes[0];
|
|
215
|
+
const oldId = incomingFragment.id;
|
|
216
|
+
const targetId = helloFragment.id;
|
|
217
|
+
|
|
218
|
+
if (VERBOSE)
|
|
219
|
+
console.log(
|
|
220
|
+
`[prepareHydrationContext] TAKEOVER: Mapping 'hello' (${targetId}) with content from '${incomingFragment.title}'`
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
incomingFragment.id = targetId;
|
|
224
|
+
incomingFragment.slug = 'hello';
|
|
225
|
+
incomingFragment.tractStackId = tractStackNodeItem.id;
|
|
226
|
+
incomingFragment.parentId = tractStackNodeItem.id;
|
|
227
|
+
|
|
228
|
+
if (!data.tractstackNodes) {
|
|
229
|
+
data.tractstackNodes = [];
|
|
230
|
+
}
|
|
231
|
+
const rootTractStack = {
|
|
232
|
+
id: tractStackNodeItem.id,
|
|
233
|
+
nodeType: 'TractStack' as const,
|
|
234
|
+
parentId: null,
|
|
235
|
+
title: tractStackNodeItem.title || 'Tract Stack',
|
|
236
|
+
slug: tractStackNodeItem.slug || 'root',
|
|
237
|
+
socialImagePath: '',
|
|
238
|
+
};
|
|
239
|
+
data.tractstackNodes.push(rootTractStack as TractStackNode);
|
|
240
|
+
|
|
241
|
+
if (data.paneNodes) {
|
|
242
|
+
let patchedPanes = 0;
|
|
243
|
+
data.paneNodes.forEach((pane) => {
|
|
244
|
+
if (pane.parentId === oldId) {
|
|
245
|
+
pane.parentId = targetId;
|
|
246
|
+
patchedPanes++;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
if (VERBOSE)
|
|
250
|
+
console.log(
|
|
251
|
+
`[prepareHydrationContext] Re-parented ${patchedPanes} panes from ${oldId} to ${targetId}`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (VERBOSE)
|
|
256
|
+
console.log(`[prepareHydrationContext] Building the Nodes Context tree`);
|
|
257
|
+
|
|
258
|
+
ctx.buildNodesTreeFromRowDataMadeNodes(data);
|
|
259
|
+
|
|
260
|
+
if (VERBOSE)
|
|
261
|
+
console.log(
|
|
262
|
+
`[prepareHydrationContext] Marking the Nodes Context tree as dirty`
|
|
263
|
+
);
|
|
264
|
+
forceMarkAllDirty(ctx);
|
|
265
|
+
|
|
266
|
+
if (VERBOSE)
|
|
267
|
+
console.log(
|
|
268
|
+
'[prepareHydrationContext] Setting pending home page slug to "hello"'
|
|
269
|
+
);
|
|
270
|
+
pendingHomePageSlugStore.set('hello');
|
|
114
271
|
}
|
|
@@ -20,28 +20,13 @@ export interface TractStackEvent {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function getConfig() {
|
|
23
|
-
if (typeof window === 'undefined') {
|
|
24
|
-
return {
|
|
25
|
-
goBackend: import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080',
|
|
26
|
-
tenantId: import.meta.env.PUBLIC_TENANTID || 'default',
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
23
|
return {
|
|
31
|
-
goBackend:
|
|
32
|
-
window.TRACTSTACK_CONFIG?.backendUrl ||
|
|
33
|
-
import.meta.env.PUBLIC_GO_BACKEND ||
|
|
34
|
-
'http://localhost:8080',
|
|
35
|
-
tenantId:
|
|
36
|
-
window.TRACTSTACK_CONFIG?.tenantId ||
|
|
37
|
-
import.meta.env.PUBLIC_TENANTID ||
|
|
38
|
-
'default',
|
|
24
|
+
goBackend: import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080',
|
|
39
25
|
};
|
|
40
26
|
}
|
|
41
27
|
|
|
42
28
|
export class TractStackAPI {
|
|
43
29
|
private explicitTenantId?: string;
|
|
44
|
-
|
|
45
30
|
constructor(tenantId?: string) {
|
|
46
31
|
this.explicitTenantId = tenantId;
|
|
47
32
|
}
|
|
@@ -51,7 +36,19 @@ export class TractStackAPI {
|
|
|
51
36
|
options: RequestInit = {}
|
|
52
37
|
): Promise<APIResponse<T>> {
|
|
53
38
|
const config = getConfig();
|
|
54
|
-
|
|
39
|
+
|
|
40
|
+
const effectiveTenantId = this.explicitTenantId;
|
|
41
|
+
|
|
42
|
+
if (!effectiveTenantId) {
|
|
43
|
+
console.error(
|
|
44
|
+
'[TractStackAPI] CRITICAL ERROR: Tenant ID is required but was not provided to the constructor. Failing request.'
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: 'Tenant ID missing. Must be provided in constructor.',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
55
52
|
const baseUrl = config.goBackend;
|
|
56
53
|
|
|
57
54
|
const url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
|
@@ -147,15 +144,6 @@ export class TractStackAPI {
|
|
|
147
144
|
return this.get(`/api/v1/fragments/${fragmentId}`);
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
getTenantId(): string {
|
|
151
|
-
const config = getConfig();
|
|
152
|
-
return this.explicitTenantId || config.tenantId;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
setTenantId(tenantId: string): void {
|
|
156
|
-
this.explicitTenantId = tenantId;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
147
|
async getContentMapWithTimestamp(
|
|
160
148
|
lastUpdated?: number
|
|
161
149
|
): Promise<APIResponse<{ data: any[]; lastUpdated: number }>> {
|
|
@@ -326,6 +326,7 @@ export function createEmptyStorykeep(id: string) {
|
|
|
326
326
|
return {
|
|
327
327
|
id,
|
|
328
328
|
nodeType: 'StoryFragment',
|
|
329
|
+
tractStackId: 'temp',
|
|
329
330
|
parentId: null,
|
|
330
331
|
isChanged: false,
|
|
331
332
|
paneIds: [],
|
|
@@ -503,13 +504,16 @@ export function extractClassesFromNodes(dirtyNodes: BaseNode[]): string[] {
|
|
|
503
504
|
const uniqueClasses = new Set<string>();
|
|
504
505
|
|
|
505
506
|
dirtyNodes.forEach((node) => {
|
|
506
|
-
// Extract from parentCss arrays (like legacy getTailwindWhitelist)
|
|
507
507
|
if ('parentCss' in node && Array.isArray(node.parentCss)) {
|
|
508
508
|
node.parentCss.forEach((classString: string) => {
|
|
509
509
|
classString.split(' ').forEach((className: string) => {
|
|
510
510
|
if (className.trim()) uniqueClasses.add(className.trim());
|
|
511
511
|
});
|
|
512
512
|
});
|
|
513
|
+
} else if ('parentCss' in node && typeof node.parentCss === `string`) {
|
|
514
|
+
node.parentCss.split(' ').forEach((className: string) => {
|
|
515
|
+
if (className.trim()) uniqueClasses.add(className.trim());
|
|
516
|
+
});
|
|
513
517
|
}
|
|
514
518
|
|
|
515
519
|
// Extract from elementCss strings (like legacy getTailwindWhitelist)
|
package/utils/inject-files.ts
CHANGED
|
@@ -2105,9 +2105,15 @@ export async function injectTemplateFiles(
|
|
|
2105
2105
|
},
|
|
2106
2106
|
{
|
|
2107
2107
|
src: resolve(
|
|
2108
|
-
'../templates/src/components/storykeep/widgets/
|
|
2108
|
+
'../templates/src/components/storykeep/widgets/HydrateWizard.tsx'
|
|
2109
2109
|
),
|
|
2110
|
-
dest: 'src/components/storykeep/widgets/
|
|
2110
|
+
dest: 'src/components/storykeep/widgets/HydrateWizard.tsx',
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
src: resolve(
|
|
2114
|
+
'../templates/src/components/storykeep/widgets/InitWizard.tsx'
|
|
2115
|
+
),
|
|
2116
|
+
dest: 'src/components/storykeep/widgets/InitWizard.tsx',
|
|
2111
2117
|
},
|
|
2112
2118
|
{
|
|
2113
2119
|
src: resolve('../templates/src/pages/storykeep/init.astro'),
|