Add offline cachine for file group elements and contents (not files, though)
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
02d8505b05
commit
294b312641
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
97
src/app/service/db/FileGroup.ts
Normal file
97
src/app/service/db/FileGroup.ts
Normal 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>;
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user