Initial pass of the new file box node

This commit is contained in:
Garrett Mills 2021-02-04 12:57:34 -06:00
parent fc247b3570
commit bb3eda2577
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
9 changed files with 706 additions and 4 deletions

View File

@ -46,6 +46,7 @@ import {LinkRendererComponent} from './editor/database/renderers/link-renderer.c
import {FormInputComponent} from './nodes/form-input/form-input.component'; import {FormInputComponent} from './nodes/form-input/form-input.component';
import {FormInputOptionsComponent} from './nodes/form-input/options/form-input-options.component'; import {FormInputOptionsComponent} from './nodes/form-input/options/form-input-options.component';
import {DatabaseLinkComponent} from './editor/forms/database-link.component'; import {DatabaseLinkComponent} from './editor/forms/database-link.component';
import {FileBoxComponent} from './nodes/file-box/file-box.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -85,6 +86,7 @@ import {DatabaseLinkComponent} from './editor/forms/database-link.component';
FormInputComponent, FormInputComponent,
FormInputOptionsComponent, FormInputOptionsComponent,
DatabaseLinkComponent, DatabaseLinkComponent,
FileBoxComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -137,6 +139,7 @@ import {DatabaseLinkComponent} from './editor/forms/database-link.component';
FormInputComponent, FormInputComponent,
FormInputOptionsComponent, FormInputOptionsComponent,
DatabaseLinkComponent, DatabaseLinkComponent,
FileBoxComponent,
], ],
exports: [ exports: [
NodePickerComponent, NodePickerComponent,
@ -175,6 +178,7 @@ import {DatabaseLinkComponent} from './editor/forms/database-link.component';
FormInputComponent, FormInputComponent,
FormInputOptionsComponent, FormInputOptionsComponent,
DatabaseLinkComponent, DatabaseLinkComponent,
FileBoxComponent,
] ]
}) })
export class ComponentsModule {} export class ComponentsModule {}

View File

@ -17,7 +17,11 @@
</ion-item> </ion-item>
<ion-item button (click)="onSelect('file_ref')" class="files" *ngIf="!formMode"> <ion-item button (click)="onSelect('file_ref')" class="files" *ngIf="!formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.files"></i> <i class="fa" slot="start" [ngClass]="typeIcons.files"></i>
<ion-label>Upload Files</ion-label> <ion-label>Simple Files</ion-label>
</ion-item>
<ion-item button (click)="onSelect('file_box')" class="files" *ngIf="!formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.file_box"></i>
<ion-label>File Box</ion-label>
</ion-item> </ion-item>
<!-- Form inputs --> <!-- Form inputs -->

View File

@ -0,0 +1,48 @@
<div class="file-box-wrapper" *ngIf="!notAvailableOffline">
<ion-toolbar>
<div style="display: flex; flex-direction: row">
<button
style="padding: 0 15px;" *ngIf="history.length > 0"
(click)="navigateUp()"
><i class="fa fa-arrow-up"></i></button>
<button
*ngFor="let rec of history"
(click)="navigateBack(rec)"
>{{ rec.name }}&nbsp;&nbsp;/</button>
<ion-input
[readonly]="readonly"
[(ngModel)]="fileBoxName"
(change)="onRecordNameChange($event)"
style="flex: 1; font-size: 15pt;"
></ion-input>
</div>
<ion-buttons *ngIf="!readonly" style="flex-wrap: wrap;">
<ion-button (click)="onActionClick('folder-add')"><i class="fa fa-folder-plus"></i>&nbsp;&nbsp;Folder</ion-button>
<ion-button (click)="onUploadFilesClick($event)"><i class="fa fa-upload"></i>&nbsp;&nbsp;Upload Files</ion-button>
<input type="file" name="fileInput" #fileInput multiple style="display: none;" (change)="onUploadFilesChange($event)">
</ion-buttons>
</ion-toolbar>
<div class="content-wrapper" (contextmenu)="onSurfaceContextMenu($event)">
<div *ngIf="!items || !items.length" class="empty-text" (contextmenu)="onSurfaceContextMenu($event)">No files or folders.</div>
<div *ngIf="items && items.length" class="folders">
<ng-container *ngFor="let item of items">
<div class="folder item" *ngIf="item.type === 'folder'" (contextmenu)="onItemContextMenu(item, $event)" (dblclick)="onItemActivate(item)">
<div class="icon" style="color: #ecd3a9"><i class="fa fa-folder"></i></div>
<div class="title">{{ item.title }}</div>
</div>
</ng-container>
</div>
<div *ngIf="items && items.length" class="files">
<ng-container *ngFor="let item of items">
<div class="folder item" *ngIf="item.type === 'file'" (contextmenu)="onItemContextMenu(item, $event)" (dblclick)="onItemActivate(item)">
<div class="icon"><i [ngClass]="categoryIcons[item.category]" [ngStyle]="{ color: categoryColors[item.category] }"></i></div>
<div class="title">{{ item.title }}</div>
</div>
</ng-container>
</div>
</div>
</div>
<div class="file-box-wrapper not-available" *ngIf="notAvailableOffline">
Sorry, this file box is not available offline yet.
</div>

View File

@ -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;
}
}
}

View File

@ -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<boolean> {
return this.dirty;
}
public writeChangesToNode(): void | Promise<void> {
this.node.Value.Mode = 'files';
}
public needsLoad(): boolean | Promise<boolean> {
return this.node && this.pendingSetup;
}
public async performLoad(): Promise<void> {
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<void> {
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<void> {
}
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();
}
}

View File

@ -73,6 +73,9 @@
<ng-container *ngIf="node.type === 'file_ref'"> <ng-container *ngIf="node.type === 'file_ref'">
<editor-files style="flex: 1;" [nodeId]="node.UUID" [editorUUID]="editorService.instanceUUID" #nodeContainer></editor-files> <editor-files style="flex: 1;" [nodeId]="node.UUID" [editorUUID]="editorService.instanceUUID" #nodeContainer></editor-files>
</ng-container> </ng-container>
<ng-container *ngIf="node.type === 'file_box'">
<editor-file-box style="flex: 1;" [nodeId]="node.UUID" [editorUUID]="editorService.instanceUUID" #nodeContainer></editor-file-box>
</ng-container>
<ng-container *ngIf="node.isForm()"> <ng-container *ngIf="node.isForm()">
<editor-node-form-input style="flex: 1;" [isFilloutMode]="isFilloutMode" [nodeId]="node.UUID" [editorUUID]="editorService.instanceUUID" #nodeContainer></editor-node-form-input> <editor-node-form-input style="flex: 1;" [isFilloutMode]="isFilloutMode" [nodeId]="node.UUID" [editorUUID]="editorService.instanceUUID" #nodeContainer></editor-node-form-input>
</ng-container> </ng-container>

View File

@ -29,7 +29,7 @@ ion-icon.invisible {
color: var(--noded-background-db); color: var(--noded-background-db);
} }
&.file_ref { &.file_ref, &.file_box {
color: var(--noded-background-files); color: var(--noded-background-files);
} }

View File

@ -199,7 +199,11 @@ export class ApiService {
return this.request(endpoint, body, 'post'); return this.request(endpoint, body, 'post');
} }
public request(endpoint, params = {}, method: 'get'|'post' = 'get'): Observable<ApiResponse> { public delete(endpoint, body = {}): Observable<ApiResponse> {
return this.request(endpoint, body, 'delete');
}
public request(endpoint, params = {}, method: 'get'|'post'|'delete' = 'get'): Observable<ApiResponse> {
return this._request(this._build_url(endpoint), params, method); 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<ApiResponse> { protected _request(endpoint, params = {}, method: 'get'|'post'|'delete' = 'get'): Observable<ApiResponse> {
return new Observable<ApiResponse>(sub => { return new Observable<ApiResponse>(sub => {
let data: any = {}; let data: any = {};
if ( method === 'get' ) { if ( method === 'get' ) {
@ -1223,6 +1227,145 @@ export class ApiService {
}); });
} }
public createFileBox(PageId: string, name: string, rootUUID?: string, parentUUID?: string): Promise<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> { public moveMenuNode(MovedPageId: string, ParentPageId: string): Promise<any> {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
if ( this.isOffline ) { if ( this.isOffline ) {

View File

@ -10,6 +10,7 @@ export const NodeTypeIcons = {
code_ref: 'fa fa-code noded-code', code_ref: 'fa fa-code noded-code',
file_ref: 'fa fa-archive noded-files', file_ref: 'fa fa-archive noded-files',
files: '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', markdown: 'fab fa-markdown noded-markdown',
form_input_text: 'fa fa-font noded-form', form_input_text: 'fa fa-font noded-form',
form_input_number: 'fa fa-hashtag noded-form', form_input_number: 'fa fa-hashtag noded-form',