@olonjs/cli 3.0.147 → 3.0.149
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 +176 -10
- package/assets/templates/alpha/src_tenant.sh +176 -10
- package/package.json +1 -1
|
@@ -1817,7 +1817,7 @@ 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.16",
|
|
1821
1821
|
"class-variance-authority": "^0.7.1",
|
|
1822
1822
|
"clsx": "^2.1.1",
|
|
1823
1823
|
"lucide-react": "^0.474.0",
|
|
@@ -2981,6 +2981,7 @@ import { useAssetsManifest } from '@/lib/useAssetsManifest';
|
|
|
2981
2981
|
import { useCloudSave } from '@/lib/useCloudSave';
|
|
2982
2982
|
import { useTenantBootstrap } from '@/lib/useTenantBootstrap';
|
|
2983
2983
|
import { useAdminStudioContent } from '@/lib/cloud/useAdminStudioContent';
|
|
2984
|
+
import { hydrateLocalProjectState } from '@/lib/hydrateLocalProjectState';
|
|
2984
2985
|
|
|
2985
2986
|
import tenantCss from './index.css?inline';
|
|
2986
2987
|
|
|
@@ -3047,7 +3048,10 @@ function App() {
|
|
|
3047
3048
|
};
|
|
3048
3049
|
}, [canPaintVisitor, bootstrap.enginePages, bootstrap.siteConfig]);
|
|
3049
3050
|
|
|
3050
|
-
const engineCollections =
|
|
3051
|
+
const engineCollections =
|
|
3052
|
+
bootstrap.isHotSaveMode || Object.keys(bootstrap.collections).length > 0
|
|
3053
|
+
? bootstrap.collections
|
|
3054
|
+
: fileCollections;
|
|
3051
3055
|
const engineRefDocuments = useMemo(
|
|
3052
3056
|
() => ({
|
|
3053
3057
|
'menu.json': bootstrap.menuConfig,
|
|
@@ -3085,6 +3089,15 @@ function App() {
|
|
|
3085
3089
|
});
|
|
3086
3090
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
3087
3091
|
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
3092
|
+
hydrateLocalProjectState({
|
|
3093
|
+
state,
|
|
3094
|
+
slug,
|
|
3095
|
+
setPages: bootstrap.setPages,
|
|
3096
|
+
setSiteConfig: bootstrap.setSiteConfig,
|
|
3097
|
+
setMenuConfig: bootstrap.setMenuConfig,
|
|
3098
|
+
setThemeConfig: bootstrap.setThemeConfig,
|
|
3099
|
+
setCollections: bootstrap.setCollections,
|
|
3100
|
+
});
|
|
3088
3101
|
},
|
|
3089
3102
|
async hotSave(state: ProjectState, slug: string): Promise<void> {
|
|
3090
3103
|
if (!bootstrap.isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
@@ -11790,6 +11803,47 @@ export function getFilePages(): Record<string, PageConfig> {
|
|
|
11790
11803
|
return record;
|
|
11791
11804
|
}
|
|
11792
11805
|
|
|
11806
|
+
END_OF_FILE_CONTENT
|
|
11807
|
+
echo "Creating src/lib/hydrateLocalProjectState.ts..."
|
|
11808
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/hydrateLocalProjectState.ts"
|
|
11809
|
+
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
11810
|
+
import type { MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
11811
|
+
|
|
11812
|
+
type HydrateLocalProjectStateArgs = {
|
|
11813
|
+
state: ProjectState;
|
|
11814
|
+
slug: string;
|
|
11815
|
+
setPages: (updater: (prev: Record<string, PageConfig>) => Record<string, PageConfig>) => void;
|
|
11816
|
+
setSiteConfig: (site: SiteConfig) => void;
|
|
11817
|
+
setMenuConfig: (menu: MenuConfig) => void;
|
|
11818
|
+
setThemeConfig: (theme: ThemeConfig) => void;
|
|
11819
|
+
setCollections: (collections: NonNullable<JsonPagesConfig['collections']>) => void;
|
|
11820
|
+
};
|
|
11821
|
+
|
|
11822
|
+
/**
|
|
11823
|
+
* Write-through hydrate after local `/api/save-to-file`.
|
|
11824
|
+
* Pushes only the slices present in the saved ProjectState into bootstrap state —
|
|
11825
|
+
* no disk re-read, no full remount. Brand-agnostic.
|
|
11826
|
+
*/
|
|
11827
|
+
export function hydrateLocalProjectState({
|
|
11828
|
+
state,
|
|
11829
|
+
slug,
|
|
11830
|
+
setPages,
|
|
11831
|
+
setSiteConfig,
|
|
11832
|
+
setMenuConfig,
|
|
11833
|
+
setThemeConfig,
|
|
11834
|
+
setCollections,
|
|
11835
|
+
}: HydrateLocalProjectStateArgs): void {
|
|
11836
|
+
if (state.menu != null) setMenuConfig(state.menu);
|
|
11837
|
+
if (state.site != null) setSiteConfig(state.site);
|
|
11838
|
+
if (state.theme != null) setThemeConfig(state.theme);
|
|
11839
|
+
if (state.page != null) {
|
|
11840
|
+
setPages((prev) => ({ ...prev, [slug]: state.page }));
|
|
11841
|
+
}
|
|
11842
|
+
if (state.collections != null) {
|
|
11843
|
+
setCollections(state.collections as NonNullable<JsonPagesConfig['collections']>);
|
|
11844
|
+
}
|
|
11845
|
+
}
|
|
11846
|
+
|
|
11793
11847
|
END_OF_FILE_CONTENT
|
|
11794
11848
|
echo "Creating src/lib/schemas.ts..."
|
|
11795
11849
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
@@ -12862,10 +12916,18 @@ export function buildThemeFontVarsCss(input: unknown): string {
|
|
|
12862
12916
|
const tokens = isObjectRecord(input.tokens) ? input.tokens : null;
|
|
12863
12917
|
const typography = tokens && isObjectRecord(tokens.typography) ? tokens.typography : null;
|
|
12864
12918
|
const fontFamily = typography && isObjectRecord(typography.fontFamily) ? typography.fontFamily : null;
|
|
12865
|
-
const primary =
|
|
12866
|
-
|
|
12919
|
+
const primary =
|
|
12920
|
+
typeof fontFamily?.primary === 'string'
|
|
12921
|
+
? fontFamily.primary
|
|
12922
|
+
: "'Instrument Sans', system-ui, sans-serif";
|
|
12923
|
+
const display =
|
|
12924
|
+
typeof fontFamily?.display === 'string'
|
|
12925
|
+
? fontFamily.display
|
|
12926
|
+
: typeof fontFamily?.serif === 'string'
|
|
12927
|
+
? fontFamily.serif
|
|
12928
|
+
: "'Instrument Serif', Georgia, serif";
|
|
12867
12929
|
const mono = typeof fontFamily?.mono === 'string' ? fontFamily.mono : "'JetBrains Mono', monospace";
|
|
12868
|
-
return `:root{--theme-font-primary:${primary};--theme-font-
|
|
12930
|
+
return `:root{--theme-font-primary:${primary};--theme-font-display:${display};--theme-font-mono:${mono};}`;
|
|
12869
12931
|
}
|
|
12870
12932
|
|
|
12871
12933
|
const REMOTE_CSS_LINK_ATTR = 'data-jp-tenant-remote-css';
|
|
@@ -13094,7 +13156,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/lib/useCloudSave.ts"
|
|
|
13094
13156
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13095
13157
|
import type { DeployPhase, ProjectState, StepId } from '@olonjs/core';
|
|
13096
13158
|
import { DEPLOY_STEPS, startCloudSaveStream } from '@olonjs/core';
|
|
13097
|
-
import { CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
|
|
13159
|
+
import { APP_BASE_PATH, CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
|
|
13098
13160
|
|
|
13099
13161
|
interface CloudSaveUiState {
|
|
13100
13162
|
isOpen: boolean;
|
|
@@ -13120,6 +13182,100 @@ function stepProgress(doneSteps: StepId[]): number {
|
|
|
13120
13182
|
return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
|
|
13121
13183
|
}
|
|
13122
13184
|
|
|
13185
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13186
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13187
|
+
}
|
|
13188
|
+
|
|
13189
|
+
function cloneJson<T>(value: T): T {
|
|
13190
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
13191
|
+
}
|
|
13192
|
+
|
|
13193
|
+
function normalizePathSegments(value: string): string {
|
|
13194
|
+
return value
|
|
13195
|
+
.split('/')
|
|
13196
|
+
.map((segment) => segment.trim())
|
|
13197
|
+
.filter(Boolean)
|
|
13198
|
+
.join('/');
|
|
13199
|
+
}
|
|
13200
|
+
|
|
13201
|
+
function resolveAdminContentSlug(): string | null {
|
|
13202
|
+
if (typeof window === 'undefined') return null;
|
|
13203
|
+
|
|
13204
|
+
const normalizedBase = APP_BASE_PATH.replace(/\/+$/, '');
|
|
13205
|
+
let path = window.location.pathname;
|
|
13206
|
+
if (normalizedBase && normalizedBase !== '/' && path.startsWith(normalizedBase)) {
|
|
13207
|
+
path = path.slice(normalizedBase.length) || '/';
|
|
13208
|
+
}
|
|
13209
|
+
|
|
13210
|
+
const slug = normalizePathSegments(path.replace(/^\/admin\/?/, ''));
|
|
13211
|
+
return slug || null;
|
|
13212
|
+
}
|
|
13213
|
+
|
|
13214
|
+
function resolveTemplateParamValue(templateSlug: string, concreteSlug: string, paramKey: string): string | null {
|
|
13215
|
+
const templateSegments = normalizePathSegments(templateSlug).split('/').filter(Boolean);
|
|
13216
|
+
const concreteSegments = normalizePathSegments(concreteSlug).split('/').filter(Boolean);
|
|
13217
|
+
if (templateSegments.length !== concreteSegments.length) return null;
|
|
13218
|
+
|
|
13219
|
+
for (let index = 0; index < templateSegments.length; index += 1) {
|
|
13220
|
+
const templateSegment = templateSegments[index];
|
|
13221
|
+
const concreteSegment = concreteSegments[index];
|
|
13222
|
+
const paramMatch = templateSegment.match(/^\[([A-Za-z0-9_-]+)\]$/);
|
|
13223
|
+
if (paramMatch?.[1] === paramKey) return concreteSegment;
|
|
13224
|
+
if (!paramMatch && templateSegment !== concreteSegment) return null;
|
|
13225
|
+
}
|
|
13226
|
+
|
|
13227
|
+
return null;
|
|
13228
|
+
}
|
|
13229
|
+
|
|
13230
|
+
function hasCollectionCurrentRef(value: unknown): boolean {
|
|
13231
|
+
if (Array.isArray(value)) return value.some(hasCollectionCurrentRef);
|
|
13232
|
+
if (!isRecord(value)) return false;
|
|
13233
|
+
if (value.$ref === 'collection:current') return true;
|
|
13234
|
+
return Object.values(value).some(hasCollectionCurrentRef);
|
|
13235
|
+
}
|
|
13236
|
+
|
|
13237
|
+
function replaceCollectionCurrentRefs(value: unknown, currentItem: unknown): unknown {
|
|
13238
|
+
if (Array.isArray(value)) return value.map((item) => replaceCollectionCurrentRefs(item, currentItem));
|
|
13239
|
+
if (!isRecord(value)) return value;
|
|
13240
|
+
if (value.$ref === 'collection:current') return cloneJson(currentItem);
|
|
13241
|
+
return Object.fromEntries(
|
|
13242
|
+
Object.entries(value).map(([key, entryValue]) => [key, replaceCollectionCurrentRefs(entryValue, currentItem)]),
|
|
13243
|
+
);
|
|
13244
|
+
}
|
|
13245
|
+
|
|
13246
|
+
function buildSaveStreamPagePayload(state: ProjectState, fallbackSlug: string): { slug: string; page: ProjectState['page'] } {
|
|
13247
|
+
const page = state.page;
|
|
13248
|
+
const collection = page.collection;
|
|
13249
|
+
if (!collection || !hasCollectionCurrentRef(page)) {
|
|
13250
|
+
return { slug: fallbackSlug, page };
|
|
13251
|
+
}
|
|
13252
|
+
|
|
13253
|
+
const concreteSlug = resolveAdminContentSlug();
|
|
13254
|
+
if (!concreteSlug) {
|
|
13255
|
+
throw new Error('Cannot resolve concrete admin route for collection page save.');
|
|
13256
|
+
}
|
|
13257
|
+
|
|
13258
|
+
const paramValue = resolveTemplateParamValue(page.slug, concreteSlug, collection.paramKey);
|
|
13259
|
+
if (!paramValue) {
|
|
13260
|
+
throw new Error(`Cannot resolve collection param "${collection.paramKey}" from route "${concreteSlug}".`);
|
|
13261
|
+
}
|
|
13262
|
+
|
|
13263
|
+
const collectionDocument = state.collections?.[collection.source];
|
|
13264
|
+
const currentItem = isRecord(collectionDocument) ? collectionDocument[paramValue] : undefined;
|
|
13265
|
+
if (!currentItem) {
|
|
13266
|
+
throw new Error(`Cannot resolve collection item "${collection.source}/${paramValue}" for save.`);
|
|
13267
|
+
}
|
|
13268
|
+
|
|
13269
|
+
const resolvedPage = replaceCollectionCurrentRefs(page, currentItem) as ProjectState['page'];
|
|
13270
|
+
return {
|
|
13271
|
+
slug: concreteSlug,
|
|
13272
|
+
page: {
|
|
13273
|
+
...resolvedPage,
|
|
13274
|
+
slug: concreteSlug,
|
|
13275
|
+
},
|
|
13276
|
+
};
|
|
13277
|
+
}
|
|
13278
|
+
|
|
13123
13279
|
export function useCloudSave() {
|
|
13124
13280
|
const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
|
|
13125
13281
|
const activeCloudSaveController = useRef<AbortController | null>(null);
|
|
@@ -13153,12 +13309,18 @@ export function useCloudSave() {
|
|
|
13153
13309
|
});
|
|
13154
13310
|
|
|
13155
13311
|
try {
|
|
13312
|
+
const savePage = buildSaveStreamPagePayload(payload.state, payload.slug);
|
|
13156
13313
|
await startCloudSaveStream({
|
|
13157
13314
|
apiBaseUrl: CLOUD_API_URL,
|
|
13158
13315
|
apiKey: CLOUD_API_KEY,
|
|
13159
|
-
path: `src/data/pages/${
|
|
13160
|
-
content:
|
|
13161
|
-
|
|
13316
|
+
path: `src/data/pages/${savePage.slug}.json`,
|
|
13317
|
+
content: savePage.page,
|
|
13318
|
+
additionalFiles: [
|
|
13319
|
+
{ path: 'src/data/config/site.json', content: payload.state.site },
|
|
13320
|
+
{ path: 'src/data/config/menu.json', content: payload.state.menu },
|
|
13321
|
+
],
|
|
13322
|
+
changedScopes: ['page', 'site', 'menu'],
|
|
13323
|
+
message: `Content update for ${savePage.slug} via Visual Editor`,
|
|
13162
13324
|
signal: controller.signal,
|
|
13163
13325
|
onStep: (event) => {
|
|
13164
13326
|
setCloudSaveUi((prev) => {
|
|
@@ -13598,7 +13760,7 @@ export function useTenantBootstrap({
|
|
|
13598
13760
|
|
|
13599
13761
|
const [menuConfig, setMenuConfig] = useState<MenuConfig>(menuConfigSeed);
|
|
13600
13762
|
|
|
13601
|
-
const [themeConfig] = useState<ThemeConfig>(themeConfigSeed);
|
|
13763
|
+
const [themeConfig, setThemeConfig] = useState<ThemeConfig>(themeConfigSeed);
|
|
13602
13764
|
|
|
13603
13765
|
const [collections, setCollections] = useState<NonNullable<JsonPagesConfig['collections']>>(EMPTY_COLLECTIONS);
|
|
13604
13766
|
|
|
@@ -14114,6 +14276,10 @@ export function useTenantBootstrap({
|
|
|
14114
14276
|
|
|
14115
14277
|
setSiteConfig,
|
|
14116
14278
|
|
|
14279
|
+
setMenuConfig,
|
|
14280
|
+
|
|
14281
|
+
setThemeConfig,
|
|
14282
|
+
|
|
14117
14283
|
setCollections,
|
|
14118
14284
|
|
|
14119
14285
|
cloudApiCandidates,
|
|
@@ -1817,7 +1817,7 @@ 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.16",
|
|
1821
1821
|
"class-variance-authority": "^0.7.1",
|
|
1822
1822
|
"clsx": "^2.1.1",
|
|
1823
1823
|
"lucide-react": "^0.474.0",
|
|
@@ -2981,6 +2981,7 @@ import { useAssetsManifest } from '@/lib/useAssetsManifest';
|
|
|
2981
2981
|
import { useCloudSave } from '@/lib/useCloudSave';
|
|
2982
2982
|
import { useTenantBootstrap } from '@/lib/useTenantBootstrap';
|
|
2983
2983
|
import { useAdminStudioContent } from '@/lib/cloud/useAdminStudioContent';
|
|
2984
|
+
import { hydrateLocalProjectState } from '@/lib/hydrateLocalProjectState';
|
|
2984
2985
|
|
|
2985
2986
|
import tenantCss from './index.css?inline';
|
|
2986
2987
|
|
|
@@ -3047,7 +3048,10 @@ function App() {
|
|
|
3047
3048
|
};
|
|
3048
3049
|
}, [canPaintVisitor, bootstrap.enginePages, bootstrap.siteConfig]);
|
|
3049
3050
|
|
|
3050
|
-
const engineCollections =
|
|
3051
|
+
const engineCollections =
|
|
3052
|
+
bootstrap.isHotSaveMode || Object.keys(bootstrap.collections).length > 0
|
|
3053
|
+
? bootstrap.collections
|
|
3054
|
+
: fileCollections;
|
|
3051
3055
|
const engineRefDocuments = useMemo(
|
|
3052
3056
|
() => ({
|
|
3053
3057
|
'menu.json': bootstrap.menuConfig,
|
|
@@ -3085,6 +3089,15 @@ function App() {
|
|
|
3085
3089
|
});
|
|
3086
3090
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
3087
3091
|
if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
|
|
3092
|
+
hydrateLocalProjectState({
|
|
3093
|
+
state,
|
|
3094
|
+
slug,
|
|
3095
|
+
setPages: bootstrap.setPages,
|
|
3096
|
+
setSiteConfig: bootstrap.setSiteConfig,
|
|
3097
|
+
setMenuConfig: bootstrap.setMenuConfig,
|
|
3098
|
+
setThemeConfig: bootstrap.setThemeConfig,
|
|
3099
|
+
setCollections: bootstrap.setCollections,
|
|
3100
|
+
});
|
|
3088
3101
|
},
|
|
3089
3102
|
async hotSave(state: ProjectState, slug: string): Promise<void> {
|
|
3090
3103
|
if (!bootstrap.isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
@@ -11790,6 +11803,47 @@ export function getFilePages(): Record<string, PageConfig> {
|
|
|
11790
11803
|
return record;
|
|
11791
11804
|
}
|
|
11792
11805
|
|
|
11806
|
+
END_OF_FILE_CONTENT
|
|
11807
|
+
echo "Creating src/lib/hydrateLocalProjectState.ts..."
|
|
11808
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/hydrateLocalProjectState.ts"
|
|
11809
|
+
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
11810
|
+
import type { MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
11811
|
+
|
|
11812
|
+
type HydrateLocalProjectStateArgs = {
|
|
11813
|
+
state: ProjectState;
|
|
11814
|
+
slug: string;
|
|
11815
|
+
setPages: (updater: (prev: Record<string, PageConfig>) => Record<string, PageConfig>) => void;
|
|
11816
|
+
setSiteConfig: (site: SiteConfig) => void;
|
|
11817
|
+
setMenuConfig: (menu: MenuConfig) => void;
|
|
11818
|
+
setThemeConfig: (theme: ThemeConfig) => void;
|
|
11819
|
+
setCollections: (collections: NonNullable<JsonPagesConfig['collections']>) => void;
|
|
11820
|
+
};
|
|
11821
|
+
|
|
11822
|
+
/**
|
|
11823
|
+
* Write-through hydrate after local `/api/save-to-file`.
|
|
11824
|
+
* Pushes only the slices present in the saved ProjectState into bootstrap state —
|
|
11825
|
+
* no disk re-read, no full remount. Brand-agnostic.
|
|
11826
|
+
*/
|
|
11827
|
+
export function hydrateLocalProjectState({
|
|
11828
|
+
state,
|
|
11829
|
+
slug,
|
|
11830
|
+
setPages,
|
|
11831
|
+
setSiteConfig,
|
|
11832
|
+
setMenuConfig,
|
|
11833
|
+
setThemeConfig,
|
|
11834
|
+
setCollections,
|
|
11835
|
+
}: HydrateLocalProjectStateArgs): void {
|
|
11836
|
+
if (state.menu != null) setMenuConfig(state.menu);
|
|
11837
|
+
if (state.site != null) setSiteConfig(state.site);
|
|
11838
|
+
if (state.theme != null) setThemeConfig(state.theme);
|
|
11839
|
+
if (state.page != null) {
|
|
11840
|
+
setPages((prev) => ({ ...prev, [slug]: state.page }));
|
|
11841
|
+
}
|
|
11842
|
+
if (state.collections != null) {
|
|
11843
|
+
setCollections(state.collections as NonNullable<JsonPagesConfig['collections']>);
|
|
11844
|
+
}
|
|
11845
|
+
}
|
|
11846
|
+
|
|
11793
11847
|
END_OF_FILE_CONTENT
|
|
11794
11848
|
echo "Creating src/lib/schemas.ts..."
|
|
11795
11849
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
@@ -12862,10 +12916,18 @@ export function buildThemeFontVarsCss(input: unknown): string {
|
|
|
12862
12916
|
const tokens = isObjectRecord(input.tokens) ? input.tokens : null;
|
|
12863
12917
|
const typography = tokens && isObjectRecord(tokens.typography) ? tokens.typography : null;
|
|
12864
12918
|
const fontFamily = typography && isObjectRecord(typography.fontFamily) ? typography.fontFamily : null;
|
|
12865
|
-
const primary =
|
|
12866
|
-
|
|
12919
|
+
const primary =
|
|
12920
|
+
typeof fontFamily?.primary === 'string'
|
|
12921
|
+
? fontFamily.primary
|
|
12922
|
+
: "'Instrument Sans', system-ui, sans-serif";
|
|
12923
|
+
const display =
|
|
12924
|
+
typeof fontFamily?.display === 'string'
|
|
12925
|
+
? fontFamily.display
|
|
12926
|
+
: typeof fontFamily?.serif === 'string'
|
|
12927
|
+
? fontFamily.serif
|
|
12928
|
+
: "'Instrument Serif', Georgia, serif";
|
|
12867
12929
|
const mono = typeof fontFamily?.mono === 'string' ? fontFamily.mono : "'JetBrains Mono', monospace";
|
|
12868
|
-
return `:root{--theme-font-primary:${primary};--theme-font-
|
|
12930
|
+
return `:root{--theme-font-primary:${primary};--theme-font-display:${display};--theme-font-mono:${mono};}`;
|
|
12869
12931
|
}
|
|
12870
12932
|
|
|
12871
12933
|
const REMOTE_CSS_LINK_ATTR = 'data-jp-tenant-remote-css';
|
|
@@ -13094,7 +13156,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/lib/useCloudSave.ts"
|
|
|
13094
13156
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13095
13157
|
import type { DeployPhase, ProjectState, StepId } from '@olonjs/core';
|
|
13096
13158
|
import { DEPLOY_STEPS, startCloudSaveStream } from '@olonjs/core';
|
|
13097
|
-
import { CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
|
|
13159
|
+
import { APP_BASE_PATH, CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
|
|
13098
13160
|
|
|
13099
13161
|
interface CloudSaveUiState {
|
|
13100
13162
|
isOpen: boolean;
|
|
@@ -13120,6 +13182,100 @@ function stepProgress(doneSteps: StepId[]): number {
|
|
|
13120
13182
|
return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
|
|
13121
13183
|
}
|
|
13122
13184
|
|
|
13185
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13186
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13187
|
+
}
|
|
13188
|
+
|
|
13189
|
+
function cloneJson<T>(value: T): T {
|
|
13190
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
13191
|
+
}
|
|
13192
|
+
|
|
13193
|
+
function normalizePathSegments(value: string): string {
|
|
13194
|
+
return value
|
|
13195
|
+
.split('/')
|
|
13196
|
+
.map((segment) => segment.trim())
|
|
13197
|
+
.filter(Boolean)
|
|
13198
|
+
.join('/');
|
|
13199
|
+
}
|
|
13200
|
+
|
|
13201
|
+
function resolveAdminContentSlug(): string | null {
|
|
13202
|
+
if (typeof window === 'undefined') return null;
|
|
13203
|
+
|
|
13204
|
+
const normalizedBase = APP_BASE_PATH.replace(/\/+$/, '');
|
|
13205
|
+
let path = window.location.pathname;
|
|
13206
|
+
if (normalizedBase && normalizedBase !== '/' && path.startsWith(normalizedBase)) {
|
|
13207
|
+
path = path.slice(normalizedBase.length) || '/';
|
|
13208
|
+
}
|
|
13209
|
+
|
|
13210
|
+
const slug = normalizePathSegments(path.replace(/^\/admin\/?/, ''));
|
|
13211
|
+
return slug || null;
|
|
13212
|
+
}
|
|
13213
|
+
|
|
13214
|
+
function resolveTemplateParamValue(templateSlug: string, concreteSlug: string, paramKey: string): string | null {
|
|
13215
|
+
const templateSegments = normalizePathSegments(templateSlug).split('/').filter(Boolean);
|
|
13216
|
+
const concreteSegments = normalizePathSegments(concreteSlug).split('/').filter(Boolean);
|
|
13217
|
+
if (templateSegments.length !== concreteSegments.length) return null;
|
|
13218
|
+
|
|
13219
|
+
for (let index = 0; index < templateSegments.length; index += 1) {
|
|
13220
|
+
const templateSegment = templateSegments[index];
|
|
13221
|
+
const concreteSegment = concreteSegments[index];
|
|
13222
|
+
const paramMatch = templateSegment.match(/^\[([A-Za-z0-9_-]+)\]$/);
|
|
13223
|
+
if (paramMatch?.[1] === paramKey) return concreteSegment;
|
|
13224
|
+
if (!paramMatch && templateSegment !== concreteSegment) return null;
|
|
13225
|
+
}
|
|
13226
|
+
|
|
13227
|
+
return null;
|
|
13228
|
+
}
|
|
13229
|
+
|
|
13230
|
+
function hasCollectionCurrentRef(value: unknown): boolean {
|
|
13231
|
+
if (Array.isArray(value)) return value.some(hasCollectionCurrentRef);
|
|
13232
|
+
if (!isRecord(value)) return false;
|
|
13233
|
+
if (value.$ref === 'collection:current') return true;
|
|
13234
|
+
return Object.values(value).some(hasCollectionCurrentRef);
|
|
13235
|
+
}
|
|
13236
|
+
|
|
13237
|
+
function replaceCollectionCurrentRefs(value: unknown, currentItem: unknown): unknown {
|
|
13238
|
+
if (Array.isArray(value)) return value.map((item) => replaceCollectionCurrentRefs(item, currentItem));
|
|
13239
|
+
if (!isRecord(value)) return value;
|
|
13240
|
+
if (value.$ref === 'collection:current') return cloneJson(currentItem);
|
|
13241
|
+
return Object.fromEntries(
|
|
13242
|
+
Object.entries(value).map(([key, entryValue]) => [key, replaceCollectionCurrentRefs(entryValue, currentItem)]),
|
|
13243
|
+
);
|
|
13244
|
+
}
|
|
13245
|
+
|
|
13246
|
+
function buildSaveStreamPagePayload(state: ProjectState, fallbackSlug: string): { slug: string; page: ProjectState['page'] } {
|
|
13247
|
+
const page = state.page;
|
|
13248
|
+
const collection = page.collection;
|
|
13249
|
+
if (!collection || !hasCollectionCurrentRef(page)) {
|
|
13250
|
+
return { slug: fallbackSlug, page };
|
|
13251
|
+
}
|
|
13252
|
+
|
|
13253
|
+
const concreteSlug = resolveAdminContentSlug();
|
|
13254
|
+
if (!concreteSlug) {
|
|
13255
|
+
throw new Error('Cannot resolve concrete admin route for collection page save.');
|
|
13256
|
+
}
|
|
13257
|
+
|
|
13258
|
+
const paramValue = resolveTemplateParamValue(page.slug, concreteSlug, collection.paramKey);
|
|
13259
|
+
if (!paramValue) {
|
|
13260
|
+
throw new Error(`Cannot resolve collection param "${collection.paramKey}" from route "${concreteSlug}".`);
|
|
13261
|
+
}
|
|
13262
|
+
|
|
13263
|
+
const collectionDocument = state.collections?.[collection.source];
|
|
13264
|
+
const currentItem = isRecord(collectionDocument) ? collectionDocument[paramValue] : undefined;
|
|
13265
|
+
if (!currentItem) {
|
|
13266
|
+
throw new Error(`Cannot resolve collection item "${collection.source}/${paramValue}" for save.`);
|
|
13267
|
+
}
|
|
13268
|
+
|
|
13269
|
+
const resolvedPage = replaceCollectionCurrentRefs(page, currentItem) as ProjectState['page'];
|
|
13270
|
+
return {
|
|
13271
|
+
slug: concreteSlug,
|
|
13272
|
+
page: {
|
|
13273
|
+
...resolvedPage,
|
|
13274
|
+
slug: concreteSlug,
|
|
13275
|
+
},
|
|
13276
|
+
};
|
|
13277
|
+
}
|
|
13278
|
+
|
|
13123
13279
|
export function useCloudSave() {
|
|
13124
13280
|
const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
|
|
13125
13281
|
const activeCloudSaveController = useRef<AbortController | null>(null);
|
|
@@ -13153,12 +13309,18 @@ export function useCloudSave() {
|
|
|
13153
13309
|
});
|
|
13154
13310
|
|
|
13155
13311
|
try {
|
|
13312
|
+
const savePage = buildSaveStreamPagePayload(payload.state, payload.slug);
|
|
13156
13313
|
await startCloudSaveStream({
|
|
13157
13314
|
apiBaseUrl: CLOUD_API_URL,
|
|
13158
13315
|
apiKey: CLOUD_API_KEY,
|
|
13159
|
-
path: `src/data/pages/${
|
|
13160
|
-
content:
|
|
13161
|
-
|
|
13316
|
+
path: `src/data/pages/${savePage.slug}.json`,
|
|
13317
|
+
content: savePage.page,
|
|
13318
|
+
additionalFiles: [
|
|
13319
|
+
{ path: 'src/data/config/site.json', content: payload.state.site },
|
|
13320
|
+
{ path: 'src/data/config/menu.json', content: payload.state.menu },
|
|
13321
|
+
],
|
|
13322
|
+
changedScopes: ['page', 'site', 'menu'],
|
|
13323
|
+
message: `Content update for ${savePage.slug} via Visual Editor`,
|
|
13162
13324
|
signal: controller.signal,
|
|
13163
13325
|
onStep: (event) => {
|
|
13164
13326
|
setCloudSaveUi((prev) => {
|
|
@@ -13598,7 +13760,7 @@ export function useTenantBootstrap({
|
|
|
13598
13760
|
|
|
13599
13761
|
const [menuConfig, setMenuConfig] = useState<MenuConfig>(menuConfigSeed);
|
|
13600
13762
|
|
|
13601
|
-
const [themeConfig] = useState<ThemeConfig>(themeConfigSeed);
|
|
13763
|
+
const [themeConfig, setThemeConfig] = useState<ThemeConfig>(themeConfigSeed);
|
|
13602
13764
|
|
|
13603
13765
|
const [collections, setCollections] = useState<NonNullable<JsonPagesConfig['collections']>>(EMPTY_COLLECTIONS);
|
|
13604
13766
|
|
|
@@ -14114,6 +14276,10 @@ export function useTenantBootstrap({
|
|
|
14114
14276
|
|
|
14115
14277
|
setSiteConfig,
|
|
14116
14278
|
|
|
14279
|
+
setMenuConfig,
|
|
14280
|
+
|
|
14281
|
+
setThemeConfig,
|
|
14282
|
+
|
|
14117
14283
|
setCollections,
|
|
14118
14284
|
|
|
14119
14285
|
cloudApiCandidates,
|