feat(streaming): add global activity watchers, client-side buffering, and improved real-time streaming UX
This commit is contained in:
@@ -6,6 +6,25 @@ import { themeStyles } from '../styles/index.js';
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
const { DeesContextmenu } = plugins.deesCatalog;
|
||||
|
||||
// FileSystem API types for drag-and-drop folder support
|
||||
interface FileSystemEntry {
|
||||
isFile: boolean;
|
||||
isDirectory: boolean;
|
||||
name: string;
|
||||
}
|
||||
interface FileSystemFileEntry extends FileSystemEntry {
|
||||
file(successCallback: (file: File) => void, errorCallback?: (error: Error) => void): void;
|
||||
}
|
||||
interface FileSystemDirectoryEntry extends FileSystemEntry {
|
||||
createReader(): FileSystemDirectoryReader;
|
||||
}
|
||||
interface FileSystemDirectoryReader {
|
||||
readEntries(
|
||||
successCallback: (entries: FileSystemEntry[]) => void,
|
||||
errorCallback?: (error: Error) => void
|
||||
): void;
|
||||
}
|
||||
|
||||
interface IColumn {
|
||||
prefix: string;
|
||||
objects: IS3Object[];
|
||||
@@ -364,10 +383,10 @@ export class TsviewS3Columns extends DeesElement {
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
// Only reset columns when bucket changes or refresh is triggered
|
||||
// Internal folder navigation is handled by selectFolder() which appends columns
|
||||
if (changedProperties.has('bucketName') || changedProperties.has('refreshKey')) {
|
||||
if (changedProperties.has('bucketName')) {
|
||||
this.loadInitialColumn();
|
||||
} else if (changedProperties.has('refreshKey')) {
|
||||
this.refreshAllColumns();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +410,20 @@ export class TsviewS3Columns extends DeesElement {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private async refreshAllColumns() {
|
||||
const updatedColumns = await Promise.all(
|
||||
this.columns.map(async (col) => {
|
||||
try {
|
||||
const result = await apiService.listObjects(this.bucketName, col.prefix, '/');
|
||||
return { ...col, objects: result.objects, prefixes: result.prefixes };
|
||||
} catch {
|
||||
return col;
|
||||
}
|
||||
})
|
||||
);
|
||||
this.columns = updatedColumns;
|
||||
}
|
||||
|
||||
private async selectFolder(columnIndex: number, prefix: string) {
|
||||
// Update selection in current column
|
||||
this.columns = this.columns.map((col, i) => {
|
||||
@@ -533,7 +566,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
action: async () => this.openCreateDialog('file', prefix),
|
||||
},
|
||||
{
|
||||
name: 'Upload Files',
|
||||
name: 'Upload...',
|
||||
iconName: 'lucide:upload',
|
||||
action: async () => this.triggerFileUpload(prefix),
|
||||
},
|
||||
@@ -545,7 +578,12 @@ export class TsviewS3Columns extends DeesElement {
|
||||
if (confirm(`Delete folder "${getFileName(prefix)}" and all its contents?`)) {
|
||||
const success = await apiService.deletePrefix(this.bucketName, prefix);
|
||||
if (success) {
|
||||
await this.loadInitialColumn();
|
||||
// Remove columns that were inside the deleted folder
|
||||
this.columns = this.columns.slice(0, columnIndex + 1);
|
||||
this.columns = this.columns.map((col, i) =>
|
||||
i === columnIndex ? { ...col, selectedItem: null } : col
|
||||
);
|
||||
await this.refreshColumnByPrefix(this.columns[columnIndex].prefix);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -589,7 +627,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
if (confirm(`Delete file "${getFileName(key)}"?`)) {
|
||||
const success = await apiService.deleteObject(this.bucketName, key);
|
||||
if (success) {
|
||||
await this.loadInitialColumn();
|
||||
await this.refreshColumnByPrefix(this.columns[columnIndex].prefix);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -616,7 +654,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
action: async () => this.openCreateDialog('file', prefix),
|
||||
},
|
||||
{
|
||||
name: 'Upload Files',
|
||||
name: 'Upload...',
|
||||
iconName: 'lucide:upload',
|
||||
action: async () => this.triggerFileUpload(prefix),
|
||||
},
|
||||
@@ -664,6 +702,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
this.fileInputElement = document.createElement('input');
|
||||
this.fileInputElement.type = 'file';
|
||||
this.fileInputElement.multiple = true;
|
||||
(this.fileInputElement as any).webkitdirectory = true; // Enable folder selection
|
||||
this.fileInputElement.style.display = 'none';
|
||||
this.shadowRoot!.appendChild(this.fileInputElement);
|
||||
}
|
||||
@@ -692,7 +731,9 @@ export class TsviewS3Columns extends DeesElement {
|
||||
this.uploadProgress = { current: i + 1, total: files.length };
|
||||
try {
|
||||
const base64 = await this.readFileAsBase64(file);
|
||||
const key = targetPrefix + file.name;
|
||||
// Use webkitRelativePath if available (folder upload), otherwise just filename
|
||||
const relativePath = (file as any).webkitRelativePath || file.name;
|
||||
const key = targetPrefix + relativePath;
|
||||
const contentType = file.type || this.getContentType(file.name.split('.').pop()?.toLowerCase() || '');
|
||||
await apiService.putObject(this.bucketName, key, base64, contentType);
|
||||
} catch (err) {
|
||||
@@ -705,6 +746,31 @@ export class TsviewS3Columns extends DeesElement {
|
||||
await this.refreshColumnByPrefix(targetPrefix);
|
||||
}
|
||||
|
||||
private async uploadFilesWithPaths(
|
||||
files: { file: File; relativePath: string }[],
|
||||
targetPrefix: string
|
||||
) {
|
||||
this.uploading = true;
|
||||
this.uploadProgress = { current: 0, total: files.length };
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const { file, relativePath } = files[i];
|
||||
this.uploadProgress = { current: i + 1, total: files.length };
|
||||
try {
|
||||
const base64 = await this.readFileAsBase64(file);
|
||||
const key = targetPrefix + relativePath;
|
||||
const contentType = file.type || this.getContentType(file.name.split('.').pop()?.toLowerCase() || '');
|
||||
await apiService.putObject(this.bucketName, key, base64, contentType);
|
||||
} catch (err) {
|
||||
console.error(`Failed to upload ${relativePath}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
this.uploading = false;
|
||||
this.uploadProgress = null;
|
||||
await this.refreshColumnByPrefix(targetPrefix);
|
||||
}
|
||||
|
||||
private readFileAsBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -720,7 +786,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
private async refreshColumnByPrefix(prefix: string) {
|
||||
const columnIndex = this.columns.findIndex(col => col.prefix === prefix);
|
||||
if (columnIndex === -1) {
|
||||
await this.loadInitialColumn();
|
||||
await this.refreshAllColumns();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -729,7 +795,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
i === columnIndex ? { ...col, objects: result.objects, prefixes: result.prefixes } : col
|
||||
);
|
||||
} catch {
|
||||
await this.loadInitialColumn();
|
||||
await this.refreshAllColumns();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,18 +826,54 @@ export class TsviewS3Columns extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private handleColumnDrop(e: DragEvent, columnIndex: number) {
|
||||
private async handleColumnDrop(e: DragEvent, columnIndex: number) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dragCounters.clear();
|
||||
this.dragOverColumnIndex = -1;
|
||||
|
||||
const files = e.dataTransfer?.files;
|
||||
if (!files || files.length === 0) return;
|
||||
const items = e.dataTransfer?.items;
|
||||
if (!items || items.length === 0) return;
|
||||
|
||||
const targetPrefix = this.dragOverFolderPrefix ?? this.columns[columnIndex].prefix;
|
||||
this.clearFolderHover();
|
||||
this.uploadFiles(Array.from(files), targetPrefix);
|
||||
|
||||
// Collect all files (including from nested folders)
|
||||
const allFiles: { file: File; relativePath: string }[] = [];
|
||||
|
||||
const processEntry = async (entry: FileSystemEntry, path: string): Promise<void> => {
|
||||
if (entry.isFile) {
|
||||
const fileEntry = entry as FileSystemFileEntry;
|
||||
const file = await new Promise<File>((resolve, reject) => {
|
||||
fileEntry.file(resolve, reject);
|
||||
});
|
||||
allFiles.push({ file, relativePath: path + file.name });
|
||||
} else if (entry.isDirectory) {
|
||||
const dirEntry = entry as FileSystemDirectoryEntry;
|
||||
const reader = dirEntry.createReader();
|
||||
const entries = await new Promise<FileSystemEntry[]>((resolve, reject) => {
|
||||
reader.readEntries(resolve, reject);
|
||||
});
|
||||
for (const childEntry of entries) {
|
||||
await processEntry(childEntry, path + entry.name + '/');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Process all dropped items
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.kind === 'file') {
|
||||
const entry = (item as any).webkitGetAsEntry();
|
||||
if (entry) {
|
||||
await processEntry(entry, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allFiles.length > 0) {
|
||||
await this.uploadFilesWithPaths(allFiles, targetPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
private handleFolderDragEnter(e: DragEvent, folderPrefix: string) {
|
||||
@@ -822,7 +924,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
|
||||
if (success) {
|
||||
this.showCreateDialog = false;
|
||||
await this.loadInitialColumn();
|
||||
await this.refreshColumnByPrefix(this.createDialogPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user