Add offline cachine for file group elements and contents (not files, though)
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Garrett Mills 2020-10-21 23:12:57 -05:00
parent 02d8505b05
commit 294b312641
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
9 changed files with 265 additions and 47 deletions

View File

@ -1,4 +1,4 @@
<div class="database-wrapper"> <div class="database-wrapper" *ngIf="!notAvailableOffline">
<ion-toolbar> <ion-toolbar>
<ion-input <ion-input
[readonly]="readonly" [readonly]="readonly"
@ -25,3 +25,7 @@
></ag-grid-angular> ></ag-grid-angular>
</div> </div>
</div> </div>
<div class="database-wrapper not-available" *ngIf="notAvailableOffline">
Sorry, this database is not available offline yet.
</div>

View File

@ -1,4 +1,11 @@
div.database-wrapper { div.database-wrapper {
border: 2px solid #8c8c8c; border: 2px solid #8c8c8c;
border-radius: 3px; border-radius: 3px;
&.not-available {
height: 600px;
text-align: center;
padding-top: 100px;
color: #494949;
}
} }

View File

@ -1,4 +1,4 @@
<div class="files-wrapper"> <div class="files-wrapper" *ngIf="!notAvailableOffline">
<div class="new-uploads" style="margin: 20px;" *ngIf="!readonly"> <div class="new-uploads" style="margin: 20px;" *ngIf="!readonly">
<form #uploadForm [action]="getApiSubmit()" enctype="multipart/form-data" method="post"> <form #uploadForm [action]="getApiSubmit()" enctype="multipart/form-data" method="post">
<input style="margin-top: 10px;" type="file" id="file" name="uploaded_file"> <input style="margin-top: 10px;" type="file" id="file" name="uploaded_file">
@ -21,3 +21,6 @@
</ion-grid> </ion-grid>
</div> </div>
</div> </div>
<div class="files-wrapper not-available" *ngIf="notAvailableOffline">
Sorry, this file group is not available offline yet.
</div>

View File

@ -2,4 +2,11 @@ div.files-wrapper {
border: 2px solid #8c8c8c; border: 2px solid #8c8c8c;
border-radius: 3px; border-radius: 3px;
margin-top: 15px; margin-top: 15px;
&.not-available {
height: 150px;
text-align: center;
padding-top: 40px;
color: #494949;
}
} }

View File

@ -1,8 +1,6 @@
import {Component, ElementRef, EventEmitter, Inject, Input, OnInit, Output, ViewChild} from '@angular/core'; import {Component, ElementRef, Inject, Input, OnInit, ViewChild} from '@angular/core';
import HostRecord from '../../../structures/HostRecord'; import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
import {ApiService} from '../../../service/api.service';
import {AlertController} from '@ionic/angular'; import {AlertController} from '@ionic/angular';
import {Observable} from 'rxjs';
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import {EditorService} from '../../../service/editor.service'; import {EditorService} from '../../../service/editor.service';
import {EditorNodeContract} from '../../nodes/EditorNode.contract'; import {EditorNodeContract} from '../../nodes/EditorNode.contract';
@ -15,16 +13,12 @@ import {EditorNodeContract} from '../../nodes/EditorNode.contract';
export class FilesComponent extends EditorNodeContract implements OnInit { export class FilesComponent extends EditorNodeContract implements OnInit {
@Input() nodeId: string; @Input() nodeId: string;
@ViewChild('uploadForm') uploadForm: ElementRef; @ViewChild('uploadForm') uploadForm: ElementRef;
// @Input() readonly = false;
// @Input() hostRecord: HostRecord;
// @Output() hostRecordChange = new EventEmitter<HostRecord>();
// @Output() requestParentSave = new EventEmitter<FilesComponent>();
// @Output() requestParentDelete = new EventEmitter<FilesComponent>();
public fileRecords: Array<any> = []; public fileRecords: Array<any> = [];
public pendingSetup = true; public pendingSetup = true;
public dbRecord: any = {}; public dbRecord: any = {};
public dirty = false; public dirty = false;
public notAvailableOffline = false;
public get readonly() { public get readonly() {
return !this.node || !this.editorService.canEdit(); return !this.node || !this.editorService.canEdit();
@ -32,7 +26,6 @@ export class FilesComponent extends EditorNodeContract implements OnInit {
constructor( constructor(
protected api: ApiService, protected api: ApiService,
protected alerts: AlertController,
public readonly editorService: EditorService, public readonly editorService: EditorService,
@Inject(APP_BASE_HREF) private baseHref: string @Inject(APP_BASE_HREF) private baseHref: string
) { super(); } ) { super(); }
@ -55,45 +48,31 @@ export class FilesComponent extends EditorNodeContract implements OnInit {
} }
if ( !this.node.Value.Value && !this.readonly ) { if ( !this.node.Value.Value && !this.readonly ) {
await new Promise((res, rej) => { this.dbRecord = await this.api.createFileGroup(this.page.UUID, this.node.UUID);
this.api.post(`/files/${this.page.UUID}/${this.node.UUID}/create`).subscribe({ this.fileRecords = this.dbRecord.files;
next: result => { this.node.Value.Mode = 'files';
this.dbRecord = result.data; this.node.Value.Value = this.dbRecord.UUID;
this.fileRecords = result.data.files; this.node.value = this.dbRecord.UUID;
this.node.Value.Mode = 'files'; this.dirty = true;
this.node.Value.Value = result.data.UUID;
this.node.value = result.data.UUID;
this.dirty = true;
res();
},
error: rej,
});
});
} else { } else {
await new Promise((res, rej) => { try {
this.api.get(`/files/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}`).subscribe({ this.dbRecord = await this.api.getFileGroup(this.page.UUID, this.node.UUID, this.node.Value.Value);
next: result => { this.fileRecords = this.dbRecord.files;
this.dbRecord = result.data; this.notAvailableOffline = false;
this.fileRecords = result.data.files; } catch (e) {
res(); if ( e instanceof ResourceNotAvailableOfflineError ) {
}, this.notAvailableOffline = true;
error: rej, } else {
}); throw e;
}); }
}
} }
this.pendingSetup = false; this.pendingSetup = false;
} }
public async performDelete(): Promise<void> { public async performDelete(): Promise<void> {
await new Promise((res, rej) => { await this.api.deleteFileGroup(this.page.UUID, this.node.UUID, this.node.Value.Value);
this.api.post(`/files/${this.page.UUID}/${this.node.UUID}/delete/${this.node.Value.Value}`).subscribe({
next: result => {
res();
},
error: rej,
});
});
} }
ngOnInit() { ngOnInit() {

View File

@ -2,6 +2,7 @@ import {Component, Input, OnInit} from '@angular/core';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {ApiService} from '../../service/api.service'; import {ApiService} from '../../service/api.service';
import {PopoverController} from '@ionic/angular'; import {PopoverController} from '@ionic/angular';
import {DatabaseService} from "../../service/db/database.service";
@Component({ @Component({
selector: 'app-option-picker', selector: 'app-option-picker',
@ -17,14 +18,16 @@ export class OptionPickerComponent implements OnInit {
protected api: ApiService, protected api: ApiService,
protected router: Router, protected router: Router,
protected popover: PopoverController, protected popover: PopoverController,
protected db: DatabaseService,
) { } ) { }
ngOnInit() {} ngOnInit() {}
onSelect(key) { async onSelect(key) {
if ( key === 'html_export' ) { if ( key === 'html_export' ) {
window.open(this.api._build_url('/data/export/html'), '_blank'); window.open(this.api._build_url('/data/export/html'), '_blank');
} else if ( key === 'logout' ) { } else if ( key === 'logout' ) {
await this.db.purge();
window.location.href = '/auth/logout'; window.location.href = '/auth/logout';
} else if ( key === 'toggle_darkmode' ) { } else if ( key === 'toggle_darkmode' ) {
this.toggleDark(); this.toggleDark();

View File

@ -10,6 +10,7 @@ import {Codium} from './db/Codium';
import {Database} from './db/Database'; import {Database} from './db/Database';
import {DatabaseColumn} from './db/DatabaseColumn'; import {DatabaseColumn} from './db/DatabaseColumn';
import {DatabaseEntry} from './db/DatabaseEntry'; import {DatabaseEntry} from './db/DatabaseEntry';
import {FileGroup} from "./db/FileGroup";
export class ResourceNotAvailableOfflineError extends Error { export class ResourceNotAvailableOfflineError extends Error {
constructor(msg = 'This resource is not yet available offline on this device.') { constructor(msg = 'This resource is not yet available offline on this device.') {
@ -703,4 +704,101 @@ export class ApiService {
}); });
}); });
} }
public deleteFileGroup(PageId: string, NodeId: string, FileGroupId: string): Promise<void> {
return new Promise(async (res, rej) => {
const existingFileGroup = await this.db.fileGroups.where({ UUID: FileGroupId }).first() as FileGroup;
if ( this.isOffline ) {
if ( existingFileGroup ) {
existingFileGroup.deleted = true;
existingFileGroup.needsServerUpdate = true;
await existingFileGroup.save();
return res();
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.post(`/files/${PageId}/${NodeId}/delete/${FileGroupId}`).subscribe({
next: async result => {
if ( existingFileGroup ) {
await this.db.fileGroups.delete(existingFileGroup.id);
res();
}
},
error: rej,
});
});
}
public createFileGroup(PageId: string, NodeId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
const newFileGroup = new FileGroup(
NodeId,
PageId,
[],
JSON.stringify([]),
FileGroup.getUUID(),
true
);
await newFileGroup.save();
return res(newFileGroup.inflateToRecord());
}
this.post(`/files/${PageId}/${NodeId}/create`).subscribe({
next: async result => {
const newFileGroup = new FileGroup(
result.data.NodeId,
result.data.PageId,
result.data.FileIds,
JSON.stringify(result.data.files),
result.data.UUID
);
await newFileGroup.save();
res(result.data);
},
error: rej,
});
});
}
public getFileGroup(PageId: string, NodeId: string, FileGroupId: string): Promise<any> {
return new Promise(async (res, rej) => {
const existingFileGroup = await this.db.fileGroups.where({ UUID: FileGroupId }).first() as FileGroup;
if ( this.isOffline ) {
if ( existingFileGroup ) {
return res(existingFileGroup.inflateToRecord());
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.get(`/files/${PageId}/${NodeId}/get/${FileGroupId}`).subscribe({
next: async result => {
if ( existingFileGroup ) {
existingFileGroup.fillFromRecord(result.data);
existingFileGroup.needsServerUpdate = false;
await existingFileGroup.save();
} else {
const newFileGroup = new FileGroup(
result.data.NodeId,
result.data.PageId,
result.data.FileIds,
JSON.stringify(result.data.files),
result.data.UUID
);
await newFileGroup.save();
}
res(result.data);
},
error: rej,
});
});
}
} }

View File

@ -0,0 +1,97 @@
import {Model} from './Model';
export interface IFileGroup {
id?: number;
NodeId: string;
PageId: string;
FileIds: string[];
filesJSON: string;
UUID: string;
needsServerUpdate?: boolean;
deleted?: boolean;
}
export class FileGroup extends Model<IFileGroup> implements IFileGroup {
id?: number;
NodeId: string;
PageId: string;
FileIds: string[];
filesJSON: string;
UUID: string;
needsServerUpdate?: boolean;
deleted?: boolean;
public static getTableName() {
return 'fileGroups';
}
public static getSchema() {
return '++id, NodeId, PageId, FileIds, filesJSON, UUID, needsServerUpdate, deleted';
}
constructor(
NodeId: string,
PageId: string,
FileIds: string[],
filesJSON: string,
UUID: string,
needsServerUpdate?: boolean,
deleted?: boolean,
id?: number
) {
super();
this.NodeId = NodeId;
this.PageId = PageId;
this.FileIds = FileIds;
this.filesJSON = filesJSON;
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.NodeId = record.NodeId;
this.PageId = record.PageId;
this.FileIds = record.FileIds;
this.filesJSON = JSON.stringify(record.files);
this.UUID = record.UUID;
}
public inflateToRecord() {
return {
NodeId: this.NodeId,
PageId: this.PageId,
FileIds: this.FileIds,
files: JSON.parse(this.filesJSON),
UUID: this.UUID,
};
}
public getSaveRecord(): any {
return {
...(this.id ? { id: this.id } : {}),
NodeId: this.NodeId,
PageId: this.PageId,
FileIds: this.FileIds,
filesJSON: this.filesJSON,
UUID: this.UUID,
...(typeof this.needsServerUpdate === 'undefined' ? {} : { needsServerUpdate: this.needsServerUpdate }),
...(typeof this.deleted === 'undefined' ? {} : { deleted: this.deleted }),
};
}
public getDatabase(): Dexie.Table<IFileGroup, number> {
return this.staticClass().dbService.table('fileGroups') as Dexie.Table<IFileGroup, number>;
}
}

View File

@ -7,12 +7,13 @@ import {Codium, ICodium} from './Codium';
import {Database, IDatabase} from './Database'; 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';
@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]; protected static registeredModels = [Migration, MenuItem, KeyValue, Codium, Database, DatabaseColumn, DatabaseEntry, FileGroup];
protected initialized = false; protected initialized = false;
migrations!: Dexie.Table<IMigration, number>; migrations!: Dexie.Table<IMigration, number>;
@ -22,6 +23,7 @@ export class DatabaseService extends Dexie {
databases!: Dexie.Table<IDatabase, number>; databases!: Dexie.Table<IDatabase, number>;
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>;
constructor( constructor(
) { ) {
@ -54,7 +56,7 @@ export class DatabaseService extends Dexie {
schema[ModelClass.getTableName()] = ModelClass.getSchema(); schema[ModelClass.getTableName()] = ModelClass.getSchema();
} }
await this.version(9).stores(schema); await this.version(11).stores(schema);
await this.open(); await this.open();
this.migrations = this.table('migrations'); this.migrations = this.table('migrations');
@ -77,5 +79,23 @@ export class DatabaseService extends Dexie {
this.databaseEntries = this.table('databaseEntries'); this.databaseEntries = this.table('databaseEntries');
this.databaseEntries.mapToClass(DatabaseEntry); this.databaseEntries.mapToClass(DatabaseEntry);
this.fileGroups = this.table('fileGroups');
this.fileGroups.mapToClass(FileGroup);
}
public async purge() {
console.warn('Purging all local data!');
await Promise.all([
this.migrations.clear(),
this.menuItems.clear(),
this.keyValues.clear(),
this.codiums.clear(),
this.databases.clear(),
this.databaseColumns.clear(),
this.databaseEntries.clear(),
this.fileGroups.clear(),
]);
} }
} }