diff --git a/src/app/components/editor/database/database.component.html b/src/app/components/editor/database/database.component.html index b22834a..1da1a76 100644 --- a/src/app/components/editor/database/database.component.html +++ b/src/app/components/editor/database/database.component.html @@ -1,4 +1,4 @@ -
+
+ +
+ Sorry, this database is not available offline yet. +
diff --git a/src/app/components/editor/database/database.component.scss b/src/app/components/editor/database/database.component.scss index a11b532..48c7947 100644 --- a/src/app/components/editor/database/database.component.scss +++ b/src/app/components/editor/database/database.component.scss @@ -1,4 +1,11 @@ div.database-wrapper { border: 2px solid #8c8c8c; border-radius: 3px; + + &.not-available { + height: 600px; + text-align: center; + padding-top: 100px; + color: #494949; + } } diff --git a/src/app/components/editor/files/files.component.html b/src/app/components/editor/files/files.component.html index 654c650..9159fb6 100644 --- a/src/app/components/editor/files/files.component.html +++ b/src/app/components/editor/files/files.component.html @@ -1,4 +1,4 @@ -
+
@@ -21,3 +21,6 @@
+
+ Sorry, this file group is not available offline yet. +
diff --git a/src/app/components/editor/files/files.component.scss b/src/app/components/editor/files/files.component.scss index 2a2d50b..72344ed 100644 --- a/src/app/components/editor/files/files.component.scss +++ b/src/app/components/editor/files/files.component.scss @@ -2,4 +2,11 @@ div.files-wrapper { border: 2px solid #8c8c8c; border-radius: 3px; margin-top: 15px; + + &.not-available { + height: 150px; + text-align: center; + padding-top: 40px; + color: #494949; + } } diff --git a/src/app/components/editor/files/files.component.ts b/src/app/components/editor/files/files.component.ts index fc0ce56..234f724 100644 --- a/src/app/components/editor/files/files.component.ts +++ b/src/app/components/editor/files/files.component.ts @@ -1,8 +1,6 @@ -import {Component, ElementRef, EventEmitter, Inject, Input, OnInit, Output, ViewChild} from '@angular/core'; -import HostRecord from '../../../structures/HostRecord'; -import {ApiService} from '../../../service/api.service'; +import {Component, ElementRef, Inject, Input, OnInit, ViewChild} from '@angular/core'; +import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service'; import {AlertController} from '@ionic/angular'; -import {Observable} from 'rxjs'; import { APP_BASE_HREF } from '@angular/common'; import {EditorService} from '../../../service/editor.service'; import {EditorNodeContract} from '../../nodes/EditorNode.contract'; @@ -15,16 +13,12 @@ import {EditorNodeContract} from '../../nodes/EditorNode.contract'; export class FilesComponent extends EditorNodeContract implements OnInit { @Input() nodeId: string; @ViewChild('uploadForm') uploadForm: ElementRef; - // @Input() readonly = false; - // @Input() hostRecord: HostRecord; - // @Output() hostRecordChange = new EventEmitter(); - // @Output() requestParentSave = new EventEmitter(); - // @Output() requestParentDelete = new EventEmitter(); public fileRecords: Array = []; public pendingSetup = true; public dbRecord: any = {}; public dirty = false; + public notAvailableOffline = false; public get readonly() { return !this.node || !this.editorService.canEdit(); @@ -32,7 +26,6 @@ export class FilesComponent extends EditorNodeContract implements OnInit { constructor( protected api: ApiService, - protected alerts: AlertController, public readonly editorService: EditorService, @Inject(APP_BASE_HREF) private baseHref: string ) { super(); } @@ -55,45 +48,31 @@ export class FilesComponent extends EditorNodeContract implements OnInit { } if ( !this.node.Value.Value && !this.readonly ) { - await new Promise((res, rej) => { - this.api.post(`/files/${this.page.UUID}/${this.node.UUID}/create`).subscribe({ - next: result => { - this.dbRecord = result.data; - this.fileRecords = result.data.files; - this.node.Value.Mode = 'files'; - this.node.Value.Value = result.data.UUID; - this.node.value = result.data.UUID; - this.dirty = true; - res(); - }, - error: rej, - }); - }); + this.dbRecord = await this.api.createFileGroup(this.page.UUID, this.node.UUID); + this.fileRecords = this.dbRecord.files; + this.node.Value.Mode = 'files'; + this.node.Value.Value = this.dbRecord.UUID; + this.node.value = this.dbRecord.UUID; + this.dirty = true; } else { - await new Promise((res, rej) => { - this.api.get(`/files/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}`).subscribe({ - next: result => { - this.dbRecord = result.data; - this.fileRecords = result.data.files; - res(); - }, - error: rej, - }); - }); + try { + this.dbRecord = await this.api.getFileGroup(this.page.UUID, this.node.UUID, this.node.Value.Value); + this.fileRecords = this.dbRecord.files; + this.notAvailableOffline = false; + } catch (e) { + if ( e instanceof ResourceNotAvailableOfflineError ) { + this.notAvailableOffline = true; + } else { + throw e; + } + } } this.pendingSetup = false; } public async performDelete(): Promise { - await new Promise((res, rej) => { - this.api.post(`/files/${this.page.UUID}/${this.node.UUID}/delete/${this.node.Value.Value}`).subscribe({ - next: result => { - res(); - }, - error: rej, - }); - }); + await this.api.deleteFileGroup(this.page.UUID, this.node.UUID, this.node.Value.Value); } ngOnInit() { diff --git a/src/app/components/option-picker/option-picker.component.ts b/src/app/components/option-picker/option-picker.component.ts index 0a072d4..179e1ec 100644 --- a/src/app/components/option-picker/option-picker.component.ts +++ b/src/app/components/option-picker/option-picker.component.ts @@ -2,6 +2,7 @@ import {Component, Input, OnInit} from '@angular/core'; import {Router} from '@angular/router'; import {ApiService} from '../../service/api.service'; import {PopoverController} from '@ionic/angular'; +import {DatabaseService} from "../../service/db/database.service"; @Component({ selector: 'app-option-picker', @@ -17,14 +18,16 @@ export class OptionPickerComponent implements OnInit { protected api: ApiService, protected router: Router, protected popover: PopoverController, + protected db: DatabaseService, ) { } ngOnInit() {} - onSelect(key) { + async onSelect(key) { if ( key === 'html_export' ) { window.open(this.api._build_url('/data/export/html'), '_blank'); } else if ( key === 'logout' ) { + await this.db.purge(); window.location.href = '/auth/logout'; } else if ( key === 'toggle_darkmode' ) { this.toggleDark(); diff --git a/src/app/service/api.service.ts b/src/app/service/api.service.ts index 2f89f4c..56e6439 100644 --- a/src/app/service/api.service.ts +++ b/src/app/service/api.service.ts @@ -10,6 +10,7 @@ import {Codium} from './db/Codium'; import {Database} from './db/Database'; import {DatabaseColumn} from './db/DatabaseColumn'; import {DatabaseEntry} from './db/DatabaseEntry'; +import {FileGroup} from "./db/FileGroup"; export class ResourceNotAvailableOfflineError extends Error { 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 { + 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 { + 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 { + 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, + }); + }); + } } diff --git a/src/app/service/db/FileGroup.ts b/src/app/service/db/FileGroup.ts new file mode 100644 index 0000000..e90b87d --- /dev/null +++ b/src/app/service/db/FileGroup.ts @@ -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 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 { + return this.staticClass().dbService.table('fileGroups') as Dexie.Table; + } +} diff --git a/src/app/service/db/database.service.ts b/src/app/service/db/database.service.ts index ac3c32e..21552c5 100644 --- a/src/app/service/db/database.service.ts +++ b/src/app/service/db/database.service.ts @@ -7,12 +7,13 @@ import {Codium, ICodium} from './Codium'; import {Database, IDatabase} from './Database'; import {DatabaseColumn, IDatabaseColumn} from './DatabaseColumn'; import {DatabaseEntry, IDatabaseEntry} from './DatabaseEntry'; +import {FileGroup, IFileGroup} from './FileGroup'; @Injectable({ providedIn: 'root' }) 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; migrations!: Dexie.Table; @@ -22,6 +23,7 @@ export class DatabaseService extends Dexie { databases!: Dexie.Table; databaseColumns!: Dexie.Table; databaseEntries!: Dexie.Table; + fileGroups!: Dexie.Table; constructor( ) { @@ -54,7 +56,7 @@ export class DatabaseService extends Dexie { schema[ModelClass.getTableName()] = ModelClass.getSchema(); } - await this.version(9).stores(schema); + await this.version(11).stores(schema); await this.open(); this.migrations = this.table('migrations'); @@ -77,5 +79,23 @@ export class DatabaseService extends Dexie { this.databaseEntries = this.table('databaseEntries'); 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(), + ]); } }