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
## 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)
set executable permission on cli.js

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
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'
}

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
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'
}

View File

@@ -81,6 +81,7 @@ export class TsviewS3Columns extends DeesElement {
private dragCounters: Map<number, number> = new Map();
private folderHoverTimer: ReturnType<typeof setTimeout> | null = null;
private fileInputElement: HTMLInputElement | null = null;
private folderInputElement: HTMLInputElement | null = null;
public static styles = [
cssManager.defaultStyles,
@@ -380,10 +381,11 @@ export class TsviewS3Columns extends DeesElement {
this.clearFolderHover();
this.dragCounters.clear();
if (this.fileInputElement) { this.fileInputElement.remove(); this.fileInputElement = null; }
if (this.folderInputElement) { this.folderInputElement.remove(); this.folderInputElement = null; }
}
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('bucketName')) {
if (changedProperties.has('bucketName') || changedProperties.has('currentPrefix')) {
this.loadInitialColumn();
} else if (changedProperties.has('refreshKey')) {
this.refreshAllColumns();
@@ -393,16 +395,37 @@ export class TsviewS3Columns extends DeesElement {
private async loadInitialColumn() {
this.loading = true;
try {
const result = await apiService.listObjects(this.bucketName, this.currentPrefix, '/');
this.columns = [
{
prefix: this.currentPrefix,
// Parse the path segments from currentPrefix
// e.g., "folder1/folder2/folder3/" → ["folder1/", "folder1/folder2/", "folder1/folder2/folder3/"]
const pathSegments = this.getPathSegments(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,
prefixes: result.prefixes,
selectedItem: null,
selectedItem,
width: this.DEFAULT_COLUMN_WIDTH,
},
];
};
});
// Auto-scroll to show the rightmost column
this.updateComplete.then(() => this.scrollToEnd());
} catch (err) {
console.error('Error loading objects:', err);
this.columns = [];
@@ -410,6 +433,22 @@ export class TsviewS3Columns extends DeesElement {
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() {
const updatedColumns = await Promise.all(
this.columns.map(async (col) => {
@@ -566,9 +605,14 @@ export class TsviewS3Columns extends DeesElement {
action: async () => this.openCreateDialog('file', prefix),
},
{
name: 'Upload...',
iconName: 'lucide:upload',
action: async () => this.triggerFileUpload(prefix),
name: 'Upload Files...',
iconName: 'lucide:file',
action: async () => this.triggerFileUpload(prefix, 'files'),
},
{
name: 'Upload Folder...',
iconName: 'lucide:folderUp',
action: async () => this.triggerFileUpload(prefix, 'folder'),
},
{ divider: true },
{
@@ -654,9 +698,14 @@ export class TsviewS3Columns extends DeesElement {
action: async () => this.openCreateDialog('file', prefix),
},
{
name: 'Upload...',
iconName: 'lucide:upload',
action: async () => this.triggerFileUpload(prefix),
name: 'Upload Files...',
iconName: 'lucide:file',
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 ---
private ensureFileInput(): HTMLInputElement {
private ensureFileInputs(): void {
if (!this.fileInputElement) {
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);
}
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) {
const input = this.ensureFileInput();
private triggerFileUpload(targetPrefix: string, type: 'files' | 'folder') {
this.ensureFileInputs();
const input = type === 'folder' ? this.folderInputElement! : this.fileInputElement!;
input.value = '';
const handler = async () => {
input.removeEventListener('change', handler);