Add versioned design profile releases

This commit is contained in:
DCCONSTRUCTIONS
2026-07-11 22:34:01 +03:00
parent 50718a33a1
commit 1f2dffc031
8 changed files with 473 additions and 17 deletions
+61 -9
View File
@@ -56,6 +56,7 @@ import {
type ApplicationManifestV01,
type ApplicationSummary,
type DesignProfileSummary,
type DesignProfileStatus,
} from "./applicationManifest.js";
type CatalogSection = "controls" | "media" | "glass" | "modals" | "icons";
@@ -315,6 +316,7 @@ export function CatalogApp() {
const [designProfileSaveOpen, setDesignProfileSaveOpen] = useState(false);
const [designProfileSaveMode, setDesignProfileSaveMode] = useState<"save" | "save-as">("save");
const [designProfileName, setDesignProfileName] = useState("");
const [designProfilePublishState, setDesignProfilePublishState] = useState<"idle" | "publishing" | "published" | "error">("idle");
const [activeDesignProfileId, setActiveDesignProfileId] = useState("default");
const workspace = useApplicationWorkspace<StudioView>();
const { navigationOpen: guidelineOpen, activeView, contentExpanded: panelExpanded } = workspace;
@@ -885,8 +887,18 @@ export function CatalogApp() {
body: JSON.stringify({ name: creating ? designProfileName : selected?.name, layout }),
});
if (!response.ok) return setLayoutSaveState("error");
const saved = await response.json() as { id: string; name: string; version: string; layout: StoredLayout; timestamps: { updatedAt: string } };
const summary: DesignProfileSummary = { id: saved.id, name: saved.name, version: saved.version, theme: saved.layout.theme === "light" ? "light" : "dark", updatedAt: saved.timestamps.updatedAt };
const saved = await response.json() as { id: string; name: string; version: string; status: DesignProfileStatus; layout: StoredLayout; timestamps: { updatedAt: string } };
const previous = designProfiles.find((profile) => profile.id === saved.id);
const summary: DesignProfileSummary = {
id: saved.id,
name: saved.name,
version: saved.version,
status: saved.status,
theme: saved.layout.theme === "light" ? "light" : "dark",
updatedAt: saved.timestamps.updatedAt,
versions: previous?.versions ?? [],
latestPublishedVersion: previous?.latestPublishedVersion ?? null,
};
setDesignProfiles((current) => [...current.filter((profile) => profile.id !== summary.id), summary].sort((left, right) => left.name.localeCompare(right.name)));
setActiveDesignProfileId(summary.id);
setDesignProfileSaveOpen(false);
@@ -894,6 +906,25 @@ export function CatalogApp() {
setLayoutSaveState("saved");
};
const publishDesignProfile = async () => {
const current = designProfiles.find((profile) => profile.id === activeDesignProfileId);
if (!current || current.versions.some((release) => release.version === current.version)) return;
setDesignProfilePublishState("publishing");
const response = await fetch(`/api/design-profiles/${current.id}/publish`, { method: "POST" });
if (!response.ok) {
setDesignProfilePublishState("error");
return;
}
const release = await response.json() as { version: string; status: DesignProfileStatus; layout: StoredLayout; timestamps: { publishedAt?: string } };
setDesignProfiles((profiles) => profiles.map((profile) => profile.id !== current.id ? profile : {
...profile,
versions: [{ version: release.version, status: release.status, theme: release.layout.theme === "light" ? "light" : "dark", publishedAt: release.timestamps.publishedAt }, ...profile.versions.filter((item) => item.version !== release.version)],
latestPublishedVersion: release.version,
}));
setDesignProfilePublishState("published");
setDesignProfileSaveOpen(false);
};
const addPageTemplateToApplication = (template: PageTemplateDefinition) => {
updateApplicationDraft((current) => {
const order = current.pages.length;
@@ -1312,6 +1343,19 @@ export function CatalogApp() {
const renderApplicationDraft = () => {
if (!applicationDraft || applicationDraft.id !== activeApplicationId) return null;
const selectedProfile = designProfiles.find((profile) => profile.id === applicationDraft.designProfile.id) ?? designProfiles[0];
const designProfileOptions = designProfiles.flatMap((profile) => [
...profile.versions.map((release) => ({
value: `${profile.id}@${release.version}@published`,
label: profile.name,
description: `${release.version} · Published · ${release.theme}`,
})),
{
value: `${profile.id}@${profile.version}@draft`,
label: `${profile.name} — Draft`,
description: `${profile.version} · Draft · ${profile.theme}`,
},
]);
const selectedProfileValue = `${applicationDraft.designProfile.id}@${applicationDraft.designProfile.version}@${applicationDraft.designProfile.status}`;
const availableTemplates = pageTemplates;
if (applicationMode === "preview") {
@@ -1373,13 +1417,17 @@ export function CatalogApp() {
>
<Select
label="Design Profile"
value={selectedProfile?.id ?? "default"}
options={designProfiles.map((profile) => ({ value: profile.id, label: profile.name, description: `${profile.version} · ${profile.theme}` }))}
onChange={(profileId) => {
value={designProfileOptions.some((option) => option.value === selectedProfileValue) ? selectedProfileValue : `${selectedProfile?.id ?? "default"}@${selectedProfile?.version ?? "0.6.0"}@draft`}
options={designProfileOptions}
onChange={(profileReference) => {
const [profileId, version, statusValue] = profileReference.split("@");
const profile = designProfiles.find((item) => item.id === profileId);
if (!profile) return;
setTheme(profile.theme);
updateApplicationDraft((current) => ({ ...current, designProfile: { id: profile.id, version: profile.version, theme: profile.theme } }));
const status: DesignProfileStatus = statusValue === "published" ? "published" : "draft";
const release = status === "published" ? profile.versions.find((item) => item.version === version) : undefined;
const profileTheme = release?.theme ?? profile.theme;
setTheme(profileTheme);
updateApplicationDraft((current) => ({ ...current, designProfile: { id: profile.id, version, status, theme: profileTheme } }));
}}
/>
</SettingsCard>
@@ -1623,7 +1671,7 @@ export function CatalogApp() {
className="catalog-module-switcher"
label="Design Profile"
value={activeDesignProfileId}
options={designProfiles.map((profile) => ({ value: profile.id, label: profile.name, description: `${profile.version} · ${profile.theme}`, icon: <Icon name="settings" /> }))}
options={designProfiles.map((profile) => ({ value: profile.id, label: profile.name, description: `${profile.version} · Draft · ${profile.versions.length} releases`, icon: <Icon name="settings" /> }))}
onChange={(id) => { void openDesignProfile(id); }}
/>
) : undefined}
@@ -1798,6 +1846,10 @@ export function CatalogApp() {
{designProfileSaveMode === "save" ? (
<>
<Button onClick={() => { void saveDesignProfile("save"); }}>Сохранить</Button>
<Button
disabled={designProfilePublishState === "publishing" || Boolean(designProfiles.find((profile) => profile.id === activeDesignProfileId)?.versions.some((release) => release.version === designProfiles.find((profile) => profile.id === activeDesignProfileId)?.version))}
onClick={() => { void publishDesignProfile(); }}
>{designProfilePublishState === "publishing" ? "Публикуется" : `Опубликовать v${designProfiles.find((profile) => profile.id === activeDesignProfileId)?.version ?? ""}`}</Button>
<Button variant="primary" shape="pill" onClick={() => { setDesignProfileName(""); setDesignProfileSaveMode("save-as"); }}>Сохранить как новый</Button>
</>
) : (
@@ -1810,7 +1862,7 @@ export function CatalogApp() {
<div className="catalog-form">
<SegmentedControl label="Режим сохранения" value={designProfileSaveMode} items={[{ value: "save", label: "Сохранить" }, { value: "save-as", label: "Сохранить как новый" }]} onChange={setDesignProfileSaveMode} />
{designProfileSaveMode === "save-as" ? <TextField label="Название нового профиля" hint="обязательно" value={designProfileName} onChange={(event) => setDesignProfileName(event.target.value)} /> : (
<div className="catalog-save-profile-current"><Icon name="settings" /><span><strong>{designProfiles.find((profile) => profile.id === activeDesignProfileId)?.name ?? "NODE.DC Default"}</strong><small>Будет создана новая draft-версия текущего профиля.</small></span></div>
<div className="catalog-save-profile-current"><Icon name="settings" /><span><strong>{designProfiles.find((profile) => profile.id === activeDesignProfileId)?.name ?? "NODE.DC Default"}</strong><small>Сохранение создаёт новую draft-версию. Publish фиксирует текущую сохранённую версию неизменяемым release.</small></span></div>
)}
</div>
</Window>
+12
View File
@@ -3,6 +3,7 @@ import type { NodedcTheme } from "@nodedc/ui-core";
export const applicationManifestSchemaVersion = "0.1.0" as const;
export type ApplicationDraftStatus = "draft";
export type DesignProfileStatus = "draft" | "published";
export interface ApplicationPageManifest {
id: string;
@@ -33,6 +34,7 @@ export interface ApplicationManifestV01 {
designProfile: {
id: string;
version: string;
status: DesignProfileStatus;
theme: NodedcTheme;
};
pages: ApplicationPageManifest[];
@@ -60,8 +62,18 @@ export interface DesignProfileSummary {
id: string;
name: string;
version: string;
status: DesignProfileStatus;
theme: NodedcTheme;
updatedAt: string;
versions: DesignProfileVersionSummary[];
latestPublishedVersion: string | null;
}
export interface DesignProfileVersionSummary {
version: string;
status: DesignProfileStatus;
theme: NodedcTheme;
publishedAt?: string;
}
export type ApplicationDraftSaveState = "idle" | "loading" | "saving" | "saved" | "error";