You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
frontend/src/app/service/editor.service.ts

218 lines
6.5 KiB

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<boolean> = new BehaviorSubject<boolean>(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<PageRecord> {
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<HostRecord[]> {
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,
});
});
}
}