tabby-sftp-ui 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -126,8 +126,13 @@ Then restart Tabby.
126
126
 
127
127
  ### Changelog
128
128
 
129
- - See [`CHANGELOG.md`](CHANGELOG.md)
130
-
129
+ - **0.2.4**
130
+ - Fix: OS drag-and-drop from Explorer
131
+ - Fix: Multiple files drag-and-drop between panes works again.
132
+ - **0.2.3**
133
+ - Fix: Remote folders drag-and-drop (Remote → Local) works again.
134
+ - Feature: Replace confirmation is now symmetric (both Local → Remote and Remote → Local).
135
+ - UI: Added “Go up” row to the Local pane.
131
136
  - **0.2.2**
132
137
  - Integrate Tabby’s native `startUploadFromDragEvent` for more reliable OS drag-and-drop (files & folders).
133
138
  - Refine Delete key handling so it works normally inside input dialogs while still triggering delete in lists.
package/dist/index.js CHANGED
@@ -309,6 +309,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
309
309
  this.deleteConfirmText = '';
310
310
  this.pendingLocalDelete = [];
311
311
  this.pendingRemoteDelete = [];
312
+ this.replaceConfirmVisible = false;
313
+ this.replaceConfirmText = '';
314
+ this.replaceConfirmResolve = null;
312
315
  this.inputDialogVisible = false;
313
316
  this.inputDialogTitle = '';
314
317
  this.inputDialogPlaceholder = '';
@@ -593,12 +596,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
593
596
  const sources = this.selectedLocal.includes(e) && this.selectedLocal.length ? this.selectedLocal : [e];
594
597
  const movePayload = sources.map(x => x.fullPath);
595
598
  ev.dataTransfer?.setData('application/x-tabby-sftp-ui-local-move', JSON.stringify(movePayload));
596
- // Existing cross-device drag (local -> remote) only for files
597
- if (!e.isDirectory) {
598
- const payload = { kind: 'local-file', fullPath: e.fullPath, name: e.name };
599
- ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
600
- ev.dataTransfer?.setData('text/plain', e.fullPath);
601
- }
599
+ // Cross-device drag (local -> remote) supports multi-select
600
+ const payload = {
601
+ kind: 'local-paths',
602
+ paths: sources.map(x => ({ fullPath: x.fullPath, name: x.name, isDirectory: x.isDirectory })),
603
+ };
604
+ ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
605
+ ev.dataTransfer?.setData('text/plain', e.fullPath);
602
606
  ev.dataTransfer?.setDragImage?.(ev.target ?? document.body, 0, 0);
603
607
  }
604
608
  onDragStartRemote(ev, item) {
@@ -608,18 +612,19 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
608
612
  const sources = this.selectedRemote.includes(item) && this.selectedRemote.length ? this.selectedRemote : [item];
609
613
  const movePayload = sources.map(x => x.fullPath);
610
614
  ev.dataTransfer?.setData('application/x-tabby-sftp-ui-remote-move', JSON.stringify(movePayload));
611
- // Existing cross-device drag (remote -> local) only for files
612
- if (!item.isDirectory) {
613
- const payload = {
614
- kind: 'remote-file',
615
- remotePath: item.fullPath,
616
- name: item.name,
617
- size: item.size,
618
- mode: item.mode,
619
- };
620
- ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
621
- ev.dataTransfer?.setData('text/plain', item.fullPath);
622
- }
615
+ // Cross-device drag (remote -> local) supports multi-select
616
+ const payload = {
617
+ kind: 'remote-paths',
618
+ paths: sources.map(x => ({
619
+ remotePath: x.fullPath,
620
+ name: x.name,
621
+ isDirectory: x.isDirectory,
622
+ size: x.size,
623
+ mode: x.mode,
624
+ })),
625
+ };
626
+ ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
627
+ ev.dataTransfer?.setData('text/plain', item.fullPath);
623
628
  ev.dataTransfer?.setDragImage?.(ev.target ?? document.body, 0, 0);
624
629
  }
625
630
  async onDropOnRemote(ev) {
@@ -636,6 +641,16 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
636
641
  if (osPaths.length) {
637
642
  try {
638
643
  for (const p of osPaths) {
644
+ const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(p);
645
+ const existing = this.remoteEntries.find(e => e.name === baseName);
646
+ if (existing) {
647
+ const ok = await this.showReplaceConfirm(`Replace existing "${baseName}" on remote?`);
648
+ if (!ok) {
649
+ continue;
650
+ }
651
+ const remoteTarget = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, baseName);
652
+ await this.deleteRemotePathRecursive(remoteTarget);
653
+ }
639
654
  await this.uploadLocalPathToRemote(this.remotePath, p);
640
655
  }
641
656
  await this.refreshRemote();
@@ -668,15 +683,40 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
668
683
  catch {
669
684
  return;
670
685
  }
671
- if (payload.kind !== 'local-file') {
672
- return;
673
- }
674
686
  try {
675
- const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
676
- const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
677
- this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
678
- await this.sftpSession.upload(targetRemotePath, upload);
679
- await this.refreshRemote();
687
+ if (payload.kind === 'local-file') {
688
+ const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
689
+ const existsOnRemote = this.remoteEntries.some(e => e.name === payload.name);
690
+ if (existsOnRemote) {
691
+ const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}" on remote?`);
692
+ if (!ok) {
693
+ return;
694
+ }
695
+ await this.deleteRemotePathRecursive(targetRemotePath);
696
+ }
697
+ const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
698
+ this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
699
+ await this.sftpSession.upload(targetRemotePath, upload);
700
+ await this.refreshRemote();
701
+ return;
702
+ }
703
+ if (payload.kind === 'local-paths') {
704
+ for (const p of payload.paths) {
705
+ const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, p.name);
706
+ const existsOnRemote = this.remoteEntries.some(e => e.name === p.name);
707
+ if (existsOnRemote) {
708
+ const ok = await this.showReplaceConfirm(`Replace existing "${p.name}" on remote?`);
709
+ if (!ok) {
710
+ continue;
711
+ }
712
+ await this.deleteRemotePathRecursive(targetRemotePath);
713
+ }
714
+ // uploadLocalPathToRemote handles both files and directories
715
+ await this.uploadLocalPathToRemote(this.remotePath, p.fullPath);
716
+ }
717
+ await this.refreshRemote();
718
+ return;
719
+ }
680
720
  }
681
721
  catch (e) {
682
722
  console.error('[SFTP-UI] Upload failed', e);
@@ -685,11 +725,103 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
685
725
  async onDropOnLocal(ev) {
686
726
  ev.preventDefault();
687
727
  this.localDropActive = false;
728
+ // 1) Tabby's internal drag (remote -> local download)
729
+ const rawInternal = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
730
+ if (rawInternal) {
731
+ let payload;
732
+ try {
733
+ payload = JSON.parse(rawInternal);
734
+ }
735
+ catch {
736
+ payload = null;
737
+ }
738
+ if (payload && payload.kind === 'remote-file') {
739
+ try {
740
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
741
+ if (!this.sftpSession) {
742
+ throw new Error('Not connected');
743
+ }
744
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
745
+ const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}"?`);
746
+ if (!ok) {
747
+ return;
748
+ }
749
+ }
750
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
751
+ this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
752
+ await this.sftpSession.download(payload.remotePath, dl);
753
+ await this.refreshLocal();
754
+ }
755
+ catch (e) {
756
+ console.error('[SFTP-UI] Download failed', e);
757
+ }
758
+ return;
759
+ }
760
+ if (payload && payload.kind === 'remote-dir') {
761
+ try {
762
+ if (!this.sftpSession) {
763
+ throw new Error('Not connected');
764
+ }
765
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
766
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
767
+ const ok = await this.showReplaceConfirm(`Replace existing folder "${payload.name}"?`);
768
+ if (!ok) {
769
+ return;
770
+ }
771
+ await this.deleteLocalPathRecursive(targetLocalPath);
772
+ }
773
+ await this.downloadRemoteDirectoryRecursive(payload.remotePath, targetLocalPath);
774
+ await this.refreshLocal();
775
+ }
776
+ catch (e) {
777
+ console.error('[SFTP-UI] Download directory failed', e);
778
+ }
779
+ return;
780
+ }
781
+ if (payload && payload.kind === 'remote-paths') {
782
+ try {
783
+ if (!this.sftpSession) {
784
+ throw new Error('Not connected');
785
+ }
786
+ for (const it of payload.paths) {
787
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, it.name);
788
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
789
+ const ok = await this.showReplaceConfirm(it.isDirectory ? `Replace existing folder "${it.name}"?` : `Replace existing "${it.name}"?`);
790
+ if (!ok) {
791
+ continue;
792
+ }
793
+ await this.deleteLocalPathRecursive(targetLocalPath);
794
+ }
795
+ if (it.isDirectory) {
796
+ await this.downloadRemoteDirectoryRecursive(it.remotePath, targetLocalPath);
797
+ }
798
+ else {
799
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, it.mode ?? 0o644, it.size ?? 0);
800
+ this.trackTransfer(dl, 'download', it.remotePath, targetLocalPath);
801
+ await this.sftpSession.download(it.remotePath, dl);
802
+ }
803
+ }
804
+ await this.refreshLocal();
805
+ }
806
+ catch (e) {
807
+ console.error('[SFTP-UI] Download paths failed', e);
808
+ }
809
+ return;
810
+ }
811
+ }
688
812
  // Drag & drop from OS file manager into the local pane (copy into current local folder)
689
813
  const osPaths = this.getDroppedOsPaths(ev);
690
814
  if (osPaths.length) {
691
815
  try {
692
816
  for (const p of osPaths) {
817
+ const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(p);
818
+ const destPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, baseName);
819
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(destPath)) {
820
+ const ok = await this.showReplaceConfirm(`Replace existing "${baseName}"?`);
821
+ if (!ok) {
822
+ continue;
823
+ }
824
+ }
693
825
  await this.copyLocalPathIntoLocalDir(this.localPath, p);
694
826
  }
695
827
  await this.refreshLocal();
@@ -722,17 +854,41 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
722
854
  catch {
723
855
  return;
724
856
  }
725
- if (payload.kind !== 'remote-file') {
857
+ if (payload.kind !== 'remote-file' && payload.kind !== 'remote-dir') {
726
858
  return;
727
859
  }
728
860
  try {
729
- const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
861
+ if (payload.kind === 'remote-file') {
862
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
863
+ if (!this.sftpSession) {
864
+ throw new Error('Not connected');
865
+ }
866
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
867
+ const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}"?`);
868
+ if (!ok) {
869
+ return;
870
+ }
871
+ await this.deleteLocalPathRecursive(targetLocalPath);
872
+ }
873
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
874
+ this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
875
+ await this.sftpSession.download(payload.remotePath, dl);
876
+ await this.refreshLocal();
877
+ return;
878
+ }
879
+ // remote-dir -> local-dir (recursive download)
730
880
  if (!this.sftpSession) {
731
881
  throw new Error('Not connected');
732
882
  }
733
- const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
734
- this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
735
- await this.sftpSession.download(payload.remotePath, dl);
883
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
884
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
885
+ const ok = await this.showReplaceConfirm(`Replace existing folder "${payload.name}"?`);
886
+ if (!ok) {
887
+ return;
888
+ }
889
+ await this.deleteLocalPathRecursive(targetLocalPath);
890
+ }
891
+ await this.downloadRemoteDirectoryRecursive(payload.remotePath, targetLocalPath);
736
892
  await this.refreshLocal();
737
893
  }
738
894
  catch (e) {
@@ -837,11 +993,40 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
837
993
  }
838
994
  }
839
995
  }
996
+ async downloadRemoteDirectoryRecursive(remoteDir, localDir) {
997
+ if (!this.sftpSession) {
998
+ return;
999
+ }
1000
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(localDir, { recursive: true });
1001
+ const entries = await this.sftpSession.readdir(remoteDir).catch(() => null);
1002
+ if (!entries) {
1003
+ return;
1004
+ }
1005
+ for (const e of entries) {
1006
+ const targetLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, e.name);
1007
+ if (e.isDirectory) {
1008
+ await this.downloadRemoteDirectoryRecursive(e.fullPath, targetLocal);
1009
+ }
1010
+ else {
1011
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocal, e.mode, e.size);
1012
+ this.trackTransfer(dl, 'download', e.fullPath, targetLocal);
1013
+ await this.sftpSession.download(e.fullPath, dl);
1014
+ }
1015
+ }
1016
+ }
840
1017
  getDroppedOsPaths(ev) {
841
1018
  const dt = ev.dataTransfer;
842
1019
  if (!dt) {
843
1020
  return [];
844
1021
  }
1022
+ const isWin = os__WEBPACK_IMPORTED_MODULE_1__.platform() === 'win32';
1023
+ const isLocalPath = (p) => {
1024
+ if (isWin) {
1025
+ // Accept both `C:\...` and `C:/...` (some drag sources provide forward slashes)
1026
+ return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith('\\\\');
1027
+ }
1028
+ return p.startsWith('/');
1029
+ };
845
1030
  // 1) Electron-style File.path
846
1031
  const filePaths = Array.from(dt.files ?? [])
847
1032
  .map(f => f.path)
@@ -866,7 +1051,14 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
866
1051
  }
867
1052
  return x;
868
1053
  })
869
- .filter(x => x && (x.includes(':\\') || x.startsWith('/') || x.startsWith('\\\\')));
1054
+ .map(x => {
1055
+ // Some sources may produce `/C:/Users/...`
1056
+ if (isWin && /^\/[A-Za-z]:[\\/]/.test(x)) {
1057
+ return x.slice(1);
1058
+ }
1059
+ return x;
1060
+ })
1061
+ .filter(x => x && isLocalPath(x));
870
1062
  if (uris.length) {
871
1063
  return uris;
872
1064
  }
@@ -885,7 +1077,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
885
1077
  }
886
1078
  return x;
887
1079
  })
888
- .filter(x => x.includes(':\\') || x.startsWith('/') || x.startsWith('\\\\'));
1080
+ .map(x => {
1081
+ if (isWin && /^\/[A-Za-z]:[\\/]/.test(x)) {
1082
+ return x.slice(1);
1083
+ }
1084
+ return x;
1085
+ })
1086
+ .filter(x => x && isLocalPath(x));
889
1087
  return textPaths;
890
1088
  }
891
1089
  getFilteredLocalEntries() {
@@ -1800,6 +1998,11 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1800
1998
  this.cancelDelete();
1801
1999
  return;
1802
2000
  }
2001
+ if (this.replaceConfirmVisible) {
2002
+ event.preventDefault();
2003
+ this.cancelReplace();
2004
+ return;
2005
+ }
1803
2006
  }
1804
2007
  if (event.key === 'Delete' || event.key === 'Backspace') {
1805
2008
  // Don't intercept Delete/Backspace while typing in inputs
@@ -1881,6 +2084,37 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1881
2084
  this.pendingLocalDelete = [];
1882
2085
  this.pendingRemoteDelete = [];
1883
2086
  }
2087
+ async showReplaceConfirm(text) {
2088
+ if (this.replaceConfirmVisible) {
2089
+ // Prevent stacking multiple confirmations; choose the latest replacement intent.
2090
+ return false;
2091
+ }
2092
+ this.replaceConfirmText = text;
2093
+ this.replaceConfirmVisible = true;
2094
+ return new Promise(resolve => {
2095
+ this.replaceConfirmResolve = resolve;
2096
+ });
2097
+ }
2098
+ async confirmReplace() {
2099
+ if (!this.replaceConfirmVisible) {
2100
+ return;
2101
+ }
2102
+ this.replaceConfirmVisible = false;
2103
+ const resolve = this.replaceConfirmResolve;
2104
+ this.replaceConfirmResolve = null;
2105
+ this.replaceConfirmText = '';
2106
+ resolve?.(true);
2107
+ }
2108
+ cancelReplace() {
2109
+ if (!this.replaceConfirmVisible) {
2110
+ return;
2111
+ }
2112
+ this.replaceConfirmVisible = false;
2113
+ const resolve = this.replaceConfirmResolve;
2114
+ this.replaceConfirmResolve = null;
2115
+ this.replaceConfirmText = '';
2116
+ resolve?.(false);
2117
+ }
1884
2118
  async deleteLocalEntry(entry) {
1885
2119
  await this.deleteLocalPathRecursive(entry.fullPath);
1886
2120
  }
@@ -2188,6 +2422,16 @@ SftpManagerTabComponent = __decorate([
2188
2422
  <span class="size sortable" (click)="setLocalSort('size')">Size</span>
2189
2423
  <span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
2190
2424
  </div>
2425
+ <div
2426
+ class="entry"
2427
+ *ngIf="canLocalUp()"
2428
+ (dblclick)="localUp()"
2429
+ >
2430
+ <span class="icon">⬆</span>
2431
+ <span class="name">Go up</span>
2432
+ <span class="size"></span>
2433
+ <span class="date"></span>
2434
+ </div>
2191
2435
  <div
2192
2436
  class="entry"
2193
2437
  *ngFor="let e of getFilteredLocalEntries()"
@@ -2366,6 +2610,16 @@ SftpManagerTabComponent = __decorate([
2366
2610
  </div>
2367
2611
  </div>
2368
2612
 
2613
+ <div class="delete-overlay" *ngIf="replaceConfirmVisible">
2614
+ <div class="delete-dialog">
2615
+ <div class="delete-text">{{ replaceConfirmText }}</div>
2616
+ <div class="delete-buttons">
2617
+ <button class="danger" (click)="confirmReplace()">Replace</button>
2618
+ <button (click)="cancelReplace()">Cancel</button>
2619
+ </div>
2620
+ </div>
2621
+ </div>
2622
+
2369
2623
  <div class="delete-overlay" *ngIf="inputDialogVisible">
2370
2624
  <div class="delete-dialog" (click)="$event.stopPropagation()">
2371
2625
  <div class="delete-text">{{ inputDialogTitle }}</div>