feat(dataview): add an S3 browser component with column and list views, file preview, editing, and object management
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-12 - 3.48.0 - feat(dataview)
|
||||
add an S3 browser component with column and list views, file preview, editing, and object management
|
||||
|
||||
- introduces a new dees-s3-browser module with shared interfaces, utilities, demo, and exports
|
||||
- supports browsing S3-style prefixes in both column and list layouts with breadcrumb navigation
|
||||
- adds file preview with text editing, download, and delete actions
|
||||
- includes create, rename, move, delete, upload, and drag-and-drop handling for files and folders
|
||||
- adds optional live change stream integration with refresh indicators
|
||||
|
||||
## 2026-03-11 - 3.47.2 - fix(deps)
|
||||
bump @design.estate/dees-domtools and @design.estate/dees-element dependencies
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '3.47.2',
|
||||
version: '3.48.0',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import type { IS3DataProvider, IS3Object } from './interfaces.js';
|
||||
import './dees-s3-browser.js';
|
||||
|
||||
// Mock in-memory S3 data provider for demo purposes
|
||||
class MockS3DataProvider implements IS3DataProvider {
|
||||
private objects: Map<string, { content: string; contentType: string; size: number; lastModified: string }> = new Map();
|
||||
|
||||
constructor() {
|
||||
const now = new Date().toISOString();
|
||||
// Seed with sample data
|
||||
this.objects.set('documents/readme.md', {
|
||||
content: btoa('# Welcome\n\nThis is a demo S3 browser.\n'),
|
||||
contentType: 'text/markdown',
|
||||
size: 42,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('documents/config.json', {
|
||||
content: btoa('{\n "name": "demo",\n "version": "1.0.0"\n}'),
|
||||
contentType: 'application/json',
|
||||
size: 48,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('documents/notes/todo.txt', {
|
||||
content: btoa('Buy milk\nFix bug #42\nDeploy to production'),
|
||||
contentType: 'text/plain',
|
||||
size: 45,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('images/logo.png', {
|
||||
content: btoa('fake-png-data'),
|
||||
contentType: 'image/png',
|
||||
size: 24500,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('images/banner.jpg', {
|
||||
content: btoa('fake-jpg-data'),
|
||||
contentType: 'image/jpeg',
|
||||
size: 156000,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('scripts/deploy.sh', {
|
||||
content: btoa('#!/bin/bash\necho "Deploying..."\n'),
|
||||
contentType: 'text/plain',
|
||||
size: 34,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('index.html', {
|
||||
content: btoa('<!DOCTYPE html>\n<html>\n<body>\n <h1>Hello World</h1>\n</body>\n</html>'),
|
||||
contentType: 'text/html',
|
||||
size: 72,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('styles.css', {
|
||||
content: btoa('body { margin: 0; font-family: sans-serif; }'),
|
||||
contentType: 'text/css',
|
||||
size: 44,
|
||||
lastModified: now,
|
||||
});
|
||||
}
|
||||
|
||||
async listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IS3Object[]; prefixes: string[] }> {
|
||||
const pfx = prefix || '';
|
||||
const objects: IS3Object[] = [];
|
||||
const prefixes = new Set<string>();
|
||||
|
||||
for (const [key, data] of this.objects) {
|
||||
if (!key.startsWith(pfx)) continue;
|
||||
const rest = key.slice(pfx.length);
|
||||
|
||||
if (delimiter) {
|
||||
const slashIndex = rest.indexOf(delimiter);
|
||||
if (slashIndex >= 0) {
|
||||
prefixes.add(pfx + rest.slice(0, slashIndex + 1));
|
||||
} else {
|
||||
objects.push({ key, size: data.size, lastModified: data.lastModified });
|
||||
}
|
||||
} else {
|
||||
objects.push({ key, size: data.size, lastModified: data.lastModified });
|
||||
}
|
||||
}
|
||||
|
||||
return { objects, prefixes: Array.from(prefixes).sort() };
|
||||
}
|
||||
|
||||
async getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }> {
|
||||
const obj = this.objects.get(key);
|
||||
if (!obj) throw new Error('Not found');
|
||||
return { ...obj };
|
||||
}
|
||||
|
||||
async putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean> {
|
||||
this.objects.set(key, {
|
||||
content: base64Content,
|
||||
contentType,
|
||||
size: atob(base64Content).length,
|
||||
lastModified: new Date().toISOString(),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteObject(bucket: string, key: string): Promise<boolean> {
|
||||
return this.objects.delete(key);
|
||||
}
|
||||
|
||||
async deletePrefix(bucket: string, prefix: string): Promise<boolean> {
|
||||
for (const key of this.objects.keys()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.objects.delete(key);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async getObjectUrl(bucket: string, key: string): Promise<string> {
|
||||
const obj = this.objects.get(key);
|
||||
if (!obj) return '';
|
||||
const blob = new Blob([Uint8Array.from(atob(obj.content), c => c.charCodeAt(0))], { type: obj.contentType });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }> {
|
||||
const obj = this.objects.get(sourceKey);
|
||||
if (!obj) return { success: false, error: 'Source not found' };
|
||||
this.objects.set(destKey, { ...obj, lastModified: new Date().toISOString() });
|
||||
this.objects.delete(sourceKey);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }> {
|
||||
let count = 0;
|
||||
const toMove = Array.from(this.objects.entries()).filter(([k]) => k.startsWith(sourcePrefix));
|
||||
for (const [key, data] of toMove) {
|
||||
const newKey = destPrefix + key.slice(sourcePrefix.length);
|
||||
this.objects.set(newKey, { ...data, lastModified: new Date().toISOString() });
|
||||
this.objects.delete(key);
|
||||
count++;
|
||||
}
|
||||
return { success: true, movedCount: count };
|
||||
}
|
||||
}
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
height: 600px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<dees-s3-browser
|
||||
.dataProvider=${new MockS3DataProvider()}
|
||||
.bucketName=${'demo-bucket'}
|
||||
></dees-s3-browser>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,439 @@
|
||||
import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { demoFunc } from './dees-s3-browser.demo.js';
|
||||
import type { IS3DataProvider, IS3ChangeEvent } from './interfaces.js';
|
||||
import './dees-s3-columns.js';
|
||||
import './dees-s3-keys.js';
|
||||
import './dees-s3-preview.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-s3-browser': DeesS3Browser;
|
||||
}
|
||||
}
|
||||
|
||||
type TViewType = 'columns' | 'keys';
|
||||
|
||||
@customElement('dees-s3-browser')
|
||||
export class DeesS3Browser extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Data View'];
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor dataProvider: IS3DataProvider | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
public accessor bucketName: string = '';
|
||||
|
||||
/**
|
||||
* Optional change stream subscription.
|
||||
* Pass a function that takes a callback and returns an unsubscribe function.
|
||||
*/
|
||||
@property({ type: Object })
|
||||
public accessor onChangeEvent: ((callback: (event: IS3ChangeEvent) => void) => (() => void)) | null = null;
|
||||
|
||||
@state()
|
||||
private accessor viewType: TViewType = 'columns';
|
||||
|
||||
@state()
|
||||
private accessor currentPrefix: string = '';
|
||||
|
||||
@state()
|
||||
private accessor selectedKey: string = '';
|
||||
|
||||
@state()
|
||||
private accessor refreshKey: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor previewWidth: number = 700;
|
||||
|
||||
@state()
|
||||
private accessor isResizingPreview: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor recentChangeCount: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor isStreamConnected: boolean = false;
|
||||
|
||||
private changeUnsubscribe: (() => void) | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeDefaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.browser-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#999')};
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#fff')};
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: ${cssManager.bdTheme('#d4d4d8', '#555')};
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#444')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
border-color: ${cssManager.bdTheme('#a1a1aa', '#666')};
|
||||
color: ${cssManager.bdTheme('#3f3f46', '#aaa')};
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
border-color: ${cssManager.bdTheme('#a1a1aa', '#404040')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#e0e0e0')};
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr 4px var(--preview-width, 700px);
|
||||
}
|
||||
|
||||
.resize-divider {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.resize-divider:hover,
|
||||
.resize-divider.active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.2)')};
|
||||
}
|
||||
|
||||
.main-view {
|
||||
overflow: auto;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.content,
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-panel,
|
||||
.resize-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
margin-left: auto;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.stream-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#a1a1aa', '#888')};
|
||||
}
|
||||
|
||||
.stream-dot.connected {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.change-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
color: #f59e0b;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.change-indicator.pulse {
|
||||
animation: pulse-orange 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse-orange {
|
||||
0% { background: rgba(245, 158, 11, 0.4); }
|
||||
100% { background: rgba(245, 158, 11, 0.2); }
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
this.unsubscribeFromChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to trigger a refresh of child components
|
||||
*/
|
||||
public refresh() {
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
private setViewType(type: TViewType) {
|
||||
this.viewType = type;
|
||||
}
|
||||
|
||||
private navigateToPrefix(prefix: string) {
|
||||
this.currentPrefix = prefix;
|
||||
this.selectedKey = '';
|
||||
}
|
||||
|
||||
private handleKeySelected(e: CustomEvent) {
|
||||
this.selectedKey = e.detail.key;
|
||||
}
|
||||
|
||||
private handleNavigate(e: CustomEvent) {
|
||||
this.navigateToPrefix(e.detail.prefix);
|
||||
}
|
||||
|
||||
private handleObjectDeleted(e: CustomEvent) {
|
||||
this.selectedKey = '';
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('bucketName')) {
|
||||
this.selectedKey = '';
|
||||
this.currentPrefix = '';
|
||||
this.recentChangeCount = 0;
|
||||
this.unsubscribeFromChanges();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
if (changedProperties.has('onChangeEvent')) {
|
||||
this.unsubscribeFromChanges();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToChanges() {
|
||||
if (!this.onChangeEvent) {
|
||||
this.isStreamConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.changeUnsubscribe = this.onChangeEvent((event: IS3ChangeEvent) => {
|
||||
this.handleChange(event);
|
||||
});
|
||||
this.isStreamConnected = true;
|
||||
} catch (error) {
|
||||
console.warn('[S3Browser] Failed to subscribe to changes:', error);
|
||||
this.isStreamConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsubscribeFromChanges() {
|
||||
if (this.changeUnsubscribe) {
|
||||
this.changeUnsubscribe();
|
||||
this.changeUnsubscribe = null;
|
||||
}
|
||||
this.isStreamConnected = false;
|
||||
}
|
||||
|
||||
private handleChange(event: IS3ChangeEvent) {
|
||||
this.recentChangeCount++;
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
private startPreviewResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.isResizingPreview = true;
|
||||
document.addEventListener('mousemove', this.handlePreviewResize);
|
||||
document.addEventListener('mouseup', this.endPreviewResize);
|
||||
};
|
||||
|
||||
private handlePreviewResize = (e: MouseEvent) => {
|
||||
if (!this.isResizingPreview) return;
|
||||
const contentEl = this.shadowRoot?.querySelector('.content');
|
||||
if (!contentEl) return;
|
||||
const containerRect = contentEl.getBoundingClientRect();
|
||||
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 1000);
|
||||
this.previewWidth = newWidth;
|
||||
};
|
||||
|
||||
private endPreviewResize = () => {
|
||||
this.isResizingPreview = false;
|
||||
document.removeEventListener('mousemove', this.handlePreviewResize);
|
||||
document.removeEventListener('mouseup', this.endPreviewResize);
|
||||
};
|
||||
|
||||
render() {
|
||||
const breadcrumbParts = this.currentPrefix
|
||||
? this.currentPrefix.split('/').filter(Boolean)
|
||||
: [];
|
||||
|
||||
return html`
|
||||
<div class="browser-container">
|
||||
<div class="toolbar">
|
||||
<div class="breadcrumb">
|
||||
<span
|
||||
class="breadcrumb-item"
|
||||
@click=${() => this.navigateToPrefix('')}
|
||||
>
|
||||
${this.bucketName}
|
||||
</span>
|
||||
${breadcrumbParts.map((part, index) => {
|
||||
const prefix = breadcrumbParts.slice(0, index + 1).join('/') + '/';
|
||||
return html`
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<span
|
||||
class="breadcrumb-item"
|
||||
@click=${() => this.navigateToPrefix(prefix)}
|
||||
>
|
||||
${part}
|
||||
</span>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
${this.onChangeEvent ? html`
|
||||
<div class="stream-status">
|
||||
<span class="stream-dot ${this.isStreamConnected ? 'connected' : ''}"></span>
|
||||
${this.isStreamConnected ? 'Live' : 'Offline'}
|
||||
</div>
|
||||
` : ''}
|
||||
${this.recentChangeCount > 0
|
||||
? html`
|
||||
<div class="change-indicator pulse">
|
||||
${this.recentChangeCount} change${this.recentChangeCount > 1 ? 's' : ''}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="view-btn ${this.viewType === 'columns' ? 'active' : ''}"
|
||||
@click=${() => this.setViewType('columns')}
|
||||
>
|
||||
Columns
|
||||
</button>
|
||||
<button
|
||||
class="view-btn ${this.viewType === 'keys' ? 'active' : ''}"
|
||||
@click=${() => this.setViewType('keys')}
|
||||
>
|
||||
List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content ${this.selectedKey ? 'has-preview' : ''}" style="--preview-width: ${this.previewWidth}px">
|
||||
<div class="main-view">
|
||||
${this.viewType === 'columns'
|
||||
? html`
|
||||
<dees-s3-columns
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.currentPrefix=${this.currentPrefix}
|
||||
.refreshKey=${this.refreshKey}
|
||||
@key-selected=${this.handleKeySelected}
|
||||
@navigate=${this.handleNavigate}
|
||||
></dees-s3-columns>
|
||||
`
|
||||
: html`
|
||||
<dees-s3-keys
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.currentPrefix=${this.currentPrefix}
|
||||
.refreshKey=${this.refreshKey}
|
||||
@key-selected=${this.handleKeySelected}
|
||||
@navigate=${this.handleNavigate}
|
||||
></dees-s3-keys>
|
||||
`}
|
||||
</div>
|
||||
|
||||
${this.selectedKey
|
||||
? html`
|
||||
<div
|
||||
class="resize-divider ${this.isResizingPreview ? 'active' : ''}"
|
||||
@mousedown=${this.startPreviewResize}
|
||||
></div>
|
||||
<div class="preview-panel">
|
||||
<dees-s3-preview
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.objectKey=${this.selectedKey}
|
||||
@object-deleted=${this.handleObjectDeleted}
|
||||
></dees-s3-preview>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1652
ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-columns.ts
Normal file
1652
ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-columns.ts
Normal file
File diff suppressed because it is too large
Load Diff
1094
ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-keys.ts
Normal file
1094
ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-keys.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,540 @@
|
||||
import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import type { IS3DataProvider } from './interfaces.js';
|
||||
import { formatSize, getFileName } from './utilities.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-s3-preview': DeesS3Preview;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-s3-preview')
|
||||
export class DeesS3Preview extends DeesElement {
|
||||
@property({ type: Object })
|
||||
public accessor dataProvider: IS3DataProvider | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
public accessor bucketName: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public accessor objectKey: string = '';
|
||||
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor saving: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor content: string = '';
|
||||
|
||||
@state()
|
||||
private accessor originalTextContent: string = '';
|
||||
|
||||
@state()
|
||||
private accessor hasChanges: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor editing: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor contentType: string = '';
|
||||
|
||||
@state()
|
||||
private accessor fileSize: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor lastModified: string = '';
|
||||
|
||||
@state()
|
||||
private accessor error: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeDefaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-content dees-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-content.code-editor {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-content.code-editor dees-input-code {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
padding: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#404040')};
|
||||
color: ${cssManager.bdTheme('#3f3f46', '#e0e0e0')};
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.15)')};
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: #ef4444;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
border-color: #3b82f6;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.action-btn.primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.05)')};
|
||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#555')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#aaa')};
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#fff')};
|
||||
}
|
||||
|
||||
.unsaved-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.unsaved-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #fbbf24;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('#a1a1aa', '#666')};
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
}
|
||||
|
||||
.error-state {
|
||||
padding: 16px;
|
||||
color: #f87171;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('objectKey') || changedProperties.has('bucketName')) {
|
||||
if (this.objectKey) {
|
||||
this.loadObject();
|
||||
} else {
|
||||
this.content = '';
|
||||
this.contentType = '';
|
||||
this.error = '';
|
||||
this.originalTextContent = '';
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadObject() {
|
||||
if (!this.objectKey || !this.bucketName || !this.dataProvider) return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
|
||||
try {
|
||||
const result = await this.dataProvider.getObject(this.bucketName, this.objectKey);
|
||||
if (!result) {
|
||||
this.error = 'Object not found';
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.content = result.content || '';
|
||||
this.contentType = result.contentType || '';
|
||||
this.fileSize = result.size || 0;
|
||||
this.lastModified = result.lastModified || '';
|
||||
|
||||
if (this.isText()) {
|
||||
this.originalTextContent = this.getTextContent();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading object:', err);
|
||||
this.error = 'Failed to load object';
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
private isImage(): boolean {
|
||||
return this.contentType.startsWith('image/');
|
||||
}
|
||||
|
||||
private isText(): boolean {
|
||||
return (
|
||||
this.contentType.startsWith('text/') ||
|
||||
this.contentType === 'application/json' ||
|
||||
this.contentType === 'application/xml' ||
|
||||
this.contentType === 'application/javascript'
|
||||
);
|
||||
}
|
||||
|
||||
private getTextContent(): string {
|
||||
try {
|
||||
const binaryString = atob(this.content);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder('utf-8').decode(bytes);
|
||||
} catch {
|
||||
return 'Unable to decode content';
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownload() {
|
||||
try {
|
||||
const blob = new Blob([Uint8Array.from(atob(this.content), (c) => c.charCodeAt(0))], {
|
||||
type: this.contentType,
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = getFileName(this.objectKey);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('Error downloading:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDelete() {
|
||||
if (!this.dataProvider) return;
|
||||
if (!confirm(`Delete "${getFileName(this.objectKey)}"?`)) return;
|
||||
|
||||
try {
|
||||
await this.dataProvider.deleteObject(this.bucketName, this.objectKey);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('object-deleted', {
|
||||
detail: { key: this.objectKey },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error deleting object:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private getLanguage(): string {
|
||||
const ext = this.objectKey.split('.').pop()?.toLowerCase() || '';
|
||||
const languageMap: Record<string, string> = {
|
||||
ts: 'typescript',
|
||||
tsx: 'typescript',
|
||||
js: 'javascript',
|
||||
jsx: 'javascript',
|
||||
mjs: 'javascript',
|
||||
cjs: 'javascript',
|
||||
json: 'json',
|
||||
html: 'html',
|
||||
htm: 'html',
|
||||
css: 'css',
|
||||
scss: 'scss',
|
||||
sass: 'scss',
|
||||
less: 'less',
|
||||
md: 'markdown',
|
||||
markdown: 'markdown',
|
||||
xml: 'xml',
|
||||
yaml: 'yaml',
|
||||
yml: 'yaml',
|
||||
py: 'python',
|
||||
rb: 'ruby',
|
||||
go: 'go',
|
||||
rs: 'rust',
|
||||
java: 'java',
|
||||
c: 'c',
|
||||
cpp: 'cpp',
|
||||
h: 'c',
|
||||
hpp: 'cpp',
|
||||
cs: 'csharp',
|
||||
php: 'php',
|
||||
sh: 'shell',
|
||||
bash: 'shell',
|
||||
zsh: 'shell',
|
||||
sql: 'sql',
|
||||
graphql: 'graphql',
|
||||
gql: 'graphql',
|
||||
dockerfile: 'dockerfile',
|
||||
txt: 'plaintext',
|
||||
};
|
||||
return languageMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
private handleContentChange(event: CustomEvent) {
|
||||
const newValue = event.detail as string;
|
||||
this.hasChanges = newValue !== this.originalTextContent;
|
||||
}
|
||||
|
||||
private handleEdit() {
|
||||
this.editing = true;
|
||||
}
|
||||
|
||||
private handleDiscard() {
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
if (codeEditor) {
|
||||
codeEditor.value = this.originalTextContent;
|
||||
}
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
}
|
||||
|
||||
private async handleSave() {
|
||||
if (!this.hasChanges || this.saving || !this.dataProvider) return;
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
const currentContent = codeEditor?.value ?? '';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(currentContent);
|
||||
const base64Content = btoa(String.fromCharCode(...bytes));
|
||||
|
||||
const success = await this.dataProvider.putObject(
|
||||
this.bucketName,
|
||||
this.objectKey,
|
||||
base64Content,
|
||||
this.contentType
|
||||
);
|
||||
|
||||
if (success) {
|
||||
this.originalTextContent = currentContent;
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
this.content = base64Content;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving object:', err);
|
||||
}
|
||||
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.objectKey) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
<p>Select a file to preview</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="loading-state">Loading...</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="error-state">${this.error}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">${getFileName(this.objectKey)}</div>
|
||||
<div class="preview-meta">
|
||||
<span class="meta-item">${this.contentType}</span>
|
||||
<span class="meta-item">${formatSize(this.fileSize)}</span>
|
||||
<span class="meta-item">${this.formatDate(this.lastModified)}</span>
|
||||
${this.hasChanges ? html`
|
||||
<span class="unsaved-indicator">
|
||||
<span class="unsaved-dot"></span>
|
||||
Unsaved changes
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-content ${this.editing ? 'code-editor' : ''}">
|
||||
${this.editing
|
||||
? html`
|
||||
<dees-input-code
|
||||
.value=${this.originalTextContent}
|
||||
.language=${this.getLanguage()}
|
||||
height="100%"
|
||||
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
|
||||
></dees-input-code>
|
||||
`
|
||||
: this.isText()
|
||||
? html`
|
||||
<dees-preview
|
||||
.textContent=${this.originalTextContent}
|
||||
.filename=${getFileName(this.objectKey)}
|
||||
.language=${this.getLanguage()}
|
||||
.showToolbar=${true}
|
||||
.showFilename=${false}
|
||||
></dees-preview>
|
||||
`
|
||||
: html`
|
||||
<dees-preview
|
||||
.base64=${this.content}
|
||||
.mimeType=${this.contentType}
|
||||
.filename=${getFileName(this.objectKey)}
|
||||
.showToolbar=${true}
|
||||
.showFilename=${false}
|
||||
></dees-preview>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="preview-actions">
|
||||
${this.editing
|
||||
? html`
|
||||
<button class="action-btn secondary" @click=${this.handleDiscard}>
|
||||
${this.hasChanges ? 'Discard' : 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
@click=${this.handleSave}
|
||||
?disabled=${this.saving || !this.hasChanges}
|
||||
>
|
||||
${this.saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${this.isText()
|
||||
? html`<button class="action-btn" @click=${this.handleEdit}>Edit</button>`
|
||||
: ''}
|
||||
<button class="action-btn" @click=${this.handleDownload}>Download</button>
|
||||
<button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './dees-s3-browser.js';
|
||||
export * from './dees-s3-columns.js';
|
||||
export * from './dees-s3-keys.js';
|
||||
export * from './dees-s3-preview.js';
|
||||
export * from './interfaces.js';
|
||||
export { formatSize, formatCount, getFileName, validateMove, getParentPrefix, getContentType, getDefaultContent, getPathSegments } from './utilities.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* S3 Data Provider interface - implement this to connect the S3 browser to your backend
|
||||
*/
|
||||
|
||||
export interface IS3Object {
|
||||
key: string;
|
||||
size?: number;
|
||||
lastModified?: string;
|
||||
isPrefix?: boolean;
|
||||
}
|
||||
|
||||
export interface IS3ChangeEvent {
|
||||
type: 'add' | 'modify' | 'delete';
|
||||
key: string;
|
||||
bucket: string;
|
||||
size?: number;
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
export interface IS3DataProvider {
|
||||
listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IS3Object[]; prefixes: string[] }>;
|
||||
getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }>;
|
||||
putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean>;
|
||||
deleteObject(bucket: string, key: string): Promise<boolean>;
|
||||
deletePrefix(bucket: string, prefix: string): Promise<boolean>;
|
||||
getObjectUrl(bucket: string, key: string): Promise<string>;
|
||||
moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }>;
|
||||
movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }>;
|
||||
}
|
||||
|
||||
export interface IColumn {
|
||||
prefix: string;
|
||||
objects: IS3Object[];
|
||||
prefixes: string[];
|
||||
selectedItem: string | null;
|
||||
width: number;
|
||||
}
|
||||
120
ts_web/elements/00group-dataview/dees-s3-browser/utilities.ts
Normal file
120
ts_web/elements/00group-dataview/dees-s3-browser/utilities.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Shared utilities for S3 browser components
|
||||
*/
|
||||
|
||||
export interface IMoveValidation {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a byte size into a human-readable string
|
||||
*/
|
||||
export function formatSize(bytes?: number): string {
|
||||
if (bytes === undefined || bytes === null) return '-';
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a count into a compact human-readable string
|
||||
*/
|
||||
export function formatCount(count?: number): string {
|
||||
if (count === undefined || count === null) return '';
|
||||
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the file name from a path
|
||||
*/
|
||||
export function getFileName(path: string): string {
|
||||
const parts = path.replace(/\/$/, '').split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a move operation is allowed
|
||||
*/
|
||||
export function validateMove(sourceKey: string, destPrefix: string): IMoveValidation {
|
||||
if (sourceKey.endsWith('/')) {
|
||||
if (destPrefix.startsWith(sourceKey)) {
|
||||
return { valid: false, error: 'Cannot move a folder into itself' };
|
||||
}
|
||||
}
|
||||
|
||||
const sourceParent = getParentPrefix(sourceKey);
|
||||
if (sourceParent === destPrefix) {
|
||||
return { valid: false, error: 'Item is already in this location' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent prefix (directory) of a given key
|
||||
*/
|
||||
export function getParentPrefix(key: string): string {
|
||||
const trimmed = key.endsWith('/') ? key.slice(0, -1) : key;
|
||||
const lastSlash = trimmed.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? trimmed.substring(0, lastSlash + 1) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content type from file extension
|
||||
*/
|
||||
export function getContentType(ext: string): string {
|
||||
const contentTypes: Record<string, string> = {
|
||||
json: 'application/json',
|
||||
txt: 'text/plain',
|
||||
html: 'text/html',
|
||||
css: 'text/css',
|
||||
js: 'application/javascript',
|
||||
ts: 'text/typescript',
|
||||
md: 'text/markdown',
|
||||
xml: 'application/xml',
|
||||
yaml: 'text/yaml',
|
||||
yml: 'text/yaml',
|
||||
csv: 'text/csv',
|
||||
};
|
||||
return contentTypes[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default content for a new file based on extension
|
||||
*/
|
||||
export function getDefaultContent(ext: string): string {
|
||||
const defaults: Record<string, string> = {
|
||||
json: '{\n \n}',
|
||||
html: '<!DOCTYPE html>\n<html>\n<head>\n <title></title>\n</head>\n<body>\n \n</body>\n</html>',
|
||||
md: '# Title\n\n',
|
||||
txt: '',
|
||||
};
|
||||
return defaults[ext] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a prefix into cumulative path segments
|
||||
*/
|
||||
export function getPathSegments(prefix: string): string[] {
|
||||
if (!prefix) return [];
|
||||
const parts = prefix.split('/').filter(p => p);
|
||||
const segments: string[] = [];
|
||||
let cumulative = '';
|
||||
for (const part of parts) {
|
||||
cumulative += part + '/';
|
||||
segments.push(cumulative);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './dees-dataview-codebox/index.js';
|
||||
export * from './dees-dataview-statusobject/index.js';
|
||||
export * from './dees-table/index.js';
|
||||
export * from './dees-statsgrid/index.js';
|
||||
export * from './dees-s3-browser/index.js';
|
||||
|
||||
Reference in New Issue
Block a user