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.
+
+
+
+
+
+ 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',