feat(streaming): add global activity watchers, client-side buffering, and improved real-time streaming UX
This commit is contained in:
@@ -23,8 +23,12 @@ export class TsviewActivityStream extends DeesElement {
|
||||
@state()
|
||||
private accessor autoScroll: boolean = true;
|
||||
|
||||
@state()
|
||||
private accessor now: number = Date.now();
|
||||
|
||||
private subscription: plugins.smartrx.rxjs.Subscription | null = null;
|
||||
private connectionSubscription: plugins.smartrx.rxjs.Subscription | null = null;
|
||||
private nowInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
@@ -159,6 +163,15 @@ export class TsviewActivityStream extends DeesElement {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.event-time-col {
|
||||
width: 70px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.event-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -192,7 +205,6 @@ export class TsviewActivityStream extends DeesElement {
|
||||
.event-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -202,11 +214,6 @@ export class TsviewActivityStream extends DeesElement {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.event-details {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
@@ -285,50 +292,66 @@ export class TsviewActivityStream extends DeesElement {
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.nowInterval = setInterval(() => {
|
||||
this.now = Date.now();
|
||||
}, 1000);
|
||||
await this.initializeStreaming();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.nowInterval) {
|
||||
clearInterval(this.nowInterval);
|
||||
this.nowInterval = null;
|
||||
}
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private async initializeStreaming() {
|
||||
this.isLoading = true;
|
||||
|
||||
// Subscribe to connection status and trigger re-subscription on reconnect
|
||||
this.connectionSubscription = changeStreamService.connectionStatus$.subscribe(async (status) => {
|
||||
const wasConnected = this.isConnected;
|
||||
this.isConnected = status === 'connected';
|
||||
if (status === 'connected' && !wasConnected) {
|
||||
await this.setupSubscriptions();
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Connect to WebSocket if not connected
|
||||
await changeStreamService.connect();
|
||||
|
||||
// Subscribe to connection status
|
||||
this.connectionSubscription = changeStreamService.connectionStatus$.subscribe((status) => {
|
||||
this.isConnected = status === 'connected';
|
||||
});
|
||||
|
||||
// Subscribe to activity stream
|
||||
await changeStreamService.subscribeToActivity();
|
||||
|
||||
// Load recent events
|
||||
const recentEvents = await changeStreamService.getRecentActivity(100);
|
||||
this.events = recentEvents;
|
||||
|
||||
// Subscribe to new events
|
||||
this.subscription = changeStreamService.getActivityStream().subscribe((event) => {
|
||||
this.events = [...this.events, event].slice(-500); // Keep last 500 events
|
||||
|
||||
// Auto-scroll if enabled
|
||||
if (this.autoScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
this.isConnected = true;
|
||||
// setupSubscriptions() is triggered by connectionStatus$ subscriber above
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize activity stream:', error);
|
||||
this.isConnected = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async setupSubscriptions() {
|
||||
// Read buffered events (captured while on other tabs by app-level subscription)
|
||||
const buffered = changeStreamService.getBufferedActivity();
|
||||
if (buffered.length > 0) {
|
||||
this.events = buffered;
|
||||
} else {
|
||||
// Buffer empty (fresh start) — fetch from server
|
||||
const recentEvents = await changeStreamService.getRecentActivity(100);
|
||||
if (recentEvents.length > 0) {
|
||||
this.events = recentEvents;
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
// Set up RxJS listener only once for new live events
|
||||
if (!this.subscription) {
|
||||
this.subscription = changeStreamService.getActivityStream().subscribe((event) => {
|
||||
this.events = [...this.events, event].slice(-500);
|
||||
if (this.autoScroll) {
|
||||
this.scrollToTop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
@@ -340,14 +363,15 @@ export class TsviewActivityStream extends DeesElement {
|
||||
this.connectionSubscription.unsubscribe();
|
||||
this.connectionSubscription = null;
|
||||
}
|
||||
changeStreamService.unsubscribeFromActivity();
|
||||
// DO NOT call changeStreamService.unsubscribeFromActivity()
|
||||
// The app-level subscription keeps the server sending events for buffering
|
||||
}
|
||||
|
||||
private scrollToBottom() {
|
||||
private scrollToTop() {
|
||||
requestAnimationFrame(() => {
|
||||
const list = this.shadowRoot?.querySelector('.events-list');
|
||||
if (list) {
|
||||
list.scrollTop = list.scrollHeight;
|
||||
list.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -361,10 +385,11 @@ export class TsviewActivityStream extends DeesElement {
|
||||
}
|
||||
|
||||
private get filteredEvents(): IActivityEvent[] {
|
||||
if (this.filterMode === 'all') {
|
||||
return this.events;
|
||||
let events = this.events;
|
||||
if (this.filterMode !== 'all') {
|
||||
events = events.filter((e) => e.source === this.filterMode);
|
||||
}
|
||||
return this.events.filter((e) => e.source === this.filterMode);
|
||||
return events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
}
|
||||
|
||||
private formatTime(timestamp: string): string {
|
||||
@@ -378,11 +403,13 @@ export class TsviewActivityStream extends DeesElement {
|
||||
|
||||
private formatRelativeTime(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const diff = this.now - date.getTime();
|
||||
|
||||
if (diff < 60000) {
|
||||
return 'just now';
|
||||
if (diff < 1000) {
|
||||
return 'now';
|
||||
} else if (diff < 60000) {
|
||||
const secs = Math.floor(diff / 1000);
|
||||
return `${secs}s ago`;
|
||||
} else if (diff < 3600000) {
|
||||
const mins = Math.floor(diff / 60000);
|
||||
return `${mins}m ago`;
|
||||
@@ -534,6 +561,9 @@ export class TsviewActivityStream extends DeesElement {
|
||||
: this.filteredEvents.map(
|
||||
(event) => html`
|
||||
<div class="event-item" @click=${() => this.handleEventClick(event)}>
|
||||
<div class="event-time-col" title=${this.formatTime(event.timestamp)}>
|
||||
${this.formatRelativeTime(event.timestamp)}
|
||||
</div>
|
||||
<div class="event-icon ${event.source}">
|
||||
${event.source === 'mongodb' ? this.renderMongoIcon() : this.renderS3Icon()}
|
||||
</div>
|
||||
@@ -543,9 +573,6 @@ export class TsviewActivityStream extends DeesElement {
|
||||
<span class="event-type ${this.getEventType(event)}">${this.getEventType(event)}</span>
|
||||
${this.getEventTitle(event)}
|
||||
</div>
|
||||
<div class="event-time" title=${this.formatTime(event.timestamp)}>
|
||||
${this.formatRelativeTime(event.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-details">
|
||||
<span class="event-path">${this.getEventDetails(event)}</span>
|
||||
|
||||
@@ -421,15 +421,18 @@ export class TsviewApp extends DeesElement {
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadData();
|
||||
// Initialize WebSocket connection for real-time updates
|
||||
// Start WebSocket connection first (non-blocking) so it's in-flight
|
||||
// before child components try to subscribe
|
||||
this.initializeChangeStream();
|
||||
await this.loadData();
|
||||
}
|
||||
|
||||
private async initializeChangeStream() {
|
||||
try {
|
||||
await changeStreamService.connect();
|
||||
console.log('[TsviewApp] ChangeStream connected');
|
||||
// Subscribe to activity globally so events are buffered regardless of active tab
|
||||
await changeStreamService.subscribeToActivity();
|
||||
} catch (error) {
|
||||
console.warn('[TsviewApp] Failed to connect to ChangeStream:', error);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
private accessor isStreamConnected: boolean = false;
|
||||
|
||||
private changeSubscription: plugins.smartrx.rxjs.Subscription | null = null;
|
||||
private connectionSubscription: plugins.smartrx.rxjs.Subscription | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
@@ -201,12 +202,20 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadStats();
|
||||
this.subscribeToChanges();
|
||||
// Subscription is handled by updated() when databaseName/collectionName are set.
|
||||
// Only track connection status for UI indicator here.
|
||||
this.connectionSubscription = changeStreamService.connectionStatus$.subscribe((status) => {
|
||||
this.isStreamConnected = status === 'connected';
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.unsubscribeFromChanges();
|
||||
if (this.connectionSubscription) {
|
||||
this.connectionSubscription.unsubscribe();
|
||||
this.connectionSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
@@ -224,18 +233,16 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
if (!this.databaseName || !this.collectionName) return;
|
||||
|
||||
try {
|
||||
// Subscribe to collection changes
|
||||
const success = await changeStreamService.subscribeToCollection(this.databaseName, this.collectionName);
|
||||
this.isStreamConnected = success;
|
||||
|
||||
if (success) {
|
||||
// Listen for changes
|
||||
// Set up RxJS listener first so events aren't missed on reconnect
|
||||
if (!this.changeSubscription) {
|
||||
this.changeSubscription = changeStreamService
|
||||
.getCollectionChanges(this.databaseName, this.collectionName)
|
||||
.subscribe((event) => {
|
||||
this.handleChange(event);
|
||||
});
|
||||
.subscribe((event) => this.handleChange(event));
|
||||
}
|
||||
|
||||
// Subscribe on the server side (will auto-connect if needed)
|
||||
const success = await changeStreamService.subscribeToCollection(this.databaseName, this.collectionName);
|
||||
this.isStreamConnected = success;
|
||||
} catch (error) {
|
||||
console.warn('[MongoBrowser] Failed to subscribe to changes:', error);
|
||||
this.isStreamConnected = false;
|
||||
|
||||
@@ -36,6 +36,7 @@ export class TsviewS3Browser extends DeesElement {
|
||||
private accessor isStreamConnected: boolean = false;
|
||||
|
||||
private changeSubscription: plugins.smartrx.rxjs.Subscription | null = null;
|
||||
private connectionSubscription: plugins.smartrx.rxjs.Subscription | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
@@ -209,12 +210,20 @@ export class TsviewS3Browser extends DeesElement {
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.subscribeToChanges();
|
||||
// Subscription is handled by updated() when bucketName is set.
|
||||
// Only track connection status for UI indicator here.
|
||||
this.connectionSubscription = changeStreamService.connectionStatus$.subscribe((status) => {
|
||||
this.isStreamConnected = status === 'connected';
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.unsubscribeFromChanges();
|
||||
if (this.connectionSubscription) {
|
||||
this.connectionSubscription.unsubscribe();
|
||||
this.connectionSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setViewType(type: TViewType) {
|
||||
@@ -256,18 +265,16 @@ export class TsviewS3Browser extends DeesElement {
|
||||
if (!this.bucketName) return;
|
||||
|
||||
try {
|
||||
// Subscribe to bucket changes (with optional prefix)
|
||||
const success = await changeStreamService.subscribeToBucket(this.bucketName, this.currentPrefix || undefined);
|
||||
this.isStreamConnected = success;
|
||||
|
||||
if (success) {
|
||||
// Listen for changes
|
||||
// Set up RxJS listener first so events aren't missed on reconnect
|
||||
if (!this.changeSubscription) {
|
||||
this.changeSubscription = changeStreamService
|
||||
.getBucketChanges(this.bucketName, this.currentPrefix || undefined)
|
||||
.subscribe((event) => {
|
||||
this.handleChange(event);
|
||||
});
|
||||
.subscribe((event) => this.handleChange(event));
|
||||
}
|
||||
|
||||
// Subscribe on the server side (will auto-connect if needed)
|
||||
const success = await changeStreamService.subscribeToBucket(this.bucketName, this.currentPrefix || undefined);
|
||||
this.isStreamConnected = success;
|
||||
} catch (error) {
|
||||
console.warn('[S3Browser] Failed to subscribe to changes:', error);
|
||||
this.isStreamConnected = false;
|
||||
|
||||
@@ -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