import * as plugins from '../plugins.js'; import { apiService, type IS3Object } from '../services/index.js'; const { html, css, cssManager, customElement, property, state, DeesElement } = plugins; interface IColumn { prefix: string; objects: IS3Object[]; prefixes: string[]; selectedItem: string | null; } @customElement('tsview-s3-columns') export class TsviewS3Columns extends DeesElement { @property({ type: String }) public accessor bucketName: string = ''; @property({ type: String }) public accessor currentPrefix: string = ''; @state() private accessor columns: IColumn[] = []; @state() private accessor loading: boolean = false; public static styles = [ cssManager.defaultStyles, css` :host { display: block; height: 100%; overflow-x: auto; } .columns-container { display: flex; height: 100%; min-width: 100%; } .column { min-width: 220px; max-width: 280px; border-right: 1px solid #333; display: flex; flex-direction: column; height: 100%; } .column:last-child { border-right: none; } .column-header { padding: 8px 12px; font-size: 12px; font-weight: 500; color: #666; background: rgba(0, 0, 0, 0.2); border-bottom: 1px solid #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .column-items { flex: 1; overflow-y: auto; padding: 4px; } .column-item { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 13px; transition: background 0.1s; } .column-item:hover { background: rgba(255, 255, 255, 0.05); } .column-item.selected { background: rgba(99, 102, 241, 0.2); color: #818cf8; } .column-item.folder { color: #fbbf24; } .column-item .icon { width: 16px; height: 16px; flex-shrink: 0; } .column-item .name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .column-item .chevron { width: 14px; height: 14px; color: #555; } .empty-state { padding: 16px; text-align: center; color: #666; font-size: 13px; } .loading { padding: 16px; text-align: center; color: #666; } `, ]; async connectedCallback() { super.connectedCallback(); await this.loadInitialColumn(); } updated(changedProperties: Map) { if (changedProperties.has('bucketName') || changedProperties.has('currentPrefix')) { this.loadInitialColumn(); } } private async loadInitialColumn() { this.loading = true; try { const result = await apiService.listObjects(this.bucketName, this.currentPrefix, '/'); this.columns = [ { prefix: this.currentPrefix, objects: result.objects, prefixes: result.prefixes, selectedItem: null, }, ]; } catch (err) { console.error('Error loading objects:', err); this.columns = []; } this.loading = false; } private async selectFolder(columnIndex: number, prefix: string) { // Update selection in current column this.columns = this.columns.map((col, i) => { if (i === columnIndex) { return { ...col, selectedItem: prefix }; } return col; }); // Remove columns after current this.columns = this.columns.slice(0, columnIndex + 1); // Load new column try { const result = await apiService.listObjects(this.bucketName, prefix, '/'); this.columns = [ ...this.columns, { prefix, objects: result.objects, prefixes: result.prefixes, selectedItem: null, }, ]; } catch (err) { console.error('Error loading folder:', err); } // Dispatch navigate event this.dispatchEvent( new CustomEvent('navigate', { detail: { prefix }, bubbles: true, composed: true, }) ); } private selectFile(columnIndex: number, key: string) { // Update selection this.columns = this.columns.map((col, i) => { if (i === columnIndex) { return { ...col, selectedItem: key }; } return col; }); // Remove columns after current this.columns = this.columns.slice(0, columnIndex + 1); // Dispatch key-selected event this.dispatchEvent( new CustomEvent('key-selected', { detail: { key }, bubbles: true, composed: true, }) ); } private getFileName(path: string): string { const parts = path.replace(/\/$/, '').split('/'); return parts[parts.length - 1] || path; } private getFileIcon(key: string): string { const ext = key.split('.').pop()?.toLowerCase() || ''; const iconMap: Record = { json: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z', txt: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z', png: 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z', jpg: 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z', jpeg: 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z', gif: 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z', pdf: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z', }; return iconMap[ext] || 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'; } render() { if (this.loading && this.columns.length === 0) { return html`
Loading...
`; } return html`
${this.columns.map((column, index) => this.renderColumn(column, index))}
`; } private renderColumn(column: IColumn, index: number) { const headerName = column.prefix ? this.getFileName(column.prefix) : this.bucketName; return html`
${headerName}
${column.prefixes.length === 0 && column.objects.length === 0 ? html`
Empty folder
` : ''} ${column.prefixes.map( (prefix) => html`
this.selectFolder(index, prefix)} > ${this.getFileName(prefix)}
` )} ${column.objects.map( (obj) => html`
this.selectFile(index, obj.key)} > ${this.getFileName(obj.key)}
` )}
`; } }