From 223f908887ad3912d956bcabf9e0cc9e30a1263d Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 13 Jul 2026 11:01:04 +0400 Subject: [PATCH] feat(project-editor): add draft/publish status, dirty tracking, save/publish to facade Wires ProjectValidator into ProjectEditorFacade and extends ProjectEditorState with status ('draft'|'published') and lastSavedBootstrap. Adds dirty computed (diffed against lastSavedBootstrap), validationIssues computed, and save()/publish() methods. publish() calls PlatformRuntimeService.reloadFromBootstrap() and refuses when validation issues exist. loadBootstrap() seeds lastSavedBootstrap so a freshly-loaded bootstrap is not dirty. --- .../facade/project-editor.facade.ts | 45 ++++++++++++++++++- .../models/project-editor.model.ts | 2 + 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/app/features/project-editor/facade/project-editor.facade.ts b/src/app/features/project-editor/facade/project-editor.facade.ts index c0ae30e..7edad35 100644 --- a/src/app/features/project-editor/facade/project-editor.facade.ts +++ b/src/app/features/project-editor/facade/project-editor.facade.ts @@ -7,6 +7,8 @@ import { ProjectEditorIoService } from '../services/project-editor-io.service'; import { ProjectEditorPreviewService } from '../services/project-editor-preview.service'; import { LocaleSyncService } from '../services/locale-sync.service'; import { ProjectEditorState } from '../models/project-editor.model'; +import { PlatformRuntimeService } from '../../../core/runtime/platform-runtime.service'; +import { ProjectValidator } from '../services/project-validator.service'; @Injectable({ providedIn: 'root' }) export class ProjectEditorFacade { @@ -14,11 +16,15 @@ export class ProjectEditorFacade { private readonly ioService = inject(ProjectEditorIoService); private readonly previewService = inject(ProjectEditorPreviewService); private readonly localeSync = inject(LocaleSyncService); + private readonly runtime = inject(PlatformRuntimeService); + private readonly validator = inject(ProjectValidator); private readonly state = signal({ bootstrap: null, importError: null, activeSection: 'general', + status: 'draft', + lastSavedBootstrap: null, }); readonly bootstrap = computed(() => this.state().bootstrap); @@ -26,10 +32,25 @@ export class ProjectEditorFacade { readonly activeSection = computed(() => this.state().activeSection); readonly homepagePage = computed(() => this.bootstrap()?.pages.find(page => page.key === 'home' || page.route.path === '/') ?? null); readonly homepageWidgets = computed(() => this.homepagePage()?.sections.flatMap(section => section.widgets.map(widget => ({ sectionId: section.id, sectionType: section.type, widget }))) ?? []); + readonly status = computed(() => this.state().status); + readonly validationIssues = computed(() => { + const current = this.bootstrap(); + return current ? this.validator.validate(current) : []; + }); + readonly dirty = computed(() => { + const current = this.bootstrap(); + if (!current) { + return false; + } + return JSON.stringify(current) !== JSON.stringify(this.state().lastSavedBootstrap); + }); loadBootstrap(): void { this.configService.loadBootstrap(true).pipe(take(1)).subscribe({ - next: config => this.state.update(current => ({ ...current, bootstrap: this.normalize(JSON.parse(JSON.stringify(config)) as BootstrapConfig), importError: null })), + next: config => { + const normalized = this.normalize(JSON.parse(JSON.stringify(config)) as BootstrapConfig); + this.state.update(current => ({ ...current, bootstrap: normalized, importError: null, lastSavedBootstrap: normalized, status: 'draft' })); + }, error: () => this.state.update(current => ({ ...current, bootstrap: null, importError: 'builder.importError' })), }); } @@ -89,6 +110,28 @@ export class ProjectEditorFacade { })); } + save(): void { + const current = this.state().bootstrap; + if (!current) { + return; + } + this.state.update(state => ({ ...state, lastSavedBootstrap: JSON.parse(JSON.stringify(current)) })); + } + + publish(): boolean { + const current = this.state().bootstrap; + if (!current || this.validationIssues().length > 0) { + return false; + } + this.runtime.reloadFromBootstrap(current); + this.state.update(state => ({ + ...state, + status: 'published', + lastSavedBootstrap: JSON.parse(JSON.stringify(current)), + })); + return true; + } + addNavLink(target: 'header' | 'footer'): void { this.updateBootstrap(current => { const list = current.navigation[target]; diff --git a/src/app/features/project-editor/models/project-editor.model.ts b/src/app/features/project-editor/models/project-editor.model.ts index 1635e13..339ec17 100644 --- a/src/app/features/project-editor/models/project-editor.model.ts +++ b/src/app/features/project-editor/models/project-editor.model.ts @@ -18,6 +18,8 @@ export interface ProjectEditorState { bootstrap: BootstrapConfig | null; importError: string | null; activeSection: ProjectEditorSectionId; + status: 'draft' | 'published'; + lastSavedBootstrap: BootstrapConfig | null; } export interface ProjectEditorWidgetPreset {