feat(module-studio): add configurable map page
This commit is contained in:
@@ -58,6 +58,7 @@ import {
|
||||
type DesignProfileSummary,
|
||||
type DesignProfileStatus,
|
||||
} from "./applicationManifest.js";
|
||||
import { MapFixturePreview, type MapFixturePreviewHandle, type MapPageLayout } from "./MapFixturePreview.js";
|
||||
|
||||
type CatalogSection = "controls" | "media" | "glass" | "modals" | "icons";
|
||||
type StudioContext = "visual" | "pages" | "applications";
|
||||
@@ -306,6 +307,10 @@ export function CatalogApp() {
|
||||
const [applicationDraft, setApplicationDraft] = useState<ApplicationManifestV01 | null>(null);
|
||||
const [applicationSaveState, setApplicationSaveState] = useState<ApplicationDraftSaveState>("idle");
|
||||
const [applicationError, setApplicationError] = useState("");
|
||||
const [mapTemplateLayout, setMapTemplateLayout] = useState<MapPageLayout | null>(null);
|
||||
const [mapTemplateSaveState, setMapTemplateSaveState] = useState<"idle" | "loading" | "saving" | "saved" | "error">("loading");
|
||||
const mapTemplatePreviewRef = useRef<MapFixturePreviewHandle>(null);
|
||||
const applicationMapPreviewRef = useRef<MapFixturePreviewHandle>(null);
|
||||
const [createModuleOpen, setCreateModuleOpen] = useState(false);
|
||||
const [createModuleName, setCreateModuleName] = useState("");
|
||||
const [createModuleSlug, setCreateModuleSlug] = useState("");
|
||||
@@ -496,6 +501,24 @@ export function CatalogApp() {
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
fetch("/api/page-layouts/map", { cache: "no-store" })
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error("map_page_layout_load_failed");
|
||||
return await response.json() as MapPageLayout | null;
|
||||
})
|
||||
.then((layout) => {
|
||||
if (!active) return;
|
||||
setMapTemplateLayout(layout);
|
||||
setMapTemplateSaveState(layout ? "saved" : "idle");
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) setMapTemplateSaveState("error");
|
||||
});
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
fetch("/api/design-profiles", { cache: "no-store" })
|
||||
@@ -712,13 +735,21 @@ export function CatalogApp() {
|
||||
|
||||
const saveApplicationDraft = async () => {
|
||||
if (!applicationDraft) return;
|
||||
const activeMapLayout = activeApplicationPageId ? applicationMapPreviewRef.current?.getLayout() : null;
|
||||
const draftToSave = activeMapLayout && activeApplicationPageId ? {
|
||||
...applicationDraft,
|
||||
pages: applicationDraft.pages.map((page) => page.id === activeApplicationPageId ? {
|
||||
...page,
|
||||
layout: { ...page.layout, map: activeMapLayout },
|
||||
} : page),
|
||||
} : applicationDraft;
|
||||
setApplicationSaveState("saving");
|
||||
setApplicationError("");
|
||||
try {
|
||||
const response = await fetch(`/api/applications/${applicationDraft.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(applicationDraft),
|
||||
body: JSON.stringify(draftToSave),
|
||||
});
|
||||
if (!response.ok) throw new Error("application_save_failed");
|
||||
const saved = await response.json() as ApplicationManifestV01;
|
||||
@@ -743,6 +774,27 @@ export function CatalogApp() {
|
||||
}
|
||||
};
|
||||
|
||||
const saveMapPageTemplate = async () => {
|
||||
const layout = mapTemplatePreviewRef.current?.getLayout();
|
||||
if (!layout) {
|
||||
setMapTemplateSaveState("error");
|
||||
return;
|
||||
}
|
||||
setMapTemplateSaveState("saving");
|
||||
try {
|
||||
const response = await fetch("/api/page-layouts/map", {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(layout),
|
||||
});
|
||||
if (!response.ok) throw new Error("map_page_layout_save_failed");
|
||||
setMapTemplateLayout(await response.json() as MapPageLayout);
|
||||
setMapTemplateSaveState("saved");
|
||||
} catch {
|
||||
setMapTemplateSaveState("error");
|
||||
}
|
||||
};
|
||||
|
||||
const changeStudioContext = (next: StudioContext) => {
|
||||
setStudioContext(next);
|
||||
workspace.openNavigation();
|
||||
@@ -1504,27 +1556,28 @@ export function CatalogApp() {
|
||||
};
|
||||
|
||||
const renderPageTemplate = (template: PageTemplateDefinition) => (
|
||||
template.id === "map" ? (
|
||||
<div className="catalog-page-template catalog-page-template--map">
|
||||
<MapFixturePreview
|
||||
key={`map-template-${mapTemplateLayout?.savedAt ?? "default"}`}
|
||||
ref={mapTemplatePreviewRef}
|
||||
expanded
|
||||
initialLayout={mapTemplateLayout}
|
||||
features={Object.fromEntries(template.features.map((feature) => [feature.id, feature.required ? true : feature.defaultVisible]))}
|
||||
/>
|
||||
<div className="catalog-page-template__actions">
|
||||
<Button variant="primary" shape="pill" icon={<Icon name="plus" />} onClick={() => { void createApplicationDraft(template); }}>Создать Application Draft</Button>
|
||||
<span>Карта масштабируется по высоте за правый нижний угол. Настройки — через Inspector.</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="catalog-page-template">
|
||||
<SettingsCard
|
||||
eyebrow={`PAGE TEMPLATE ${template.version}`}
|
||||
title={template.title}
|
||||
description={template.description}
|
||||
>
|
||||
<div className="catalog-map-template-preview" aria-label="Превью Map Page Template">
|
||||
<div className="catalog-map-template-preview__grid" />
|
||||
<div className="catalog-map-template-preview__route" />
|
||||
<div className="catalog-map-template-preview__point catalog-map-template-preview__point--one" />
|
||||
<div className="catalog-map-template-preview__point catalog-map-template-preview__point--two" />
|
||||
<div className="catalog-map-template-preview__actions">
|
||||
{template.actions.map((action) => <span key={action.id}><Icon name={action.feature === "assistant" ? "apps" : action.feature === "toolbar" ? "sliders" : "panel"} /></span>)}
|
||||
</div>
|
||||
<div className="catalog-map-template-preview__inspector">
|
||||
<strong>Inspector</strong>
|
||||
<span>Selection</span>
|
||||
<span>Layers</span>
|
||||
<span>Appearance</span>
|
||||
</div>
|
||||
</div>
|
||||
<MapFixturePreview features={Object.fromEntries(template.features.map((feature) => [feature.id, feature.required ? true : feature.defaultVisible]))} />
|
||||
<div className="catalog-page-template__actions">
|
||||
<Button variant="primary" shape="pill" icon={<Icon name="plus" />} onClick={() => { void createApplicationDraft(template); }}>Создать Application Draft</Button>
|
||||
<span>Только утверждённые features и slots — без свободного canvas.</span>
|
||||
@@ -1548,6 +1601,7 @@ export function CatalogApp() {
|
||||
</SettingsCard>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
const renderApplicationPage = () => {
|
||||
@@ -1562,14 +1616,7 @@ export function CatalogApp() {
|
||||
}));
|
||||
return (
|
||||
<div className="catalog-application-page">
|
||||
<div className="catalog-map-template-preview catalog-map-template-preview--application" aria-label={`Preview ${page.title}`}>
|
||||
<div className="catalog-map-template-preview__grid" />
|
||||
<div className="catalog-map-template-preview__route" />
|
||||
<div className="catalog-map-template-preview__point catalog-map-template-preview__point--one" />
|
||||
<div className="catalog-map-template-preview__point catalog-map-template-preview__point--two" />
|
||||
{page.features.toolbar ? <div className="catalog-map-template-preview__actions"><span><Icon name="panel" /></span><span><Icon name="sliders" /></span>{page.features.assistant ? <span><Icon name="apps" /></span> : null}</div> : null}
|
||||
{page.features.inspector ? <div className="catalog-map-template-preview__inspector"><strong>Inspector</strong><span>Selection</span><span>Layers</span><span>Appearance</span></div> : null}
|
||||
</div>
|
||||
<MapFixturePreview key={page.id} ref={applicationMapPreviewRef} initialLayout={page.layout?.map ?? null} features={page.features} expanded />
|
||||
{applicationMode === "edit" ? (
|
||||
<SettingsCard eyebrow="PAGE SETTINGS" title={page.title} description={`${template.title} · ${template.version}`}>
|
||||
<div className="catalog-application-draft__features">
|
||||
@@ -1735,6 +1782,12 @@ export function CatalogApp() {
|
||||
description={`${activePageTemplate.category} · contract ${activePageTemplate.schemaVersion} · template ${activePageTemplate.version}`}
|
||||
expanded={panelExpanded}
|
||||
onExpandedChange={workspace.setContentExpanded}
|
||||
utilityActions={activePageTemplate.id === "map" ? [{
|
||||
label: mapTemplateSaveState === "saving" ? "Сохраняем layout страницы" : mapTemplateSaveState === "saved" ? "Layout страницы сохранён" : mapTemplateSaveState === "error" ? "Повторить сохранение layout страницы" : "Сохранить layout страницы",
|
||||
icon: "save",
|
||||
onClick: () => { void saveMapPageTemplate(); },
|
||||
disabled: mapTemplateSaveState === "saving" || mapTemplateSaveState === "loading",
|
||||
}] : undefined}
|
||||
onClose={workspace.closeView}
|
||||
>
|
||||
<div className="catalog-panel-content">{renderPageTemplate(activePageTemplate)}</div>
|
||||
|
||||
Reference in New Issue
Block a user