import {Injectable} from '@angular/core'; import {ApiService} from './api.service'; import PageRecord from '../structures/PageRecord'; import HostRecord from '../structures/HostRecord'; import {EditorNodeContract} from '../components/nodes/EditorNode.contract'; import {BehaviorSubject, Subscription} from 'rxjs'; export class NoPageLoadedError extends Error { constructor(msg = 'There is no page open for editing.') { super(msg); } } @Injectable({ providedIn: 'root' }) export class EditorService { protected currentPage?: PageRecord; protected currentNodes: HostRecord[] = []; protected nodeIdToEditorContract: { [key: string]: EditorNodeContract } = {}; protected dirtyOverride = false; protected ready$: BehaviorSubject = new BehaviorSubject(false); protected subs: Subscription[] = []; public get immutableNodes(): HostRecord[] { return [...this.currentNodes]; } public get mutablePageName(): string { if ( this.currentPage ) { return this.currentPage.Name; } return ''; } public set mutablePageName(name: string) { if ( this.currentPage && this.canEdit() ) { if ( this.currentPage.Name !== name ) { this.dirtyOverride = true; } this.currentPage.Name = name; } } constructor( protected api: ApiService, ) { console.log('editor service', this); } async startEditing(pageId: string) { if ( this.currentPage ) { await this.stopEditing(); } this.currentPage = await this.loadPage(pageId); this.currentNodes = await this.loadNodes(pageId); await this.ready$.next(true); console.log('editing', this.currentPage); console.log('nodes', this.currentNodes); } async stopEditing() { delete this.currentPage; this.currentNodes = []; this.nodeIdToEditorContract = {}; this.subs.forEach(sub => sub.unsubscribe()); this.subs = []; this.ready$.next(false); } async save() { if ( !(await this.needsSave()) ) { return; } const editors = Object.values(this.nodeIdToEditorContract); // Save all editors that handle their data independently first await Promise.all(editors.map(async editor => { if ( await editor.needsSave() ) { await editor.performSave(); } })); // Tell the editors to write their state changes to the HostRecords await Promise.all(editors.map(async editor => { await editor.writeChangesToNode(); })); await this.saveNodesAsPage(this.currentPage, this.currentNodes); } async saveNodesAsPage(page: PageRecord, nodes: HostRecord[]) { await new Promise((res, rej) => { const saveNodes = nodes.map(x => { x.PageId = page.UUID; return x.toSave(); }); this.api.post(`/page/${page.UUID}/nodes/save`, saveNodes).subscribe({ next: result => { res(); // TODO load in returned data!! }, error: rej, }); }); } async needsSave() { if ( this.dirtyOverride ) { return true; } const dirties = await Promise.all(Object.values(this.nodeIdToEditorContract).map(editor => editor.isDirty())); const needSaves = await Promise.all(Object.values(this.nodeIdToEditorContract).map(editor => editor.needsSave())); return dirties.some(Boolean) || needSaves.some(Boolean); } async deleteNode(nodeId: string) { if ( !this.currentPage ) { throw new NoPageLoadedError(); } const node = this.currentNodes.find(maybeNode => maybeNode.UUID === nodeId); if ( !node ) { throw new Error('Invalid node ID.'); } const editor = this.nodeIdToEditorContract[nodeId]; if ( editor ) { await editor.performDelete(); delete this.nodeIdToEditorContract[nodeId]; } this.currentNodes = this.currentNodes.filter(x => x.UUID !== nodeId); this.dirtyOverride = true; } canEdit() { if ( !this.currentPage ) { throw new NoPageLoadedError(); } return !this.currentPage.isViewOnly(); } async registerNodeEditor(nodeId: string, editor: EditorNodeContract) { return new Promise((res, rej) => { const sub = this.ready$.subscribe(async val => { if ( val ) { try { if ( !this.currentPage ) { return rej(new NoPageLoadedError()); } const node = this.currentNodes.find(maybeNode => maybeNode.UUID === nodeId); if ( !node ) { return rej(new Error('Invalid node ID.')); } editor.page = this.currentPage; editor.node = node; this.nodeIdToEditorContract[nodeId] = editor; if ( editor.needsLoad() ) { await editor.performLoad(); } res(); } catch (e) { rej(e); } } }); this.subs.push(sub); }); } async unregisterNodeEditor(nodeId: string) { if ( !this.currentPage ) { throw new NoPageLoadedError(); } delete this.nodeIdToEditorContract[nodeId]; } async loadPage(pageId: string): Promise { return new Promise((res, rej) => { this.api.get(`/page/${pageId}`).subscribe({ next: result => { res(new PageRecord(result.data)); }, error: rej, }); }); } async loadNodes(pageId: string): Promise { return new Promise((res, rej) => { this.api.get(`/page/${pageId}/nodes`).subscribe({ next: result => { res(result.data.map(rec => { const host = new HostRecord(rec.Value.Value); host.load(rec); return host; })); }, error: rej, }); }); } }