feat(s3): add rename support for files and folders in S3 UI columns and keys

This commit is contained in:
2026-01-28 16:49:34 +00:00
parent b41adc184e
commit d9fc7f8257
8 changed files with 366 additions and 58 deletions

View File

@@ -106,6 +106,22 @@ export class TsviewS3Columns extends DeesElement {
@state()
private accessor movePickerLoading: boolean = false;
// Rename dialog state
@state()
private accessor showRenameDialog: boolean = false;
@state()
private accessor renameSource: { key: string; isFolder: boolean } | null = null;
@state()
private accessor renameName: string = '';
@state()
private accessor renameInProgress: boolean = false;
@state()
private accessor renameError: string | null = null;
// Internal drag state
@state()
private accessor draggedItem: { key: string; isFolder: boolean } | null = null;
@@ -757,6 +773,11 @@ export class TsviewS3Columns extends DeesElement {
await navigator.clipboard.writeText(prefix);
},
},
{
name: 'Rename',
iconName: 'lucide:pencil',
action: async () => this.openRenameDialog(prefix, true),
},
{ divider: true },
{
name: 'Move to...',
@@ -833,6 +854,11 @@ export class TsviewS3Columns extends DeesElement {
await navigator.clipboard.writeText(key);
},
},
{
name: 'Rename',
iconName: 'lucide:pencil',
action: async () => this.openRenameDialog(key, false),
},
{ divider: true },
{
name: 'Move to...',
@@ -1277,6 +1303,129 @@ export class TsviewS3Columns extends DeesElement {
this.moveInProgress = false;
}
// --- Rename dialog methods ---
private openRenameDialog(key: string, isFolder: boolean) {
this.renameSource = { key, isFolder };
this.renameName = getFileName(key);
this.renameError = null;
this.showRenameDialog = true;
// Auto-focus and smart selection
this.updateComplete.then(() => {
const input = this.shadowRoot?.querySelector('.rename-dialog-input') as HTMLInputElement;
if (input) {
input.focus();
if (!isFolder) {
const lastDot = this.renameName.lastIndexOf('.');
if (lastDot > 0) {
input.setSelectionRange(0, lastDot);
} else {
input.select();
}
} else {
input.select();
}
}
});
}
private async executeRename() {
if (!this.renameSource || !this.renameName.trim()) return;
const newName = this.renameName.trim();
const currentName = getFileName(this.renameSource.key);
if (newName === currentName) {
this.renameError = 'Name is the same as current';
return;
}
if (!newName) {
this.renameError = 'Name cannot be empty';
return;
}
if (newName.includes('/')) {
this.renameError = 'Name cannot contain "/"';
return;
}
this.renameInProgress = true;
this.renameError = null;
try {
const parentPrefix = getParentPrefix(this.renameSource.key);
const newKey = parentPrefix + newName + (this.renameSource.isFolder ? '/' : '');
let result: { success: boolean; error?: string };
if (this.renameSource.isFolder) {
result = await apiService.movePrefix(this.bucketName, this.renameSource.key, newKey);
} else {
result = await apiService.moveObject(this.bucketName, this.renameSource.key, newKey);
}
if (result.success) {
this.closeRenameDialog();
await this.refreshAllColumns();
} else {
this.renameError = result.error || 'Rename failed';
}
} catch (err) {
this.renameError = `Error: ${err}`;
}
this.renameInProgress = false;
}
private closeRenameDialog() {
this.showRenameDialog = false;
this.renameSource = null;
this.renameName = '';
this.renameError = null;
this.renameInProgress = false;
}
private renderRenameDialog() {
if (!this.showRenameDialog || !this.renameSource) return '';
const isFolder = this.renameSource.isFolder;
const title = isFolder ? 'Rename Folder' : 'Rename File';
return html`
<div class="dialog-overlay" @click=${() => this.closeRenameDialog()}>
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
<div class="dialog-title">${title}</div>
<div class="dialog-location">
Location: ${this.bucketName}/${getParentPrefix(this.renameSource.key)}
</div>
${this.renameError ? html`<div class="move-error">${this.renameError}</div>` : ''}
<input
type="text"
class="dialog-input rename-dialog-input"
placeholder=${isFolder ? 'folder-name' : 'filename.ext'}
.value=${this.renameName}
@input=${(e: InputEvent) => {
this.renameName = (e.target as HTMLInputElement).value;
this.renameError = null;
}}
@keydown=${(e: KeyboardEvent) => {
if (e.key === 'Enter') this.executeRename();
if (e.key === 'Escape') this.closeRenameDialog();
}}
/>
<div class="dialog-actions">
<button class="dialog-btn dialog-btn-cancel"
@click=${() => this.closeRenameDialog()}
?disabled=${this.renameInProgress}>Cancel</button>
<button class="dialog-btn dialog-btn-create"
@click=${() => this.executeRename()}
?disabled=${this.renameInProgress || !this.renameName.trim()}>
${this.renameInProgress ? 'Renaming...' : 'Rename'}
</button>
</div>
</div>
</div>
`;
}
private renderMoveDialog() {
if (!this.showMoveDialog || !this.moveSource) return '';
@@ -1462,6 +1611,7 @@ export class TsviewS3Columns extends DeesElement {
${this.renderCreateDialog()}
${this.renderMoveDialog()}
${this.renderMovePickerDialog()}
${this.renderRenameDialog()}
`;
}

View File

@@ -76,6 +76,22 @@ export class TsviewS3Keys extends DeesElement {
@state()
private accessor movePickerLoading: boolean = false;
// Rename dialog state
@state()
private accessor showRenameDialog: boolean = false;
@state()
private accessor renameSource: { key: string; isFolder: boolean } | null = null;
@state()
private accessor renameName: string = '';
@state()
private accessor renameInProgress: boolean = false;
@state()
private accessor renameError: string | null = null;
public static styles = [
cssManager.defaultStyles,
themeStyles,
@@ -486,6 +502,11 @@ export class TsviewS3Keys extends DeesElement {
await navigator.clipboard.writeText(key);
},
},
{
name: 'Rename',
iconName: 'lucide:pencil',
action: async () => this.openRenameDialog(key, true),
},
{ divider: true },
{
name: 'Move to...',
@@ -544,6 +565,11 @@ export class TsviewS3Keys extends DeesElement {
await navigator.clipboard.writeText(key);
},
},
{
name: 'Rename',
iconName: 'lucide:pencil',
action: async () => this.openRenameDialog(key, false),
},
{ divider: true },
{
name: 'Move to...',
@@ -807,6 +833,129 @@ export class TsviewS3Keys extends DeesElement {
this.moveInProgress = false;
}
// --- Rename dialog methods ---
private openRenameDialog(key: string, isFolder: boolean) {
this.renameSource = { key, isFolder };
this.renameName = getFileName(key);
this.renameError = null;
this.showRenameDialog = true;
// Auto-focus and smart selection
this.updateComplete.then(() => {
const input = this.shadowRoot?.querySelector('.rename-dialog-input') as HTMLInputElement;
if (input) {
input.focus();
if (!isFolder) {
const lastDot = this.renameName.lastIndexOf('.');
if (lastDot > 0) {
input.setSelectionRange(0, lastDot);
} else {
input.select();
}
} else {
input.select();
}
}
});
}
private async executeRename() {
if (!this.renameSource || !this.renameName.trim()) return;
const newName = this.renameName.trim();
const currentName = getFileName(this.renameSource.key);
if (newName === currentName) {
this.renameError = 'Name is the same as current';
return;
}
if (!newName) {
this.renameError = 'Name cannot be empty';
return;
}
if (newName.includes('/')) {
this.renameError = 'Name cannot contain "/"';
return;
}
this.renameInProgress = true;
this.renameError = null;
try {
const parentPrefix = getParentPrefix(this.renameSource.key);
const newKey = parentPrefix + newName + (this.renameSource.isFolder ? '/' : '');
let result: { success: boolean; error?: string };
if (this.renameSource.isFolder) {
result = await apiService.movePrefix(this.bucketName, this.renameSource.key, newKey);
} else {
result = await apiService.moveObject(this.bucketName, this.renameSource.key, newKey);
}
if (result.success) {
this.closeRenameDialog();
await this.loadObjects();
} else {
this.renameError = result.error || 'Rename failed';
}
} catch (err) {
this.renameError = `Error: ${err}`;
}
this.renameInProgress = false;
}
private closeRenameDialog() {
this.showRenameDialog = false;
this.renameSource = null;
this.renameName = '';
this.renameError = null;
this.renameInProgress = false;
}
private renderRenameDialog() {
if (!this.showRenameDialog || !this.renameSource) return '';
const isFolder = this.renameSource.isFolder;
const title = isFolder ? 'Rename Folder' : 'Rename File';
return html`
<div class="dialog-overlay" @click=${() => this.closeRenameDialog()}>
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
<div class="dialog-title">${title}</div>
<div class="dialog-location">
Location: ${this.bucketName}/${getParentPrefix(this.renameSource.key)}
</div>
${this.renameError ? html`<div class="move-error">${this.renameError}</div>` : ''}
<input
type="text"
class="dialog-input rename-dialog-input"
placeholder=${isFolder ? 'folder-name' : 'filename.ext'}
.value=${this.renameName}
@input=${(e: InputEvent) => {
this.renameName = (e.target as HTMLInputElement).value;
this.renameError = null;
}}
@keydown=${(e: KeyboardEvent) => {
if (e.key === 'Enter') this.executeRename();
if (e.key === 'Escape') this.closeRenameDialog();
}}
/>
<div class="dialog-actions">
<button class="dialog-btn dialog-btn-cancel"
@click=${() => this.closeRenameDialog()}
?disabled=${this.renameInProgress}>Cancel</button>
<button class="dialog-btn dialog-btn-create"
@click=${() => this.executeRename()}
?disabled=${this.renameInProgress || !this.renameName.trim()}>
${this.renameInProgress ? 'Renaming...' : 'Rename'}
</button>
</div>
</div>
</div>
`;
}
private renderMoveDialog() {
if (!this.showMoveDialog || !this.moveSource) return '';
@@ -972,6 +1121,7 @@ export class TsviewS3Keys extends DeesElement {
${this.renderCreateDialog()}
${this.renderMoveDialog()}
${this.renderMovePickerDialog()}
${this.renderRenameDialog()}
`;
}
}