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-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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,24 +124,21 @@ 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 = result.data.UUID;
|
this.node.Value.Value = data.UUID;
|
||||||
this.node.value = result.data.UUID;
|
this.node.value = data.UUID;
|
||||||
this.codeRefId = result.data.UUID;
|
this.codeRefId = 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;
|
||||||
@ -148,9 +146,14 @@ export class CodeComponent extends EditorNodeContract implements OnInit {
|
|||||||
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.dbRecord = result.data;
|
|
||||||
this.editorOptions.language = this.dbRecord.Language;
|
this.editorOptions.language = this.dbRecord.Language;
|
||||||
this.editorValue = this.dbRecord.code;
|
this.editorValue = this.dbRecord.code;
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
res();
|
res();
|
||||||
},
|
}).catch(rej);
|
||||||
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() {
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
@ -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
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