Cache pages and page nodes for offline use
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
380a139de3
commit
fe7e955875
15
.drone.yml
15
.drone.yml
@ -128,18 +128,3 @@ steps:
|
||||
}
|
||||
when:
|
||||
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
|
@ -21,7 +21,12 @@
|
||||
</ion-header>
|
||||
|
||||
<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="host-container"
|
||||
|
@ -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 PageRecord from '../../structures/PageRecord';
|
||||
import {PageService} from '../../service/page.service';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {LoadingController, PopoverController} from '@ionic/angular';
|
||||
import {NodePickerComponent} from '../../components/editor/node-picker/node-picker.component';
|
||||
@ -15,13 +13,8 @@ import {NodeTypeIcons} from '../../structures/node-types';
|
||||
styleUrls: ['./editor.page.scss'],
|
||||
})
|
||||
export class EditorPage implements OnInit {
|
||||
// @ViewChildren('editorHosts') editorHosts;
|
||||
// @ViewChild('titleBar') titleBar;
|
||||
|
||||
public typeIcons = NodeTypeIcons;
|
||||
|
||||
@Input() pageId: string;
|
||||
public pageName = '';
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
@ -96,7 +89,6 @@ export class EditorPage implements OnInit {
|
||||
});
|
||||
|
||||
popover.onDidDismiss().then(result => {
|
||||
console.log('adding node', result.data);
|
||||
if ( !result.data ) {
|
||||
return;
|
||||
}
|
||||
@ -104,238 +96,6 @@ export class EditorPage implements OnInit {
|
||||
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();
|
||||
}
|
||||
|
||||
// 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
150
src/app/service/db/Page.ts
Normal 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>;
|
||||
}
|
||||
}
|
113
src/app/service/db/PageNode.ts
Normal file
113
src/app/service/db/PageNode.ts
Normal 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>;
|
||||
}
|
||||
}
|
@ -8,12 +8,17 @@ import {Database, IDatabase} from './Database';
|
||||
import {DatabaseColumn, IDatabaseColumn} from './DatabaseColumn';
|
||||
import {DatabaseEntry, IDatabaseEntry} from './DatabaseEntry';
|
||||
import {FileGroup, IFileGroup} from './FileGroup';
|
||||
import {Page, IPage} from './Page';
|
||||
import {PageNode, IPageNode} from './PageNode';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
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;
|
||||
|
||||
migrations!: Dexie.Table<IMigration, number>;
|
||||
@ -24,6 +29,8 @@ export class DatabaseService extends Dexie {
|
||||
databaseColumns!: Dexie.Table<IDatabaseColumn, number>;
|
||||
databaseEntries!: Dexie.Table<IDatabaseEntry, number>;
|
||||
fileGroups!: Dexie.Table<IFileGroup, number>;
|
||||
pages!: Dexie.Table<IPage, number>;
|
||||
pageNodes!: Dexie.Table<IPageNode, number>;
|
||||
|
||||
constructor(
|
||||
) {
|
||||
@ -56,7 +63,7 @@ export class DatabaseService extends Dexie {
|
||||
schema[ModelClass.getTableName()] = ModelClass.getSchema();
|
||||
}
|
||||
|
||||
await this.version(11).stores(schema);
|
||||
await this.version(14).stores(schema);
|
||||
await this.open();
|
||||
|
||||
this.migrations = this.table('migrations');
|
||||
@ -82,6 +89,12 @@ export class DatabaseService extends Dexie {
|
||||
|
||||
this.fileGroups = this.table('fileGroups');
|
||||
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() {
|
||||
@ -96,6 +109,8 @@ export class DatabaseService extends Dexie {
|
||||
this.databaseColumns.clear(),
|
||||
this.databaseEntries.clear(),
|
||||
this.fileGroups.clear(),
|
||||
this.pages.clear(),
|
||||
this.pageNodes.clear(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ApiService} from './api.service';
|
||||
import {ApiService, ResourceNotAvailableOfflineError} 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';
|
||||
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 {
|
||||
constructor(msg = 'There is no page open for editing.') {
|
||||
@ -35,6 +38,8 @@ export class EditorService {
|
||||
protected subs: Subscription[] = [];
|
||||
protected saving = false;
|
||||
protected saveTriggered = false;
|
||||
public notAvailable = false;
|
||||
|
||||
protected privTriggerSave = debounce(() => {
|
||||
if ( this.saving ) {
|
||||
this.triggerSave();
|
||||
@ -84,6 +89,7 @@ export class EditorService {
|
||||
constructor(
|
||||
protected api: ApiService,
|
||||
protected nav: NavigationService,
|
||||
protected db: DatabaseService,
|
||||
) { }
|
||||
|
||||
async startEditing(pageId: string) {
|
||||
@ -91,9 +97,19 @@ export class EditorService {
|
||||
await this.stopEditing();
|
||||
}
|
||||
|
||||
this.currentPage = await this.loadPage(pageId);
|
||||
this.currentNodes = await this.loadNodes(pageId);
|
||||
await this.ready$.next(true);
|
||||
try {
|
||||
this.currentPage = await this.loadPage(pageId);
|
||||
this.currentNodes = await this.loadNodes(pageId);
|
||||
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() {
|
||||
@ -159,12 +175,70 @@ export class EditorService {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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({
|
||||
next: result => {
|
||||
console.log('save result', result);
|
||||
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,
|
||||
false
|
||||
);
|
||||
|
||||
await newLocalPage.save();
|
||||
}
|
||||
|
||||
res();
|
||||
},
|
||||
error: rej,
|
||||
@ -173,19 +247,85 @@ export class EditorService {
|
||||
}
|
||||
|
||||
async saveNodesAsPage(page: PageRecord, nodes: HostRecord[]): Promise<HostRecord[]> {
|
||||
return new Promise((res, rej) => {
|
||||
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 = true;
|
||||
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: result => {
|
||||
res(result.data.map(rec => {
|
||||
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);
|
||||
return host;
|
||||
}));
|
||||
returns.push(host);
|
||||
}
|
||||
|
||||
return res(returns);
|
||||
},
|
||||
error: rej,
|
||||
});
|
||||
@ -193,12 +333,60 @@ export class EditorService {
|
||||
}
|
||||
|
||||
async saveNodeToPage(page: PageRecord, node: HostRecord): Promise<HostRecord> {
|
||||
return new Promise((res, rej) => {
|
||||
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());
|
||||
}
|
||||
|
||||
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({
|
||||
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);
|
||||
host.load(result.data);
|
||||
res(host);
|
||||
@ -235,6 +423,16 @@ export class EditorService {
|
||||
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();
|
||||
@ -270,6 +468,7 @@ export class EditorService {
|
||||
this.currentNodes.push(host);
|
||||
}
|
||||
|
||||
this.currentPage.NodeIds.push(host.UUID);
|
||||
this.dirtyOverride = true;
|
||||
this.triggerSave();
|
||||
return host;
|
||||
@ -325,10 +524,50 @@ export class EditorService {
|
||||
}
|
||||
|
||||
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({
|
||||
next: result => {
|
||||
res(new PageRecord(result.data));
|
||||
next: async result => {
|
||||
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,
|
||||
});
|
||||
@ -336,14 +575,44 @@ export class EditorService {
|
||||
}
|
||||
|
||||
async loadNodes(pageId: string): Promise<HostRecord[]> {
|
||||
return new Promise((res, rej) => {
|
||||
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`).subscribe({
|
||||
next: result => {
|
||||
res(result.data.map(rec => {
|
||||
const host = new HostRecord(rec.Value.Value);
|
||||
host.load(rec);
|
||||
return host;
|
||||
}));
|
||||
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,
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user