You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
frontend/src/app/components/nodes/file-box/file-box.component.ts

532 lines
15 KiB

import {Component, ElementRef, Inject, Input, OnInit, ViewChild} from '@angular/core';
import {ApiService} 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, ModalController, PopoverController} from '@ionic/angular';
import {OptionMenuComponent} from '../../option-menu/option-menu.component';
import {NavigationService} from '../../../service/navigation.service';
import {FileBoxPageComponent} from './file-box-page.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;
@Input() fullPage = false;
@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',
};
protected dirty = false;
protected pendingSetup = true;
public notAvailableOffline = false;
public fileBoxName = 'New File Box';
public record?: FileBoxRecord;
private deferredUIActivation = false;
public quickFilterValue?: string;
public visibleFilterItems?: FileBoxItem[];
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,
protected navService: NavigationService,
protected modals: ModalController,
@Inject(APP_BASE_HREF) private baseHref: string
) { super(); }
public isDark() {
return document.body.classList.contains('dark');
}
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;
}
if ( !this.node.Value.Value && !this.readonly ) {
this.record = await this.api.createFileBox(this.page.UUID, this.node.UUID, this.fileBoxName);
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.UUID, this.node.Value.Value);
}
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;
if ( this.deferredUIActivation ) {
await this.performUIActivation();
}
}
public async loadBox(): Promise<void> {
this.fileBoxName = this.record.name;
const files = await this.api.getFileBoxFiles(this.page.UUID, this.node.UUID, this.record.UUID);
const children = await this.api.getFileBoxChildren(this.page.UUID, this.node.UUID, this.record.UUID);
this.items = [...children, ...files];
this.refilter();
}
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> {
const baseRecord = this.history.length > 0 ? this.history[0] : this.record;
await this.api.deleteFileBox(this.page.UUID, this.node.UUID, baseRecord.UUID);
}
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 ( this.readonly ) {
return;
}
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) {
if ( this.readonly ) {
return;
}
const name = event.target.value;
this.fileBoxName = name;
this.record.name = name;
this.record.title = name;
await this.api.updateFileBox(this.page.UUID, this.node.UUID, this.record.UUID, { name });
this.navService.requestSidebarRefresh({ quiet: true });
}
async onItemContextMenu(item: FileBoxItem, event: MouseEvent) {
if ( this.readonly ) {
return;
}
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.readonly ) {
return;
}
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.node.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.node.UUID, this.record.UUID, item.id);
window.open(url, '_blank');
}
}
async onActionClick(action: string, item?: FileBoxItem) {
if ( this.readonly ) {
return;
}
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, this.node.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, this.node.UUID, item.UUID, { name });
} else if ( item.type === 'file' ) {
item.title = name;
await this.api.updateFileBoxFile(this.page.UUID, this.node.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.node.UUID, this.record.UUID, item.id);
await this.loadBox();
} else if ( item.type === 'folder' ) {
await this.api.deleteFileBox(this.page.UUID, this.node.UUID, item.UUID);
await this.loadBox();
}
}
});
await alert.present();
}
onQuickFilterChange(event) {
this.quickFilterValue = event.detail.value || undefined;
this.refilter();
}
refilter() {
if ( this.quickFilterValue ) {
let visible = [...this.items];
const parts = this.quickFilterValue.toLowerCase().split(/\s/);
for ( const part of parts ) {
visible = visible.filter(item => item.title.toLowerCase().includes(part));
}
this.visibleFilterItems = visible;
} else {
this.visibleFilterItems = undefined;
}
}
async openFileBox() {
const modal = await this.modals.create({
component: FileBoxPageComponent,
componentProps: {
nodeId: this.nodeId,
pageId: this.editorService.currentPageId,
},
cssClass: 'modal-big',
});
modal.onDidDismiss().then(() => {
this.record = this.history.length > 0 ? this.history[0] : this.record;
this.history = [];
this.loadBox();
});
const modalState = {
modal : true,
desc : 'Open File Box'
};
history.pushState(modalState, null);
await modal.present();
}
async dismiss() {
if ( this.fullPage ) {
await this.modals.dismiss();
}
}
performUIActivation() {
if ( this.deferredUIActivation ) {
this.deferredUIActivation = false;
}
if ( !this.pendingSetup ) {
return this.openFileBox();
} else {
this.deferredUIActivation = true;
}
}
}