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.
This commit is contained in:
sdarbinyan
2026-07-13 11:01:04 +04:00
parent 265f2fcbac
commit 223f908887
2 changed files with 46 additions and 1 deletions

View File

@@ -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<ProjectEditorState>({
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];