feat(s3-columns): load full prefix path on initial load and add folder upload support

This commit is contained in:
2026-01-28 15:04:42 +00:00
parent 319ee2a7af
commit 5051e957ec
5 changed files with 87 additions and 22 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # Changelog
## 2026-01-28 - 1.9.0 - feat(s3-columns)
load full prefix path on initial load and add folder upload support
- loadInitialColumn now loads all prefix path segments in parallel and pre-selects child prefixes so multi-column path is restored
- added getPathSegments helper and auto-scroll to show the rightmost column after load
- added separate hidden folder input (webkitdirectory) and folder upload flow; triggerFileUpload now accepts 'files' | 'folder'
- replaced generic 'Upload...' with 'Upload Files...' and added 'Upload Folder...' menu items
- updated updated() to react to currentPrefix changes and cleaned up folder input on disconnectedCallback
## 2026-01-28 - 1.8.1 - fix(cli) ## 2026-01-28 - 1.8.1 - fix(cli)
set executable permission on cli.js set executable permission on cli.js

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsview', name: '@git.zone/tsview',
version: '1.8.1', version: '1.9.0',
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI' description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
} }

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsview', name: '@git.zone/tsview',
version: '1.8.1', version: '1.9.0',
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI' description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
} }

View File

@@ -81,6 +81,7 @@ export class TsviewS3Columns extends DeesElement {
private dragCounters: Map<number, number> = new Map(); private dragCounters: Map<number, number> = new Map();
private folderHoverTimer: ReturnType<typeof setTimeout> | null = null; private folderHoverTimer: ReturnType<typeof setTimeout> | null = null;
private fileInputElement: HTMLInputElement | null = null; private fileInputElement: HTMLInputElement | null = null;
private folderInputElement: HTMLInputElement | null = null;
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
@@ -380,10 +381,11 @@ export class TsviewS3Columns extends DeesElement {
this.clearFolderHover(); this.clearFolderHover();
this.dragCounters.clear(); this.dragCounters.clear();
if (this.fileInputElement) { this.fileInputElement.remove(); this.fileInputElement = null; } if (this.fileInputElement) { this.fileInputElement.remove(); this.fileInputElement = null; }
if (this.folderInputElement) { this.folderInputElement.remove(); this.folderInputElement = null; }
} }
updated(changedProperties: Map<string, unknown>) { updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('bucketName')) { if (changedProperties.has('bucketName') || changedProperties.has('currentPrefix')) {
this.loadInitialColumn(); this.loadInitialColumn();
} else if (changedProperties.has('refreshKey')) { } else if (changedProperties.has('refreshKey')) {
this.refreshAllColumns(); this.refreshAllColumns();
@@ -393,16 +395,37 @@ export class TsviewS3Columns extends DeesElement {
private async loadInitialColumn() { private async loadInitialColumn() {
this.loading = true; this.loading = true;
try { try {
const result = await apiService.listObjects(this.bucketName, this.currentPrefix, '/'); // Parse the path segments from currentPrefix
this.columns = [ // e.g., "folder1/folder2/folder3/" → ["folder1/", "folder1/folder2/", "folder1/folder2/folder3/"]
{ const pathSegments = this.getPathSegments(this.currentPrefix);
prefix: this.currentPrefix,
// Build all prefixes we need to load (including root)
const prefixesToLoad = ['', ...pathSegments];
// Load all columns in parallel
const columnResults = await Promise.all(
prefixesToLoad.map(prefix =>
apiService.listObjects(this.bucketName, prefix, '/')
)
);
// Build columns array with proper selections
this.columns = columnResults.map((result, index) => {
const prefix = prefixesToLoad[index];
// The selected item is the next prefix in the path (if any)
const selectedItem = index < pathSegments.length ? pathSegments[index] : null;
return {
prefix,
objects: result.objects, objects: result.objects,
prefixes: result.prefixes, prefixes: result.prefixes,
selectedItem: null, selectedItem,
width: this.DEFAULT_COLUMN_WIDTH, width: this.DEFAULT_COLUMN_WIDTH,
}, };
]; });
// Auto-scroll to show the rightmost column
this.updateComplete.then(() => this.scrollToEnd());
} catch (err) { } catch (err) {
console.error('Error loading objects:', err); console.error('Error loading objects:', err);
this.columns = []; this.columns = [];
@@ -410,6 +433,22 @@ export class TsviewS3Columns extends DeesElement {
this.loading = false; this.loading = false;
} }
// Helper to parse prefix into cumulative path segments
private getPathSegments(prefix: string): string[] {
if (!prefix) return [];
const parts = prefix.split('/').filter(p => p); // Remove empty strings
const segments: string[] = [];
let cumulative = '';
for (const part of parts) {
cumulative += part + '/';
segments.push(cumulative);
}
return segments;
}
private async refreshAllColumns() { private async refreshAllColumns() {
const updatedColumns = await Promise.all( const updatedColumns = await Promise.all(
this.columns.map(async (col) => { this.columns.map(async (col) => {
@@ -566,9 +605,14 @@ export class TsviewS3Columns extends DeesElement {
action: async () => this.openCreateDialog('file', prefix), action: async () => this.openCreateDialog('file', prefix),
}, },
{ {
name: 'Upload...', name: 'Upload Files...',
iconName: 'lucide:upload', iconName: 'lucide:file',
action: async () => this.triggerFileUpload(prefix), action: async () => this.triggerFileUpload(prefix, 'files'),
},
{
name: 'Upload Folder...',
iconName: 'lucide:folderUp',
action: async () => this.triggerFileUpload(prefix, 'folder'),
}, },
{ divider: true }, { divider: true },
{ {
@@ -654,9 +698,14 @@ export class TsviewS3Columns extends DeesElement {
action: async () => this.openCreateDialog('file', prefix), action: async () => this.openCreateDialog('file', prefix),
}, },
{ {
name: 'Upload...', name: 'Upload Files...',
iconName: 'lucide:upload', iconName: 'lucide:file',
action: async () => this.triggerFileUpload(prefix), action: async () => this.triggerFileUpload(prefix, 'files'),
},
{
name: 'Upload Folder...',
iconName: 'lucide:folderUp',
action: async () => this.triggerFileUpload(prefix, 'folder'),
}, },
]); ]);
} }
@@ -697,20 +746,27 @@ export class TsviewS3Columns extends DeesElement {
// --- File upload helpers --- // --- File upload helpers ---
private ensureFileInput(): HTMLInputElement { private ensureFileInputs(): void {
if (!this.fileInputElement) { if (!this.fileInputElement) {
this.fileInputElement = document.createElement('input'); this.fileInputElement = document.createElement('input');
this.fileInputElement.type = 'file'; this.fileInputElement.type = 'file';
this.fileInputElement.multiple = true; this.fileInputElement.multiple = true;
(this.fileInputElement as any).webkitdirectory = true; // Enable folder selection
this.fileInputElement.style.display = 'none'; this.fileInputElement.style.display = 'none';
this.shadowRoot!.appendChild(this.fileInputElement); this.shadowRoot!.appendChild(this.fileInputElement);
} }
return this.fileInputElement; if (!this.folderInputElement) {
this.folderInputElement = document.createElement('input');
this.folderInputElement.type = 'file';
this.folderInputElement.multiple = true;
(this.folderInputElement as any).webkitdirectory = true;
this.folderInputElement.style.display = 'none';
this.shadowRoot!.appendChild(this.folderInputElement);
}
} }
private triggerFileUpload(targetPrefix: string) { private triggerFileUpload(targetPrefix: string, type: 'files' | 'folder') {
const input = this.ensureFileInput(); this.ensureFileInputs();
const input = type === 'folder' ? this.folderInputElement! : this.fileInputElement!;
input.value = ''; input.value = '';
const handler = async () => { const handler = async () => {
input.removeEventListener('change', handler); input.removeEventListener('change', handler);