6 Commits

Author SHA1 Message Date
c4afbdfd7f v1.11.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 13m25s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-01-29 15:56:05 +00:00
b72b36c4ea fix(mongo-browser): increase default editor width and broaden resize range in Mongo browser pane 2026-01-29 15:56:05 +00:00
8c5cea7e0b v1.11.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 14m55s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-01-28 16:49:34 +00:00
d9fc7f8257 feat(s3): add rename support for files and folders in S3 UI columns and keys 2026-01-28 16:49:34 +00:00
b41adc184e v1.10.2
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 13m2s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-01-28 16:16:26 +00:00
175e9cb691 fix(tsview-s3-columns): append trailing slash to destination key when moving folders to ensure folder destination path 2026-01-28 16:16:26 +00:00
9 changed files with 385 additions and 63 deletions

View File

@@ -1,5 +1,27 @@
# Changelog
## 2026-01-29 - 1.11.1 - fix(mongo-browser)
increase default editor width and broaden resize range in Mongo browser pane
- Set default editorWidth from 400 to 700
- Update CSS grid-template default editor width variable to 700
- Expand editor resize bounds: min 300 -> 250, max 700 -> 1000 (resizer calculation adjusted)
## 2026-01-28 - 1.11.0 - feat(s3)
add rename support for files and folders in S3 UI columns and keys
- Adds 'Rename' option to context menus in tsview-s3-columns and tsview-s3-keys for files and folders
- Implements a rename dialog with validation, auto-focus and smart selection (preserves extensions), and progress/error states
- Performs renames via apiService.moveObject / apiService.movePrefix and refreshes the UI after success
- Bumps @design.estate/dees-catalog dependency to ^3.41.2
## 2026-01-28 - 1.10.2 - fix(tsview-s3-columns)
append trailing slash to destination key when moving folders to ensure folder destination path
- ts_web/elements/tsview-s3-columns.ts: add '/' to destKey when moveSource.isFolder to preserve folder semantics
- Prevents folder moves from being treated as object moves by ensuring destination key ends with a slash
- Change affects logic that calls apiService.movePrefix for folder moves
## 2026-01-28 - 1.10.1 - fix(playwright-mcp)
remove Playwright-generated snapshot images to avoid committing autogenerated test artifacts and reduce repository size

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsview",
"version": "1.10.1",
"version": "1.11.1",
"private": false,
"description": "A CLI tool for viewing S3 and MongoDB data with a web UI",
"main": "dist_ts/index.js",
@@ -34,7 +34,7 @@
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.3.0",
"@aws-sdk/client-s3": "^3.975.0",
"@design.estate/dees-catalog": "^3.41.1",
"@design.estate/dees-catalog": "^3.41.2",
"@design.estate/dees-element": "^2.1.6",
"@push.rocks/early": "^4.0.4",
"@push.rocks/npmextra": "^5.3.3",

108
pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
specifier: ^3.975.0
version: 3.975.0
'@design.estate/dees-catalog':
specifier: ^3.41.1
version: 3.41.1(@tiptap/pm@2.27.2)
specifier: ^3.41.2
version: 3.41.2(@tiptap/pm@2.27.2)
'@design.estate/dees-element':
specifier: ^2.1.6
version: 2.1.6
@@ -331,8 +331,8 @@ packages:
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.41.1':
resolution: {integrity: sha512-AMD0VNLQEWXYRUYWwjLA8K8KEKAiUO7GiriWQm+ld7cj+LrCMsJO0jjVfOCsd4G7fURMqmab9ereBJyxqjoFgQ==}
'@design.estate/dees-catalog@3.41.2':
resolution: {integrity: sha512-G6b7TbqkEupHwty3q+Y42xbmwkfFBf3S5JBrMLAw1S0kR88ZCio0dOBcvmvQTQ5pQBz6TDRkx1prXpoQbZDt7A==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -660,74 +660,74 @@ packages:
'@mongodb-js/saslprep@1.4.5':
resolution: {integrity: sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==}
'@napi-rs/canvas-android-arm64@0.1.88':
resolution: {integrity: sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ==}
'@napi-rs/canvas-android-arm64@0.1.89':
resolution: {integrity: sha512-CXxQTXsjtQqKGENS8Ejv9pZOFJhOPIl2goenS+aU8dY4DygvkyagDhy/I07D1YLqrDtPvLEX5zZHt8qUdnuIpQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.88':
resolution: {integrity: sha512-Xgywz0dDxOKSgx3eZnK85WgGMmGrQEW7ZLA/E7raZdlEE+xXCozobgqz2ZvYigpB6DJFYkqnwHjqCOTSDGlFdg==}
'@napi-rs/canvas-darwin-arm64@0.1.89':
resolution: {integrity: sha512-k29cR/Zl20WLYM7M8YePevRu2VQRaKcRedYr1V/8FFHkyIQ8kShEV+MPoPGi+znvmd17Eqjy2Pk2F2kpM2umVg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.88':
resolution: {integrity: sha512-Yz4wSCIQOUgNucgk+8NFtQxQxZV5NO8VKRl9ePKE6XoNyNVC8JDqtvhh3b3TPqKK8W5p2EQpAr1rjjm0mfBxdg==}
'@napi-rs/canvas-darwin-x64@0.1.89':
resolution: {integrity: sha512-iUragqhBrA5FqU13pkhYBDbUD1WEAIlT8R2+fj6xHICY2nemzwMUI8OENDhRh7zuL06YDcRwENbjAVxOmaX9jg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.88':
resolution: {integrity: sha512-9gQM2SlTo76hYhxHi2XxWTAqpTOb+JtxMPEIr+H5nAhHhyEtNmTSDRtz93SP7mGd2G3Ojf2oF5tP9OdgtgXyKg==}
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.89':
resolution: {integrity: sha512-y3SM9sfDWasY58ftoaI09YBFm35Ig8tosZqgahLJ2WGqawCusGNPV9P0/4PsrLOCZqGg629WxexQMY25n7zcvA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.88':
resolution: {integrity: sha512-7qgaOBMXuVRk9Fzztzr3BchQKXDxGbY+nwsovD3I/Sx81e+sX0ReEDYHTItNb0Je4NHbAl7D0MKyd4SvUc04sg==}
'@napi-rs/canvas-linux-arm64-gnu@0.1.89':
resolution: {integrity: sha512-NEoF9y8xq5fX8HG8aZunBom1ILdTwt7ayBzSBIwrmitk7snj4W6Fz/yN/ZOmlM1iyzHDNX5Xn0n+VgWCF8BEdA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.88':
resolution: {integrity: sha512-kYyNrUsHLkoGHBc77u4Unh067GrfiCUMbGHC2+OTxbeWfZkPt2o32UOQkhnSswKd9Fko/wSqqGkY956bIUzruA==}
'@napi-rs/canvas-linux-arm64-musl@0.1.89':
resolution: {integrity: sha512-UQQkIEzV12/l60j1ziMjZ+mtodICNUbrd205uAhbyTw0t60CrC/EsKb5/aJWGq1wM0agvcgZV72JJCKfLS6+4w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.88':
resolution: {integrity: sha512-HVuH7QgzB0yavYdNZDRyAsn/ejoXB0hn8twwFnOqUbCCdkV+REna7RXjSR7+PdfW0qMQ2YYWsLvVBT5iL/mGpw==}
'@napi-rs/canvas-linux-riscv64-gnu@0.1.89':
resolution: {integrity: sha512-1/VmEoFaIO6ONeeEMGoWF17wOYZOl5hxDC1ios2Bkz/oQjbJJ8DY/X22vWTmvuUKWWhBVlo63pxLGZbjJU/heA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.88':
resolution: {integrity: sha512-hvcvKIcPEQrvvJtJnwD35B3qk6umFJ8dFIr8bSymfrSMem0EQsfn1ztys8ETIFndTwdNWJKWluvxztA41ivsEw==}
'@napi-rs/canvas-linux-x64-gnu@0.1.89':
resolution: {integrity: sha512-ebLuqkCuaPIkKgKH9q4+pqWi1tkPOfiTk5PM1LKR1tB9iO9sFNVSIgwEp+SJreTSbA2DK5rW8lQXiN78SjtcvA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.88':
resolution: {integrity: sha512-eSMpGYY2xnZSQ6UxYJ6plDboxq4KeJ4zT5HaVkUnbObNN6DlbJe0Mclh3wifAmquXfrlgTZt6zhHsUgz++AK6g==}
'@napi-rs/canvas-linux-x64-musl@0.1.89':
resolution: {integrity: sha512-w+5qxHzplvA4BkHhCaizNMLLXiI+CfP84YhpHm/PqMub4u8J0uOAv+aaGv40rYEYra5hHRWr9LUd6cfW32o9/A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/canvas-win32-arm64-msvc@0.1.88':
resolution: {integrity: sha512-qcIFfEgHrchyYqRrxsCeTQgpJZ/GqHiqPcU/Fvw/ARVlQeDX1VyFH+X+0gCR2tca6UJrq96vnW+5o7buCq+erA==}
'@napi-rs/canvas-win32-arm64-msvc@0.1.89':
resolution: {integrity: sha512-DmyXa5lJHcjOsDC78BM3bnEECqbK3xASVMrKfvtT/7S7Z8NGQOugvu+L7b41V6cexCd34mBWgMOsjoEBceeB1Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.88':
resolution: {integrity: sha512-ROVqbfS4QyZxYkqmaIBBpbz/BQvAR+05FXM5PAtTYVc0uyY8Y4BHJSMdGAaMf6TdIVRsQsiq+FG/dH9XhvWCFQ==}
'@napi-rs/canvas-win32-x64-msvc@0.1.89':
resolution: {integrity: sha512-WMej0LZrIqIncQcx0JHaMXlnAG7sncwJh7obs/GBgp0xF9qABjwoRwIooMWCZkSansapKGNUHhamY6qEnFN7gA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.88':
resolution: {integrity: sha512-/p08f93LEbsL5mDZFQ3DBxcPv/I4QG9EDYRRq1WNlCOXVfAHBTHMSVMwxlqG/AtnSfUr9+vgfN7MKiyDo0+Weg==}
'@napi-rs/canvas@0.1.89':
resolution: {integrity: sha512-7GjmkMirJHejeALCqUnZY3QwID7bbumOiLrqq2LKgxrdjdmxWQBTc6rcASa2u8wuWrH7qo4/4n/VNrOwCoKlKg==}
engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.0.7':
@@ -4066,7 +4066,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260123.0
'@design.estate/dees-catalog': 3.41.1(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.41.2(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
@@ -4694,7 +4694,7 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.41.1(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.41.2(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.3.8
'@design.estate/dees-element': 2.1.6
@@ -5203,52 +5203,52 @@ snapshots:
dependencies:
sparse-bitfield: 3.0.3
'@napi-rs/canvas-android-arm64@0.1.88':
'@napi-rs/canvas-android-arm64@0.1.89':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.88':
'@napi-rs/canvas-darwin-arm64@0.1.89':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.88':
'@napi-rs/canvas-darwin-x64@0.1.89':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.88':
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.89':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.88':
'@napi-rs/canvas-linux-arm64-gnu@0.1.89':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.88':
'@napi-rs/canvas-linux-arm64-musl@0.1.89':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.88':
'@napi-rs/canvas-linux-riscv64-gnu@0.1.89':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.88':
'@napi-rs/canvas-linux-x64-gnu@0.1.89':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.88':
'@napi-rs/canvas-linux-x64-musl@0.1.89':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.88':
'@napi-rs/canvas-win32-arm64-msvc@0.1.89':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.88':
'@napi-rs/canvas-win32-x64-msvc@0.1.89':
optional: true
'@napi-rs/canvas@0.1.88':
'@napi-rs/canvas@0.1.89':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.88
'@napi-rs/canvas-darwin-arm64': 0.1.88
'@napi-rs/canvas-darwin-x64': 0.1.88
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.88
'@napi-rs/canvas-linux-arm64-gnu': 0.1.88
'@napi-rs/canvas-linux-arm64-musl': 0.1.88
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.88
'@napi-rs/canvas-linux-x64-gnu': 0.1.88
'@napi-rs/canvas-linux-x64-musl': 0.1.88
'@napi-rs/canvas-win32-arm64-msvc': 0.1.88
'@napi-rs/canvas-win32-x64-msvc': 0.1.88
'@napi-rs/canvas-android-arm64': 0.1.89
'@napi-rs/canvas-darwin-arm64': 0.1.89
'@napi-rs/canvas-darwin-x64': 0.1.89
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.89
'@napi-rs/canvas-linux-arm64-gnu': 0.1.89
'@napi-rs/canvas-linux-arm64-musl': 0.1.89
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.89
'@napi-rs/canvas-linux-x64-gnu': 0.1.89
'@napi-rs/canvas-linux-x64-musl': 0.1.89
'@napi-rs/canvas-win32-arm64-msvc': 0.1.89
'@napi-rs/canvas-win32-x64-msvc': 0.1.89
optional: true
'@napi-rs/wasm-runtime@1.0.7':
@@ -8747,7 +8747,7 @@ snapshots:
pdfjs-dist@4.10.38:
optionalDependencies:
'@napi-rs/canvas': 0.1.88
'@napi-rs/canvas': 0.1.89
peek-readable@5.4.2: {}

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsview',
version: '1.10.1',
version: '1.11.1',
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.10.1',
version: '1.11.1',
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
}

View File

@@ -25,7 +25,7 @@ export class TsviewMongoBrowser extends DeesElement {
private accessor stats: ICollectionStats | null = null;
@state()
private accessor editorWidth: number = 400;
private accessor editorWidth: number = 700;
@state()
private accessor isResizingEditor: boolean = false;
@@ -117,7 +117,7 @@ export class TsviewMongoBrowser extends DeesElement {
.content {
flex: 1;
display: grid;
grid-template-columns: 1fr 4px var(--editor-width, 400px);
grid-template-columns: 1fr 4px var(--editor-width, 700px);
gap: 0;
overflow: hidden;
}
@@ -305,7 +305,7 @@ export class TsviewMongoBrowser extends DeesElement {
const contentEl = this.shadowRoot?.querySelector('.content');
if (!contentEl) return;
const containerRect = contentEl.getBoundingClientRect();
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 300), 700);
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 1000);
this.editorWidth = newWidth;
};

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...',
@@ -1239,7 +1265,7 @@ export class TsviewS3Columns extends DeesElement {
try {
const sourceName = getFileName(this.moveSource.key);
const destKey = this.moveDestination + sourceName;
const destKey = this.moveDestination + sourceName + (this.moveSource.isFolder ? '/' : '');
let result: { success: boolean; error?: string };
if (this.moveSource.isFolder) {
@@ -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()}
`;
}
}