feat(sprint18): editor autosave/reset, admin auth, QR reuse, Ed25519 prep
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- Project editor: persist draft to localStorage, restore on reload, last-saved/draft-restored status indicators, section/whole-draft reset with confirmation. - Extract shared QR/polling/expiry engine from TelegramLoginComponent (shared/qr-login) and reuse it for a new admin login flow. - Admin authentication kept fully separate from customer session: own cookie/localStorage keys, signals, guard, and header interceptor (core/admin-auth). - ?login=true / ?adminLogin=true open the respective login dialog for manual testing. - Ed25519 challenge/verify interfaces (fail-closed no-op binding) ready for backend delivery. - Document autosave/reset/admin-auth/QR-reuse/Ed25519 model and the remaining full-field-coverage gap in docs/Project-Editor.md.
This commit is contained in:
@@ -9,6 +9,8 @@ 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';
|
||||
import { ProjectEditorDraftStorageService } from '../services/project-editor-draft-storage.service';
|
||||
import { EDITOR_SECTION_BOOTSTRAP_KEYS } from '../models/project-editor.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProjectEditorFacade {
|
||||
@@ -18,13 +20,17 @@ export class ProjectEditorFacade {
|
||||
private readonly localeSync = inject(LocaleSyncService);
|
||||
private readonly runtime = inject(PlatformRuntimeService);
|
||||
private readonly validator = inject(ProjectValidator);
|
||||
private readonly draftStorage = inject(ProjectEditorDraftStorageService);
|
||||
|
||||
private readonly state = signal<ProjectEditorState>({
|
||||
bootstrap: null,
|
||||
originalBootstrap: null,
|
||||
importError: null,
|
||||
activeSection: 'general',
|
||||
status: 'draft',
|
||||
lastSavedBootstrap: null,
|
||||
lastSavedAt: null,
|
||||
draftRestored: false,
|
||||
});
|
||||
|
||||
readonly bootstrap = computed(() => this.state().bootstrap);
|
||||
@@ -33,6 +39,8 @@ export class ProjectEditorFacade {
|
||||
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 lastSavedAt = computed(() => this.state().lastSavedAt);
|
||||
readonly draftRestored = computed(() => this.state().draftRestored);
|
||||
readonly validationIssues = computed(() => {
|
||||
const current = this.bootstrap();
|
||||
return current ? this.validator.validate(current) : [];
|
||||
@@ -49,7 +57,19 @@ export class ProjectEditorFacade {
|
||||
this.configService.loadBootstrap(true).pipe(take(1)).subscribe({
|
||||
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' }));
|
||||
const storedDraft = this.draftStorage.load(normalized.tenant.id);
|
||||
const restoredFromDraft = storedDraft !== null;
|
||||
const bootstrap = restoredFromDraft ? this.normalize(storedDraft!.bootstrap) : normalized;
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
bootstrap,
|
||||
originalBootstrap: normalized,
|
||||
importError: null,
|
||||
lastSavedBootstrap: normalized,
|
||||
lastSavedAt: restoredFromDraft ? storedDraft!.savedAt : null,
|
||||
status: 'draft',
|
||||
draftRestored: restoredFromDraft,
|
||||
}));
|
||||
},
|
||||
error: () => this.state.update(current => ({ ...current, bootstrap: null, importError: 'builder.importError' })),
|
||||
});
|
||||
@@ -61,10 +81,13 @@ export class ProjectEditorFacade {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.update(state => ({
|
||||
...state,
|
||||
bootstrap: this.normalize(updater(JSON.parse(JSON.stringify(current)) as BootstrapConfig)),
|
||||
}));
|
||||
const next = this.normalize(updater(JSON.parse(JSON.stringify(current)) as BootstrapConfig));
|
||||
this.state.update(state => ({ ...state, bootstrap: next, draftRestored: false }));
|
||||
this.draftStorage.save(next);
|
||||
}
|
||||
|
||||
dismissDraftRestoredNotice(): void {
|
||||
this.state.update(current => ({ ...current, draftRestored: false }));
|
||||
}
|
||||
|
||||
exportBootstrap(): string {
|
||||
@@ -115,7 +138,35 @@ export class ProjectEditorFacade {
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
this.state.update(state => ({ ...state, lastSavedBootstrap: JSON.parse(JSON.stringify(current)) }));
|
||||
const savedAt = this.draftStorage.save(current);
|
||||
this.state.update(state => ({ ...state, lastSavedBootstrap: JSON.parse(JSON.stringify(current)), lastSavedAt: savedAt }));
|
||||
}
|
||||
|
||||
/** Reverts one section's bootstrap keys to the originally loaded/published snapshot. Caller is responsible for confirmation UX. */
|
||||
resetSection(sectionId: ProjectEditorState['activeSection']): void {
|
||||
const original = this.state().originalBootstrap;
|
||||
const keys = EDITOR_SECTION_BOOTSTRAP_KEYS[sectionId];
|
||||
if (!original || !keys) {
|
||||
return;
|
||||
}
|
||||
this.updateBootstrap(current => {
|
||||
const patch: Partial<BootstrapConfig> = {};
|
||||
for (const key of keys) {
|
||||
(patch as Record<string, unknown>)[key] = JSON.parse(JSON.stringify(original[key]));
|
||||
}
|
||||
return { ...current, ...patch };
|
||||
});
|
||||
}
|
||||
|
||||
/** Discards the entire draft, reverting to the originally loaded/published bootstrap. Caller is responsible for confirmation UX. */
|
||||
resetDraft(): void {
|
||||
const original = this.state().originalBootstrap;
|
||||
if (!original) {
|
||||
return;
|
||||
}
|
||||
const reset = this.normalize(JSON.parse(JSON.stringify(original)) as BootstrapConfig);
|
||||
this.draftStorage.clear();
|
||||
this.state.update(state => ({ ...state, bootstrap: reset, lastSavedAt: null, draftRestored: false }));
|
||||
}
|
||||
|
||||
publish(): boolean {
|
||||
@@ -124,10 +175,13 @@ export class ProjectEditorFacade {
|
||||
return false;
|
||||
}
|
||||
this.runtime.reloadFromBootstrap(current);
|
||||
const savedAt = this.draftStorage.save(current);
|
||||
this.state.update(state => ({
|
||||
...state,
|
||||
status: 'published',
|
||||
lastSavedBootstrap: JSON.parse(JSON.stringify(current)),
|
||||
originalBootstrap: JSON.parse(JSON.stringify(current)),
|
||||
lastSavedAt: savedAt,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user