Add offline caching for code editor contents
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Garrett Mills 2020-10-21 21:12:04 -05:00
parent 5737dd23ca
commit 8de9db08a6
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
8 changed files with 321 additions and 55 deletions

View File

@ -1,4 +1,4 @@
<div class="code-wrapper" style="width: 100%; height: 600px; margin-top: 10px;"> <div class="code-wrapper" style="width: 100%; height: 600px; margin-top: 10px;" *ngIf="!notAvailableOffline">
<ion-toolbar> <ion-toolbar>
<ion-item> <ion-item>
<ion-label position="floating">Language</ion-label> <ion-label position="floating">Language</ion-label>
@ -16,3 +16,6 @@
></ngx-monaco-editor> ></ngx-monaco-editor>
</div> </div>
</div> </div>
<div class="code-wrapper not-offline" style="width: 100%; height: 600px; margin-top: 10px;" *ngIf="notAvailableOffline">
Sorry, this code editor is not available offline yet.
</div>

View File

@ -1,4 +1,10 @@
div.code-wrapper { div.code-wrapper {
border: 2px solid #8c8c8c; border: 2px solid #8c8c8c;
border-radius: 3px; border-radius: 3px;
&.not-offline {
text-align: center;
padding-top: 100px;
color: #595959;
}
} }

View File

@ -1,6 +1,6 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {v4} from 'uuid'; import {v4} from 'uuid';
import {ApiService} from '../../../service/api.service'; import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
import {EditorNodeContract} from '../../nodes/EditorNode.contract'; import {EditorNodeContract} from '../../nodes/EditorNode.contract';
import {EditorService} from '../../../service/editor.service'; import {EditorService} from '../../../service/editor.service';
@ -14,6 +14,7 @@ export class CodeComponent extends EditorNodeContract implements OnInit {
public dirty = false; public dirty = false;
protected dbRecord: any = {}; protected dbRecord: any = {};
protected codeRefId!: string; protected codeRefId!: string;
public notAvailableOffline = false;
public editorOptions = { public editorOptions = {
language: 'javascript', language: 'javascript',
@ -123,34 +124,36 @@ export class CodeComponent extends EditorNodeContract implements OnInit {
} }
if ( !this.node.Value.Value && this.editorService.canEdit() ) { if ( !this.node.Value.Value && this.editorService.canEdit() ) {
this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/create`).subscribe({ this.api.createCodium(this.page.UUID, this.node.UUID).then(data => {
next: result => { this.dbRecord = data;
this.dbRecord = result.data; this.node.Value.Mode = 'code';
this.node.Value.Mode = 'code'; this.node.Value.Value = data.UUID;
this.node.Value.Value = result.data.UUID; this.node.value = data.UUID;
this.node.value = result.data.UUID; this.codeRefId = data.UUID;
this.codeRefId = result.data.UUID; this.editorOptions.readOnly = this.readonly;
this.editorOptions.readOnly = this.readonly; this.onSelectChange(false);
this.onSelectChange(false); this.hadLoad = true;
this.hadLoad = true; this.notAvailableOffline = false;
res(); res();
}, }).catch(rej);
error: rej,
});
} else { } else {
this.api.get(`/code/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}`).subscribe({ this.api.getCodium(this.page.UUID, this.node.UUID, this.node.Value.Value).then(data => {
next: result => { this.dbRecord = data;
this.dbRecord = result.data; this.initialValue = this.dbRecord.code;
this.initialValue = this.dbRecord.code; this.editorValue = this.dbRecord.code;
this.editorValue = this.dbRecord.code; this.editorOptions.language = this.dbRecord.Language;
this.editorOptions.language = this.dbRecord.Language; this.codeRefId = this.node.Value.Value;
this.codeRefId = this.node.Value.Value; this.editorOptions.readOnly = this.readonly;
this.editorOptions.readOnly = this.readonly; this.onSelectChange(false);
this.onSelectChange(false); this.hadLoad = true;
this.hadLoad = true; this.notAvailableOffline = false;
res(); res();
}, }).catch(e => {
error: rej, if ( e instanceof ResourceNotAvailableOfflineError ) {
this.notAvailableOffline = true;
} else {
rej(e);
}
}); });
} }
}); });
@ -165,29 +168,18 @@ export class CodeComponent extends EditorNodeContract implements OnInit {
this.dbRecord.code = this.editorValue; this.dbRecord.code = this.editorValue;
this.dbRecord.Language = this.editorOptions.language; this.dbRecord.Language = this.editorOptions.language;
this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}`, this.dbRecord) this.api.saveCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbRecord).then(data => {
.subscribe({ this.dbRecord = data;
next: result => { this.editorOptions.language = this.dbRecord.Language;
this.dbRecord = result.data; this.editorValue = this.dbRecord.code;
this.editorOptions.language = this.dbRecord.Language; this.dirty = false;
this.editorValue = this.dbRecord.code; res();
this.dirty = false; }).catch(rej);
res();
},
error: rej,
});
}); });
} }
public performDelete(): void | Promise<void> { public performDelete(): void | Promise<void> {
return new Promise((res, rej) => { return this.api.deleteCodium(this.page.UUID, this.node.UUID, this.node.Value.Value);
this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/delete/${this.node.Value.Value}`).subscribe({
next: result => {
res();
},
error: rej,
});
});
} }
ngOnInit() { ngOnInit() {

View File

@ -6,6 +6,13 @@ import ApiResponse from '../structures/ApiResponse';
import {MenuItem} from './db/MenuItem'; import {MenuItem} from './db/MenuItem';
import {DatabaseService} from './db/database.service'; import {DatabaseService} from './db/database.service';
import {ConnectionService} from 'ng-connection-service'; import {ConnectionService} from 'ng-connection-service';
import {Codium} from './db/Codium';
export class ResourceNotAvailableOfflineError extends Error {
constructor(msg = 'This resource is not yet available offline on this device.') {
super(msg);
}
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -221,4 +228,163 @@ export class ApiService {
res(); res();
}); });
} }
public deleteCodium(PageId: string, NodeId: string, CodiumId: string): Promise<void> {
return new Promise(async (res, rej) => {
const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray();
const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined;
if ( this.isOffline ) {
if ( existingLocalCodium ) {
existingLocalCodium.deleted = true;
existingLocalCodium.needsServerUpdate = true;
await existingLocalCodium.save();
return res();
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.post(`/code/${PageId}/${NodeId}/delete/${CodiumId}`).subscribe({
next: async result => {
if ( existingLocalCodium ) {
await this.db.codiums.delete(existingLocalCodium.id);
}
res();
},
error: rej,
});
});
}
public saveCodium(PageId: string, NodeId: string, CodiumId: string, data: any): Promise<any> {
return new Promise(async (res, rej) => {
const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray();
const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined;
// If we're offline, update or create the local record
if ( this.isOffline ) {
if ( existingLocalCodium ) {
existingLocalCodium.fillFromRecord(data);
existingLocalCodium.needsServerUpdate = true;
await existingLocalCodium.save();
return res(existingLocalCodium.getSaveRecord());
} else {
const newLocalCodium = new Codium(
data.Language,
NodeId,
PageId,
data.code,
data.UUID || Codium.getUUID(),
true,
);
await newLocalCodium.save();
return res(newLocalCodium.getSaveRecord());
}
}
// If we're online, save the data and update our local records
this.post(`/code/${PageId}/${NodeId}/set/${CodiumId}`, data).subscribe({
next: async result => {
if ( existingLocalCodium ) {
existingLocalCodium.fillFromRecord(result.data);
existingLocalCodium.needsServerUpdate = false;
await existingLocalCodium.save();
return res(result.data);
} else {
const newLocalCodium = new Codium(
result.data.Language,
result.data.NodeId,
result.data.PageId,
result.data.code,
result.data.UUID,
);
await newLocalCodium.save();
return res(result.data);
}
},
error: rej,
});
});
}
public createCodium(PageId: string, NodeId: string): Promise<any> {
return new Promise(async (res, rej) => {
// If offline, create a new local DB record
if ( this.isOffline ) {
const newLocalCodium = new Codium(
'javascript',
NodeId,
PageId,
'',
Codium.getUUID(),
true
);
await newLocalCodium.save();
return res(newLocalCodium.getSaveRecord());
}
// If online, create a new record on the server and sync it to the local db
this.post(`/code/${PageId}/${NodeId}/create`).subscribe({
next: async result => {
const newLocalCodium = new Codium(
result.data.Language,
result.data.NodeId,
result.data.PageId,
result.data.code,
result.data.UUID,
);
await newLocalCodium.save();
res(result.data);
},
error: rej,
});
});
}
public getCodium(PageId: string, NodeId: string, CodiumId: string): Promise<any> {
return new Promise(async (res, rej) => {
const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray();
const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined;
// If offline, try to load it from the local DB
if ( this.isOffline ) {
if ( existingLocalCodium ) {
return res(existingLocalCodium.getSaveRecord());
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
// If online, fetch the codium and store/update it locally
this.get(`/code/${PageId}/${NodeId}/get/${CodiumId}`).subscribe({
next: async result => {
if ( existingLocalCodium ) {
existingLocalCodium.fillFromRecord(result.data);
await existingLocalCodium.save();
return res(result.data);
} else {
const newLocalCodium = new Codium(
result.data.Language,
result.data.NodeId,
result.data.PageId,
result.data.code,
result.data.UUID
);
await newLocalCodium.save();
return res(result.data);
}
},
error: rej,
});
});
}
} }

View File

@ -0,0 +1,87 @@
import {Model} from './Model';
export interface ICodium {
id?: number;
Language: string;
NodeId: string;
PageId: string;
code: string;
UUID: string;
needsServerUpdate?: boolean;
deleted?: boolean;
}
export class Codium extends Model<ICodium> implements ICodium {
id?: number;
Language: string;
NodeId: string;
PageId: string;
code: string;
UUID: string;
needsServerUpdate?: boolean;
deleted?: boolean;
public static getTableName() {
return 'codiums';
}
public static getSchema() {
return '++id, Language, NodeId, PageId, code, UUID, needsServerUpdate, deleted';
}
constructor(
Language: string,
NodeId: string,
PageId: string,
code: string,
UUID: string,
needsServerUpdate?: boolean,
deleted?: boolean,
id?: number
) {
super();
this.Language = Language;
this.NodeId = NodeId;
this.PageId = PageId;
this.code = code;
this.UUID = UUID;
if ( typeof needsServerUpdate !== 'undefined' ) {
this.needsServerUpdate = needsServerUpdate;
}
if ( typeof deleted !== 'undefined' ) {
this.deleted = deleted;
}
if ( id ) {
this.id = id;
}
}
public fillFromRecord(record: any) {
this.Language = record.Language;
this.NodeId = record.NodeId;
this.PageId = record.PageId;
this.code = record.code;
this.UUID = record.UUID;
}
public getSaveRecord(): any {
return {
...(this.id ? { id: this.id } : {}),
Language: this.Language,
NodeId: this.NodeId,
PageId: this.PageId,
code: this.code,
UUID: this.UUID,
...(typeof this.needsServerUpdate === 'undefined' ? {} : { needsServerUpdate: this.needsServerUpdate }),
...(typeof this.deleted === 'undefined' ? {} : { deleted: this.deleted }),
};
}
public getDatabase(): Dexie.Table<ICodium, number> {
return this.staticClass().dbService.table('codiums') as Dexie.Table<ICodium, number>;
}
}

View File

@ -1,5 +1,6 @@
import Dexie from 'dexie'; import Dexie from 'dexie';
import {DatabaseService} from './database.service'; import {DatabaseService} from './database.service';
import {uuid_v4} from '../../utility';
export abstract class Model<InterfaceType> { export abstract class Model<InterfaceType> {
public static dbService?: DatabaseService; public static dbService?: DatabaseService;
@ -14,6 +15,10 @@ export abstract class Model<InterfaceType> {
throw new TypeError('Child class must implement.'); throw new TypeError('Child class must implement.');
} }
public static getUUID(): string {
return uuid_v4();
}
public abstract getDatabase(): Dexie.Table<InterfaceType, number>; public abstract getDatabase(): Dexie.Table<InterfaceType, number>;
public abstract getSaveRecord(): any; public abstract getSaveRecord(): any;

View File

@ -3,17 +3,19 @@ import Dexie from 'dexie';
import {IMigration, Migration} from './Migration'; import {IMigration, Migration} from './Migration';
import {IMenuItem, MenuItem} from './MenuItem'; import {IMenuItem, MenuItem} from './MenuItem';
import {KeyValue, IKeyValue} from './KeyValue'; import {KeyValue, IKeyValue} from './KeyValue';
import {Codium, ICodium} from './Codium';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DatabaseService extends Dexie { export class DatabaseService extends Dexie {
protected static registeredModels = [Migration, MenuItem, KeyValue]; protected static registeredModels = [Migration, MenuItem, KeyValue, Codium];
protected initialized = false; protected initialized = false;
migrations!: Dexie.Table<IMigration, number>; migrations!: Dexie.Table<IMigration, number>;
menuItems!: Dexie.Table<IMenuItem, number>; menuItems!: Dexie.Table<IMenuItem, number>;
keyValues!: Dexie.Table<IKeyValue, number>; keyValues!: Dexie.Table<IKeyValue, number>;
codiums!: Dexie.Table<ICodium, number>;
constructor( constructor(
) { ) {
@ -46,7 +48,7 @@ export class DatabaseService extends Dexie {
schema[ModelClass.getTableName()] = ModelClass.getSchema(); schema[ModelClass.getTableName()] = ModelClass.getSchema();
} }
await this.version(3).stores(schema); await this.version(5).stores(schema);
await this.open(); await this.open();
this.migrations = this.table('migrations'); this.migrations = this.table('migrations');
@ -58,10 +60,7 @@ export class DatabaseService extends Dexie {
this.keyValues = this.table('keyValues'); this.keyValues = this.table('keyValues');
this.keyValues.mapToClass(KeyValue); this.keyValues.mapToClass(KeyValue);
// await new Promise(res => { this.codiums = this.table('codiums');
// setTimeout(() => { this.codiums.mapToClass(Codium);
// res();
// }, 1000);
// });
} }
} }

8
src/app/utility.ts Normal file
View File

@ -0,0 +1,8 @@
export function uuid_v4() {
// @ts-ignore
return ([1e7] + - 1e3 + - 4e3 + - 8e3 + - 1e11).replace(/[018]/g, c =>
// tslint:disable-next-line:no-bitwise
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}