dsh-better-workspace 0.11.3 → 0.11.5
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/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +192 -27
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-better-workspace",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "侧边栏工作区层级树:名称中的 / 即虚拟分组(web/前端 · web/后端),添加工作区的目录流附所属分组弹窗,重命名即时重排层级,可新建空分组。Hierarchy tree for the DSH sidebar workspace list.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-better-workspace",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "DSH web plugin: a hierarchical workspace tree for the sidebar. Workspace titles containing \"/\" group into virtual folders (web/前端 · web/后端); the add-workspace directory flow gains a parent-group popup; renames re-derive the tree; a new-folder button creates empty levels.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/client.js
CHANGED
|
@@ -127,6 +127,9 @@ window.__ModuleLoader__.load({
|
|
|
127
127
|
'browse.showHidden': '显示隐藏文件',
|
|
128
128
|
'browse.editPath': '编辑路径',
|
|
129
129
|
'browse.select': '选择此文件夹',
|
|
130
|
+
'browse.enter': '进入',
|
|
131
|
+
'browse.drives': '盘符',
|
|
132
|
+
'browse.selectNamed': '选择「{name}」',
|
|
130
133
|
'error.title': '出错了',
|
|
131
134
|
'cancel': '取消',
|
|
132
135
|
'create': '创建',
|
|
@@ -246,6 +249,9 @@ window.__ModuleLoader__.load({
|
|
|
246
249
|
'browse.showHidden': 'Show hidden files',
|
|
247
250
|
'browse.editPath': 'Edit path',
|
|
248
251
|
'browse.select': 'Use this folder',
|
|
252
|
+
'browse.enter': 'Open',
|
|
253
|
+
'browse.drives': 'Drives',
|
|
254
|
+
'browse.selectNamed': 'Use "{name}"',
|
|
249
255
|
'error.title': 'Something went wrong',
|
|
250
256
|
'cancel': 'Cancel',
|
|
251
257
|
'create': 'Create',
|
|
@@ -373,6 +379,9 @@ window.__ModuleLoader__.load({
|
|
|
373
379
|
'browse.showHidden': 'إظهار الملفات المخفية',
|
|
374
380
|
'browse.editPath': 'تعديل المسار',
|
|
375
381
|
'browse.select': 'استخدام هذا المجلد',
|
|
382
|
+
'browse.enter': 'فتح',
|
|
383
|
+
'browse.drives': 'الأقراص',
|
|
384
|
+
'browse.selectNamed': 'استخدام "{name}"',
|
|
376
385
|
'error.title': 'حدث خطأ ما',
|
|
377
386
|
'cancel': 'إلغاء',
|
|
378
387
|
'create': 'إنشاء',
|
|
@@ -492,6 +501,9 @@ window.__ModuleLoader__.load({
|
|
|
492
501
|
'browse.showHidden': 'Versteckte Dateien anzeigen',
|
|
493
502
|
'browse.editPath': 'Pfad bearbeiten',
|
|
494
503
|
'browse.select': 'Diesen Ordner verwenden',
|
|
504
|
+
'browse.enter': 'Öffnen',
|
|
505
|
+
'browse.drives': 'Laufwerke',
|
|
506
|
+
'browse.selectNamed': '"{name}" verwenden',
|
|
495
507
|
'error.title': 'Etwas ist schiefgelaufen',
|
|
496
508
|
'cancel': 'Abbrechen',
|
|
497
509
|
'create': 'Erstellen',
|
|
@@ -611,6 +623,9 @@ window.__ModuleLoader__.load({
|
|
|
611
623
|
'browse.showHidden': 'Afficher les fichiers cachés',
|
|
612
624
|
'browse.editPath': 'Modifier le chemin',
|
|
613
625
|
'browse.select': 'Utiliser ce dossier',
|
|
626
|
+
'browse.enter': 'Ouvrir',
|
|
627
|
+
'browse.drives': 'Lecteurs',
|
|
628
|
+
'browse.selectNamed': 'Utiliser « {name} »',
|
|
614
629
|
'error.title': 'Une erreur est survenue',
|
|
615
630
|
'cancel': 'Annuler',
|
|
616
631
|
'create': 'Créer',
|
|
@@ -730,6 +745,9 @@ window.__ModuleLoader__.load({
|
|
|
730
745
|
'browse.showHidden': 'छिपी फ़ाइलें दिखाएँ',
|
|
731
746
|
'browse.editPath': 'पथ संपादित करें',
|
|
732
747
|
'browse.select': 'यह फ़ोल्डर चुनें',
|
|
748
|
+
'browse.enter': 'खोलें',
|
|
749
|
+
'browse.drives': 'ड्राइव',
|
|
750
|
+
'browse.selectNamed': '"{name}" चुनें',
|
|
733
751
|
'error.title': 'कुछ गड़बड़ हो गई',
|
|
734
752
|
'cancel': 'रद्द करें',
|
|
735
753
|
'create': 'बनाएँ',
|
|
@@ -849,6 +867,9 @@ window.__ModuleLoader__.load({
|
|
|
849
867
|
'browse.showHidden': 'Tampilkan file tersembunyi',
|
|
850
868
|
'browse.editPath': 'Edit jalur',
|
|
851
869
|
'browse.select': 'Gunakan folder ini',
|
|
870
|
+
'browse.enter': 'Buka',
|
|
871
|
+
'browse.drives': 'Drive',
|
|
872
|
+
'browse.selectNamed': 'Gunakan "{name}"',
|
|
852
873
|
'error.title': 'Terjadi kesalahan',
|
|
853
874
|
'cancel': 'Batal',
|
|
854
875
|
'create': 'Buat',
|
|
@@ -968,6 +989,9 @@ window.__ModuleLoader__.load({
|
|
|
968
989
|
'browse.showHidden': 'Mostra file nascosti',
|
|
969
990
|
'browse.editPath': 'Modifica percorso',
|
|
970
991
|
'browse.select': 'Usa questa cartella',
|
|
992
|
+
'browse.enter': 'Apri',
|
|
993
|
+
'browse.drives': 'Unità',
|
|
994
|
+
'browse.selectNamed': 'Usa "{name}"',
|
|
971
995
|
'error.title': 'Si è verificato un errore',
|
|
972
996
|
'cancel': 'Annulla',
|
|
973
997
|
'create': 'Crea',
|
|
@@ -1087,6 +1111,9 @@ window.__ModuleLoader__.load({
|
|
|
1087
1111
|
'browse.showHidden': '隠しファイルを表示',
|
|
1088
1112
|
'browse.editPath': 'パスを編集',
|
|
1089
1113
|
'browse.select': 'このフォルダーを選択',
|
|
1114
|
+
'browse.enter': '開く',
|
|
1115
|
+
'browse.drives': 'ドライブ',
|
|
1116
|
+
'browse.selectNamed': '「{name}」を選択',
|
|
1090
1117
|
'error.title': 'エラーが発生しました',
|
|
1091
1118
|
'cancel': 'キャンセル',
|
|
1092
1119
|
'create': '作成',
|
|
@@ -1206,6 +1233,9 @@ window.__ModuleLoader__.load({
|
|
|
1206
1233
|
'browse.showHidden': '숨김 파일 표시',
|
|
1207
1234
|
'browse.editPath': '경로 편집',
|
|
1208
1235
|
'browse.select': '이 폴더 선택',
|
|
1236
|
+
'browse.enter': '열기',
|
|
1237
|
+
'browse.drives': '드라이브',
|
|
1238
|
+
'browse.selectNamed': '"{name}" 선택',
|
|
1209
1239
|
'error.title': '문제가 발생했습니다',
|
|
1210
1240
|
'cancel': '취소',
|
|
1211
1241
|
'create': '만들기',
|
|
@@ -1325,6 +1355,9 @@ window.__ModuleLoader__.load({
|
|
|
1325
1355
|
'browse.showHidden': 'Verborgen bestanden tonen',
|
|
1326
1356
|
'browse.editPath': 'Pad bewerken',
|
|
1327
1357
|
'browse.select': 'Deze map gebruiken',
|
|
1358
|
+
'browse.enter': 'Openen',
|
|
1359
|
+
'browse.drives': 'Stations',
|
|
1360
|
+
'browse.selectNamed': '"{name}" gebruiken',
|
|
1328
1361
|
'error.title': 'Er is iets misgegaan',
|
|
1329
1362
|
'cancel': 'Annuleren',
|
|
1330
1363
|
'create': 'Aanmaken',
|
|
@@ -1444,6 +1477,9 @@ window.__ModuleLoader__.load({
|
|
|
1444
1477
|
'browse.showHidden': 'Pokaż ukryte pliki',
|
|
1445
1478
|
'browse.editPath': 'Edytuj ścieżkę',
|
|
1446
1479
|
'browse.select': 'Użyj tego folderu',
|
|
1480
|
+
'browse.enter': 'Otwórz',
|
|
1481
|
+
'browse.drives': 'Dyski',
|
|
1482
|
+
'browse.selectNamed': 'Użyj "{name}"',
|
|
1447
1483
|
'error.title': 'Coś poszło nie tak',
|
|
1448
1484
|
'cancel': 'Anuluj',
|
|
1449
1485
|
'create': 'Utwórz',
|
|
@@ -1563,6 +1599,9 @@ window.__ModuleLoader__.load({
|
|
|
1563
1599
|
'browse.showHidden': 'Mostrar arquivos ocultos',
|
|
1564
1600
|
'browse.editPath': 'Editar caminho',
|
|
1565
1601
|
'browse.select': 'Usar esta pasta',
|
|
1602
|
+
'browse.enter': 'Abrir',
|
|
1603
|
+
'browse.drives': 'Unidades',
|
|
1604
|
+
'browse.selectNamed': 'Usar "{name}"',
|
|
1566
1605
|
'error.title': 'Algo deu errado',
|
|
1567
1606
|
'cancel': 'Cancelar',
|
|
1568
1607
|
'create': 'Criar',
|
|
@@ -1682,6 +1721,9 @@ window.__ModuleLoader__.load({
|
|
|
1682
1721
|
'browse.showHidden': 'Показывать скрытые файлы',
|
|
1683
1722
|
'browse.editPath': 'Изменить путь',
|
|
1684
1723
|
'browse.select': 'Выбрать эту папку',
|
|
1724
|
+
'browse.enter': 'Открыть',
|
|
1725
|
+
'browse.drives': 'Диски',
|
|
1726
|
+
'browse.selectNamed': 'Выбрать «{name}»',
|
|
1685
1727
|
'error.title': 'Что-то пошло не так',
|
|
1686
1728
|
'cancel': 'Отмена',
|
|
1687
1729
|
'create': 'Создать',
|
|
@@ -1801,6 +1843,9 @@ window.__ModuleLoader__.load({
|
|
|
1801
1843
|
'browse.showHidden': 'Visa dolda filer',
|
|
1802
1844
|
'browse.editPath': 'Redigera sökväg',
|
|
1803
1845
|
'browse.select': 'Använd den här mappen',
|
|
1846
|
+
'browse.enter': 'Öppna',
|
|
1847
|
+
'browse.drives': 'Enheter',
|
|
1848
|
+
'browse.selectNamed': 'Använd "{name}"',
|
|
1804
1849
|
'error.title': 'Något gick fel',
|
|
1805
1850
|
'cancel': 'Avbryt',
|
|
1806
1851
|
'create': 'Skapa',
|
|
@@ -1920,6 +1965,9 @@ window.__ModuleLoader__.load({
|
|
|
1920
1965
|
'browse.showHidden': 'แสดงไฟล์ที่ซ่อนอยู่',
|
|
1921
1966
|
'browse.editPath': 'แก้ไขเส้นทาง',
|
|
1922
1967
|
'browse.select': 'ใช้โฟลเดอร์นี้',
|
|
1968
|
+
'browse.enter': 'เปิด',
|
|
1969
|
+
'browse.drives': 'ไดรฟ์',
|
|
1970
|
+
'browse.selectNamed': 'ใช้ "{name}"',
|
|
1923
1971
|
'error.title': 'เกิดข้อผิดพลาด',
|
|
1924
1972
|
'cancel': 'ยกเลิก',
|
|
1925
1973
|
'create': 'สร้าง',
|
|
@@ -2039,6 +2087,9 @@ window.__ModuleLoader__.load({
|
|
|
2039
2087
|
'browse.showHidden': 'Gizli dosyaları göster',
|
|
2040
2088
|
'browse.editPath': 'Yolu düzenle',
|
|
2041
2089
|
'browse.select': 'Bu klasörü kullan',
|
|
2090
|
+
'browse.enter': 'Aç',
|
|
2091
|
+
'browse.drives': 'Sürücüler',
|
|
2092
|
+
'browse.selectNamed': '"{name}" kullan',
|
|
2042
2093
|
'error.title': 'Bir şeyler ters gitti',
|
|
2043
2094
|
'cancel': 'İptal',
|
|
2044
2095
|
'create': 'Oluştur',
|
|
@@ -2158,6 +2209,9 @@ window.__ModuleLoader__.load({
|
|
|
2158
2209
|
'browse.showHidden': 'Hiện tệp ẩn',
|
|
2159
2210
|
'browse.editPath': 'Sửa đường dẫn',
|
|
2160
2211
|
'browse.select': 'Dùng thư mục này',
|
|
2212
|
+
'browse.enter': 'Mở',
|
|
2213
|
+
'browse.drives': 'Ổ đĩa',
|
|
2214
|
+
'browse.selectNamed': 'Dùng "{name}"',
|
|
2161
2215
|
'error.title': 'Đã xảy ra lỗi',
|
|
2162
2216
|
'cancel': 'Hủy',
|
|
2163
2217
|
'create': 'Tạo',
|
|
@@ -2277,6 +2331,9 @@ window.__ModuleLoader__.load({
|
|
|
2277
2331
|
'browse.showHidden': '顯示隱藏檔案',
|
|
2278
2332
|
'browse.editPath': '編輯路徑',
|
|
2279
2333
|
'browse.select': '揀呢個資料夾',
|
|
2334
|
+
'browse.enter': '入去',
|
|
2335
|
+
'browse.drives': '磁碟',
|
|
2336
|
+
'browse.selectNamed': '揀「{name}」',
|
|
2280
2337
|
'error.title': '出咗錯',
|
|
2281
2338
|
'cancel': '取消',
|
|
2282
2339
|
'create': '建立',
|
|
@@ -2396,6 +2453,9 @@ window.__ModuleLoader__.load({
|
|
|
2396
2453
|
'browse.showHidden': '顯示隱藏檔案',
|
|
2397
2454
|
'browse.editPath': '編輯路徑',
|
|
2398
2455
|
'browse.select': '揀呢個資料夾',
|
|
2456
|
+
'browse.enter': '入去',
|
|
2457
|
+
'browse.drives': '磁碟',
|
|
2458
|
+
'browse.selectNamed': '揀「{name}」',
|
|
2399
2459
|
'error.title': '出咗錯',
|
|
2400
2460
|
'cancel': '取消',
|
|
2401
2461
|
'create': '建立',
|
|
@@ -2515,6 +2575,9 @@ window.__ModuleLoader__.load({
|
|
|
2515
2575
|
'browse.showHidden': '顯示隱藏檔案',
|
|
2516
2576
|
'browse.editPath': '編輯路徑',
|
|
2517
2577
|
'browse.select': '選擇此資料夾',
|
|
2578
|
+
'browse.enter': '進入',
|
|
2579
|
+
'browse.drives': '磁碟',
|
|
2580
|
+
'browse.selectNamed': '選擇「{name}」',
|
|
2518
2581
|
'error.title': '發生錯誤',
|
|
2519
2582
|
'cancel': '取消',
|
|
2520
2583
|
'create': '建立',
|
|
@@ -3048,10 +3111,17 @@ window.__ModuleLoader__.load({
|
|
|
3048
3111
|
'.bw-browse-edit svg{width:14px;height:14px}',
|
|
3049
3112
|
'.bw-browse-edit-row{display:flex;gap:6px;align-items:center}',
|
|
3050
3113
|
'.bw-browse-list{display:flex;flex-direction:column;gap:1px;height:240px;overflow:auto;padding:4px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06))}',
|
|
3051
|
-
'.bw-browse-row{display:flex;align-items:center;gap:8px;width:100%;flex:none;text-align:left;border:none;background:transparent;color:var(--dsw-alias-label-primary,#e6e6e6);font:inherit;font-size:12.5px;padding:5px 8px;border-radius:6px;cursor:pointer}',
|
|
3114
|
+
'.bw-browse-row{display:flex;align-items:center;gap:8px;width:100%;flex:none;text-align:left;border:none;background:transparent;color:var(--dsw-alias-label-primary,#e6e6e6);font:inherit;font-size:12.5px;padding:5px 8px;border-radius:6px;cursor:pointer;user-select:none}',
|
|
3052
3115
|
'.bw-browse-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
|
|
3053
3116
|
'.bw-browse-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px}',
|
|
3054
3117
|
'.bw-browse-row svg{width:15px;height:15px;flex:none;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
|
|
3118
|
+
'.bw-browse-row-on{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary,#5b8def)}',
|
|
3119
|
+
'.bw-browse-open{flex:none;width:22px;height:22px;padding:0;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-tertiary,#9a9a9a);font:inherit;font-size:15px;line-height:1;cursor:pointer}',
|
|
3120
|
+
'.bw-browse-open:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
3121
|
+
'.bw-browse-drives{display:flex;align-items:center;gap:4px;margin-left:auto}',
|
|
3122
|
+
'.bw-browse-drive{min-width:30px;padding:3px 6px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);font:inherit;font-size:11.5px;cursor:pointer}',
|
|
3123
|
+
'.bw-browse-drive:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
3124
|
+
'.bw-browse-drive-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
3055
3125
|
'.bw-browse-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
3056
3126
|
'.bw-browse-note{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a);padding:10px 8px;text-align:center}',
|
|
3057
3127
|
'.bw-browse-new-row{display:flex;gap:6px;align-items:center}',
|
|
@@ -3439,6 +3509,24 @@ window.__ModuleLoader__.load({
|
|
|
3439
3509
|
* owner's error surface belongs to the pick/create conversation, and a
|
|
3440
3510
|
* denied listing must never close the flow.
|
|
3441
3511
|
*/
|
|
3512
|
+
/**
|
|
3513
|
+
* Page-lifetime cache of the Windows drive letters this Host actually
|
|
3514
|
+
* serves. The browse API can only list a path, so the first open probes the
|
|
3515
|
+
* usual letters once (six cheap listings, a missing drive fails silently)
|
|
3516
|
+
* and every later open renders the chips straight from here.
|
|
3517
|
+
*/
|
|
3518
|
+
const browseDrives = { probed: false, probing: false, list: [] }
|
|
3519
|
+
|
|
3520
|
+
/**
|
|
3521
|
+
* Run the one wire verb that actually puts a folder on disk and hand back
|
|
3522
|
+
* the created path. This is deliberately a module-level seam the smoke suite
|
|
3523
|
+
* drives for real: v0.11.4 folded the call into the success handler and
|
|
3524
|
+
* dropped it, so the form closed on a resolved `undefined` — no request, no
|
|
3525
|
+
* error, no folder. A string-level assertion cannot see that; calling it can.
|
|
3526
|
+
*/
|
|
3527
|
+
const createFolderIn = (createDirectory, path, name) => Promise.resolve()
|
|
3528
|
+
.then(() => createDirectory(path, name))
|
|
3529
|
+
|
|
3442
3530
|
function DirectoryBrowseDialog(props) {
|
|
3443
3531
|
const { open, busy, listDirectory, createDirectory, onPick, onClose, t } = props
|
|
3444
3532
|
const [listing, setListing] = React.useState(null)
|
|
@@ -3451,14 +3539,18 @@ window.__ModuleLoader__.load({
|
|
|
3451
3539
|
const [createName, setCreateName] = React.useState('')
|
|
3452
3540
|
const [createBusy, setCreateBusy] = React.useState(false)
|
|
3453
3541
|
const [createError, setCreateError] = React.useState('')
|
|
3542
|
+
const [selected, setSelected] = React.useState('')
|
|
3543
|
+
const [drives, setDrives] = React.useState(browseDrives.list)
|
|
3454
3544
|
// Guards a superseded scan: a slow level must not overwrite a newer one.
|
|
3455
3545
|
const seqRef = React.useRef(0)
|
|
3546
|
+
const crumbsRef = React.useRef(null)
|
|
3456
3547
|
|
|
3457
|
-
const go = (path) => {
|
|
3548
|
+
const go = (path, keepSelected) => {
|
|
3458
3549
|
const seq = seqRef.current + 1
|
|
3459
3550
|
seqRef.current = seq
|
|
3460
3551
|
setLoading(true)
|
|
3461
3552
|
setError('')
|
|
3553
|
+
if (keepSelected !== true) setSelected('')
|
|
3462
3554
|
Promise.resolve()
|
|
3463
3555
|
.then(() => listDirectory(path, undefined))
|
|
3464
3556
|
.then((next) => {
|
|
@@ -3484,27 +3576,59 @@ window.__ModuleLoader__.load({
|
|
|
3484
3576
|
setCreateBusy(false)
|
|
3485
3577
|
setCreateError('')
|
|
3486
3578
|
setShowHidden(false)
|
|
3579
|
+
setSelected('')
|
|
3487
3580
|
go(undefined)
|
|
3488
3581
|
return () => { seqRef.current += 1 }
|
|
3489
3582
|
}, [open])
|
|
3490
3583
|
|
|
3584
|
+
const currentPath = listing && typeof listing.path === 'string' ? listing.path : ''
|
|
3585
|
+
const homePath = listing && typeof listing.home === 'string' ? listing.home : ''
|
|
3586
|
+
const windowsHost = homePath.indexOf('\\') !== -1
|
|
3587
|
+
const hasListing = listing !== null
|
|
3588
|
+
|
|
3589
|
+
// Windows drives are separate filesystem roots and the browse API can only
|
|
3590
|
+
// list a path, so probe the usual letters once per page (cached above) and
|
|
3591
|
+
// offer the ones that answered. A drive nobody probed is still reachable
|
|
3592
|
+
// through the path editor, which is the only general answer anyway.
|
|
3593
|
+
React.useEffect(() => {
|
|
3594
|
+
if (!open || !hasListing || !windowsHost) return undefined
|
|
3595
|
+
if (browseDrives.probed || browseDrives.probing) {
|
|
3596
|
+
if (browseDrives.list.length > 0) setDrives(browseDrives.list)
|
|
3597
|
+
return undefined
|
|
3598
|
+
}
|
|
3599
|
+
browseDrives.probing = true
|
|
3600
|
+
let alive = true
|
|
3601
|
+
const letters = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:']
|
|
3602
|
+
Promise.all(letters.map((letter) => Promise.resolve()
|
|
3603
|
+
.then(() => listDirectory(letter + '\\', undefined))
|
|
3604
|
+
.then(() => letter, () => null)))
|
|
3605
|
+
.then((found) => {
|
|
3606
|
+
browseDrives.list = found.filter(Boolean)
|
|
3607
|
+
browseDrives.probed = true
|
|
3608
|
+
browseDrives.probing = false
|
|
3609
|
+
if (alive) setDrives(browseDrives.list)
|
|
3610
|
+
}, () => { browseDrives.probing = false })
|
|
3611
|
+
return () => { alive = false }
|
|
3612
|
+
}, [open, hasListing, windowsHost])
|
|
3613
|
+
|
|
3614
|
+
// A deep path must show its TAIL — that is where you are — so the chain
|
|
3615
|
+
// keeps its right edge in view instead of cutting the last crumb off.
|
|
3616
|
+
React.useEffect(() => {
|
|
3617
|
+
const node = crumbsRef.current
|
|
3618
|
+
if (node) node.scrollLeft = node.scrollWidth
|
|
3619
|
+
}, [listing])
|
|
3620
|
+
|
|
3491
3621
|
if (!open) return null
|
|
3492
3622
|
|
|
3493
|
-
//
|
|
3494
|
-
//
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
const home = listing && typeof listing.home === 'string' ? listing.home : ''
|
|
3498
|
-
const at = home === '' ? -1 : all.findIndex((crumb) => crumb.path === home)
|
|
3499
|
-
const tail = at === -1 ? all : all.slice(at)
|
|
3500
|
-
return tail.map((crumb, index) => (index === 0 && at !== -1
|
|
3501
|
-
? { path: crumb.path, name: t('browse.home') }
|
|
3502
|
-
: crumb))
|
|
3503
|
-
})()
|
|
3504
|
-
const currentPath = listing && typeof listing.path === 'string' ? listing.path : ''
|
|
3623
|
+
// The whole ancestry from the filesystem ROOT: rooting the chain at Home
|
|
3624
|
+
// (the official dialog's choice) hides where the listed level actually is,
|
|
3625
|
+
// and the drive crumb answers "which disk am I on" at a glance.
|
|
3626
|
+
const crumbs = listing && Array.isArray(listing.crumbs) ? listing.crumbs : []
|
|
3505
3627
|
const parentPath = crumbs.length > 1 ? crumbs[crumbs.length - 2].path : ''
|
|
3506
3628
|
const entries = (listing && Array.isArray(listing.entries) ? listing.entries : [])
|
|
3507
3629
|
.filter((entry) => showHidden || !entry.hidden)
|
|
3630
|
+
const selectedEntry = entries.find((entry) => entry.path === selected) || null
|
|
3631
|
+
const targetPath = selectedEntry ? selectedEntry.path : currentPath
|
|
3508
3632
|
|
|
3509
3633
|
const submitDraft = () => {
|
|
3510
3634
|
const path = draft.trim()
|
|
@@ -3517,13 +3641,15 @@ window.__ModuleLoader__.load({
|
|
|
3517
3641
|
if (name === '' || createBusy || currentPath === '') return
|
|
3518
3642
|
setCreateBusy(true)
|
|
3519
3643
|
setCreateError('')
|
|
3520
|
-
|
|
3521
|
-
.then(() =>
|
|
3522
|
-
.then(() => {
|
|
3644
|
+
createFolderIn(createDirectory, currentPath, name)
|
|
3645
|
+
.then((created) => {
|
|
3523
3646
|
setCreateBusy(false)
|
|
3524
3647
|
setCreating(false)
|
|
3525
3648
|
setCreateName('')
|
|
3526
|
-
|
|
3649
|
+
// Land on the folder just made: it is almost always the one the
|
|
3650
|
+
// operator wants to adopt next.
|
|
3651
|
+
if (typeof created === 'string' && created !== '') setSelected(created)
|
|
3652
|
+
go(currentPath, true)
|
|
3527
3653
|
})
|
|
3528
3654
|
.catch((reason) => {
|
|
3529
3655
|
setCreateBusy(false)
|
|
@@ -3556,7 +3682,7 @@ window.__ModuleLoader__.load({
|
|
|
3556
3682
|
disabled: parentPath === '',
|
|
3557
3683
|
onClick: () => { if (parentPath !== '') go(parentPath) },
|
|
3558
3684
|
}, '↑'),
|
|
3559
|
-
E('div', { className: 'bw-browse-crumbs' },
|
|
3685
|
+
E('div', { className: 'bw-browse-crumbs', ref: crumbsRef },
|
|
3560
3686
|
crumbs.map((crumb, index) => (index === crumbs.length - 1
|
|
3561
3687
|
? E('span', { key: crumb.path, className: 'bw-browse-crumb bw-browse-current', title: crumb.path }, crumb.name)
|
|
3562
3688
|
: E('button', {
|
|
@@ -3576,13 +3702,32 @@ window.__ModuleLoader__.load({
|
|
|
3576
3702
|
}, icon('IconEditOutline16')),
|
|
3577
3703
|
)
|
|
3578
3704
|
|
|
3579
|
-
|
|
3705
|
+
// One click SELECTS a row (the footer button then adopts it — no need to
|
|
3706
|
+
// step inside just to pick a folder), a double click or the row's chevron
|
|
3707
|
+
// enters it. Touch needs the chevron: a double tap is not a gesture.
|
|
3708
|
+
const rows = entries.map((entry) => E('div', {
|
|
3580
3709
|
key: entry.path,
|
|
3581
|
-
|
|
3582
|
-
className: 'bw-browse-row',
|
|
3710
|
+
className: cls('bw-browse-row', selected === entry.path && 'bw-browse-row-on'),
|
|
3583
3711
|
title: entry.path,
|
|
3584
|
-
|
|
3585
|
-
|
|
3712
|
+
role: 'button',
|
|
3713
|
+
tabIndex: 0,
|
|
3714
|
+
onClick: () => setSelected((current) => (current === entry.path ? '' : entry.path)),
|
|
3715
|
+
onDoubleClick: () => go(entry.path),
|
|
3716
|
+
onKeyDown: (event) => {
|
|
3717
|
+
if (event.key === 'Enter') go(entry.path)
|
|
3718
|
+
else if (event.key === ' ') { event.preventDefault(); setSelected((current) => (current === entry.path ? '' : entry.path)) }
|
|
3719
|
+
},
|
|
3720
|
+
},
|
|
3721
|
+
icon('IconFolderClose16'),
|
|
3722
|
+
E('span', { className: 'bw-browse-name' }, entry.name),
|
|
3723
|
+
E('button', {
|
|
3724
|
+
type: 'button',
|
|
3725
|
+
className: 'bw-browse-open',
|
|
3726
|
+
title: t('browse.enter'),
|
|
3727
|
+
'aria-label': t('browse.enter'),
|
|
3728
|
+
onClick: (event) => { event.stopPropagation(); go(entry.path) },
|
|
3729
|
+
}, '›'),
|
|
3730
|
+
))
|
|
3586
3731
|
|
|
3587
3732
|
const body = loading
|
|
3588
3733
|
? E('div', { className: 'bw-browse-note' }, t('browse.loading'))
|
|
@@ -3619,6 +3764,12 @@ window.__ModuleLoader__.load({
|
|
|
3619
3764
|
// two browsing tools sit on their own row above the list instead of
|
|
3620
3765
|
// sharing the footer with the commit pair (they used to wrap there).
|
|
3621
3766
|
const tools = E('div', { className: 'bw-browse-tools' },
|
|
3767
|
+
E('button', {
|
|
3768
|
+
type: 'button',
|
|
3769
|
+
className: cls('bw-browse-tool', currentPath !== '' && currentPath === homePath && 'bw-browse-tool-on'),
|
|
3770
|
+
disabled: homePath === '' || currentPath === homePath,
|
|
3771
|
+
onClick: () => { if (homePath !== '') go(homePath) },
|
|
3772
|
+
}, t('browse.home')),
|
|
3622
3773
|
E('button', {
|
|
3623
3774
|
type: 'button',
|
|
3624
3775
|
className: cls('bw-browse-tool', showHidden && 'bw-browse-tool-on'),
|
|
@@ -3631,15 +3782,29 @@ window.__ModuleLoader__.load({
|
|
|
3631
3782
|
disabled: creating || currentPath === '',
|
|
3632
3783
|
onClick: () => { setCreating(true); setCreateName(''); setCreateError('') },
|
|
3633
3784
|
}, t('browse.newFolder')),
|
|
3785
|
+
drives.length > 1
|
|
3786
|
+
? E('div', {
|
|
3787
|
+
className: 'bw-browse-drives',
|
|
3788
|
+
title: t('browse.drives'),
|
|
3789
|
+
role: 'group',
|
|
3790
|
+
'aria-label': t('browse.drives'),
|
|
3791
|
+
}, drives.map((drive) => E('button', {
|
|
3792
|
+
key: drive,
|
|
3793
|
+
type: 'button',
|
|
3794
|
+
className: cls('bw-browse-drive', currentPath.slice(0, 2).toUpperCase() === drive && 'bw-browse-drive-on'),
|
|
3795
|
+
title: drive + '\\',
|
|
3796
|
+
onClick: () => go(drive + '\\'),
|
|
3797
|
+
}, drive)))
|
|
3798
|
+
: null,
|
|
3634
3799
|
)
|
|
3635
3800
|
|
|
3636
3801
|
const footer = E('div', { className: 'bw-browse-actions' },
|
|
3637
3802
|
E(BTN, { variant: 'outline', onClick: onClose, disabled: busy === true }, t('cancel')),
|
|
3638
3803
|
E(BTN, {
|
|
3639
3804
|
variant: 'primary',
|
|
3640
|
-
disabled: busy === true ||
|
|
3641
|
-
onClick: () => { if (
|
|
3642
|
-
}, t('browse.select')),
|
|
3805
|
+
disabled: busy === true || targetPath === '',
|
|
3806
|
+
onClick: () => { if (targetPath !== '') onPick(targetPath) },
|
|
3807
|
+
}, selectedEntry !== null ? t('browse.selectNamed', { name: selectedEntry.name }) : t('browse.select')),
|
|
3643
3808
|
)
|
|
3644
3809
|
|
|
3645
3810
|
return E(ui.Modal, {
|