Cache pages and page nodes for offline use
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Garrett Mills 2020-10-25 18:42:27 -05:00
parent 380a139de3
commit fe7e955875
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
7 changed files with 580 additions and 283 deletions

View File

@ -128,18 +128,3 @@ steps:
} }
when: when:
status: failure status: failure
# ================ DEPLOY STAGING =====================
- name: promote staging build
image: plugins/downstream
settings:
server: https://ci.garrettmills.dev
token:
from_secret: drone_token
fork: false
last_successful: true
deploy: staging
repositories:
- Noded/frontend@master
when:
status: success

View File

@ -21,7 +21,12 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<ng-container> <ng-container *ngIf="editorService.notAvailable">
<div class="editor-root ion-padding" style="text-align: center; padding-top: 100px; color: #494949; font-size: 1.2em;">
Sorry, this page is not available offline yet.
</div>
</ng-container>
<ng-container *ngIf="!editorService.notAvailable">
<div class="editor-root ion-padding"> <div class="editor-root ion-padding">
<div <div
class="host-container" class="host-container"

View File

@ -1,7 +1,5 @@
import {Component, Host, HostListener, Input, OnInit, ViewChild, ViewChildren} from '@angular/core'; import {Component, HostListener, Input, OnInit} from '@angular/core';
import HostRecord from '../../structures/HostRecord'; import HostRecord from '../../structures/HostRecord';
import PageRecord from '../../structures/PageRecord';
import {PageService} from '../../service/page.service';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {LoadingController, PopoverController} from '@ionic/angular'; import {LoadingController, PopoverController} from '@ionic/angular';
import {NodePickerComponent} from '../../components/editor/node-picker/node-picker.component'; import {NodePickerComponent} from '../../components/editor/node-picker/node-picker.component';
@ -15,13 +13,8 @@ import {NodeTypeIcons} from '../../structures/node-types';
styleUrls: ['./editor.page.scss'], styleUrls: ['./editor.page.scss'],
}) })
export class EditorPage implements OnInit { export class EditorPage implements OnInit {
// @ViewChildren('editorHosts') editorHosts;
// @ViewChild('titleBar') titleBar;
public typeIcons = NodeTypeIcons; public typeIcons = NodeTypeIcons;
@Input() pageId: string; @Input() pageId: string;
public pageName = '';
constructor( constructor(
protected route: ActivatedRoute, protected route: ActivatedRoute,
@ -96,7 +89,6 @@ export class EditorPage implements OnInit {
}); });
popover.onDidDismiss().then(result => { popover.onDidDismiss().then(result => {
console.log('adding node', result.data);
if ( !result.data ) { if ( !result.data ) {
return; return;
} }
@ -104,238 +96,6 @@ export class EditorPage implements OnInit {
this.editorService.addNode(result.data, position, positionNodeId); this.editorService.addNode(result.data, position, positionNodeId);
}); });
// popover.onDidDismiss().then(arg => {
// const defValue = this.getDefaultValue(arg.data);
// const hostRec = new HostRecord(defValue);
// hostRec.type = arg.data;
// hostRec.PageId = this.pageRecord.UUID;
//
// if ( hostRec.type === 'ul' ) {
// hostRec.value = JSON.stringify([{value: '', indentationLevel: 0}]);
// }
//
// this.hostRecords.push(hostRec);
// if ( hostRec.isNorm() ) {
// setTimeout(() => {
// this.editorHosts.toArray().reverse()[0].takeFocus();
// }, 0);
// } else {
// this.onSaveClick();
// }
// });
await popover.present(); await popover.present();
} }
// buttonIsVisible(index) {
// return this.visibleButtons.includes(index);
// }
//
// makeVisible(index) {
// if ( !this.buttonIsVisible(index) ) {
// this.visibleButtons.push(index);
// }
// }
//
// makeInvisible(index) {
// this.visibleButtons = this.visibleButtons.filter(x => x !== index);
// }
//
// ionViewDidEnter() {
// if ( this.pageId ) {
// this.pages.load(this.pageId).subscribe(pageRecord => {
// this.pageRecord = pageRecord;
// this.pages.get_nodes(pageRecord).subscribe((hosts: Array<HostRecord>) => {
// this.hostRecords = hosts;
// if ( !pageRecord.isViewOnly() ) {
// this.onSaveClick();
// }
// });
// });
// } else {
// this.router.navigate(['/home']);
// }
// }
//
// onHostRecordChange($event, i) {
// if ( !this.pageRecord.isViewOnly() ) {
// this.hostRecords[i] = $event;
// }
// }
//
// async onAddClick($event) {
// if ( this.pageRecord.isViewOnly() ) {
// return;
// }
//
// const popover = await this.popover.create({
// component: NodePickerComponent,
// event: $event,
// });
//
// popover.onDidDismiss().then(arg => {
// const defValue = this.getDefaultValue(arg.data);
// const hostRec = new HostRecord(defValue);
// hostRec.type = arg.data;
// hostRec.PageId = this.pageRecord.UUID;
//
// if ( hostRec.type === 'ul' ) {
// hostRec.value = JSON.stringify([{value: '', indentationLevel: 0}]);
// }
//
// this.hostRecords.push(hostRec);
// if ( hostRec.isNorm() ) {
// setTimeout(() => {
// this.editorHosts.toArray().reverse()[0].takeFocus();
// }, 0);
// } else {
// this.onSaveClick();
// }
// });
//
// await popover.present();
// }
//
// getDefaultValue(type: string) {
// if ( type === 'paragraph' ) {
// return '';
// } else if ( type === 'header1' ) {
// return '# ';
// } else if ( type === 'header2' ) {
// return '## ';
// } else if ( type === 'header3' ) {
// return '### ';
// } else if ( type === 'header4' ) {
// return '#### ';
// } else if ( type === 'block_code' ) {
// return '```';
// } else if ( type === 'click_link' ) {
// return 'https://';
// } else if ( type === 'page_sep' ) {
// return '===';
// } else {
// return '';
// }
// }
//
// onNewHostRequested($event) {
// if ( this.pageRecord.isViewOnly() ) {
// return;
// }
//
// const insertAfter = this.getIndexFromRecord($event.record);
// const record = new HostRecord('');
// const newHosts = []
// this.hostRecords.forEach((rec, i) => {
// newHosts.push(rec);
// if ( i === insertAfter ) {
// newHosts.push(record);
// }
// })
//
// this.hostRecords = newHosts;
//
// setTimeout(() => {
// this.editorHosts.toArray()[insertAfter + 1].takeFocus();
// }, 0);
// }
//
// onDestroyHostRequested($event) {
// if ( this.pageRecord.isViewOnly() ) {
// return;
// }
//
// let removedIndex = 0;
// const newHostRecords = this.editorHosts.filter((host, i) => {
// if ( $event.record === host.record ) {
// removedIndex = i;
// }
// return host.record !== $event.record;
// });
//
// const removedHost = this.editorHosts[removedIndex];
//
// const hostRecords = newHostRecords.map(host => host.record);
// this.hostRecords = hostRecords;
//
// setTimeout(() => {
// let focusIndex;
// if ( removedIndex === 0 && this.editorHosts.toArray().length ) {
// focusIndex = 0;
// } else if ( removedIndex !== 0 ) {
// focusIndex = removedIndex - 1;
// }
//
// if ( focusIndex >= 0 ) {
// this.editorHosts.toArray()[focusIndex].takeFocus(false);
// }
// }, 0);
// }
//
// protected getIndexFromRecord(record) {
// let index;
// this.editorHosts.toArray().forEach((host, i) => {
// if ( host.record === record ) {
// index = i;
// }
// });
// return index;
// }
//
// onSaveClick() {
// if ( this.pageRecord.isViewOnly() ) {
// return;
// }
//
// this.loader.create({message: 'Saving changes...'}).then(loader => {
// loader.present().then(() => {
// this.pageRecord.Name = this.titleBar.el.innerText.trim();
//
// // First, save the page record itself
// this.pages.save(this.pageRecord).subscribe(pageRecord => {
// this.pageRecord = pageRecord;
//
// // Now, save the nodes
// this.pages.save_nodes(pageRecord, this.hostRecords).subscribe(result => {
// this.hostRecords = result;
// loader.dismiss();
// });
// });
// });
// });
// }
//
// async onOptionsClick($event, i) {
// if ( this.pageRecord.isViewOnly() ) {
// return;
// }
//
// const popover = await this.popover.create({
// component: HostOptionsComponent,
// event: $event,
// componentProps: {
// editor: this,
// index: i,
// event: $event,
// hostRecord: this.hostRecords[i],
// }
// });
//
// popover.onDidDismiss().then((result) => {
// if ( result.data === 'delete_node' ) {
// $event.record = this.hostRecords[i];
// this.onDestroyHostRequested($event);
// }
// })
//
// await popover.present();
// }
//
// onEditorKeydown($event) {
// if ( $event.key === 's' && $event.ctrlKey ) {
// $event.preventDefault();
// this.onSaveClick();
// }
// }
} }

150
src/app/service/db/Page.ts Normal file
View File

@ -0,0 +1,150 @@
import {Model} from './Model';
export interface IPage {
id?: number;
UUID: string;
Name: string;
OrgUserId: string;
IsPublic: boolean;
IsVisibleInMenu: boolean;
ParentId: string;
NodeIds: string[];
CreatedAt: string;
UpdatedAt: string;
Active: boolean;
CreatedUserId: string;
UpdateUserId: string;
ChildPageIds: string[];
noDelete: boolean;
virtual: boolean;
needsServerUpdate?: boolean;
deleted?: boolean;
}
export class Page extends Model<IPage> implements IPage {
id?: number;
UUID: string;
Name: string;
OrgUserId: string;
IsPublic: boolean;
IsVisibleInMenu: boolean;
ParentId: string;
NodeIds: string[];
CreatedAt: string;
UpdatedAt: string;
Active: boolean;
CreatedUserId: string;
UpdateUserId: string;
ChildPageIds: string[];
noDelete: boolean;
virtual: boolean;
needsServerUpdate?: boolean;
deleted?: boolean;
public static getTableName() {
return 'pages';
}
public static getSchema() {
// tslint:disable-next-line:max-line-length
return '++id, UUID, Name, OrgUserId, IsPublic, IsVisibleInMenu, ParentId, NodeIds, CreatedAt, UpdatedAt, Active, CreatedUserId, UpdateUserId, ChildPageIds, noDelete, virtual, needsServerUpdate, deleted';
}
constructor(
UUID: string,
Name: string,
OrgUserId: string,
IsPublic: boolean,
IsVisibleInMenu: boolean,
ParentId: string,
NodeIds: string[],
CreatedAt: string,
UpdatedAt: string,
Active: boolean,
CreatedUserId: string,
UpdateUserId: string,
ChildPageIds: string[],
noDelete: boolean,
virtual: boolean,
needsServerUpdate?: boolean,
deleted?: boolean,
id?: number
) {
super();
this.UUID = UUID;
this.Name = Name;
this.OrgUserId = OrgUserId;
this.IsPublic = IsPublic;
this.IsVisibleInMenu = IsVisibleInMenu;
this.ParentId = ParentId;
this.NodeIds = NodeIds;
this.CreatedAt = CreatedAt;
this.UpdatedAt = UpdatedAt;
this.Active = Active;
this.CreatedUserId = CreatedUserId;
this.UpdateUserId = UpdateUserId;
this.ChildPageIds = ChildPageIds;
this.noDelete = noDelete;
this.virtual = virtual;
if ( typeof needsServerUpdate !== 'undefined' ) {
this.needsServerUpdate = needsServerUpdate;
}
if ( typeof deleted !== 'undefined' ) {
this.deleted = deleted;
}
if ( id ) {
this.id = id;
}
}
public fillFromRecord(record: any) {
console.log('page fill from record', record);
this.UUID = record.UUID;
this.Name = record.Name;
this.OrgUserId = record.OrgUserId;
this.IsPublic = record.IsPublic;
this.IsVisibleInMenu = record.IsVisibleInMenu;
this.ParentId = record.ParentId;
this.NodeIds = record.NodeIds;
console.log('setting node ids', this.NodeIds, record.NodeIds);
this.CreatedAt = String(record.CreatedAt);
this.UpdatedAt = String(record.UpdatedAt);
this.Active = record.Active;
this.CreatedUserId = record.CreatedUserId;
this.UpdateUserId = record.UpdateUserId;
this.ChildPageIds = record.ChildPageIds;
this.noDelete = record.noDelete;
this.virtual = record.virtual;
}
public getSaveRecord(): any {
return {
...(this.id ? { id: this.id } : {}),
UUID: this.UUID,
Name: this.Name,
OrgUserId: this.OrgUserId,
IsPublic: this.IsPublic,
IsVisibleInMenu: this.IsVisibleInMenu,
ParentId: this.ParentId,
NodeIds: this.NodeIds,
CreatedAt: new Date(this.CreatedAt),
UpdatedAt: new Date(this.UpdatedAt),
Active: this.Active,
CreatedUserId: this.CreatedUserId,
UpdateUserId: this.UpdateUserId,
ChildPageIds: this.ChildPageIds,
noDelete: this.noDelete,
virtual: this.virtual,
...(typeof this.needsServerUpdate === 'undefined' ? {} : { needsServerUpdate: this.needsServerUpdate }),
...(typeof this.deleted === 'undefined' ? {} : { deleted: this.deleted }),
};
}
public getDatabase(): Dexie.Table<IPage, number> {
return this.staticClass().dbService.table('pages') as Dexie.Table<IPage, number>;
}
}

View File

@ -0,0 +1,113 @@
import {Model} from './Model';
export interface IPageNode {
id?: number;
UUID: string;
Type: string;
ValueJSON: string;
PageId: string;
CreatedAt: string;
UpdatedAt: string;
CreatedUserId: string;
UpdateUserId: string;
needsServerUpdate?: boolean;
deleted?: boolean;
}
export class PageNode extends Model<IPageNode> implements IPageNode {
id?: number;
UUID: string;
Type: string;
ValueJSON: string;
PageId: string;
CreatedAt: string;
UpdatedAt: string;
CreatedUserId: string;
UpdateUserId: string;
needsServerUpdate?: boolean;
deleted?: boolean;
public static getTableName() {
return 'pageNodes';
}
public static getSchema() {
// tslint:disable-next-line:max-line-length
return '++id, UUID, Type, ValueJSON, PageId, CreatedAt, UpdatedAt, CreatedUserId, UpdateUserId, needsServerUpdate, deleted';
}
constructor(
UUID: string,
Type: string,
ValueJSON: string,
PageId: string,
CreatedAt: string,
UpdatedAt: string,
CreatedUserId: string,
UpdateUserId: string,
needsServerUpdate?: boolean,
deleted?: boolean,
id?: number
) {
super();
this.UUID = UUID;
this.Type = Type;
this.ValueJSON = ValueJSON;
this.PageId = PageId;
this.CreatedAt = CreatedAt;
this.UpdatedAt = UpdatedAt;
this.CreatedUserId = CreatedUserId;
this.UpdateUserId = UpdateUserId;
if ( typeof needsServerUpdate !== 'undefined' ) {
this.needsServerUpdate = needsServerUpdate;
}
if ( typeof deleted !== 'undefined' ) {
this.deleted = deleted;
}
if ( id ) {
this.id = id;
}
}
public fillFromRecord(record: any) {
this.UUID = record.UUID;
this.Type = record.Type;
this.ValueJSON = JSON.stringify(record.Value);
this.PageId = record.PageId;
this.CreatedAt = String(record.CreatedAt);
this.UpdatedAt = String(record.UpdatedAt);
this.CreatedUserId = record.CreatedUserId;
this.UpdateUserId = record.UpdateUserId;
}
public inflateToRecord() {
const record = this.getSaveRecord();
record.Value = JSON.parse(record.ValueJSON);
delete record.ValueJSON;
return record;
}
public getSaveRecord(): any {
return {
...(this.id ? { id: this.id } : {}),
UUID: this.UUID,
Type: this.Type,
ValueJSON: this.ValueJSON,
PageId: this.PageId,
CreatedAt: new Date(this.CreatedAt),
UpdatedAt: new Date(this.UpdatedAt),
CreatedUserId: this.CreatedUserId,
UpdateUserId: this.UpdateUserId,
...(typeof this.needsServerUpdate === 'undefined' ? {} : { needsServerUpdate: this.needsServerUpdate }),
...(typeof this.deleted === 'undefined' ? {} : { deleted: this.deleted }),
};
}
public getDatabase(): Dexie.Table<IPageNode, number> {
return this.staticClass().dbService.table('pageNodes') as Dexie.Table<IPageNode, number>;
}
}

View File

@ -8,12 +8,17 @@ import {Database, IDatabase} from './Database';
import {DatabaseColumn, IDatabaseColumn} from './DatabaseColumn'; import {DatabaseColumn, IDatabaseColumn} from './DatabaseColumn';
import {DatabaseEntry, IDatabaseEntry} from './DatabaseEntry'; import {DatabaseEntry, IDatabaseEntry} from './DatabaseEntry';
import {FileGroup, IFileGroup} from './FileGroup'; import {FileGroup, IFileGroup} from './FileGroup';
import {Page, IPage} from './Page';
import {PageNode, IPageNode} from './PageNode';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DatabaseService extends Dexie { export class DatabaseService extends Dexie {
protected static registeredModels = [Migration, MenuItem, KeyValue, Codium, Database, DatabaseColumn, DatabaseEntry, FileGroup]; protected static registeredModels = [
Migration, MenuItem, KeyValue, Codium, Database, DatabaseColumn, DatabaseEntry, FileGroup, Page, PageNode
];
protected initialized = false; protected initialized = false;
migrations!: Dexie.Table<IMigration, number>; migrations!: Dexie.Table<IMigration, number>;
@ -24,6 +29,8 @@ export class DatabaseService extends Dexie {
databaseColumns!: Dexie.Table<IDatabaseColumn, number>; databaseColumns!: Dexie.Table<IDatabaseColumn, number>;
databaseEntries!: Dexie.Table<IDatabaseEntry, number>; databaseEntries!: Dexie.Table<IDatabaseEntry, number>;
fileGroups!: Dexie.Table<IFileGroup, number>; fileGroups!: Dexie.Table<IFileGroup, number>;
pages!: Dexie.Table<IPage, number>;
pageNodes!: Dexie.Table<IPageNode, number>;
constructor( constructor(
) { ) {
@ -56,7 +63,7 @@ export class DatabaseService extends Dexie {
schema[ModelClass.getTableName()] = ModelClass.getSchema(); schema[ModelClass.getTableName()] = ModelClass.getSchema();
} }
await this.version(11).stores(schema); await this.version(14).stores(schema);
await this.open(); await this.open();
this.migrations = this.table('migrations'); this.migrations = this.table('migrations');
@ -82,6 +89,12 @@ export class DatabaseService extends Dexie {
this.fileGroups = this.table('fileGroups'); this.fileGroups = this.table('fileGroups');
this.fileGroups.mapToClass(FileGroup); this.fileGroups.mapToClass(FileGroup);
this.pages = this.table('pages');
this.pages.mapToClass(Page);
this.pageNodes = this.table('pageNodes');
this.pageNodes.mapToClass(PageNode);
} }
public async purge() { public async purge() {
@ -96,6 +109,8 @@ export class DatabaseService extends Dexie {
this.databaseColumns.clear(), this.databaseColumns.clear(),
this.databaseEntries.clear(), this.databaseEntries.clear(),
this.fileGroups.clear(), this.fileGroups.clear(),
this.pages.clear(),
this.pageNodes.clear(),
]); ]);
} }
} }

View File

@ -1,10 +1,13 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ApiService} from './api.service'; import {ApiService, ResourceNotAvailableOfflineError} from './api.service';
import PageRecord from '../structures/PageRecord'; import PageRecord from '../structures/PageRecord';
import HostRecord from '../structures/HostRecord'; import HostRecord from '../structures/HostRecord';
import {EditorNodeContract} from '../components/nodes/EditorNode.contract'; import {EditorNodeContract} from '../components/nodes/EditorNode.contract';
import {BehaviorSubject, Subscription} from 'rxjs'; import {BehaviorSubject, Subscription} from 'rxjs';
import {NavigationService} from './navigation.service'; import {NavigationService} from './navigation.service';
import {DatabaseService} from './db/database.service';
import {Page} from './db/Page';
import {PageNode} from './db/PageNode';
export class NoPageLoadedError extends Error { export class NoPageLoadedError extends Error {
constructor(msg = 'There is no page open for editing.') { constructor(msg = 'There is no page open for editing.') {
@ -35,6 +38,8 @@ export class EditorService {
protected subs: Subscription[] = []; protected subs: Subscription[] = [];
protected saving = false; protected saving = false;
protected saveTriggered = false; protected saveTriggered = false;
public notAvailable = false;
protected privTriggerSave = debounce(() => { protected privTriggerSave = debounce(() => {
if ( this.saving ) { if ( this.saving ) {
this.triggerSave(); this.triggerSave();
@ -84,6 +89,7 @@ export class EditorService {
constructor( constructor(
protected api: ApiService, protected api: ApiService,
protected nav: NavigationService, protected nav: NavigationService,
protected db: DatabaseService,
) { } ) { }
async startEditing(pageId: string) { async startEditing(pageId: string) {
@ -91,9 +97,19 @@ export class EditorService {
await this.stopEditing(); await this.stopEditing();
} }
try {
this.currentPage = await this.loadPage(pageId); this.currentPage = await this.loadPage(pageId);
this.currentNodes = await this.loadNodes(pageId); this.currentNodes = await this.loadNodes(pageId);
this.notAvailable = false;
await this.ready$.next(true); await this.ready$.next(true);
} catch (e) {
if ( e instanceof ResourceNotAvailableOfflineError ) {
this.notAvailable = true;
await this.ready$.next(true);
} else {
throw e;
}
}
} }
async stopEditing() { async stopEditing() {
@ -159,12 +175,70 @@ export class EditorService {
} }
async savePage(page: PageRecord): Promise<void> { async savePage(page: PageRecord): Promise<void> {
await new Promise((res, rej) => { await new Promise(async (res, rej) => {
const existingLocalPage = await this.db.pages.where({ UUID: page.UUID }).first() as Page;
const saveData = page.toSave(); const saveData = page.toSave();
if ( this.api.isOffline ) {
if ( existingLocalPage ) {
existingLocalPage.fillFromRecord(page);
existingLocalPage.UpdatedAt = String(new Date()); // FIXME update UpdateUserId
existingLocalPage.needsServerUpdate = true;
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,
true,
);
await newLocalPage.save();
}
return res();
}
this.api.post(`/page/${page.UUID}/save`, saveData).subscribe({ this.api.post(`/page/${page.UUID}/save`, saveData).subscribe({
next: result => { next: async result => {
console.log('save result', 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,
false
);
await newLocalPage.save();
}
res(); res();
}, },
error: rej, error: rej,
@ -173,19 +247,85 @@ export class EditorService {
} }
async saveNodesAsPage(page: PageRecord, nodes: HostRecord[]): Promise<HostRecord[]> { async saveNodesAsPage(page: PageRecord, nodes: HostRecord[]): Promise<HostRecord[]> {
return new Promise((res, rej) => { return new Promise(async (res, rej) => {
const saveNodes = nodes.map(x => { const saveNodes = nodes.map(x => {
x.PageId = page.UUID; x.PageId = page.UUID;
return x.toSave(); return x.toSave();
}); });
this.api.post(`/page/${page.UUID}/nodes/save`, saveNodes).subscribe({ const existingLocalPage = await this.db.pages.where({ UUID: page.UUID }).first() as Page;
next: result => {
res(result.data.map(rec => { // 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 = true;
await existingLocalPage.save();
return res(nodeRecs.map(x => {
const rec = x.inflateToRecord();
const host = new HostRecord(rec.Value.Value); const host = new HostRecord(rec.Value.Value);
host.load(rec); host.load(rec);
return host; 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, error: rej,
}); });
@ -193,12 +333,60 @@ export class EditorService {
} }
async saveNodeToPage(page: PageRecord, node: HostRecord): Promise<HostRecord> { async saveNodeToPage(page: PageRecord, node: HostRecord): Promise<HostRecord> {
return new Promise((res, rej) => { return new Promise(async (res, rej) => {
node.PageId = page.UUID; node.PageId = page.UUID;
const nodeData = node.toSave(); 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());
}
const newLocalNode = new PageNode(
nodeData.UUID || PageNode.getUUID(),
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 = true;
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({ this.api.post(`/page/${page.UUID}/nodes/save_one`, { nodeData }).subscribe({
next: result => { 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);
console.log('saving local page data', result.data.UUID);
await localPage.save();
}
const host = new HostRecord(result.data.Value.Value); const host = new HostRecord(result.data.Value.Value);
host.load(result.data); host.load(result.data);
res(host); res(host);
@ -235,6 +423,16 @@ export class EditorService {
delete this.nodeIdToEditorContract[nodeId]; 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.currentNodes = this.currentNodes.filter(x => x.UUID !== nodeId);
this.dirtyOverride = true; this.dirtyOverride = true;
this.triggerSave(); this.triggerSave();
@ -270,6 +468,7 @@ export class EditorService {
this.currentNodes.push(host); this.currentNodes.push(host);
} }
this.currentPage.NodeIds.push(host.UUID);
this.dirtyOverride = true; this.dirtyOverride = true;
this.triggerSave(); this.triggerSave();
return host; return host;
@ -325,10 +524,50 @@ export class EditorService {
} }
async loadPage(pageId: string): Promise<PageRecord> { async loadPage(pageId: string): Promise<PageRecord> {
return new Promise((res, rej) => { 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(existingLocalPage.getSaveRecord());
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
// If we're online, fetch the page record and store it locally
this.api.get(`/page/${pageId}`).subscribe({ this.api.get(`/page/${pageId}`).subscribe({
next: result => { next: async result => {
res(new PageRecord(result.data)); const page = new PageRecord(result.data);
if ( existingLocalPage ) {
existingLocalPage.fillFromRecord(result.data);
existingLocalPage.needsServerUpdate = false;
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, error: rej,
}); });
@ -336,14 +575,44 @@ export class EditorService {
} }
async loadNodes(pageId: string): Promise<HostRecord[]> { async loadNodes(pageId: string): Promise<HostRecord[]> {
return new Promise((res, rej) => { return new Promise(async (res, rej) => {
this.api.get(`/page/${pageId}/nodes`).subscribe({ const existingNodes = await this.db.pageNodes.where({ PageId: pageId }).toArray() as PageNode[];
next: result => {
res(result.data.map(rec => { const inflateRecords = (records) => {
return records.map(rec => {
const host = new HostRecord(rec.Value.Value); const host = new HostRecord(rec.Value.Value);
host.load(rec); host.load(rec);
return host; 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`).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, error: rej,
}); });