@olonjs/cli 3.0.148 → 3.0.150
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/assets/src_tenant_alpha.sh +202 -33
- package/assets/templates/alpha/src_tenant.sh +202 -33
- package/package.json +1 -1
|
@@ -1817,11 +1817,14 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^1.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.17",
|
|
1821
|
+
"@olonjs/react": "^0.1.0",
|
|
1822
|
+
"@olonjs/studio": "^0.1.0",
|
|
1821
1823
|
"class-variance-authority": "^0.7.1",
|
|
1822
1824
|
"clsx": "^2.1.1",
|
|
1823
1825
|
"lucide-react": "^0.474.0",
|
|
1824
1826
|
"motion": "^12.23.24",
|
|
1827
|
+
"radix-ui": "^1.4.3",
|
|
1825
1828
|
"react": "^19.0.0",
|
|
1826
1829
|
"react-markdown": "^9.0.1",
|
|
1827
1830
|
"react-dom": "^19.0.0",
|
|
@@ -2191,8 +2194,6 @@ async function readJsonFile(filePath) {
|
|
|
2191
2194
|
}
|
|
2192
2195
|
|
|
2193
2196
|
async function expandCollectionTarget(slug, pageFilePath) {
|
|
2194
|
-
if (!/\[[^\]]+\]/.test(slug)) return [slug];
|
|
2195
|
-
|
|
2196
2197
|
let pageConfig;
|
|
2197
2198
|
try {
|
|
2198
2199
|
pageConfig = await readJsonFile(pageFilePath);
|
|
@@ -2206,7 +2207,14 @@ async function expandCollectionTarget(slug, pageFilePath) {
|
|
|
2206
2207
|
}
|
|
2207
2208
|
|
|
2208
2209
|
const token = `[${binding.paramKey}]`;
|
|
2209
|
-
|
|
2210
|
+
// Prefer authored route pattern (page.slug) when the file path is not parametric.
|
|
2211
|
+
const authoredSlug =
|
|
2212
|
+
typeof pageConfig.slug === 'string'
|
|
2213
|
+
? pageConfig.slug.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
|
|
2214
|
+
: '';
|
|
2215
|
+
const routePattern =
|
|
2216
|
+
authoredSlug.includes(token) ? authoredSlug : slug.includes(token) ? slug : '';
|
|
2217
|
+
if (!routePattern) return [slug];
|
|
2210
2218
|
|
|
2211
2219
|
const collectionPath = path.resolve(collectionsDir, binding.source, `${binding.source}.json`);
|
|
2212
2220
|
let collection;
|
|
@@ -2218,7 +2226,9 @@ async function expandCollectionTarget(slug, pageFilePath) {
|
|
|
2218
2226
|
|
|
2219
2227
|
if (!collection || typeof collection !== 'object' || Array.isArray(collection)) return [slug];
|
|
2220
2228
|
const itemIds = Object.keys(collection).sort((a, b) => a.localeCompare(b));
|
|
2221
|
-
return itemIds.length > 0
|
|
2229
|
+
return itemIds.length > 0
|
|
2230
|
+
? itemIds.map((itemId) => routePattern.replace(token, itemId))
|
|
2231
|
+
: [slug];
|
|
2222
2232
|
}
|
|
2223
2233
|
|
|
2224
2234
|
async function listJsonFilesRecursive(dir) {
|
|
@@ -2942,10 +2952,9 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
|
2942
2952
|
* Bootstrap, persistence, and engine wiring — logic lives in lib/hooks.
|
|
2943
2953
|
*/
|
|
2944
2954
|
import { useEffect, useMemo } from 'react';
|
|
2945
|
-
import { JsonPagesEngine } from '@olonjs/
|
|
2955
|
+
import { JsonPagesEngine, OlonFormsContext } from '@olonjs/react';
|
|
2946
2956
|
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
2947
2957
|
import { withBasePath } from '@olonjs/core';
|
|
2948
|
-
import { OlonFormsContext } from '@olonjs/core';
|
|
2949
2958
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
2950
2959
|
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
2951
2960
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
@@ -2981,6 +2990,7 @@ import { useAssetsManifest } from '@/lib/useAssetsManifest';
|
|
|
2981
2990
|
import { useCloudSave } from '@/lib/useCloudSave';
|
|
2982
2991
|
import { useTenantBootstrap } from '@/lib/useTenantBootstrap';
|
|
2983
2992
|
import { useAdminStudioContent } from '@/lib/cloud/useAdminStudioContent';
|
|
2993
|
+
import { hydrateLocalProjectState } from '@/lib/hydrateLocalProjectState';
|
|
2984
2994
|
|
|
2985
2995
|
import tenantCss from './index.css?inline';
|
|
2986
2996
|
|
|
@@ -3047,7 +3057,10 @@ function App() {
|
|
|
3047
3057
|
};
|
|
3048
3058
|
}, [canPaintVisitor, bootstrap.enginePages, bootstrap.siteConfig]);
|
|
3049
3059
|
|
|
3050
|
-
const engineCollections =
|
|
3060
|
+
const engineCollections =
|
|
3061
|
+
bootstrap.isHotSaveMode || Object.keys(bootstrap.collections).length > 0
|
|
3062
|
+
? bootstrap.collections
|
|
3063
|
+
: fileCollections;
|
|
3051
3064
|
const engineRefDocuments = useMemo(
|
|
3052
3065
|
() => ({
|
|
3053
3066
|
'menu.json': bootstrap.menuConfig,
|
|
@@ -3085,6 +3098,15 @@ function App() {
|
|
|
3085
3098
|
});
|
|
3086
3099
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
3087
3100
|
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
3101
|
+
hydrateLocalProjectState({
|
|
3102
|
+
state,
|
|
3103
|
+
slug,
|
|
3104
|
+
setPages: bootstrap.setPages,
|
|
3105
|
+
setSiteConfig: bootstrap.setSiteConfig,
|
|
3106
|
+
setMenuConfig: bootstrap.setMenuConfig,
|
|
3107
|
+
setThemeConfig: bootstrap.setThemeConfig,
|
|
3108
|
+
setCollections: bootstrap.setCollections,
|
|
3109
|
+
});
|
|
3088
3110
|
},
|
|
3089
3111
|
async hotSave(state: ProjectState, slug: string): Promise<void> {
|
|
3090
3112
|
if (!bootstrap.isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
@@ -4064,7 +4086,7 @@ END_OF_FILE_CONTENT
|
|
|
4064
4086
|
echo "Creating src/components/form-demo/View.tsx..."
|
|
4065
4087
|
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/View.tsx"
|
|
4066
4088
|
import { Icon } from '@/lib/IconResolver';
|
|
4067
|
-
import { useFormState } from '@olonjs/
|
|
4089
|
+
import { useFormState } from '@olonjs/react';
|
|
4068
4090
|
import type { FormDemoData } from './types';
|
|
4069
4091
|
|
|
4070
4092
|
type FormDemoViewProps = {
|
|
@@ -5609,7 +5631,8 @@ import {
|
|
|
5609
5631
|
Code2, Quote, SquareCode,
|
|
5610
5632
|
Link2, Unlink2, ImagePlus, Eraser,
|
|
5611
5633
|
} from 'lucide-react';
|
|
5612
|
-
import { STUDIO_EVENTS
|
|
5634
|
+
import { STUDIO_EVENTS } from '@olonjs/core';
|
|
5635
|
+
import { useConfig, useStudio } from '@olonjs/react';
|
|
5613
5636
|
import type { TiptapData, TiptapSettings } from './types';
|
|
5614
5637
|
|
|
5615
5638
|
// ── UI primitives ─────────────────────────────────────────────────
|
|
@@ -8569,7 +8592,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/collections/libri/libri.json"
|
|
|
8569
8592
|
"id": "1984",
|
|
8570
8593
|
"title": "1984",
|
|
8571
8594
|
"author": {
|
|
8572
|
-
"$ref": "../autori/autori.json#/
|
|
8595
|
+
"$ref": "../autori/autori.json#/george-orwell"
|
|
8573
8596
|
},
|
|
8574
8597
|
"year": 1949,
|
|
8575
8598
|
"genre": "Distopia",
|
|
@@ -9673,14 +9696,8 @@ echo "Creating src/entry-ssg.tsx..."
|
|
|
9673
9696
|
cat << 'END_OF_FILE_CONTENT' > "src/entry-ssg.tsx"
|
|
9674
9697
|
import { renderToString } from 'react-dom/server';
|
|
9675
9698
|
import { StaticRouter } from 'react-router-dom/server';
|
|
9676
|
-
import {
|
|
9677
|
-
|
|
9678
|
-
PageRenderer,
|
|
9679
|
-
StudioProvider,
|
|
9680
|
-
contract,
|
|
9681
|
-
resolvePageMatchFromRegistry,
|
|
9682
|
-
resolveRuntimeConfig,
|
|
9683
|
-
} from '@olonjs/core';
|
|
9699
|
+
import { ConfigProvider, PageRenderer, StudioProvider } from '@olonjs/react';
|
|
9700
|
+
import { contract, resolvePageMatchFromRegistry, resolveRuntimeConfig } from '@olonjs/core';
|
|
9684
9701
|
import type { JsonPagesConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
9685
9702
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
9686
9703
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
@@ -11432,7 +11449,6 @@ export function coerceSiteConfig(value: unknown): SiteConfig | null {
|
|
|
11432
11449
|
}
|
|
11433
11450
|
if (!isObjectRecord(input)) return null;
|
|
11434
11451
|
if (!isObjectRecord(input.identity)) return null;
|
|
11435
|
-
if (!Array.isArray(input.pages)) return null;
|
|
11436
11452
|
|
|
11437
11453
|
return input as unknown as SiteConfig;
|
|
11438
11454
|
}
|
|
@@ -11699,7 +11715,6 @@ export function getHydratedData(
|
|
|
11699
11715
|
}
|
|
11700
11716
|
|
|
11701
11717
|
END_OF_FILE_CONTENT
|
|
11702
|
-
mkdir -p "src/lib/editorial"
|
|
11703
11718
|
echo "Creating src/lib/getFileCollections.ts..."
|
|
11704
11719
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/getFileCollections.ts"
|
|
11705
11720
|
import type { JsonPagesConfig } from '@/types';
|
|
@@ -11790,6 +11805,47 @@ export function getFilePages(): Record<string, PageConfig> {
|
|
|
11790
11805
|
return record;
|
|
11791
11806
|
}
|
|
11792
11807
|
|
|
11808
|
+
END_OF_FILE_CONTENT
|
|
11809
|
+
echo "Creating src/lib/hydrateLocalProjectState.ts..."
|
|
11810
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/hydrateLocalProjectState.ts"
|
|
11811
|
+
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
11812
|
+
import type { MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
11813
|
+
|
|
11814
|
+
type HydrateLocalProjectStateArgs = {
|
|
11815
|
+
state: ProjectState;
|
|
11816
|
+
slug: string;
|
|
11817
|
+
setPages: (updater: (prev: Record<string, PageConfig>) => Record<string, PageConfig>) => void;
|
|
11818
|
+
setSiteConfig: (site: SiteConfig) => void;
|
|
11819
|
+
setMenuConfig: (menu: MenuConfig) => void;
|
|
11820
|
+
setThemeConfig: (theme: ThemeConfig) => void;
|
|
11821
|
+
setCollections: (collections: NonNullable<JsonPagesConfig['collections']>) => void;
|
|
11822
|
+
};
|
|
11823
|
+
|
|
11824
|
+
/**
|
|
11825
|
+
* Write-through hydrate after local `/api/save-to-file`.
|
|
11826
|
+
* Pushes only the slices present in the saved ProjectState into bootstrap state —
|
|
11827
|
+
* no disk re-read, no full remount. Brand-agnostic.
|
|
11828
|
+
*/
|
|
11829
|
+
export function hydrateLocalProjectState({
|
|
11830
|
+
state,
|
|
11831
|
+
slug,
|
|
11832
|
+
setPages,
|
|
11833
|
+
setSiteConfig,
|
|
11834
|
+
setMenuConfig,
|
|
11835
|
+
setThemeConfig,
|
|
11836
|
+
setCollections,
|
|
11837
|
+
}: HydrateLocalProjectStateArgs): void {
|
|
11838
|
+
if (state.menu != null) setMenuConfig(state.menu);
|
|
11839
|
+
if (state.site != null) setSiteConfig(state.site);
|
|
11840
|
+
if (state.theme != null) setThemeConfig(state.theme);
|
|
11841
|
+
if (state.page != null) {
|
|
11842
|
+
setPages((prev) => ({ ...prev, [slug]: state.page }));
|
|
11843
|
+
}
|
|
11844
|
+
if (state.collections != null) {
|
|
11845
|
+
setCollections(state.collections as NonNullable<JsonPagesConfig['collections']>);
|
|
11846
|
+
}
|
|
11847
|
+
}
|
|
11848
|
+
|
|
11793
11849
|
END_OF_FILE_CONTENT
|
|
11794
11850
|
echo "Creating src/lib/schemas.ts..."
|
|
11795
11851
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
@@ -12862,10 +12918,18 @@ export function buildThemeFontVarsCss(input: unknown): string {
|
|
|
12862
12918
|
const tokens = isObjectRecord(input.tokens) ? input.tokens : null;
|
|
12863
12919
|
const typography = tokens && isObjectRecord(tokens.typography) ? tokens.typography : null;
|
|
12864
12920
|
const fontFamily = typography && isObjectRecord(typography.fontFamily) ? typography.fontFamily : null;
|
|
12865
|
-
const primary =
|
|
12866
|
-
|
|
12921
|
+
const primary =
|
|
12922
|
+
typeof fontFamily?.primary === 'string'
|
|
12923
|
+
? fontFamily.primary
|
|
12924
|
+
: "'Instrument Sans', system-ui, sans-serif";
|
|
12925
|
+
const display =
|
|
12926
|
+
typeof fontFamily?.display === 'string'
|
|
12927
|
+
? fontFamily.display
|
|
12928
|
+
: typeof fontFamily?.serif === 'string'
|
|
12929
|
+
? fontFamily.serif
|
|
12930
|
+
: "'Instrument Serif', Georgia, serif";
|
|
12867
12931
|
const mono = typeof fontFamily?.mono === 'string' ? fontFamily.mono : "'JetBrains Mono', monospace";
|
|
12868
|
-
return `:root{--theme-font-primary:${primary};--theme-font-
|
|
12932
|
+
return `:root{--theme-font-primary:${primary};--theme-font-display:${display};--theme-font-mono:${mono};}`;
|
|
12869
12933
|
}
|
|
12870
12934
|
|
|
12871
12935
|
const REMOTE_CSS_LINK_ATTR = 'data-jp-tenant-remote-css';
|
|
@@ -12878,8 +12942,9 @@ function isRemoteStylesheetHref(value: string): boolean {
|
|
|
12878
12942
|
export function extractLeadingRemoteCssImports(cssText: string): { hrefs: string[]; rest: string } {
|
|
12879
12943
|
const hrefs = new Set<string>();
|
|
12880
12944
|
const leadingTriviaPattern = /^(?:\s+|\/\*[\s\S]*?\*\/)*/;
|
|
12945
|
+
// Vite/Tailwind may emit `@import url("...")` or compacted `@import"https://..."`.
|
|
12881
12946
|
const importPattern =
|
|
12882
|
-
/^@import
|
|
12947
|
+
/^@import(?:\s*url\(\s*(?:'([^']+)'|"([^"]+)"|([^'")\s][^)]*))\s*\)|\s*(['"])([^'"]+)\4)\s*([^;]*);/i;
|
|
12883
12948
|
let rest = cssText;
|
|
12884
12949
|
|
|
12885
12950
|
for (;;) {
|
|
@@ -12891,8 +12956,8 @@ export function extractLeadingRemoteCssImports(cssText: string): { hrefs: string
|
|
|
12891
12956
|
const match = rest.match(importPattern);
|
|
12892
12957
|
if (!match) break;
|
|
12893
12958
|
|
|
12894
|
-
const href = (match[1] ?? match[2] ?? match[3] ?? '').trim();
|
|
12895
|
-
const trailingDirectives = (match[
|
|
12959
|
+
const href = (match[1] ?? match[2] ?? match[3] ?? match[5] ?? '').trim();
|
|
12960
|
+
const trailingDirectives = (match[6] ?? '').trim();
|
|
12896
12961
|
|
|
12897
12962
|
if (!isRemoteStylesheetHref(href) || trailingDirectives.length > 0) {
|
|
12898
12963
|
break;
|
|
@@ -13094,7 +13159,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/lib/useCloudSave.ts"
|
|
|
13094
13159
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13095
13160
|
import type { DeployPhase, ProjectState, StepId } from '@olonjs/core';
|
|
13096
13161
|
import { DEPLOY_STEPS, startCloudSaveStream } from '@olonjs/core';
|
|
13097
|
-
import { CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
|
|
13162
|
+
import { APP_BASE_PATH, CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
|
|
13098
13163
|
|
|
13099
13164
|
interface CloudSaveUiState {
|
|
13100
13165
|
isOpen: boolean;
|
|
@@ -13120,6 +13185,100 @@ function stepProgress(doneSteps: StepId[]): number {
|
|
|
13120
13185
|
return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
|
|
13121
13186
|
}
|
|
13122
13187
|
|
|
13188
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13189
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13190
|
+
}
|
|
13191
|
+
|
|
13192
|
+
function cloneJson<T>(value: T): T {
|
|
13193
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
13194
|
+
}
|
|
13195
|
+
|
|
13196
|
+
function normalizePathSegments(value: string): string {
|
|
13197
|
+
return value
|
|
13198
|
+
.split('/')
|
|
13199
|
+
.map((segment) => segment.trim())
|
|
13200
|
+
.filter(Boolean)
|
|
13201
|
+
.join('/');
|
|
13202
|
+
}
|
|
13203
|
+
|
|
13204
|
+
function resolveAdminContentSlug(): string | null {
|
|
13205
|
+
if (typeof window === 'undefined') return null;
|
|
13206
|
+
|
|
13207
|
+
const normalizedBase = APP_BASE_PATH.replace(/\/+$/, '');
|
|
13208
|
+
let path = window.location.pathname;
|
|
13209
|
+
if (normalizedBase && normalizedBase !== '/' && path.startsWith(normalizedBase)) {
|
|
13210
|
+
path = path.slice(normalizedBase.length) || '/';
|
|
13211
|
+
}
|
|
13212
|
+
|
|
13213
|
+
const slug = normalizePathSegments(path.replace(/^\/admin\/?/, ''));
|
|
13214
|
+
return slug || null;
|
|
13215
|
+
}
|
|
13216
|
+
|
|
13217
|
+
function resolveTemplateParamValue(templateSlug: string, concreteSlug: string, paramKey: string): string | null {
|
|
13218
|
+
const templateSegments = normalizePathSegments(templateSlug).split('/').filter(Boolean);
|
|
13219
|
+
const concreteSegments = normalizePathSegments(concreteSlug).split('/').filter(Boolean);
|
|
13220
|
+
if (templateSegments.length !== concreteSegments.length) return null;
|
|
13221
|
+
|
|
13222
|
+
for (let index = 0; index < templateSegments.length; index += 1) {
|
|
13223
|
+
const templateSegment = templateSegments[index];
|
|
13224
|
+
const concreteSegment = concreteSegments[index];
|
|
13225
|
+
const paramMatch = templateSegment.match(/^\[([A-Za-z0-9_-]+)\]$/);
|
|
13226
|
+
if (paramMatch?.[1] === paramKey) return concreteSegment;
|
|
13227
|
+
if (!paramMatch && templateSegment !== concreteSegment) return null;
|
|
13228
|
+
}
|
|
13229
|
+
|
|
13230
|
+
return null;
|
|
13231
|
+
}
|
|
13232
|
+
|
|
13233
|
+
function hasCollectionCurrentRef(value: unknown): boolean {
|
|
13234
|
+
if (Array.isArray(value)) return value.some(hasCollectionCurrentRef);
|
|
13235
|
+
if (!isRecord(value)) return false;
|
|
13236
|
+
if (value.$ref === 'collection:current') return true;
|
|
13237
|
+
return Object.values(value).some(hasCollectionCurrentRef);
|
|
13238
|
+
}
|
|
13239
|
+
|
|
13240
|
+
function replaceCollectionCurrentRefs(value: unknown, currentItem: unknown): unknown {
|
|
13241
|
+
if (Array.isArray(value)) return value.map((item) => replaceCollectionCurrentRefs(item, currentItem));
|
|
13242
|
+
if (!isRecord(value)) return value;
|
|
13243
|
+
if (value.$ref === 'collection:current') return cloneJson(currentItem);
|
|
13244
|
+
return Object.fromEntries(
|
|
13245
|
+
Object.entries(value).map(([key, entryValue]) => [key, replaceCollectionCurrentRefs(entryValue, currentItem)]),
|
|
13246
|
+
);
|
|
13247
|
+
}
|
|
13248
|
+
|
|
13249
|
+
function buildSaveStreamPagePayload(state: ProjectState, fallbackSlug: string): { slug: string; page: ProjectState['page'] } {
|
|
13250
|
+
const page = state.page;
|
|
13251
|
+
const collection = page.collection;
|
|
13252
|
+
if (!collection || !hasCollectionCurrentRef(page)) {
|
|
13253
|
+
return { slug: fallbackSlug, page };
|
|
13254
|
+
}
|
|
13255
|
+
|
|
13256
|
+
const concreteSlug = resolveAdminContentSlug();
|
|
13257
|
+
if (!concreteSlug) {
|
|
13258
|
+
throw new Error('Cannot resolve concrete admin route for collection page save.');
|
|
13259
|
+
}
|
|
13260
|
+
|
|
13261
|
+
const paramValue = resolveTemplateParamValue(page.slug, concreteSlug, collection.paramKey);
|
|
13262
|
+
if (!paramValue) {
|
|
13263
|
+
throw new Error(`Cannot resolve collection param "${collection.paramKey}" from route "${concreteSlug}".`);
|
|
13264
|
+
}
|
|
13265
|
+
|
|
13266
|
+
const collectionDocument = state.collections?.[collection.source];
|
|
13267
|
+
const currentItem = isRecord(collectionDocument) ? collectionDocument[paramValue] : undefined;
|
|
13268
|
+
if (!currentItem) {
|
|
13269
|
+
throw new Error(`Cannot resolve collection item "${collection.source}/${paramValue}" for save.`);
|
|
13270
|
+
}
|
|
13271
|
+
|
|
13272
|
+
const resolvedPage = replaceCollectionCurrentRefs(page, currentItem) as ProjectState['page'];
|
|
13273
|
+
return {
|
|
13274
|
+
slug: concreteSlug,
|
|
13275
|
+
page: {
|
|
13276
|
+
...resolvedPage,
|
|
13277
|
+
slug: concreteSlug,
|
|
13278
|
+
},
|
|
13279
|
+
};
|
|
13280
|
+
}
|
|
13281
|
+
|
|
13123
13282
|
export function useCloudSave() {
|
|
13124
13283
|
const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
|
|
13125
13284
|
const activeCloudSaveController = useRef<AbortController | null>(null);
|
|
@@ -13153,12 +13312,18 @@ export function useCloudSave() {
|
|
|
13153
13312
|
});
|
|
13154
13313
|
|
|
13155
13314
|
try {
|
|
13315
|
+
const savePage = buildSaveStreamPagePayload(payload.state, payload.slug);
|
|
13156
13316
|
await startCloudSaveStream({
|
|
13157
13317
|
apiBaseUrl: CLOUD_API_URL,
|
|
13158
13318
|
apiKey: CLOUD_API_KEY,
|
|
13159
|
-
path: `src/data/pages/${
|
|
13160
|
-
content:
|
|
13161
|
-
|
|
13319
|
+
path: `src/data/pages/${savePage.slug}.json`,
|
|
13320
|
+
content: savePage.page,
|
|
13321
|
+
additionalFiles: [
|
|
13322
|
+
{ path: 'src/data/config/site.json', content: payload.state.site },
|
|
13323
|
+
{ path: 'src/data/config/menu.json', content: payload.state.menu },
|
|
13324
|
+
],
|
|
13325
|
+
changedScopes: ['page', 'site', 'menu'],
|
|
13326
|
+
message: `Content update for ${savePage.slug} via Visual Editor`,
|
|
13162
13327
|
signal: controller.signal,
|
|
13163
13328
|
onStep: (event) => {
|
|
13164
13329
|
setCloudSaveUi((prev) => {
|
|
@@ -13322,7 +13487,7 @@ END_OF_FILE_CONTENT
|
|
|
13322
13487
|
echo "Creating src/lib/useOlonForms.ts..."
|
|
13323
13488
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
|
|
13324
13489
|
import { useCallback, useEffect, useState } from 'react';
|
|
13325
|
-
import type { FormState } from '@olonjs/
|
|
13490
|
+
import type { FormState } from '@olonjs/react';
|
|
13326
13491
|
|
|
13327
13492
|
const API_BASE =
|
|
13328
13493
|
(import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
|
|
@@ -13598,7 +13763,7 @@ export function useTenantBootstrap({
|
|
|
13598
13763
|
|
|
13599
13764
|
const [menuConfig, setMenuConfig] = useState<MenuConfig>(menuConfigSeed);
|
|
13600
13765
|
|
|
13601
|
-
const [themeConfig] = useState<ThemeConfig>(themeConfigSeed);
|
|
13766
|
+
const [themeConfig, setThemeConfig] = useState<ThemeConfig>(themeConfigSeed);
|
|
13602
13767
|
|
|
13603
13768
|
const [collections, setCollections] = useState<NonNullable<JsonPagesConfig['collections']>>(EMPTY_COLLECTIONS);
|
|
13604
13769
|
|
|
@@ -14114,6 +14279,10 @@ export function useTenantBootstrap({
|
|
|
14114
14279
|
|
|
14115
14280
|
setSiteConfig,
|
|
14116
14281
|
|
|
14282
|
+
setMenuConfig,
|
|
14283
|
+
|
|
14284
|
+
setThemeConfig,
|
|
14285
|
+
|
|
14117
14286
|
setCollections,
|
|
14118
14287
|
|
|
14119
14288
|
cloudApiCandidates,
|
|
@@ -1817,11 +1817,14 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^1.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.17",
|
|
1821
|
+
"@olonjs/react": "^0.1.0",
|
|
1822
|
+
"@olonjs/studio": "^0.1.0",
|
|
1821
1823
|
"class-variance-authority": "^0.7.1",
|
|
1822
1824
|
"clsx": "^2.1.1",
|
|
1823
1825
|
"lucide-react": "^0.474.0",
|
|
1824
1826
|
"motion": "^12.23.24",
|
|
1827
|
+
"radix-ui": "^1.4.3",
|
|
1825
1828
|
"react": "^19.0.0",
|
|
1826
1829
|
"react-markdown": "^9.0.1",
|
|
1827
1830
|
"react-dom": "^19.0.0",
|
|
@@ -2191,8 +2194,6 @@ async function readJsonFile(filePath) {
|
|
|
2191
2194
|
}
|
|
2192
2195
|
|
|
2193
2196
|
async function expandCollectionTarget(slug, pageFilePath) {
|
|
2194
|
-
if (!/\[[^\]]+\]/.test(slug)) return [slug];
|
|
2195
|
-
|
|
2196
2197
|
let pageConfig;
|
|
2197
2198
|
try {
|
|
2198
2199
|
pageConfig = await readJsonFile(pageFilePath);
|
|
@@ -2206,7 +2207,14 @@ async function expandCollectionTarget(slug, pageFilePath) {
|
|
|
2206
2207
|
}
|
|
2207
2208
|
|
|
2208
2209
|
const token = `[${binding.paramKey}]`;
|
|
2209
|
-
|
|
2210
|
+
// Prefer authored route pattern (page.slug) when the file path is not parametric.
|
|
2211
|
+
const authoredSlug =
|
|
2212
|
+
typeof pageConfig.slug === 'string'
|
|
2213
|
+
? pageConfig.slug.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
|
|
2214
|
+
: '';
|
|
2215
|
+
const routePattern =
|
|
2216
|
+
authoredSlug.includes(token) ? authoredSlug : slug.includes(token) ? slug : '';
|
|
2217
|
+
if (!routePattern) return [slug];
|
|
2210
2218
|
|
|
2211
2219
|
const collectionPath = path.resolve(collectionsDir, binding.source, `${binding.source}.json`);
|
|
2212
2220
|
let collection;
|
|
@@ -2218,7 +2226,9 @@ async function expandCollectionTarget(slug, pageFilePath) {
|
|
|
2218
2226
|
|
|
2219
2227
|
if (!collection || typeof collection !== 'object' || Array.isArray(collection)) return [slug];
|
|
2220
2228
|
const itemIds = Object.keys(collection).sort((a, b) => a.localeCompare(b));
|
|
2221
|
-
return itemIds.length > 0
|
|
2229
|
+
return itemIds.length > 0
|
|
2230
|
+
? itemIds.map((itemId) => routePattern.replace(token, itemId))
|
|
2231
|
+
: [slug];
|
|
2222
2232
|
}
|
|
2223
2233
|
|
|
2224
2234
|
async function listJsonFilesRecursive(dir) {
|
|
@@ -2942,10 +2952,9 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
|
2942
2952
|
* Bootstrap, persistence, and engine wiring — logic lives in lib/hooks.
|
|
2943
2953
|
*/
|
|
2944
2954
|
import { useEffect, useMemo } from 'react';
|
|
2945
|
-
import { JsonPagesEngine } from '@olonjs/
|
|
2955
|
+
import { JsonPagesEngine, OlonFormsContext } from '@olonjs/react';
|
|
2946
2956
|
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
2947
2957
|
import { withBasePath } from '@olonjs/core';
|
|
2948
|
-
import { OlonFormsContext } from '@olonjs/core';
|
|
2949
2958
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
2950
2959
|
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
2951
2960
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
@@ -2981,6 +2990,7 @@ import { useAssetsManifest } from '@/lib/useAssetsManifest';
|
|
|
2981
2990
|
import { useCloudSave } from '@/lib/useCloudSave';
|
|
2982
2991
|
import { useTenantBootstrap } from '@/lib/useTenantBootstrap';
|
|
2983
2992
|
import { useAdminStudioContent } from '@/lib/cloud/useAdminStudioContent';
|
|
2993
|
+
import { hydrateLocalProjectState } from '@/lib/hydrateLocalProjectState';
|
|
2984
2994
|
|
|
2985
2995
|
import tenantCss from './index.css?inline';
|
|
2986
2996
|
|
|
@@ -3047,7 +3057,10 @@ function App() {
|
|
|
3047
3057
|
};
|
|
3048
3058
|
}, [canPaintVisitor, bootstrap.enginePages, bootstrap.siteConfig]);
|
|
3049
3059
|
|
|
3050
|
-
const engineCollections =
|
|
3060
|
+
const engineCollections =
|
|
3061
|
+
bootstrap.isHotSaveMode || Object.keys(bootstrap.collections).length > 0
|
|
3062
|
+
? bootstrap.collections
|
|
3063
|
+
: fileCollections;
|
|
3051
3064
|
const engineRefDocuments = useMemo(
|
|
3052
3065
|
() => ({
|
|
3053
3066
|
'menu.json': bootstrap.menuConfig,
|
|
@@ -3085,6 +3098,15 @@ function App() {
|
|
|
3085
3098
|
});
|
|
3086
3099
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
3087
3100
|
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
3101
|
+
hydrateLocalProjectState({
|
|
3102
|
+
state,
|
|
3103
|
+
slug,
|
|
3104
|
+
setPages: bootstrap.setPages,
|
|
3105
|
+
setSiteConfig: bootstrap.setSiteConfig,
|
|
3106
|
+
setMenuConfig: bootstrap.setMenuConfig,
|
|
3107
|
+
setThemeConfig: bootstrap.setThemeConfig,
|
|
3108
|
+
setCollections: bootstrap.setCollections,
|
|
3109
|
+
});
|
|
3088
3110
|
},
|
|
3089
3111
|
async hotSave(state: ProjectState, slug: string): Promise<void> {
|
|
3090
3112
|
if (!bootstrap.isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
@@ -4064,7 +4086,7 @@ END_OF_FILE_CONTENT
|
|
|
4064
4086
|
echo "Creating src/components/form-demo/View.tsx..."
|
|
4065
4087
|
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/View.tsx"
|
|
4066
4088
|
import { Icon } from '@/lib/IconResolver';
|
|
4067
|
-
import { useFormState } from '@olonjs/
|
|
4089
|
+
import { useFormState } from '@olonjs/react';
|
|
4068
4090
|
import type { FormDemoData } from './types';
|
|
4069
4091
|
|
|
4070
4092
|
type FormDemoViewProps = {
|
|
@@ -5609,7 +5631,8 @@ import {
|
|
|
5609
5631
|
Code2, Quote, SquareCode,
|
|
5610
5632
|
Link2, Unlink2, ImagePlus, Eraser,
|
|
5611
5633
|
} from 'lucide-react';
|
|
5612
|
-
import { STUDIO_EVENTS
|
|
5634
|
+
import { STUDIO_EVENTS } from '@olonjs/core';
|
|
5635
|
+
import { useConfig, useStudio } from '@olonjs/react';
|
|
5613
5636
|
import type { TiptapData, TiptapSettings } from './types';
|
|
5614
5637
|
|
|
5615
5638
|
// ── UI primitives ─────────────────────────────────────────────────
|
|
@@ -8569,7 +8592,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/collections/libri/libri.json"
|
|
|
8569
8592
|
"id": "1984",
|
|
8570
8593
|
"title": "1984",
|
|
8571
8594
|
"author": {
|
|
8572
|
-
"$ref": "../autori/autori.json#/
|
|
8595
|
+
"$ref": "../autori/autori.json#/george-orwell"
|
|
8573
8596
|
},
|
|
8574
8597
|
"year": 1949,
|
|
8575
8598
|
"genre": "Distopia",
|
|
@@ -9673,14 +9696,8 @@ echo "Creating src/entry-ssg.tsx..."
|
|
|
9673
9696
|
cat << 'END_OF_FILE_CONTENT' > "src/entry-ssg.tsx"
|
|
9674
9697
|
import { renderToString } from 'react-dom/server';
|
|
9675
9698
|
import { StaticRouter } from 'react-router-dom/server';
|
|
9676
|
-
import {
|
|
9677
|
-
|
|
9678
|
-
PageRenderer,
|
|
9679
|
-
StudioProvider,
|
|
9680
|
-
contract,
|
|
9681
|
-
resolvePageMatchFromRegistry,
|
|
9682
|
-
resolveRuntimeConfig,
|
|
9683
|
-
} from '@olonjs/core';
|
|
9699
|
+
import { ConfigProvider, PageRenderer, StudioProvider } from '@olonjs/react';
|
|
9700
|
+
import { contract, resolvePageMatchFromRegistry, resolveRuntimeConfig } from '@olonjs/core';
|
|
9684
9701
|
import type { JsonPagesConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
9685
9702
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
9686
9703
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
@@ -11432,7 +11449,6 @@ export function coerceSiteConfig(value: unknown): SiteConfig | null {
|
|
|
11432
11449
|
}
|
|
11433
11450
|
if (!isObjectRecord(input)) return null;
|
|
11434
11451
|
if (!isObjectRecord(input.identity)) return null;
|
|
11435
|
-
if (!Array.isArray(input.pages)) return null;
|
|
11436
11452
|
|
|
11437
11453
|
return input as unknown as SiteConfig;
|
|
11438
11454
|
}
|
|
@@ -11699,7 +11715,6 @@ export function getHydratedData(
|
|
|
11699
11715
|
}
|
|
11700
11716
|
|
|
11701
11717
|
END_OF_FILE_CONTENT
|
|
11702
|
-
mkdir -p "src/lib/editorial"
|
|
11703
11718
|
echo "Creating src/lib/getFileCollections.ts..."
|
|
11704
11719
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/getFileCollections.ts"
|
|
11705
11720
|
import type { JsonPagesConfig } from '@/types';
|
|
@@ -11790,6 +11805,47 @@ export function getFilePages(): Record<string, PageConfig> {
|
|
|
11790
11805
|
return record;
|
|
11791
11806
|
}
|
|
11792
11807
|
|
|
11808
|
+
END_OF_FILE_CONTENT
|
|
11809
|
+
echo "Creating src/lib/hydrateLocalProjectState.ts..."
|
|
11810
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/hydrateLocalProjectState.ts"
|
|
11811
|
+
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
11812
|
+
import type { MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
11813
|
+
|
|
11814
|
+
type HydrateLocalProjectStateArgs = {
|
|
11815
|
+
state: ProjectState;
|
|
11816
|
+
slug: string;
|
|
11817
|
+
setPages: (updater: (prev: Record<string, PageConfig>) => Record<string, PageConfig>) => void;
|
|
11818
|
+
setSiteConfig: (site: SiteConfig) => void;
|
|
11819
|
+
setMenuConfig: (menu: MenuConfig) => void;
|
|
11820
|
+
setThemeConfig: (theme: ThemeConfig) => void;
|
|
11821
|
+
setCollections: (collections: NonNullable<JsonPagesConfig['collections']>) => void;
|
|
11822
|
+
};
|
|
11823
|
+
|
|
11824
|
+
/**
|
|
11825
|
+
* Write-through hydrate after local `/api/save-to-file`.
|
|
11826
|
+
* Pushes only the slices present in the saved ProjectState into bootstrap state —
|
|
11827
|
+
* no disk re-read, no full remount. Brand-agnostic.
|
|
11828
|
+
*/
|
|
11829
|
+
export function hydrateLocalProjectState({
|
|
11830
|
+
state,
|
|
11831
|
+
slug,
|
|
11832
|
+
setPages,
|
|
11833
|
+
setSiteConfig,
|
|
11834
|
+
setMenuConfig,
|
|
11835
|
+
setThemeConfig,
|
|
11836
|
+
setCollections,
|
|
11837
|
+
}: HydrateLocalProjectStateArgs): void {
|
|
11838
|
+
if (state.menu != null) setMenuConfig(state.menu);
|
|
11839
|
+
if (state.site != null) setSiteConfig(state.site);
|
|
11840
|
+
if (state.theme != null) setThemeConfig(state.theme);
|
|
11841
|
+
if (state.page != null) {
|
|
11842
|
+
setPages((prev) => ({ ...prev, [slug]: state.page }));
|
|
11843
|
+
}
|
|
11844
|
+
if (state.collections != null) {
|
|
11845
|
+
setCollections(state.collections as NonNullable<JsonPagesConfig['collections']>);
|
|
11846
|
+
}
|
|
11847
|
+
}
|
|
11848
|
+
|
|
11793
11849
|
END_OF_FILE_CONTENT
|
|
11794
11850
|
echo "Creating src/lib/schemas.ts..."
|
|
11795
11851
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
@@ -12862,10 +12918,18 @@ export function buildThemeFontVarsCss(input: unknown): string {
|
|
|
12862
12918
|
const tokens = isObjectRecord(input.tokens) ? input.tokens : null;
|
|
12863
12919
|
const typography = tokens && isObjectRecord(tokens.typography) ? tokens.typography : null;
|
|
12864
12920
|
const fontFamily = typography && isObjectRecord(typography.fontFamily) ? typography.fontFamily : null;
|
|
12865
|
-
const primary =
|
|
12866
|
-
|
|
12921
|
+
const primary =
|
|
12922
|
+
typeof fontFamily?.primary === 'string'
|
|
12923
|
+
? fontFamily.primary
|
|
12924
|
+
: "'Instrument Sans', system-ui, sans-serif";
|
|
12925
|
+
const display =
|
|
12926
|
+
typeof fontFamily?.display === 'string'
|
|
12927
|
+
? fontFamily.display
|
|
12928
|
+
: typeof fontFamily?.serif === 'string'
|
|
12929
|
+
? fontFamily.serif
|
|
12930
|
+
: "'Instrument Serif', Georgia, serif";
|
|
12867
12931
|
const mono = typeof fontFamily?.mono === 'string' ? fontFamily.mono : "'JetBrains Mono', monospace";
|
|
12868
|
-
return `:root{--theme-font-primary:${primary};--theme-font-
|
|
12932
|
+
return `:root{--theme-font-primary:${primary};--theme-font-display:${display};--theme-font-mono:${mono};}`;
|
|
12869
12933
|
}
|
|
12870
12934
|
|
|
12871
12935
|
const REMOTE_CSS_LINK_ATTR = 'data-jp-tenant-remote-css';
|
|
@@ -12878,8 +12942,9 @@ function isRemoteStylesheetHref(value: string): boolean {
|
|
|
12878
12942
|
export function extractLeadingRemoteCssImports(cssText: string): { hrefs: string[]; rest: string } {
|
|
12879
12943
|
const hrefs = new Set<string>();
|
|
12880
12944
|
const leadingTriviaPattern = /^(?:\s+|\/\*[\s\S]*?\*\/)*/;
|
|
12945
|
+
// Vite/Tailwind may emit `@import url("...")` or compacted `@import"https://..."`.
|
|
12881
12946
|
const importPattern =
|
|
12882
|
-
/^@import
|
|
12947
|
+
/^@import(?:\s*url\(\s*(?:'([^']+)'|"([^"]+)"|([^'")\s][^)]*))\s*\)|\s*(['"])([^'"]+)\4)\s*([^;]*);/i;
|
|
12883
12948
|
let rest = cssText;
|
|
12884
12949
|
|
|
12885
12950
|
for (;;) {
|
|
@@ -12891,8 +12956,8 @@ export function extractLeadingRemoteCssImports(cssText: string): { hrefs: string
|
|
|
12891
12956
|
const match = rest.match(importPattern);
|
|
12892
12957
|
if (!match) break;
|
|
12893
12958
|
|
|
12894
|
-
const href = (match[1] ?? match[2] ?? match[3] ?? '').trim();
|
|
12895
|
-
const trailingDirectives = (match[
|
|
12959
|
+
const href = (match[1] ?? match[2] ?? match[3] ?? match[5] ?? '').trim();
|
|
12960
|
+
const trailingDirectives = (match[6] ?? '').trim();
|
|
12896
12961
|
|
|
12897
12962
|
if (!isRemoteStylesheetHref(href) || trailingDirectives.length > 0) {
|
|
12898
12963
|
break;
|
|
@@ -13094,7 +13159,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/lib/useCloudSave.ts"
|
|
|
13094
13159
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13095
13160
|
import type { DeployPhase, ProjectState, StepId } from '@olonjs/core';
|
|
13096
13161
|
import { DEPLOY_STEPS, startCloudSaveStream } from '@olonjs/core';
|
|
13097
|
-
import { CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
|
|
13162
|
+
import { APP_BASE_PATH, CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
|
|
13098
13163
|
|
|
13099
13164
|
interface CloudSaveUiState {
|
|
13100
13165
|
isOpen: boolean;
|
|
@@ -13120,6 +13185,100 @@ function stepProgress(doneSteps: StepId[]): number {
|
|
|
13120
13185
|
return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
|
|
13121
13186
|
}
|
|
13122
13187
|
|
|
13188
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13189
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13190
|
+
}
|
|
13191
|
+
|
|
13192
|
+
function cloneJson<T>(value: T): T {
|
|
13193
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
13194
|
+
}
|
|
13195
|
+
|
|
13196
|
+
function normalizePathSegments(value: string): string {
|
|
13197
|
+
return value
|
|
13198
|
+
.split('/')
|
|
13199
|
+
.map((segment) => segment.trim())
|
|
13200
|
+
.filter(Boolean)
|
|
13201
|
+
.join('/');
|
|
13202
|
+
}
|
|
13203
|
+
|
|
13204
|
+
function resolveAdminContentSlug(): string | null {
|
|
13205
|
+
if (typeof window === 'undefined') return null;
|
|
13206
|
+
|
|
13207
|
+
const normalizedBase = APP_BASE_PATH.replace(/\/+$/, '');
|
|
13208
|
+
let path = window.location.pathname;
|
|
13209
|
+
if (normalizedBase && normalizedBase !== '/' && path.startsWith(normalizedBase)) {
|
|
13210
|
+
path = path.slice(normalizedBase.length) || '/';
|
|
13211
|
+
}
|
|
13212
|
+
|
|
13213
|
+
const slug = normalizePathSegments(path.replace(/^\/admin\/?/, ''));
|
|
13214
|
+
return slug || null;
|
|
13215
|
+
}
|
|
13216
|
+
|
|
13217
|
+
function resolveTemplateParamValue(templateSlug: string, concreteSlug: string, paramKey: string): string | null {
|
|
13218
|
+
const templateSegments = normalizePathSegments(templateSlug).split('/').filter(Boolean);
|
|
13219
|
+
const concreteSegments = normalizePathSegments(concreteSlug).split('/').filter(Boolean);
|
|
13220
|
+
if (templateSegments.length !== concreteSegments.length) return null;
|
|
13221
|
+
|
|
13222
|
+
for (let index = 0; index < templateSegments.length; index += 1) {
|
|
13223
|
+
const templateSegment = templateSegments[index];
|
|
13224
|
+
const concreteSegment = concreteSegments[index];
|
|
13225
|
+
const paramMatch = templateSegment.match(/^\[([A-Za-z0-9_-]+)\]$/);
|
|
13226
|
+
if (paramMatch?.[1] === paramKey) return concreteSegment;
|
|
13227
|
+
if (!paramMatch && templateSegment !== concreteSegment) return null;
|
|
13228
|
+
}
|
|
13229
|
+
|
|
13230
|
+
return null;
|
|
13231
|
+
}
|
|
13232
|
+
|
|
13233
|
+
function hasCollectionCurrentRef(value: unknown): boolean {
|
|
13234
|
+
if (Array.isArray(value)) return value.some(hasCollectionCurrentRef);
|
|
13235
|
+
if (!isRecord(value)) return false;
|
|
13236
|
+
if (value.$ref === 'collection:current') return true;
|
|
13237
|
+
return Object.values(value).some(hasCollectionCurrentRef);
|
|
13238
|
+
}
|
|
13239
|
+
|
|
13240
|
+
function replaceCollectionCurrentRefs(value: unknown, currentItem: unknown): unknown {
|
|
13241
|
+
if (Array.isArray(value)) return value.map((item) => replaceCollectionCurrentRefs(item, currentItem));
|
|
13242
|
+
if (!isRecord(value)) return value;
|
|
13243
|
+
if (value.$ref === 'collection:current') return cloneJson(currentItem);
|
|
13244
|
+
return Object.fromEntries(
|
|
13245
|
+
Object.entries(value).map(([key, entryValue]) => [key, replaceCollectionCurrentRefs(entryValue, currentItem)]),
|
|
13246
|
+
);
|
|
13247
|
+
}
|
|
13248
|
+
|
|
13249
|
+
function buildSaveStreamPagePayload(state: ProjectState, fallbackSlug: string): { slug: string; page: ProjectState['page'] } {
|
|
13250
|
+
const page = state.page;
|
|
13251
|
+
const collection = page.collection;
|
|
13252
|
+
if (!collection || !hasCollectionCurrentRef(page)) {
|
|
13253
|
+
return { slug: fallbackSlug, page };
|
|
13254
|
+
}
|
|
13255
|
+
|
|
13256
|
+
const concreteSlug = resolveAdminContentSlug();
|
|
13257
|
+
if (!concreteSlug) {
|
|
13258
|
+
throw new Error('Cannot resolve concrete admin route for collection page save.');
|
|
13259
|
+
}
|
|
13260
|
+
|
|
13261
|
+
const paramValue = resolveTemplateParamValue(page.slug, concreteSlug, collection.paramKey);
|
|
13262
|
+
if (!paramValue) {
|
|
13263
|
+
throw new Error(`Cannot resolve collection param "${collection.paramKey}" from route "${concreteSlug}".`);
|
|
13264
|
+
}
|
|
13265
|
+
|
|
13266
|
+
const collectionDocument = state.collections?.[collection.source];
|
|
13267
|
+
const currentItem = isRecord(collectionDocument) ? collectionDocument[paramValue] : undefined;
|
|
13268
|
+
if (!currentItem) {
|
|
13269
|
+
throw new Error(`Cannot resolve collection item "${collection.source}/${paramValue}" for save.`);
|
|
13270
|
+
}
|
|
13271
|
+
|
|
13272
|
+
const resolvedPage = replaceCollectionCurrentRefs(page, currentItem) as ProjectState['page'];
|
|
13273
|
+
return {
|
|
13274
|
+
slug: concreteSlug,
|
|
13275
|
+
page: {
|
|
13276
|
+
...resolvedPage,
|
|
13277
|
+
slug: concreteSlug,
|
|
13278
|
+
},
|
|
13279
|
+
};
|
|
13280
|
+
}
|
|
13281
|
+
|
|
13123
13282
|
export function useCloudSave() {
|
|
13124
13283
|
const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
|
|
13125
13284
|
const activeCloudSaveController = useRef<AbortController | null>(null);
|
|
@@ -13153,12 +13312,18 @@ export function useCloudSave() {
|
|
|
13153
13312
|
});
|
|
13154
13313
|
|
|
13155
13314
|
try {
|
|
13315
|
+
const savePage = buildSaveStreamPagePayload(payload.state, payload.slug);
|
|
13156
13316
|
await startCloudSaveStream({
|
|
13157
13317
|
apiBaseUrl: CLOUD_API_URL,
|
|
13158
13318
|
apiKey: CLOUD_API_KEY,
|
|
13159
|
-
path: `src/data/pages/${
|
|
13160
|
-
content:
|
|
13161
|
-
|
|
13319
|
+
path: `src/data/pages/${savePage.slug}.json`,
|
|
13320
|
+
content: savePage.page,
|
|
13321
|
+
additionalFiles: [
|
|
13322
|
+
{ path: 'src/data/config/site.json', content: payload.state.site },
|
|
13323
|
+
{ path: 'src/data/config/menu.json', content: payload.state.menu },
|
|
13324
|
+
],
|
|
13325
|
+
changedScopes: ['page', 'site', 'menu'],
|
|
13326
|
+
message: `Content update for ${savePage.slug} via Visual Editor`,
|
|
13162
13327
|
signal: controller.signal,
|
|
13163
13328
|
onStep: (event) => {
|
|
13164
13329
|
setCloudSaveUi((prev) => {
|
|
@@ -13322,7 +13487,7 @@ END_OF_FILE_CONTENT
|
|
|
13322
13487
|
echo "Creating src/lib/useOlonForms.ts..."
|
|
13323
13488
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
|
|
13324
13489
|
import { useCallback, useEffect, useState } from 'react';
|
|
13325
|
-
import type { FormState } from '@olonjs/
|
|
13490
|
+
import type { FormState } from '@olonjs/react';
|
|
13326
13491
|
|
|
13327
13492
|
const API_BASE =
|
|
13328
13493
|
(import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
|
|
@@ -13598,7 +13763,7 @@ export function useTenantBootstrap({
|
|
|
13598
13763
|
|
|
13599
13764
|
const [menuConfig, setMenuConfig] = useState<MenuConfig>(menuConfigSeed);
|
|
13600
13765
|
|
|
13601
|
-
const [themeConfig] = useState<ThemeConfig>(themeConfigSeed);
|
|
13766
|
+
const [themeConfig, setThemeConfig] = useState<ThemeConfig>(themeConfigSeed);
|
|
13602
13767
|
|
|
13603
13768
|
const [collections, setCollections] = useState<NonNullable<JsonPagesConfig['collections']>>(EMPTY_COLLECTIONS);
|
|
13604
13769
|
|
|
@@ -14114,6 +14279,10 @@ export function useTenantBootstrap({
|
|
|
14114
14279
|
|
|
14115
14280
|
setSiteConfig,
|
|
14116
14281
|
|
|
14282
|
+
setMenuConfig,
|
|
14283
|
+
|
|
14284
|
+
setThemeConfig,
|
|
14285
|
+
|
|
14117
14286
|
setCollections,
|
|
14118
14287
|
|
|
14119
14288
|
cloudApiCandidates,
|