feat(s3): add S3 create file/folder dialogs and in-place text editor; export mongodb plugin
This commit is contained in:
@@ -16,9 +16,18 @@ export class TsviewS3Preview extends DeesElement {
|
||||
@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 contentType: string = '';
|
||||
|
||||
@@ -78,6 +87,15 @@ export class TsviewS3Preview extends DeesElement {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.preview-content.code-editor {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-content.code-editor dees-input-code {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@@ -130,6 +148,51 @@ export class TsviewS3Preview extends DeesElement {
|
||||
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: rgba(255, 255, 255, 0.05);
|
||||
border-color: #555;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #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;
|
||||
@@ -177,7 +240,9 @@ export class TsviewS3Preview extends DeesElement {
|
||||
} else {
|
||||
this.content = '';
|
||||
this.contentType = '';
|
||||
this.error = ''; // Clear error when no file selected
|
||||
this.error = '';
|
||||
this.originalTextContent = '';
|
||||
this.hasChanges = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +252,7 @@ export class TsviewS3Preview extends DeesElement {
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
this.hasChanges = false;
|
||||
|
||||
try {
|
||||
const result = await apiService.getObject(this.bucketName, this.objectKey);
|
||||
@@ -194,6 +260,11 @@ export class TsviewS3Preview extends DeesElement {
|
||||
this.contentType = result.contentType;
|
||||
this.size = result.size;
|
||||
this.lastModified = result.lastModified;
|
||||
|
||||
// For text files, decode and store original content
|
||||
if (this.isText()) {
|
||||
this.originalTextContent = this.getTextContent();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading object:', err);
|
||||
this.error = 'Failed to load object';
|
||||
@@ -270,6 +341,98 @@ export class TsviewS3Preview extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
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 handleDiscard() {
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
if (codeEditor) {
|
||||
codeEditor.value = this.originalTextContent;
|
||||
}
|
||||
this.hasChanges = false;
|
||||
}
|
||||
|
||||
private async handleSave() {
|
||||
if (!this.hasChanges || this.saving) return;
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
// Get current content from the editor
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
const currentContent = codeEditor?.value ?? '';
|
||||
|
||||
// Encode the text content to base64
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(currentContent);
|
||||
const base64Content = btoa(String.fromCharCode(...bytes));
|
||||
|
||||
const success = await apiService.putObject(
|
||||
this.bucketName,
|
||||
this.objectKey,
|
||||
base64Content,
|
||||
this.contentType
|
||||
);
|
||||
|
||||
if (success) {
|
||||
this.originalTextContent = currentContent;
|
||||
this.hasChanges = false;
|
||||
// Update the stored content as well
|
||||
this.content = base64Content;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving object:', err);
|
||||
}
|
||||
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.objectKey) {
|
||||
return html`
|
||||
@@ -309,14 +472,27 @@ export class TsviewS3Preview extends DeesElement {
|
||||
<span class="meta-item">${this.contentType}</span>
|
||||
<span class="meta-item">${formatSize(this.size)}</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">
|
||||
<div class="preview-content ${this.isText() ? 'code-editor' : ''}">
|
||||
${this.isImage()
|
||||
? html`<img class="preview-image" src="data:${this.contentType};base64,${this.content}" />`
|
||||
: this.isText()
|
||||
? html`<pre class="preview-text">${this.getTextContent()}</pre>`
|
||||
? html`
|
||||
<dees-input-code
|
||||
.value=${this.originalTextContent}
|
||||
.language=${this.getLanguage()}
|
||||
height="100%"
|
||||
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
|
||||
></dees-input-code>
|
||||
`
|
||||
: html`
|
||||
<div class="binary-preview">
|
||||
<p>Binary file preview not available</p>
|
||||
@@ -326,8 +502,19 @@ export class TsviewS3Preview extends DeesElement {
|
||||
</div>
|
||||
|
||||
<div class="preview-actions">
|
||||
<button class="action-btn" @click=${this.handleDownload}>Download</button>
|
||||
<button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
|
||||
${this.hasChanges ? html`
|
||||
<button class="action-btn secondary" @click=${this.handleDiscard}>Discard</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
@click=${this.handleSave}
|
||||
?disabled=${this.saving}
|
||||
>
|
||||
${this.saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
` : html`
|
||||
<button class="action-btn" @click=${this.handleDownload}>Download</button>
|
||||
<button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user