feat(s3): add rename support for files and folders in S3 UI columns and keys
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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
108
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsview',
|
||||
version: '1.10.2',
|
||||
version: '1.11.0',
|
||||
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
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsview',
|
||||
version: '1.10.2',
|
||||
version: '1.11.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user