Add offline caching for code editor contents
This commit is contained in:
parent
5737dd23ca
commit
8de9db08a6
@ -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-item>
|
||||
<ion-label position="floating">Language</ion-label>
|
||||
@ -16,3 +16,6 @@
|
||||
></ngx-monaco-editor>
|
||||
</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>
|
||||
|
@ -1,4 +1,10 @@
|
||||
div.code-wrapper {
|
||||
border: 2px solid #8c8c8c;
|
||||
border-radius: 3px;
|
||||
|
||||
&.not-offline {
|
||||
text-align: center;
|
||||
padding-top: 100px;
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {v4} from 'uuid';
|
||||
import {ApiService} from '../../../service/api.service';
|
||||
import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
|
||||
import {EditorNodeContract} from '../../nodes/EditorNode.contract';
|
||||
import {EditorService} from '../../../service/editor.service';
|
||||
|
||||
@ -14,6 +14,7 @@ export class CodeComponent extends EditorNodeContract implements OnInit {
|
||||
public dirty = false;
|
||||
protected dbRecord: any = {};
|
||||
protected codeRefId!: string;
|
||||
public notAvailableOffline = false;
|
||||
|
||||
public editorOptions = {
|
||||
language: 'javascript',
|
||||
@ -123,34 +124,36 @@ export class CodeComponent extends EditorNodeContract implements OnInit {
|
||||
}
|
||||
|
||||
if ( !this.node.Value.Value && this.editorService.canEdit() ) {
|
||||
this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/create`).subscribe({
|
||||
next: result => {
|
||||
this.dbRecord = result.data;
|
||||
this.node.Value.Mode = 'code';
|
||||
this.node.Value.Value = result.data.UUID;
|
||||
this.node.value = result.data.UUID;
|
||||
this.codeRefId = result.data.UUID;
|
||||
this.editorOptions.readOnly = this.readonly;
|
||||
this.onSelectChange(false);
|
||||
this.hadLoad = true;
|
||||
res();
|
||||
},
|
||||
error: rej,
|
||||
});
|
||||
this.api.createCodium(this.page.UUID, this.node.UUID).then(data => {
|
||||
this.dbRecord = data;
|
||||
this.node.Value.Mode = 'code';
|
||||
this.node.Value.Value = data.UUID;
|
||||
this.node.value = data.UUID;
|
||||
this.codeRefId = data.UUID;
|
||||
this.editorOptions.readOnly = this.readonly;
|
||||
this.onSelectChange(false);
|
||||
this.hadLoad = true;
|
||||
this.notAvailableOffline = false;
|
||||
res();
|
||||
}).catch(rej);
|
||||
} else {
|
||||
this.api.get(`/code/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}`).subscribe({
|
||||
next: result => {
|
||||
this.dbRecord = result.data;
|
||||
this.initialValue = this.dbRecord.code;
|
||||
this.editorValue = this.dbRecord.code;
|
||||
this.editorOptions.language = this.dbRecord.Language;
|
||||
this.codeRefId = this.node.Value.Value;
|
||||
this.editorOptions.readOnly = this.readonly;
|
||||
this.onSelectChange(false);
|
||||
this.hadLoad = true;
|
||||
res();
|
||||
},
|
||||
error: rej,
|
||||
this.api.getCodium(this.page.UUID, this.node.UUID, this.node.Value.Value).then(data => {
|
||||
this.dbRecord = data;
|
||||
this.initialValue = this.dbRecord.code;
|
||||
this.editorValue = this.dbRecord.code;
|
||||
this.editorOptions.language = this.dbRecord.Language;
|
||||
this.codeRefId = this.node.Value.Value;
|
||||
this.editorOptions.readOnly = this.readonly;
|
||||
this.onSelectChange(false);
|
||||
this.hadLoad = true;
|
||||
this.notAvailableOffline = false;
|
||||
res();
|
||||
}).catch(e => {
|
||||
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.Language = this.editorOptions.language;
|
||||
|
||||
this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}`, this.dbRecord)
|
||||
.subscribe({
|
||||
next: result => {
|
||||
this.dbRecord = result.data;
|
||||
this.editorOptions.language = this.dbRecord.Language;
|
||||
this.editorValue = this.dbRecord.code;
|
||||
this.dirty = false;
|
||||
res();
|
||||
},
|
||||
error: rej,
|
||||
});
|
||||
this.api.saveCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbRecord).then(data => {
|
||||
this.dbRecord = data;
|
||||
this.editorOptions.language = this.dbRecord.Language;
|
||||
this.editorValue = this.dbRecord.code;
|
||||
this.dirty = false;
|
||||
res();
|
||||
}).catch(rej);
|
||||
});
|
||||
}
|
||||
|
||||
public performDelete(): void | Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
this.api.post(`/code/${this.page.UUID}/${this.node.UUID}/delete/${this.node.Value.Value}`).subscribe({
|
||||
next: result => {
|
||||
res();
|
||||
},
|
||||
error: rej,
|
||||
});
|
||||
});
|
||||
return this.api.deleteCodium(this.page.UUID, this.node.UUID, this.node.Value.Value);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -6,6 +6,13 @@ import ApiResponse from '../structures/ApiResponse';
|
||||
import {MenuItem} from './db/MenuItem';
|
||||
import {DatabaseService} from './db/database.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({
|
||||
providedIn: 'root'
|
||||
@ -221,4 +228,163 @@ export class ApiService {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
87
src/app/service/db/Codium.ts
Normal file
87
src/app/service/db/Codium.ts
Normal 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>;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import Dexie from 'dexie';
|
||||
import {DatabaseService} from './database.service';
|
||||
import {uuid_v4} from '../../utility';
|
||||
|
||||
export abstract class Model<InterfaceType> {
|
||||
public static dbService?: DatabaseService;
|
||||
@ -14,6 +15,10 @@ export abstract class Model<InterfaceType> {
|
||||
throw new TypeError('Child class must implement.');
|
||||
}
|
||||
|
||||
public static getUUID(): string {
|
||||
return uuid_v4();
|
||||
}
|
||||
|
||||
public abstract getDatabase(): Dexie.Table<InterfaceType, number>;
|
||||
public abstract getSaveRecord(): any;
|
||||
|
||||
|
@ -3,17 +3,19 @@ import Dexie from 'dexie';
|
||||
import {IMigration, Migration} from './Migration';
|
||||
import {IMenuItem, MenuItem} from './MenuItem';
|
||||
import {KeyValue, IKeyValue} from './KeyValue';
|
||||
import {Codium, ICodium} from './Codium';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DatabaseService extends Dexie {
|
||||
protected static registeredModels = [Migration, MenuItem, KeyValue];
|
||||
protected static registeredModels = [Migration, MenuItem, KeyValue, Codium];
|
||||
protected initialized = false;
|
||||
|
||||
migrations!: Dexie.Table<IMigration, number>;
|
||||
menuItems!: Dexie.Table<IMenuItem, number>;
|
||||
keyValues!: Dexie.Table<IKeyValue, number>;
|
||||
codiums!: Dexie.Table<ICodium, number>;
|
||||
|
||||
constructor(
|
||||
) {
|
||||
@ -46,7 +48,7 @@ export class DatabaseService extends Dexie {
|
||||
schema[ModelClass.getTableName()] = ModelClass.getSchema();
|
||||
}
|
||||
|
||||
await this.version(3).stores(schema);
|
||||
await this.version(5).stores(schema);
|
||||
await this.open();
|
||||
|
||||
this.migrations = this.table('migrations');
|
||||
@ -58,10 +60,7 @@ export class DatabaseService extends Dexie {
|
||||
this.keyValues = this.table('keyValues');
|
||||
this.keyValues.mapToClass(KeyValue);
|
||||
|
||||
// await new Promise(res => {
|
||||
// setTimeout(() => {
|
||||
// res();
|
||||
// }, 1000);
|
||||
// });
|
||||
this.codiums = this.table('codiums');
|
||||
this.codiums.mapToClass(Codium);
|
||||
}
|
||||
}
|
||||
|
8
src/app/utility.ts
Normal file
8
src/app/utility.ts
Normal 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)
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user