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.
854 lines
29 KiB
854 lines
29 KiB
import {Injectable} from '@angular/core';
|
|
import {ApiService, ResourceNotAvailableOfflineError} from './api.service';
|
|
import PageRecord, {PageVersionRecord} from '../structures/PageRecord';
|
|
import HostRecord from '../structures/HostRecord';
|
|
import {EditorNodeContract} from '../components/nodes/EditorNode.contract';
|
|
import {BehaviorSubject, Subscription} from 'rxjs';
|
|
import {NavigationService} from './navigation.service';
|
|
import {DatabaseService} from './db/database.service';
|
|
import {Page} from './db/Page';
|
|
import {PageNode} from './db/PageNode';
|
|
import {MenuItem} from './db/MenuItem';
|
|
import {SessionService} from './session.service';
|
|
import {debounce, uuid_v4} from '../utility';
|
|
|
|
export class NoPageLoadedError extends Error {
|
|
constructor(msg = 'There is no page open for editing.') {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class EditorService {
|
|
private static instances: {[key: string]: 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[] = [];
|
|
protected saving = false;
|
|
protected currentPageVersion?: number;
|
|
public forceReadonly = false;
|
|
protected saveTriggered = false;
|
|
public notAvailable = false;
|
|
public readonly instanceUUID: string;
|
|
|
|
protected privTriggerSave = debounce(() => {
|
|
if ( this.saving ) {
|
|
this.triggerSave();
|
|
} else {
|
|
this.save();
|
|
}
|
|
|
|
this.saveTriggered = false;
|
|
}, 3000);
|
|
|
|
public static registerInstance(inst: EditorService) {
|
|
this.instances[inst.instanceUUID] = inst;
|
|
}
|
|
|
|
public triggerSave() {
|
|
this.saveTriggered = true;
|
|
this.privTriggerSave();
|
|
}
|
|
|
|
public get currentPageId() {
|
|
return this.currentPage?.UUID;
|
|
}
|
|
|
|
public get isSaving() {
|
|
return this.saving;
|
|
}
|
|
|
|
public get willSave() {
|
|
return this.saveTriggered;
|
|
}
|
|
|
|
public get isEditing() {
|
|
return !!this.currentPage;
|
|
}
|
|
|
|
public get metadata() {
|
|
if ( this.currentPage.AdditionalData ) {
|
|
return this.currentPage.AdditionalData;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
public set metadata(data: any) {
|
|
this.currentPage.AdditionalData = data;
|
|
this.dirtyOverride = true;
|
|
}
|
|
|
|
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.triggerSave();
|
|
}
|
|
|
|
this.currentPage.Name = name;
|
|
}
|
|
}
|
|
|
|
public get currentPageType() {
|
|
return this.currentPage?.PageType;
|
|
}
|
|
|
|
constructor(
|
|
protected api: ApiService,
|
|
protected nav: NavigationService,
|
|
protected db: DatabaseService,
|
|
protected session: SessionService,
|
|
) {
|
|
this.instanceUUID = uuid_v4();
|
|
console.log('editor service', this);
|
|
}
|
|
|
|
getEditor(uuid?: string) {
|
|
if ( uuid ) {
|
|
return EditorService.instances[uuid];
|
|
}
|
|
|
|
const inst = new EditorService(this.api, this.nav, this.db, this.session);
|
|
EditorService.registerInstance(inst);
|
|
return inst;
|
|
}
|
|
|
|
async reload() {
|
|
await this.startEditing(this.currentPageId);
|
|
}
|
|
|
|
async startEditing(pageId: string, version?: number) {
|
|
if ( this.currentPage ) {
|
|
await this.stopEditing();
|
|
}
|
|
|
|
try {
|
|
this.currentPageVersion = version;
|
|
this.currentPage = await this.loadPage(pageId, version);
|
|
this.currentNodes = await this.loadNodes(pageId, version);
|
|
this.notAvailable = false;
|
|
await this.ready$.next(true);
|
|
} catch (e) {
|
|
if ( e instanceof ResourceNotAvailableOfflineError ) {
|
|
this.notAvailable = true;
|
|
await this.ready$.next(true);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
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()) || this.saving || this.forceReadonly ) {
|
|
return;
|
|
}
|
|
|
|
this.saving = true;
|
|
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.savePage(this.currentPage);
|
|
await this.saveNodesAsPage(this.currentPage, this.currentNodes);
|
|
this.dirtyOverride = false;
|
|
this.saving = false;
|
|
this.nav.requestSidebarRefresh({ quiet: true });
|
|
}
|
|
|
|
async moveNode(node: HostRecord, direction: 'up' | 'down') {
|
|
if ( !this.currentPage ) {
|
|
throw new NoPageLoadedError();
|
|
}
|
|
|
|
if ( this.forceReadonly ) {
|
|
return;
|
|
}
|
|
|
|
const nodeIndex = this.currentNodes.findIndex(maybeNode => maybeNode.UUID === node.UUID);
|
|
if ( nodeIndex < 0 ) {
|
|
return;
|
|
}
|
|
|
|
if ( direction === 'up' && nodeIndex > 0 ) {
|
|
const otherIdx = nodeIndex - 1;
|
|
const otherNode = this.currentNodes[otherIdx];
|
|
this.currentNodes[otherIdx] = this.currentNodes[nodeIndex];
|
|
this.currentNodes[nodeIndex] = otherNode;
|
|
} else if ( direction === 'down' && nodeIndex !== (this.currentNodes.length - 1) ) {
|
|
const otherIdx = nodeIndex + 1;
|
|
const otherNode = this.currentNodes[otherIdx];
|
|
this.currentNodes[otherIdx] = this.currentNodes[nodeIndex];
|
|
this.currentNodes[nodeIndex] = otherNode;
|
|
}
|
|
|
|
this.dirtyOverride = true;
|
|
this.triggerSave();
|
|
}
|
|
|
|
async savePage(page: PageRecord): Promise<void> {
|
|
await new Promise(async (res, rej) => {
|
|
const existingLocalPage = await this.db.pages.where({ UUID: page.UUID }).first() as Page;
|
|
const saveData = page.toSave();
|
|
|
|
if ( this.api.isOffline ) {
|
|
if ( existingLocalPage ) {
|
|
existingLocalPage.fillFromRecord(page);
|
|
existingLocalPage.UpdatedAt = String(new Date()); // FIXME update UpdateUserId
|
|
existingLocalPage.needsServerUpdate = 1;
|
|
await existingLocalPage.save();
|
|
} else {
|
|
const newLocalPage = new Page(
|
|
page.UUID,
|
|
page.Name,
|
|
page.OrgUserId,
|
|
page.IsPublic,
|
|
page.IsVisibleInMenu,
|
|
page.ParentId,
|
|
page.NodeIds,
|
|
String(page.CreatedAt || new Date()),
|
|
String(new Date()),
|
|
true,
|
|
page.CreatedUserId,
|
|
page.UpdateUserId, // FIXME fill in the current user's ID
|
|
page.ChildPageIds,
|
|
false,
|
|
false,
|
|
1,
|
|
);
|
|
|
|
await newLocalPage.save();
|
|
}
|
|
|
|
return res();
|
|
}
|
|
|
|
this.api.post(`/page/${page.UUID}/save`, saveData).subscribe({
|
|
next: async result => {
|
|
if ( existingLocalPage ) {
|
|
existingLocalPage.fillFromRecord(page);
|
|
await existingLocalPage.save();
|
|
} else {
|
|
const newLocalPage = new Page(
|
|
result.data.UUID,
|
|
result.data.Name,
|
|
result.data.OrgUserId,
|
|
result.data.IsPublic,
|
|
result.data.IsVisibleInMenu,
|
|
result.data.ParentId,
|
|
result.data.NodeIds,
|
|
String(result.data.CreatedAt),
|
|
String(result.data.UpdatedAt),
|
|
true,
|
|
result.data.CreatedUserId,
|
|
result.data.UpdateUserId,
|
|
result.data.ChildPageIds,
|
|
result.data.noDelete,
|
|
result.data.virtual,
|
|
0
|
|
);
|
|
|
|
await newLocalPage.save();
|
|
}
|
|
|
|
res();
|
|
},
|
|
error: rej,
|
|
});
|
|
});
|
|
}
|
|
|
|
async saveNodesAsPage(page: PageRecord, nodes: HostRecord[]): Promise<HostRecord[]> {
|
|
return new Promise(async (res, rej) => {
|
|
const saveNodes = nodes.map(x => {
|
|
x.PageId = page.UUID;
|
|
return x.toSave();
|
|
});
|
|
|
|
const existingLocalPage = await this.db.pages.where({ UUID: page.UUID }).first() as Page;
|
|
|
|
// If we're offline save the nodes locally
|
|
if ( this.api.isOffline ) {
|
|
if ( !existingLocalPage ) {
|
|
return rej(new ResourceNotAvailableOfflineError());
|
|
}
|
|
|
|
const nodeRecs: PageNode[] = [];
|
|
for ( const nodeRec of saveNodes ) {
|
|
const existingLocalNode = await this.db.pageNodes.where({ UUID: nodeRec.UUID }).first() as PageNode;
|
|
if ( existingLocalNode ) {
|
|
existingLocalNode.fillFromRecord(nodeRec);
|
|
existingLocalNode.needsServerUpdate = true;
|
|
await existingLocalNode.save();
|
|
nodeRecs.push(existingLocalNode);
|
|
} else {
|
|
const newLocalNode = new PageNode(
|
|
nodeRec.UUID || PageNode.getUUID(),
|
|
nodeRec.Type,
|
|
JSON.stringify(nodeRec.Value),
|
|
nodeRec.PageId,
|
|
nodeRec.CreatedAt,
|
|
nodeRec.UpdatedAt,
|
|
nodeRec.CreatedUserId,
|
|
nodeRec.UpdateUserId,
|
|
true
|
|
);
|
|
|
|
await newLocalNode.save();
|
|
nodeRecs.push(newLocalNode);
|
|
}
|
|
}
|
|
|
|
page.NodeIds = nodeRecs.map(x => x.UUID);
|
|
existingLocalPage.NodeIds = nodeRecs.map(x => x.UUID);
|
|
existingLocalPage.needsServerUpdate = 1;
|
|
await existingLocalPage.save();
|
|
|
|
return res(nodeRecs.map(x => {
|
|
const rec = x.inflateToRecord();
|
|
const host = new HostRecord(rec.Value.Value);
|
|
host.load(rec);
|
|
return host;
|
|
}));
|
|
}
|
|
|
|
// Otherwise, use the server to save them and update the local records
|
|
this.api.post(`/page/${page.UUID}/nodes/save`, saveNodes).subscribe({
|
|
next: async result => {
|
|
await this.db.pageNodes.where({ PageId: page.UUID }).delete();
|
|
|
|
const returns = [];
|
|
for ( const rec of result.data ) {
|
|
const newLocalNode = new PageNode(
|
|
rec.UUID,
|
|
rec.Type,
|
|
JSON.stringify(rec.Value),
|
|
rec.PageId,
|
|
rec.CreatedAt,
|
|
rec.UpdatedAt,
|
|
rec.CreatedUserId,
|
|
rec.UpdateUsetId,
|
|
);
|
|
|
|
await newLocalNode.save();
|
|
|
|
const host = new HostRecord(rec.Value.Value);
|
|
host.load(rec);
|
|
returns.push(host);
|
|
}
|
|
|
|
return res(returns);
|
|
},
|
|
error: rej,
|
|
});
|
|
});
|
|
}
|
|
|
|
async saveNodeToPage(page: PageRecord, node: HostRecord): Promise<HostRecord> {
|
|
return new Promise(async (res, rej) => {
|
|
node.PageId = page.UUID;
|
|
const nodeData = node.toSave();
|
|
const localPage = await this.db.pages.where({ UUID: page.UUID }).first() as Page;
|
|
|
|
if ( this.api.isOffline ) {
|
|
if ( !localPage ) {
|
|
return rej(new ResourceNotAvailableOfflineError());
|
|
}
|
|
|
|
if ( !nodeData.UUID ) {
|
|
nodeData.UUID = PageNode.getUUID();
|
|
}
|
|
|
|
const newLocalNode = new PageNode(
|
|
nodeData.UUID,
|
|
nodeData.Type,
|
|
JSON.stringify(nodeData.Value),
|
|
nodeData.PageId,
|
|
nodeData.CreatedAt,
|
|
nodeData.UpdatedAt,
|
|
nodeData.CreatedUserId,
|
|
nodeData.UpdateUserId,
|
|
true
|
|
);
|
|
|
|
await newLocalNode.save();
|
|
|
|
localPage.NodeIds.push(newLocalNode.UUID);
|
|
localPage.needsServerUpdate = 1;
|
|
await localPage.save();
|
|
|
|
const host = new HostRecord(nodeData.Value.Value);
|
|
host.load(nodeData);
|
|
return res(host);
|
|
}
|
|
|
|
this.api.post(`/page/${page.UUID}/nodes/save_one`, { nodeData }).subscribe({
|
|
next: async result => {
|
|
const newLocalNode = new PageNode(
|
|
result.data.UUID,
|
|
result.data.Type,
|
|
JSON.stringify(result.data.Value),
|
|
result.data.PageId,
|
|
result.data.CreatedAt,
|
|
result.data.UpdatedAt,
|
|
result.data.CreatedUserId,
|
|
result.data.UpdateUserId
|
|
);
|
|
|
|
await newLocalNode.save();
|
|
|
|
if ( localPage ) {
|
|
localPage.NodeIds.push(result.data.UUID);
|
|
await localPage.save();
|
|
}
|
|
|
|
const host = new HostRecord(result.data.Value.Value);
|
|
host.load(result.data);
|
|
res(host);
|
|
},
|
|
error: rej,
|
|
});
|
|
});
|
|
}
|
|
|
|
async needsSave() {
|
|
if ( this.forceReadonly ) {
|
|
return false;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
if ( this.forceReadonly ) {
|
|
return;
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
// If we're offline, we need to flag the local node record for deletion
|
|
if ( this.api.isOffline ) {
|
|
const existingLocalNode = await this.db.pageNodes.where({ UUID: nodeId }).first() as PageNode;
|
|
if ( existingLocalNode ) {
|
|
existingLocalNode.deleted = true;
|
|
existingLocalNode.needsServerUpdate = true;
|
|
await existingLocalNode.save();
|
|
}
|
|
}
|
|
|
|
this.currentNodes = this.currentNodes.filter(x => x.UUID !== nodeId);
|
|
this.dirtyOverride = true;
|
|
this.triggerSave();
|
|
}
|
|
|
|
async addNode(type: 'paragraph' | 'code_ref' | 'database_ref' | 'file_ref', position?: 'before' | 'after', positionNodeId?: string) {
|
|
if ( !this.currentPage ) {
|
|
throw new NoPageLoadedError();
|
|
}
|
|
|
|
if ( this.forceReadonly ) {
|
|
return;
|
|
}
|
|
|
|
const baseHost = new HostRecord();
|
|
baseHost.type = type;
|
|
baseHost.PageId = this.currentPage.UUID;
|
|
|
|
const host = await this.saveNodeToPage(this.currentPage, baseHost);
|
|
|
|
let placed = false;
|
|
if ( position === 'before' && positionNodeId ) {
|
|
const index = this.currentNodes.findIndex(node => node.UUID === positionNodeId);
|
|
if ( index > -1 ) {
|
|
this.currentNodes.splice(index, 0, host);
|
|
placed = true;
|
|
}
|
|
} else if ( position === 'after' && positionNodeId ) {
|
|
const index = this.currentNodes.findIndex(node => node.UUID === positionNodeId);
|
|
if ( index > -1 ) {
|
|
this.currentNodes.splice(index + 1, 0, host);
|
|
placed = true;
|
|
}
|
|
}
|
|
|
|
if ( !placed ) {
|
|
this.currentNodes.push(host);
|
|
}
|
|
|
|
this.currentPage.NodeIds.push(host.UUID);
|
|
this.dirtyOverride = true;
|
|
this.triggerSave();
|
|
return host;
|
|
}
|
|
|
|
canEdit() {
|
|
return this.currentPage && !this.currentPage.isViewOnly() && !this.forceReadonly;
|
|
}
|
|
|
|
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, version?: number): Promise<PageRecord> {
|
|
return new Promise(async (res, rej) => {
|
|
const existingLocalPage = await this.db.pages.where({ UUID: pageId }).first() as Page;
|
|
|
|
// If we're offline, return the local record, or throw an error.
|
|
if ( this.api.isOffline ) {
|
|
if ( existingLocalPage ) {
|
|
return res(new PageRecord(existingLocalPage.getSaveRecord()));
|
|
} else {
|
|
return rej(new ResourceNotAvailableOfflineError());
|
|
}
|
|
}
|
|
|
|
// If we're online, fetch the page record and store it locally
|
|
this.api.get(`/page/${pageId}${version ? '?version=' + version : ''}`).subscribe({
|
|
next: async result => {
|
|
const page = new PageRecord(result.data);
|
|
|
|
if ( existingLocalPage ) {
|
|
existingLocalPage.fillFromRecord(result.data);
|
|
existingLocalPage.needsServerUpdate = 0;
|
|
await existingLocalPage.save();
|
|
} else {
|
|
const newLocalPage = new Page(
|
|
page.UUID,
|
|
page.Name,
|
|
page.OrgUserId,
|
|
page.IsPublic,
|
|
page.IsVisibleInMenu,
|
|
page.ParentId,
|
|
page.NodeIds,
|
|
String(page.CreatedAt),
|
|
String(page.UpdatedAt),
|
|
true,
|
|
page.CreatedUserId,
|
|
page.UpdateUserId,
|
|
page.ChildPageIds,
|
|
result.data.noDelete,
|
|
result.data.virtual,
|
|
);
|
|
|
|
await newLocalPage.save();
|
|
}
|
|
|
|
res(page);
|
|
},
|
|
error: rej,
|
|
});
|
|
});
|
|
}
|
|
|
|
async loadPageVersions(pageId: string): Promise<PageVersionRecord[]> {
|
|
return new Promise(async (res, rej) => {
|
|
if ( this.api.isOffline ) {
|
|
return rej(new ResourceNotAvailableOfflineError());
|
|
}
|
|
|
|
this.api.get(`/page/${pageId}/versions`).subscribe({
|
|
next: results => {
|
|
return res(results.data.map(data => {
|
|
return {
|
|
currentVersion: Boolean(data.current_version),
|
|
versionNum: Number(data.version_num),
|
|
versionUserId: data.version_user_id,
|
|
versionMessage: data.version_message,
|
|
versionUUID: data.version_UUID,
|
|
versionCreateDate: new Date(data.version_create_date),
|
|
userDisplay: data.user_display,
|
|
};
|
|
}));
|
|
},
|
|
error: rej,
|
|
});
|
|
});
|
|
}
|
|
|
|
async revertPageToVersion(pageId: string, versionNum: number): Promise<PageRecord> {
|
|
return new Promise(async (res, rej) => {
|
|
const existingLocalPage = await this.db.pages.where({ UUID: pageId }).first() as Page;
|
|
|
|
if ( this.api.isOffline ) {
|
|
return rej(new ResourceNotAvailableOfflineError());
|
|
}
|
|
|
|
this.api.post(`/page/${pageId}/versions/revert`, { version_num: versionNum }).subscribe({
|
|
next: async result => {
|
|
const page = new PageRecord(result.data);
|
|
|
|
if ( existingLocalPage ) {
|
|
existingLocalPage.fillFromRecord(result.data);
|
|
existingLocalPage.needsServerUpdate = 0;
|
|
await existingLocalPage.save();
|
|
} else {
|
|
const newLocalPage = new Page(
|
|
page.UUID,
|
|
page.Name,
|
|
page.OrgUserId,
|
|
page.IsPublic,
|
|
page.IsVisibleInMenu,
|
|
page.ParentId,
|
|
page.NodeIds,
|
|
String(page.CreatedAt),
|
|
String(page.UpdatedAt),
|
|
true,
|
|
page.CreatedUserId,
|
|
page.UpdateUserId,
|
|
page.ChildPageIds,
|
|
result.data.noDelete,
|
|
result.data.virtual,
|
|
);
|
|
|
|
await newLocalPage.save();
|
|
}
|
|
|
|
res(page);
|
|
},
|
|
error: rej,
|
|
});
|
|
});
|
|
}
|
|
|
|
async loadNodes(pageId: string, version?: number): Promise<HostRecord[]> {
|
|
return new Promise(async (res, rej) => {
|
|
const existingNodes = await this.db.pageNodes.where({ PageId: pageId }).toArray() as PageNode[];
|
|
|
|
const inflateRecords = (records) => {
|
|
return records.map(rec => {
|
|
const host = new HostRecord(rec.Value.Value);
|
|
host.load(rec);
|
|
return host;
|
|
});
|
|
};
|
|
|
|
// If we're offline, just resolve the offline nodes
|
|
if ( this.api.isOffline ) {
|
|
const parsedRecords = existingNodes.map(x => x.inflateToRecord());
|
|
return res(inflateRecords(parsedRecords));
|
|
}
|
|
|
|
this.api.get(`/page/${pageId}/nodes${version ? '?version=' + version : ''}`).subscribe({
|
|
next: async result => {
|
|
// If we got resolved records, delete the local ones to replace them
|
|
await this.db.pageNodes.where({ PageId: pageId }).delete();
|
|
|
|
for ( const rawRec of result.data ) {
|
|
const newLocalNode = new PageNode(
|
|
rawRec.UUID,
|
|
rawRec.Type,
|
|
JSON.stringify(rawRec.Value),
|
|
rawRec.PageId,
|
|
rawRec.CreatedAt,
|
|
rawRec.UpdatedAt,
|
|
rawRec.CreatedUserId,
|
|
rawRec.UpdateUserId,
|
|
);
|
|
|
|
await newLocalNode.save();
|
|
}
|
|
|
|
res(inflateRecords(result.data));
|
|
},
|
|
error: rej,
|
|
});
|
|
});
|
|
}
|
|
|
|
public createPage(name: string): Promise<any> {
|
|
return new Promise(async (res, rej) => {
|
|
const userId = this.session.get('user.id');
|
|
if ( !userId ) {
|
|
throw new ResourceNotAvailableOfflineError();
|
|
}
|
|
|
|
// If we're offline, create a stub page to be saved later
|
|
if ( this.api.isOffline ) {
|
|
const page = new Page(
|
|
Page.getUUID(),
|
|
name,
|
|
userId,
|
|
true,
|
|
true,
|
|
'0',
|
|
[],
|
|
String(new Date()),
|
|
String(new Date()),
|
|
true,
|
|
userId,
|
|
userId,
|
|
[],
|
|
false,
|
|
false,
|
|
1
|
|
);
|
|
|
|
const firstNode = new PageNode(
|
|
PageNode.getUUID(),
|
|
'paragraph',
|
|
JSON.stringify({ Value: 'Double-click to edit...' }),
|
|
page.UUID,
|
|
String(new Date()),
|
|
String(new Date()),
|
|
userId,
|
|
userId,
|
|
true
|
|
);
|
|
|
|
await firstNode.save();
|
|
|
|
page.NodeIds.push(firstNode.UUID);
|
|
await page.save();
|
|
|
|
// Because we're offline, we need to manually create the menu item node
|
|
const topLevelItem = await this.db.menuItems.where({
|
|
serverId: 0,
|
|
name: 'My Info Tree',
|
|
}).first() as MenuItem;
|
|
|
|
if ( topLevelItem ) {
|
|
const newItem = new MenuItem(
|
|
page.Name,
|
|
page.UUID,
|
|
[],
|
|
false,
|
|
false,
|
|
false,
|
|
'page',
|
|
false,
|
|
true
|
|
);
|
|
|
|
await newItem.save();
|
|
|
|
topLevelItem.childIds.push(newItem.serverId);
|
|
await topLevelItem.save();
|
|
}
|
|
|
|
return res(page.getSaveRecord());
|
|
}
|
|
|
|
// If we're online, the server will handle all of that mess...
|
|
this.api.post('/page/create', { name }).subscribe({
|
|
next: async result => {
|
|
const page = new Page(
|
|
result.data.UUID,
|
|
result.data.Name,
|
|
result.data.OrgUserId,
|
|
result.data.IsPublic,
|
|
result.data.IsVisibleInMenu,
|
|
result.data.ParentId,
|
|
result.data.NodeIds,
|
|
result.data.CreatedAt,
|
|
result.data.UpdatedAt,
|
|
true,
|
|
result.data.CreatedUserId,
|
|
result.data.UpdateUserId,
|
|
result.data.ChildPageIds,
|
|
result.data.noDelete,
|
|
result.data.virtual
|
|
);
|
|
|
|
await page.save();
|
|
return res(result.data);
|
|
},
|
|
error: rej,
|
|
});
|
|
});
|
|
}
|
|
}
|