From bb3eda2577fc83748c398fa41816f600b836791f Mon Sep 17 00:00:00 2001 From: garrettmills Date: Thu, 4 Feb 2021 12:57:34 -0600 Subject: [PATCH] Initial pass of the new file box node --- src/app/components/components.module.ts | 4 + .../node-picker/node-picker.component.html | 6 +- .../nodes/file-box/file-box.component.html | 48 ++ .../nodes/file-box/file-box.component.scss | 53 +++ .../nodes/file-box/file-box.component.ts | 446 ++++++++++++++++++ src/app/pages/editor/editor.page.html | 3 + src/app/pages/editor/editor.page.scss | 2 +- src/app/service/api.service.ts | 147 +++++- src/app/structures/node-types.ts | 1 + 9 files changed, 706 insertions(+), 4 deletions(-) create mode 100644 src/app/components/nodes/file-box/file-box.component.html create mode 100644 src/app/components/nodes/file-box/file-box.component.scss create mode 100644 src/app/components/nodes/file-box/file-box.component.ts diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 511e408..38915b8 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -46,6 +46,7 @@ import {LinkRendererComponent} from './editor/database/renderers/link-renderer.c import {FormInputComponent} from './nodes/form-input/form-input.component'; import {FormInputOptionsComponent} from './nodes/form-input/options/form-input-options.component'; import {DatabaseLinkComponent} from './editor/forms/database-link.component'; +import {FileBoxComponent} from './nodes/file-box/file-box.component'; @NgModule({ declarations: [ @@ -85,6 +86,7 @@ import {DatabaseLinkComponent} from './editor/forms/database-link.component'; FormInputComponent, FormInputOptionsComponent, DatabaseLinkComponent, + FileBoxComponent, ], imports: [ CommonModule, @@ -137,6 +139,7 @@ import {DatabaseLinkComponent} from './editor/forms/database-link.component'; FormInputComponent, FormInputOptionsComponent, DatabaseLinkComponent, + FileBoxComponent, ], exports: [ NodePickerComponent, @@ -175,6 +178,7 @@ import {DatabaseLinkComponent} from './editor/forms/database-link.component'; FormInputComponent, FormInputOptionsComponent, DatabaseLinkComponent, + FileBoxComponent, ] }) export class ComponentsModule {} diff --git a/src/app/components/editor/node-picker/node-picker.component.html b/src/app/components/editor/node-picker/node-picker.component.html index 7eb1c81..694bec2 100644 --- a/src/app/components/editor/node-picker/node-picker.component.html +++ b/src/app/components/editor/node-picker/node-picker.component.html @@ -17,7 +17,11 @@ - Upload Files + Simple Files + + + + File Box diff --git a/src/app/components/nodes/file-box/file-box.component.html b/src/app/components/nodes/file-box/file-box.component.html new file mode 100644 index 0000000..90c5eac --- /dev/null +++ b/src/app/components/nodes/file-box/file-box.component.html @@ -0,0 +1,48 @@ +
+ +
+ + + +
+ +   Folder +   Upload Files + + + +
+
+
No files or folders.
+
+ +
+
+
{{ item.title }}
+
+
+
+
+ +
+
+
{{ item.title }}
+
+
+
+
+
+
+ Sorry, this file box is not available offline yet. +
diff --git a/src/app/components/nodes/file-box/file-box.component.scss b/src/app/components/nodes/file-box/file-box.component.scss new file mode 100644 index 0000000..3fe0bd1 --- /dev/null +++ b/src/app/components/nodes/file-box/file-box.component.scss @@ -0,0 +1,53 @@ +div.file-box-wrapper { + border: 2px solid #8c8c8c; + border-radius: 3px; + margin-top: 15px; + + &.not-available { + height: 150px; + text-align: center; + padding-top: 40px; + color: #494949; + } +} + +.content-wrapper { + min-height: 200px; + background: #222; + display: flex; + flex-direction: column; + + .empty-text { + flex: 1; + text-align: center; + justify-content: center; + display: flex; + flex-direction: column; + color: #ccc; + } +} + +.folders, .files { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + .item { + display: flex; + flex-direction: row; + background: #393939; + padding: 10px; + margin: 10px; + border-radius: 3px; + transition: all 0.2s linear; + + &:hover { + cursor: pointer; + background: #424242; + } + + .icon { + margin-right: 10px; + } + } +} diff --git a/src/app/components/nodes/file-box/file-box.component.ts b/src/app/components/nodes/file-box/file-box.component.ts new file mode 100644 index 0000000..1970fe6 --- /dev/null +++ b/src/app/components/nodes/file-box/file-box.component.ts @@ -0,0 +1,446 @@ +import {Component, ElementRef, Inject, Input, OnInit, ViewChild} from '@angular/core'; +import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service'; +import { APP_BASE_HREF } from '@angular/common'; +import {EditorService} from '../../../service/editor.service'; +import {EditorNodeContract} from '../EditorNode.contract'; +import {HttpClient} from '@angular/common/http'; +import {AlertController, LoadingController, PopoverController} from '@ionic/angular'; +import {OptionMenuComponent} from '../../option-menu/option-menu.component'; + +export interface FileBoxFile { + type: 'file'; + title: string; + mime: string; + uploaded: string; + id: string; + category: string; +} + +export interface FileBoxRecord { + type: 'folder'; + title: string; + UUID: string; + name: string; + pageId: string; + fileIds: string[]; + rootUUID: string; + parentUUID?: string; +} + +export type FileBoxItem = FileBoxFile | FileBoxRecord; + +@Component({ + selector: 'editor-file-box', + templateUrl: './file-box.component.html', + styleUrls: ['./file-box.component.scss'], +}) +export class FileBoxComponent extends EditorNodeContract implements OnInit { + @Input() nodeId: string; + @Input() editorUUID: string; + @ViewChild('fileInput') fileInput: ElementRef; + + categoryIcons = { + document: 'fa fa-file-word', + spreadsheet: 'fa fa-file-excel', + presentation: 'fa fa-file-powerpoint', + image: 'fa fa-file-image', + pdf: 'fa fa-file-pdf', + video: 'fa fa-file-video', + code: 'fa fa-file-code', + text: 'fa fa-file-alt', + other: 'fa fa-file', + }; + + categoryColors = { + document: '#4269a5', + spreadsheet: '#39825a', + presentation: '#dc6141', + image: '#ffbf50', + pdf: '#d32f2f', + video: '#8049c0', + code: '#ff4500', + text: '#cccccc', + other: '#ffffff', + }; + + protected dirty = false; + protected pendingSetup = true; + public notAvailableOffline = false; + public fileBoxName = 'New File Box'; + public record?: FileBoxRecord; + + public history: FileBoxRecord[] = []; + + public items: FileBoxItem[] = []; + + public get readonly() { + return !this.node || !this.editorService.canEdit(); + } + + constructor( + protected api: ApiService, + protected http: HttpClient, + public editorService: EditorService, + protected loading: LoadingController, + protected popover: PopoverController, + protected alerts: AlertController, + @Inject(APP_BASE_HREF) private baseHref: string + ) { super(); } + + public isDirty(): boolean | Promise { + return this.dirty; + } + + public writeChangesToNode(): void | Promise { + this.node.Value.Mode = 'files'; + } + + public needsLoad(): boolean | Promise { + return this.node && this.pendingSetup; + } + + public async performLoad(): Promise { + if ( !this.node.Value ) { + this.node.Value = {}; + } + + if ( this.api.isOffline ) { + this.notAvailableOffline = true; + this.pendingSetup = false; + return; + } + + console.log('file box compt', this); + if ( !this.node.Value.Value && !this.readonly ) { + this.record = await this.api.createFileBox(this.page.UUID, this.fileBoxName); + console.log(this.record); + + this.node.Value.Value = this.record.UUID; + this.node.value = this.record.UUID; + this.dirty = true; + } else if ( this.node.Value.Value ) { + this.record = await this.api.getFileBox(this.page.UUID, this.node.Value.Value); + console.log(this.record); + } + + if ( !this.record ) { + this.notAvailableOffline = true; + this.pendingSetup = false; + return; + } + + this.fileBoxName = this.record.name; + + await this.loadBox(); + + if ( this.dirty ) { + this.editorService.triggerSave(); + } + + if ( this.fileInput ) { + this.fileInput.nativeElement.value = null; + } + + this.pendingSetup = false; + } + + public async loadBox(): Promise { + this.fileBoxName = this.record.name; + const files = await this.api.getFileBoxFiles(this.page.UUID, this.record.UUID); + const children = await this.api.getFileBoxChildren(this.page.UUID, this.record.UUID); + + this.items = [...children, ...files]; + } + + public async navigateUp() { + if ( this.history.length < 1 ) { + return; + } + + const last = this.history[this.history.length - 1]; + if ( last ) { + await this.navigateBack(last); + } + } + + public async navigateBack(record: FileBoxRecord) { + const newHistory: FileBoxRecord[] = []; + + const found = this.history.some(row => { + if ( row.UUID === record.UUID ) { + return true; + } else { + newHistory.push(row); + } + }); + + if ( found ) { + this.history = newHistory; + } else { + this.history = []; + } + + this.record = record; + await this.loadBox(); + } + + public async performDelete(): Promise { + + } + + ngOnInit() { + this.editorService = this.editorService.getEditor(this.editorUUID); + this.editorService.registerNodeEditor(this.nodeId, this).then(() => { + + }); + } + + surfaceContextItems() { + return [ + { + name: 'New Folder', + icon: 'fa fa-folder-plus', + value: 'folder-add', + title: 'Create a new folder in the current file box', + }, + ]; + } + + async onSurfaceContextMenu(event: MouseEvent) { + if ( !event.ctrlKey ) { + event.preventDefault(); + event.stopPropagation(); + + const popover = await this.popover.create({ + event, + component: OptionMenuComponent, + componentProps: { + menuItems: [ + ...this.surfaceContextItems(), + ], + }, + }); + + popover.onDidDismiss().then(result => { + if ( result.data ) { + this.onActionClick(result.data); + } + }); + + await popover.present(); + } + } + + itemContextItems(item: FileBoxItem) { + return [ + { + name: 'Rename', + value: 'rename', + icon: 'fa fa-edit', + title: 'Rename this ' + item.type, + }, + { + name: 'Delete', + value: 'delete', + icon: 'fa fa-trash', + title: 'Delete this ' + item.type, + }, + ]; + } + + async onRecordNameChange(event) { + const name = event.target.value; + this.fileBoxName = name; + this.record.name = name; + this.record.title = name; + await this.api.updateFileBox(this.page.UUID, this.record.UUID, { name }); + } + + async onItemContextMenu(item: FileBoxItem, event: MouseEvent) { + if ( !event.ctrlKey ) { + event.preventDefault(); + event.stopPropagation(); + + const popover = await this.popover.create({ + event, + component: OptionMenuComponent, + componentProps: { + menuItems: [ + ...this.itemContextItems(item), + ...this.surfaceContextItems(), + ], + }, + }); + + popover.onDidDismiss().then(result => { + if ( result.data ) { + this.onActionClick(result.data, item); + } + }); + + await popover.present(); + } + } + + async onUploadFilesClick(event) { + if ( this.fileInput ) { + this.fileInput.nativeElement.click(); + } + } + + async onUploadFilesChange(event) { + if ( this.readonly ) { + return; + } + + const loader = await this.loading.create({ + message: 'Uploading files...', + }); + + await loader.present(); + + const fileList: FileList = this.fileInput?.nativeElement?.files; + if ( !fileList ) { + return; + } + + if ( fileList.length > 0 ) { + const formData: FormData = new FormData(); + + // tslint:disable-next-line:prefer-for-of + for ( let i = 0; i < fileList.length; i += 1 ) { + const file = fileList[i]; + formData.append(`uploaded_file_${i}`, file, file.name); + } + + await this.api.uploadFileBoxFiles(this.page.UUID, this.record.UUID, formData); + await this.loadBox(); + } + + await loader.dismiss(); + } + + async onItemActivate(item: FileBoxItem) { + if ( item.type === 'folder' ) { + this.history.push(this.record); + this.record = item; + await this.loadBox(); + } else if ( item.type === 'file' ) { + const url = this.api.getFileBoxFileDownloadUrl(this.page.UUID, this.record.UUID, item.id); + window.open(url, '_blank'); + } + } + + async onActionClick(action: string, item?: FileBoxItem) { + if ( action === 'folder-add' ) { + await this.actionNewFolder(); + } else if ( action === 'rename' && item ) { + await this.actionRename(item); + } else if ( action === 'delete' && item ) { + await this.actionDelete(item); + } + } + + async actionNewFolder() { + const alert = await this.alerts.create({ + header: 'New Folder', + message: 'Enter a name for the new folder:', + inputs: [ + { + name: 'name', + placeholder: 'New Folder', + }, + ], + buttons: [ + { + text: 'Create', + role: 'ok', + }, + { + text: 'Cancel', + role: 'cancel', + }, + ], + }); + + alert.onDidDismiss().then(async result => { + if ( result.role === 'ok' ) { + const name = result.data?.values?.name?.trim() || 'New Folder'; + await this.api.createFileBox(this.page.UUID, name, this.record.rootUUID, this.record.UUID); + await this.loadBox(); + } + }); + + await alert.present(); + } + + async actionRename(item: FileBoxItem) { + const alert = await this.alerts.create({ + header: 'Rename ' + item.type, + message: `Enter a new name for the ${item.type}:`, + inputs: [ + { + name: 'name', + value: item.title, + }, + ], + buttons: [ + { + text: 'Rename', + role: 'ok', + }, + { + text: 'Cancel', + role: 'cancel', + }, + ], + }); + + alert.onDidDismiss().then(async result => { + const name = result.data?.values?.name?.trim(); + if ( result.role === 'ok' && name ) { + if ( item.type === 'folder' ) { + item.name = name; + item.title = name; + await this.api.updateFileBox(this.page.UUID, item.UUID, { name }); + } else if ( item.type === 'file' ) { + item.title = name; + await this.api.updateFileBoxFile(this.page.UUID, this.record.UUID, item.id, { name }); + } + } + }); + + await alert.present(); + } + + async actionDelete(item: FileBoxItem) { + const alert = await this.alerts.create({ + header: `Delete ${item.type}?`, + message: `Are you sure you want to delete the ${item.type} "${item.title}"? This action cannot be undone.`, + buttons: [ + { + text: 'Delete It', + role: 'ok', + }, + { + text: 'Keep It', + role: 'cancel', + }, + ], + }); + + alert.onDidDismiss().then(async result => { + if ( result.role === 'ok' ) { + if ( item.type === 'file' ) { + await this.api.deleteFileBoxFile(this.page.UUID, this.record.UUID, item.id); + await this.loadBox(); + } else if ( item.type === 'folder' ) { + await this.api.deleteFileBox(this.page.UUID, item.UUID); + await this.loadBox(); + } + } + }); + + await alert.present(); + } +} diff --git a/src/app/pages/editor/editor.page.html b/src/app/pages/editor/editor.page.html index e4886a3..d73d5fb 100644 --- a/src/app/pages/editor/editor.page.html +++ b/src/app/pages/editor/editor.page.html @@ -73,6 +73,9 @@ + + + diff --git a/src/app/pages/editor/editor.page.scss b/src/app/pages/editor/editor.page.scss index 7b00075..a0e933f 100644 --- a/src/app/pages/editor/editor.page.scss +++ b/src/app/pages/editor/editor.page.scss @@ -29,7 +29,7 @@ ion-icon.invisible { color: var(--noded-background-db); } - &.file_ref { + &.file_ref, &.file_box { color: var(--noded-background-files); } diff --git a/src/app/service/api.service.ts b/src/app/service/api.service.ts index e4d2237..53751bd 100644 --- a/src/app/service/api.service.ts +++ b/src/app/service/api.service.ts @@ -199,7 +199,11 @@ export class ApiService { return this.request(endpoint, body, 'post'); } - public request(endpoint, params = {}, method: 'get'|'post' = 'get'): Observable { + public delete(endpoint, body = {}): Observable { + return this.request(endpoint, body, 'delete'); + } + + public request(endpoint, params = {}, method: 'get'|'post'|'delete' = 'get'): Observable { return this._request(this._build_url(endpoint), params, method); } @@ -259,7 +263,7 @@ export class ApiService { }); } - protected _request(endpoint, params = {}, method: 'get'|'post' = 'get'): Observable { + protected _request(endpoint, params = {}, method: 'get'|'post'|'delete' = 'get'): Observable { return new Observable(sub => { let data: any = {}; if ( method === 'get' ) { @@ -1223,6 +1227,145 @@ export class ApiService { }); } + public createFileBox(PageId: string, name: string, rootUUID?: string, parentUUID?: string): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.post(`/file-box/${PageId}/create`, { name, rootUUID, parentUUID }).subscribe({ + next: async result => { + res(result.data); + }, + error: rej, + }); + }); + } + + public getFileBox(PageId: string, FileBoxId: string): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.get(`/file-box/${PageId}/${FileBoxId}`).subscribe({ + next: async result => { + res(result.data); + }, + error: rej, + }); + }); + } + + public getFileBoxFiles(PageId: string, FileBoxId: string): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.get(`/file-box/${PageId}/${FileBoxId}/files`).subscribe({ + next: async result => { + res(result.data); + }, + error: rej, + }); + }); + } + + public getFileBoxChildren(PageId: string, FileBoxId: string): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.get(`/file-box/${PageId}/${FileBoxId}/children`).subscribe({ + next: async result => { + res(result.data); + }, + error: rej, + }); + }); + } + + public updateFileBox(PageId: string, FileBoxId: string, data: any): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.post(`/file-box/${PageId}/${FileBoxId}`, data).subscribe({ + next: result => { + res(result.data); + }, + error: rej, + }); + }); + } + + public updateFileBoxFile(PageId: string, FileBoxId: string, FileBoxFileId: string, data: any): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.post(`/file-box/${PageId}/${FileBoxId}/files/${FileBoxFileId}`, data).subscribe({ + next: result => { + res(result.data); + }, + error: rej, + }); + }); + } + + public deleteFileBoxFile(PageId: string, FileBoxId: string, FileBoxFileId: string): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.delete(`/file-box/${PageId}/${FileBoxId}/files/${FileBoxFileId}`).subscribe({ + next: result => { + res(result.data); + }, + error: rej, + }); + }); + } + + public deleteFileBox(PageId: string, FileBoxId: string): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.delete(`/file-box/${PageId}/${FileBoxId}`).subscribe({ + next: result => { + res(result.data); + }, + error: rej, + }); + }); + } + + public uploadFileBoxFiles(PageId: string, FileBoxId: string, formData: FormData): Promise { + return new Promise(async (res, rej) => { + if ( this.isOffline ) { + return rej(new ResourceNotAvailableOfflineError()); + } + + this.post(`/file-box/${PageId}/${FileBoxId}/files`, formData).subscribe({ + next: async result => { + return res(result.data); + }, + error: rej, + }); + }); + } + + public getFileBoxFileDownloadUrl(PageId: string, FileBoxId: string, FileBoxFileId: string): string { + return this._build_url(`/file-box/${PageId}/${FileBoxId}/files/${FileBoxFileId}`); + } + public moveMenuNode(MovedPageId: string, ParentPageId: string): Promise { return new Promise(async (res, rej) => { if ( this.isOffline ) { diff --git a/src/app/structures/node-types.ts b/src/app/structures/node-types.ts index b361a67..92504ec 100644 --- a/src/app/structures/node-types.ts +++ b/src/app/structures/node-types.ts @@ -10,6 +10,7 @@ export const NodeTypeIcons = { code_ref: 'fa fa-code noded-code', file_ref: 'fa fa-archive noded-files', files: 'fa fa-archive noded-files', + file_box: 'fa fa-archive noded-file-box', markdown: 'fab fa-markdown noded-markdown', form_input_text: 'fa fa-font noded-form', form_input_number: 'fa fa-hashtag noded-form',