@parallelworks/ui 0.16.0 → 0.18.0

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.
Files changed (59) hide show
  1. package/LICENSE +202 -21
  2. package/dist/chunks/{CodePreviewEditor-DuMZMA_k.js → CodePreviewEditor-4lx3kE-w.js} +2 -2
  3. package/dist/chunks/{CreateModal-B7smkHkv.js → CreateModal-BY6DLmur.js} +11 -11
  4. package/dist/chunks/{lib-CUTRDpa6.js → FieldWrapper-Bef01EWO.js} +49 -48
  5. package/dist/chunks/{Monaco-BXDH2jVj.js → Monaco-C1POOdVv.js} +485 -172
  6. package/dist/chunks/{PageHeader-CoZWF0pJ.js → PageHeader-l49p1nM9.js} +145 -82
  7. package/dist/chunks/{Provider-CbFVFB7O.js → Provider-Ghua9PkN.js} +34 -6
  8. package/dist/chunks/{StatusBadge-zvEtK16a.js → StatusBadge-C1Blx_cW.js} +23 -6
  9. package/dist/chunks/{SwitchToggle-3V836Cn7.js → SwitchToggle-CHPP8uZQ.js} +17 -14
  10. package/dist/chunks/{Table-CutP1bRt.js → Table-4OHjdKdO.js} +2 -2
  11. package/dist/chunks/{Tooltip-n9w7Tkt1.js → Tooltip-Crxo_eU_.js} +3 -3
  12. package/dist/chunks/{editor-Ko3zX22Y.js → editor-9_uVtUyy.js} +4 -4
  13. package/dist/chunks/{list-CduoUsMW.js → list-qrojZZNv.js} +146 -33
  14. package/dist/components/AddMenu.d.ts +1 -0
  15. package/dist/components/Avatar.d.ts +13 -0
  16. package/dist/components/Button.d.ts +2 -1
  17. package/dist/components/Dropdown.d.ts +2 -0
  18. package/dist/components/Provider.d.ts +59 -2
  19. package/dist/components/SectionHeader.d.ts +3 -1
  20. package/dist/components/StatusBadge.d.ts +7 -2
  21. package/dist/components/relativeTime.d.ts +15 -0
  22. package/dist/editor/index.js +1 -1
  23. package/dist/editor/usesGitlab.d.ts +97 -0
  24. package/dist/file-explorer/index.js +275 -274
  25. package/dist/file-explorer/lib/paging.d.ts +27 -28
  26. package/dist/file-explorer/lib/types.d.ts +0 -1
  27. package/dist/file-explorer/lib/utils.d.ts +16 -5
  28. package/dist/form/FieldWrapper.d.ts +2 -1
  29. package/dist/form/Form.d.ts +3 -1
  30. package/dist/form/index.js +23 -21
  31. package/dist/form/types/fieldComponentTypes.d.ts +1 -0
  32. package/dist/form/types/fieldTypes.d.ts +4 -4
  33. package/dist/form/types/workflow-schema.d.ts +38 -0
  34. package/dist/form/useParsedOpts.d.ts +1 -1
  35. package/dist/icons.d.ts +1 -1
  36. package/dist/icons.js +3 -3
  37. package/dist/index.d.ts +5 -2
  38. package/dist/index.js +49 -18
  39. package/dist/list/ListPage.d.ts +3 -1
  40. package/dist/list/ListPager.d.ts +3 -1
  41. package/dist/list/ListTable.d.ts +2 -1
  42. package/dist/list/RowContextMenu.d.ts +9 -9
  43. package/dist/list/UserHoverCard.d.ts +45 -0
  44. package/dist/list/index.d.ts +2 -1
  45. package/dist/list/index.js +3 -3
  46. package/dist/list/listView.d.ts +1 -1
  47. package/dist/logviewer/index.js +7 -7
  48. package/dist/styles/Geist-Variable.woff2 +0 -0
  49. package/dist/styles/GeistMono-Variable.woff2 +0 -0
  50. package/dist/styles/base.css +57 -0
  51. package/dist/styles/fonts.css +15 -0
  52. package/dist/styles/theme.css +6 -40
  53. package/dist/styles.css +1 -1
  54. package/dist/workflow/DependencyGraph.d.ts +2 -0
  55. package/dist/workflow/index.d.ts +1 -0
  56. package/dist/workflow/index.js +10 -9
  57. package/dist/workflow/types.d.ts +5 -0
  58. package/package.json +12 -11
  59. package/dist/chunks/{BareModal-BqroLbZ5.js → BareModal-BtRiQezg.js} +1 -1
@@ -1,21 +1,21 @@
1
+ import { i as useNotify, l as useSlots, o as useProvisionCorsRules, u as useStrings } from "../chunks/Provider-Ghua9PkN.js";
2
+ import { _ as useRowMenuActive, d as assembleRowMenu, g as useRowMenu, h as useCopySubmenu, u as MoreButton } from "../chunks/PageHeader-l49p1nM9.js";
1
3
  import { AccessIcon, AlertIcon, AngleDownIcon, AngleRightIcon, AngleUpIcon, ChevronLeftIcon, ChevronRightIcon, ClipboardIcon, CloseIcon, CloudUploadIcon, CollapseIcon, CustomizeIcon, DiskIcon, DocumentIcon, DownloadFileIcon, ExpandIcon, ExpandVerticalIcon, EyeIcon, FileIcon, FileTextIcon, FolderIcon, FolderOpenIcon, ImageIcon, LinkIcon, LockIcon, MinimizeIcon, MusicIcon, PdfIcon, RefreshIcon, ShareIcon, SpreadsheetIcon, SuccessIcon, TrashIcon, UploadIcon, UserIcon, VideoIcon, ZipIcon, ZoomInIcon, ZoomOutIcon } from "../icons.js";
2
- import { c as ConfirmModal, f as Markdown, h as Callout, m as CopyToClipboard, n as CreateModal, p as CopyCodeBlock } from "../chunks/CreateModal-B7smkHkv.js";
3
- import { a as useProvisionCorsRules, c as useStrings, i as useNotify, s as useSlots } from "../chunks/Provider-CbFVFB7O.js";
4
- import { n as TOOLTIP_ID, r as TooltipInfo } from "../chunks/Tooltip-n9w7Tkt1.js";
5
- import { t as StatusBadge } from "../chunks/StatusBadge-zvEtK16a.js";
4
+ import { c as ConfirmModal, f as Markdown, h as Callout, m as CopyToClipboard, n as CreateModal, p as CopyCodeBlock } from "../chunks/CreateModal-BY6DLmur.js";
5
+ import { n as TOOLTIP_ID, r as TooltipInfo } from "../chunks/Tooltip-Crxo_eU_.js";
6
+ import { t as StatusBadge } from "../chunks/StatusBadge-C1Blx_cW.js";
6
7
  import { t as Loader } from "../chunks/Loader-BXoXTZGc.js";
7
- import { _ as useRowMenuActive, d as assembleRowMenu, f as copySubmenu, g as useRowMenu, u as MoreButton } from "../chunks/PageHeader-CoZWF0pJ.js";
8
- import { n as Table } from "../chunks/Table-CutP1bRt.js";
9
- import { C as ListSkeleton, E as RowSelectCheckbox, O as listTableFixedProps, k as listTableProps, x as ListActionsHeader } from "../chunks/list-CduoUsMW.js";
8
+ import { n as Table } from "../chunks/Table-4OHjdKdO.js";
9
+ import { E as ListSkeleton, M as listTableProps, j as listTableFixedProps, k as RowSelectCheckbox, w as ListActionsHeader } from "../chunks/list-qrojZZNv.js";
10
10
  import cx from "classnames";
11
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
11
  import { Suspense, createContext, lazy, memo, use, useCallback, useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState, useTransition } from "react";
12
+ import { useLocalStorage } from "usehooks-ts";
13
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
13
14
  import { createPortal } from "react-dom";
14
15
  import { Menu, MenuButton, MenuItem, MenuItems, MenuSeparator } from "@headlessui/react";
15
16
  import Skeleton from "react-loading-skeleton";
16
17
  import { DateTime } from "luxon";
17
18
  import { Resizable } from "re-resizable";
18
- import { useLocalStorage } from "usehooks-ts";
19
19
  import { useVirtualizer } from "@tanstack/react-virtual";
20
20
  //#region src/file-explorer/lib/utils.ts
21
21
  function formatFileSize(bytes) {
@@ -38,143 +38,143 @@ function getNodeName(node) {
38
38
  const parts = removeTrailingSlash(node.path || "").split("/");
39
39
  return parts[parts.length - 1] ?? "";
40
40
  }
41
- function buildTreeFromStorage(storage, unsortedParents) {
42
- if (!storage) return {
43
- treeMap: {},
44
- parentToChildrenMap: {}
45
- };
46
- const objects = storage.objects ?? [];
47
- const cspBucketName = storage.bucketName || storage.name;
48
- const storagePath = storage.user ? ensureTrailingSlash(`${storage.user}/${storage.name}`) : ensureTrailingSlash(storage.name);
49
- const treeMap = { [storagePath]: {
50
- name: storage.name,
51
- displayName: storage?.displayName,
52
- path: storagePath,
53
- relativePath: "",
54
- type: "directory",
55
- created: storage.created,
56
- modified: storage.modified,
57
- storageId: storage.id,
58
- storageName: cspBucketName,
59
- storageNodeIcon: storage.type,
60
- storageImageUrl: storage.imageUrl,
61
- storageType: storage.type,
62
- root: true
63
- } };
64
- const directories = objects.filter((obj) => obj.type === "directory");
65
- const files = objects.filter((obj) => obj.type === "file");
66
- directories.forEach((dir) => {
67
- const fullPath = ensureTrailingSlash(`${storagePath}${removeLeadingSlash(removeTrailingSlash(dir.path))}`);
68
- treeMap[fullPath] = {
69
- name: getNodeName(dir),
70
- path: fullPath,
71
- relativePath: dir?.key || dir.path,
72
- type: dir.type,
73
- created: dir?.created,
74
- modified: dir?.modified,
75
- storageId: storage.id,
76
- storageName: cspBucketName
77
- };
78
- });
79
- files.forEach((file) => {
80
- const relativeFilePath = removeLeadingSlash(file.path);
81
- const fullPath = `${storagePath}${relativeFilePath}`;
82
- treeMap[fullPath] = {
83
- name: getNodeName(file),
84
- path: fullPath,
85
- relativePath: file.key || relativeFilePath,
86
- type: file.type,
87
- size: file.size,
88
- created: file?.created,
89
- modified: file?.modified,
90
- contentType: file?.contentType,
91
- hash: file?.hash,
92
- storageClass: file?.storageClass,
93
- storageId: storage.id,
94
- storageName: cspBucketName
95
- };
96
- });
97
- const parentToChildrenMap = { [storagePath]: [] };
98
- Object.values(treeMap).forEach((node) => {
99
- if (node.path === storagePath) return;
100
- const cleanPath = node.path.endsWith("/") && node.path.length > 1 ? node.path.slice(0, -1) : node.path;
101
- const lastSlash = cleanPath.lastIndexOf("/");
102
- const parentPath = lastSlash <= 0 ? storagePath : ensureTrailingSlash(cleanPath.substring(0, lastSlash + 1));
103
- if (!parentToChildrenMap[parentPath]) parentToChildrenMap[parentPath] = [];
104
- parentToChildrenMap[parentPath].push(node);
105
- });
106
- Object.entries(parentToChildrenMap).forEach(([parentPath, childrenArr]) => {
107
- if (unsortedParents?.has(parentPath)) return;
108
- childrenArr.sort((a, b) => {
109
- if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
110
- return a.name.localeCompare(b.name);
111
- });
112
- });
41
+ function toNode(entry, storage, storagePath, storageName) {
42
+ const relativePath = removeLeadingSlash(entry.path);
43
+ const isDirectory = entry.type === "directory";
113
44
  return {
114
- treeMap,
115
- parentToChildrenMap
45
+ name: getNodeName(entry),
46
+ path: isDirectory ? ensureTrailingSlash(`${storagePath}${removeTrailingSlash(relativePath)}`) : `${storagePath}${relativePath}`,
47
+ relativePath: entry.key || (isDirectory ? entry.path : relativePath),
48
+ type: entry.type,
49
+ size: entry.size,
50
+ created: entry.created,
51
+ modified: entry.modified,
52
+ contentType: entry.contentType,
53
+ hash: entry.hash,
54
+ storageClass: entry.storageClass,
55
+ storageId: storage.id,
56
+ storageName
116
57
  };
117
58
  }
118
- function buildTreeFromUserStorages(storages, options) {
119
- if (storages.length === 1 && !options?.forceUserHierarchy) return buildTreeFromStorage(storages[0], options?.unsortedParents);
120
- const treeMap = {};
121
- const parentToChildrenMap = {};
122
- const userGroups = {};
123
- for (const storage of storages) {
124
- const username = storage.user ?? "";
125
- if (!userGroups[username]) userGroups[username] = [];
126
- userGroups[username].push(storage);
59
+ function compareNodes(a, b) {
60
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
61
+ return (a.displayName || a.name).localeCompare(b.displayName || b.name);
62
+ }
63
+ function extendsEntries(next, previous) {
64
+ return next.length >= previous.length && previous.every((entry, i) => next[i] === entry);
65
+ }
66
+ function childrenOf(dir, sorted) {
67
+ if (sorted) {
68
+ dir.sorted ??= [...dir.directories, ...dir.files].sort(compareNodes);
69
+ return dir.sorted;
127
70
  }
128
- for (const username of Object.keys(userGroups)) {
129
- const userPath = ensureTrailingSlash(username);
130
- treeMap[userPath] = {
131
- name: username,
132
- path: userPath,
71
+ dir.unsorted ??= [...dir.directories, ...dir.files];
72
+ return dir.unsorted;
73
+ }
74
+ /** One accumulator per listed directory, so a landed page costs the page and not
75
+ * the tree. One builder per explorer instance. */
76
+ function createTreeBuilder() {
77
+ const directories = /* @__PURE__ */ new Map();
78
+ let last = null;
79
+ function unchanged(storages, options) {
80
+ if (!last || last.storages !== storages || last.options.forceUserHierarchy !== options.forceUserHierarchy || last.options.unsortedParents !== options.unsortedParents) return false;
81
+ const before = last.options.listings;
82
+ if (options.listings.size !== before.size) return false;
83
+ for (const [dirPath, listing] of options.listings) if (before.get(dirPath)?.entries !== listing.entries) return false;
84
+ return true;
85
+ }
86
+ function nodesFor(dirPath, storage, storagePath, storageName, entries) {
87
+ const cached = directories.get(dirPath);
88
+ const reusable = cached?.storageName === storageName ? cached : void 0;
89
+ if (reusable && reusable.entries === entries) return reusable;
90
+ let dir;
91
+ if (reusable && extendsEntries(entries, reusable.entries)) dir = reusable;
92
+ else {
93
+ dir = {
94
+ entries: [],
95
+ storageName,
96
+ directories: [],
97
+ files: []
98
+ };
99
+ directories.set(dirPath, dir);
100
+ }
101
+ for (let i = dir.entries.length; i < entries.length; i++) {
102
+ const node = toNode(entries[i], storage, storagePath, storageName);
103
+ if (node.path === dirPath) continue;
104
+ if (node.type === "directory") dir.directories.push(node);
105
+ else dir.files.push(node);
106
+ }
107
+ dir.entries = entries;
108
+ dir.unsorted = void 0;
109
+ dir.sorted = void 0;
110
+ return dir;
111
+ }
112
+ function addStorage(storage, storagePath, options, tree) {
113
+ const storageName = storage.bucketName || storage.name;
114
+ const root = {
115
+ name: storage.name,
116
+ displayName: storage.displayName,
117
+ path: storagePath,
118
+ relativePath: "",
133
119
  type: "directory",
134
- displayName: username
120
+ created: storage.created,
121
+ modified: storage.modified,
122
+ storageId: storage.id,
123
+ storageName,
124
+ storageNodeIcon: storage.type,
125
+ storageImageUrl: storage.imageUrl,
126
+ storageType: storage.type,
127
+ root: true
135
128
  };
136
- parentToChildrenMap[userPath] = [];
137
- for (const storage of userGroups[username] ?? []) {
138
- const cspBucketName = storage.bucketName || storage.name;
139
- const storageFullPath = ensureTrailingSlash(`${userPath}${removeLeadingAndTrailingSlashes(storage.name)}`);
140
- treeMap[storageFullPath] = {
141
- name: storage.name,
142
- displayName: storage?.displayName,
143
- path: storageFullPath,
144
- relativePath: "",
145
- type: "directory",
146
- created: storage?.created,
147
- modified: storage?.modified,
148
- storageId: storage.id,
149
- storageName: cspBucketName,
150
- storageNodeIcon: storage.type,
151
- storageImageUrl: storage.imageUrl,
152
- storageType: storage.type,
153
- root: true
154
- };
155
- parentToChildrenMap[userPath].push(treeMap[storageFullPath]);
156
- if (storage.objects) {
157
- const { treeMap: storageTree, parentToChildrenMap: storageChildren } = buildTreeFromStorage(storage, options?.unsortedParents);
158
- Object.assign(treeMap, storageTree);
159
- for (const [parent, children] of Object.entries(storageChildren)) {
160
- if (!parentToChildrenMap[parent]) parentToChildrenMap[parent] = [];
161
- parentToChildrenMap[parent].push(...children);
162
- }
163
- }
129
+ tree.treeMap[storagePath] = root;
130
+ for (const [dirPath, listing] of options.listings) {
131
+ if (!dirPath.startsWith(storagePath)) continue;
132
+ const children = childrenOf(nodesFor(dirPath, storage, storagePath, storageName, listing.entries), !options.unsortedParents?.has(dirPath));
133
+ if (children.length === 0) continue;
134
+ for (const node of children) tree.treeMap[node.path] = node;
135
+ tree.parentToChildrenMap[dirPath] = children;
164
136
  }
137
+ return root;
165
138
  }
166
- Object.entries(parentToChildrenMap).forEach(([parentPath, childrenArr]) => {
167
- if (options?.unsortedParents?.has(parentPath)) return;
168
- childrenArr.sort((a, b) => {
169
- if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
170
- const aLabel = a.displayName || a.name;
171
- const bLabel = b.displayName || b.name;
172
- return aLabel.localeCompare(bLabel);
173
- });
174
- });
175
- return {
176
- treeMap,
177
- parentToChildrenMap
139
+ return (storages, options) => {
140
+ if (last && unchanged(storages, options)) return last.tree;
141
+ const tree = {
142
+ treeMap: {},
143
+ parentToChildrenMap: {},
144
+ rootNodes: []
145
+ };
146
+ if (storages.length === 1 && !options.forceUserHierarchy) {
147
+ const storage = storages[0];
148
+ const storagePath = ensureTrailingSlash(storage.user ? `${storage.user}/${storage.name}` : storage.name);
149
+ tree.rootNodes.push(addStorage(storage, storagePath, options, tree));
150
+ } else {
151
+ const userGroups = {};
152
+ for (const storage of storages) {
153
+ const username = storage.user ?? "";
154
+ if (!userGroups[username]) userGroups[username] = [];
155
+ userGroups[username].push(storage);
156
+ }
157
+ for (const username of Object.keys(userGroups)) {
158
+ const userPath = ensureTrailingSlash(username);
159
+ const userNode = {
160
+ name: username,
161
+ path: userPath,
162
+ type: "directory",
163
+ displayName: username
164
+ };
165
+ tree.treeMap[userPath] = userNode;
166
+ tree.rootNodes.push(userNode);
167
+ const storageRoots = (userGroups[username] ?? []).map((storage) => addStorage(storage, ensureTrailingSlash(`${userPath}${removeLeadingAndTrailingSlashes(storage.name)}`), options, tree));
168
+ tree.parentToChildrenMap[userPath] = storageRoots.sort(compareNodes);
169
+ }
170
+ }
171
+ for (const dirPath of directories.keys()) if (!options.listings.has(dirPath)) directories.delete(dirPath);
172
+ last = {
173
+ storages,
174
+ options,
175
+ tree
176
+ };
177
+ return tree;
178
178
  };
179
179
  }
180
180
  function getNodeChildren(parentToChildrenMap, parentPath) {
@@ -277,6 +277,9 @@ function getCspUri(csp, cspBucketName, key) {
277
277
  if (!scheme) return null;
278
278
  return `${scheme}://${cspBucketName}/${key}`;
279
279
  }
280
+ function getUriScheme(uri) {
281
+ return uri.match(/^([a-z0-9+.-]+):\/\//i)?.[1] ?? "";
282
+ }
280
283
  function removeLeadingSlash(path) {
281
284
  return path.replace(/^\/+/, "");
282
285
  }
@@ -448,7 +451,7 @@ function FileProgressBar({ fileProgress, fileName, progressBarColorScheme, onCan
448
451
  children: [/* @__PURE__ */ jsx("div", {
449
452
  className: cx("h-full transition-all duration-300 relative flex items-center justify-center", barColor, isHovered && "opacity-80"),
450
453
  style: { width: `${percentage}%` }
451
- }), isHovered && !isCompleted && totalBytes > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
454
+ }), isHovered && !isCompleted && totalBytes > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
452
455
  className: "absolute inset-0 flex items-center justify-center text-xs whitespace-nowrap p-0.5 z-10",
453
456
  style: {
454
457
  WebkitClipPath: `inset(0 ${100 - percentage}% 0 0)`,
@@ -622,7 +625,7 @@ function UploadSessionToast({ session, queueLength, onCancel, onCancelFile }) {
622
625
  })
623
626
  ]
624
627
  })]
625
- }), isToastExpanded && /* @__PURE__ */ jsxs(Fragment, { children: [
628
+ }), isToastExpanded && /* @__PURE__ */ jsxs(Fragment$1, { children: [
626
629
  /* @__PURE__ */ jsxs("div", {
627
630
  className: "flex items-center justify-between",
628
631
  children: [/* @__PURE__ */ jsxs("div", {
@@ -1490,7 +1493,7 @@ function PropagationStage({ status, label }) {
1490
1493
  "aria-hidden": "true",
1491
1494
  children: /* @__PURE__ */ jsx("polyline", { points: "3.5 8.5 6.5 11.5 12.5 5" })
1492
1495
  }),
1493
- status === "active" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400/60" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-blue-500" })] }),
1496
+ status === "active" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400/60" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-blue-500" })] }),
1494
1497
  status === "pending" && /* @__PURE__ */ jsx("span", { className: "inline-flex h-1.5 w-1.5 rounded-full theme-muted-text opacity-60" })
1495
1498
  ]
1496
1499
  }), /* @__PURE__ */ jsx("span", {
@@ -1686,46 +1689,60 @@ function beginLoadingMore(listings, path) {
1686
1689
  const listing = listings.get(path);
1687
1690
  if (!listing) return listings;
1688
1691
  return new Map(listings).set(path, {
1689
- ...listing.cursor ? { cursor: listing.cursor } : {},
1690
- pagesLoaded: listing.pagesLoaded,
1691
- loadingMore: true
1692
+ ...listing,
1693
+ loadingMore: true,
1694
+ error: void 0
1692
1695
  });
1693
1696
  }
1694
- /** `expectedPagesLoaded` is captured before the request: a mismatch means a refresh
1695
- * reset this directory while the page was in flight. */
1696
- function advanceListing(listings, path, page, expectedPagesLoaded) {
1697
- if ((listings.get(path)?.pagesLoaded ?? 0) !== expectedPagesLoaded) return listings;
1697
+ function advanceListing(listings, path, page) {
1698
+ const listing = listings.get(path);
1698
1699
  return new Map(listings).set(path, {
1699
- ...page.cursor ? { cursor: page.cursor } : {},
1700
- pagesLoaded: expectedPagesLoaded + 1,
1701
- loadingMore: false
1700
+ cursor: page.cursor,
1701
+ pagesLoaded: (listing?.pagesLoaded ?? 0) + 1,
1702
+ loadingMore: false,
1703
+ entries: [...listing?.entries ?? [], ...page.entries]
1702
1704
  });
1703
1705
  }
1704
- /** Always a new map, since the hydration walk only re-runs on a `listings` change.
1705
- * `pagesLoaded` stays put, so a failed first page still reads as unlisted. */
1706
+ /** Subfolders the page no longer names lose their records, or a re-created folder
1707
+ * would show old contents; the rest keep theirs, since a refresh lists them
1708
+ * alongside this one and a parent landing later must not undo a child's page. */
1709
+ function replaceListing(listings, path, page) {
1710
+ const prefix = ensureTrailingSlash(path);
1711
+ const subfolders = new Set(page.entries.filter((entry) => entry.type === "directory").map(getNodeName));
1712
+ const next = new Map(listings);
1713
+ next.delete(path);
1714
+ for (const key of listings.keys()) {
1715
+ if (!key.startsWith(prefix)) continue;
1716
+ if (!subfolders.has(key.slice(prefix.length).split("/")[0] ?? "")) next.delete(key);
1717
+ }
1718
+ return advanceListing(next, path, page);
1719
+ }
1720
+ /** `pagesLoaded` stays put, so a failed first page still reads as unlisted. */
1706
1721
  function recordListingError(listings, path, error) {
1707
1722
  const listing = listings.get(path);
1708
1723
  return new Map(listings).set(path, {
1709
- ...listing?.cursor ? { cursor: listing.cursor } : {},
1724
+ cursor: listing?.cursor,
1710
1725
  pagesLoaded: listing?.pagesLoaded ?? 0,
1711
1726
  loadingMore: false,
1712
- error
1727
+ error,
1728
+ entries: listing?.entries ?? []
1729
+ });
1730
+ }
1731
+ /** Keeps the cursor and pages, so a retry resumes paging instead of starting over. */
1732
+ function clearListingError(listings, path) {
1733
+ const listing = listings.get(path);
1734
+ if (!listing) return listings;
1735
+ return new Map(listings).set(path, {
1736
+ ...listing,
1737
+ error: void 0
1713
1738
  });
1714
1739
  }
1715
- /** Slash-terminates the prefix: without it, clearing `logs` would also clear
1716
- * `logs-archive/`. */
1717
1740
  function clearListingsUnder(listings, path) {
1718
- const prefix = path.endsWith("/") ? path : `${path}/`;
1741
+ const prefix = ensureTrailingSlash(path);
1719
1742
  const next = new Map(listings);
1720
1743
  for (const key of listings.keys()) if (key === path || key.startsWith(prefix)) next.delete(key);
1721
1744
  return next;
1722
1745
  }
1723
- /** The same prefix rule for the attempt sets that shadow `listings`. They gate
1724
- * re-listing, so a path left behind here reads as tried-and-empty forever. */
1725
- function forgetPathsUnder(paths, path) {
1726
- const prefix = path.endsWith("/") ? path : `${path}/`;
1727
- for (const key of [...paths]) if (key === path || key.startsWith(prefix)) paths.delete(key);
1728
- }
1729
1746
  /** Tree paths whose children should keep arrival order, since sorting them would
1730
1747
  * reshuffle rows under the user mid-scroll. */
1731
1748
  function incompletePaths(listings) {
@@ -1734,11 +1751,11 @@ function incompletePaths(listings) {
1734
1751
  return paths;
1735
1752
  }
1736
1753
  /** What a deep link needs next. Pure so the walk is table-testable. */
1737
- function nextHydrationStep({ targetResolved, ancestors, isListableDir, listings, inFlight, attempted, pagesPulled }) {
1754
+ function nextHydrationStep({ targetResolved, ancestors, isListableDir, listings, inFlight, pagesPulled }) {
1738
1755
  for (const path of ancestors) {
1739
1756
  if (!isListableDir(path)) continue;
1740
1757
  if (inFlight.has(path)) return { kind: "done" };
1741
- if (!isListed(listings, path) && !attempted.has(path)) return {
1758
+ if (!listings.has(path)) return {
1742
1759
  kind: "fetch",
1743
1760
  path
1744
1761
  };
@@ -1746,20 +1763,23 @@ function nextHydrationStep({ targetResolved, ancestors, isListableDir, listings,
1746
1763
  if (targetResolved) return { kind: "done" };
1747
1764
  const deepest = [...ancestors].reverse().find(isListableDir);
1748
1765
  if (!deepest) return { kind: "done" };
1749
- if (!hasMore(listings, deepest)) return { kind: "stuck" };
1750
- if (loadMoreError(listings, deepest)) return { kind: "stuck" };
1751
- if (pagesPulled >= 20) return { kind: "stuck" };
1766
+ if (!hasMore(listings, deepest)) return {
1767
+ kind: "stuck",
1768
+ path: deepest
1769
+ };
1770
+ if (loadMoreError(listings, deepest)) return {
1771
+ kind: "stuck",
1772
+ path: deepest
1773
+ };
1774
+ if (pagesPulled >= 20) return {
1775
+ kind: "stuck",
1776
+ path: deepest
1777
+ };
1752
1778
  return isLoadingMore(listings, deepest) ? { kind: "done" } : {
1753
1779
  kind: "more",
1754
1780
  path: deepest
1755
1781
  };
1756
1782
  }
1757
- /** `replace` drops the directory's previous contents first, which is how a refresh
1758
- * sheds deleted objects rather than re-adding them. */
1759
- function nextObjects(previous, page, options) {
1760
- if (!options.replace) return [...previous, ...page];
1761
- return [...previous.filter((obj) => !(obj.path.startsWith(options.dirPath) && obj.path !== options.dirPath)), ...page];
1762
- }
1763
1783
  //#endregion
1764
1784
  //#region src/file-explorer/FilePreview/StreamedBadge.tsx
1765
1785
  function StreamedBadge() {
@@ -2537,7 +2557,7 @@ function useObjectText(url, { enabled, maxBytes, maxLines, knownSize, cacheKey,
2537
2557
  }
2538
2558
  //#endregion
2539
2559
  //#region src/file-explorer/FilePreview/Viewer.tsx
2540
- var CodePreviewEditor = lazy(() => import("../chunks/CodePreviewEditor-DuMZMA_k.js"));
2560
+ var CodePreviewEditor = lazy(() => import("../chunks/CodePreviewEditor-4lx3kE-w.js"));
2541
2561
  var PREVIEW_MARKDOWN_CONTROLS = {
2542
2562
  table: { fullscreen: false },
2543
2563
  mermaid: { fullscreen: false }
@@ -2978,7 +2998,7 @@ function PreviewFetchState({ loading, tooLarge, error, corsError, storage, onCor
2978
2998
  });
2979
2999
  if (tooLarge) return /* @__PURE__ */ jsx(TooLargeInline, { onDownload });
2980
3000
  if (error) return /* @__PURE__ */ jsx(PreviewError, { onDownload });
2981
- return /* @__PURE__ */ jsx(Fragment, { children });
3001
+ return /* @__PURE__ */ jsx(Fragment$1, { children });
2982
3002
  }
2983
3003
  function NotPreviewable({ icon, title, message, onDownload }) {
2984
3004
  const t = useStrings().fileExplorer;
@@ -3227,7 +3247,7 @@ function FilePreview({ open, node, siblings, storage, getPresignedUrl, onNavigat
3227
3247
  onClick: (e) => {
3228
3248
  if (e.target === e.currentTarget) onClose();
3229
3249
  },
3230
- children: [isOverlay && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(EdgeButton, {
3250
+ children: [isOverlay && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(EdgeButton, {
3231
3251
  side: "left",
3232
3252
  label: t.preview.previousFile,
3233
3253
  disabled: !hasPrev,
@@ -3572,7 +3592,7 @@ function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeI
3572
3592
  const storageIconAlt = useStrings().fileExplorer.chrome.storageIconAlt;
3573
3593
  useNestedScrollerWarning(scrollEl, nodes.length);
3574
3594
  useRowPitchAudit(scrollEl, nodes, items);
3575
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3595
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3576
3596
  padTop > 0 && /* @__PURE__ */ jsx(Spacer, { height: padTop }, "pad-top"),
3577
3597
  items.map((item) => {
3578
3598
  const node = nodes[item.index];
@@ -3605,7 +3625,7 @@ function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeI
3605
3625
  ] });
3606
3626
  }
3607
3627
  function Spacer({ height }) {
3608
- return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("tr", {
3628
+ return /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx("tr", {
3609
3629
  "aria-hidden": "true",
3610
3630
  className: "border-0",
3611
3631
  children: /* @__PURE__ */ jsx("td", {
@@ -3638,7 +3658,7 @@ function useRowPitchAudit(scrollEl, nodes, items) {
3638
3658
  * would land in `<tbody>`, since Children.toArray only flattens arrays. */
3639
3659
  function FileTableHeaders({ allSelected, someSelected, onSelectAll }) {
3640
3660
  const t = useStrings().fileExplorer;
3641
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3661
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3642
3662
  /* @__PURE__ */ jsx(Table.Header, {
3643
3663
  caps: false,
3644
3664
  className: "py-2",
@@ -3873,8 +3893,8 @@ function flattenTreeRows(roots, parentToChildrenMap, expandedPaths, options) {
3873
3893
  isExpanded
3874
3894
  });
3875
3895
  if (!isExpanded) return;
3876
- if (options?.loadingPaths?.has(node.path)) {
3877
- if (children.length === 0) rows.push(statusRow("skeleton", node, depth + 1));
3896
+ if (options?.loadingPaths?.has(node.path) && children.length === 0) {
3897
+ rows.push(statusRow("skeleton", node, depth + 1));
3878
3898
  return;
3879
3899
  }
3880
3900
  const limit = options?.fileLimits?.get(node.path) ?? 50;
@@ -4080,7 +4100,7 @@ function indentStyle(depth, isDirectory) {
4080
4100
  }
4081
4101
  function IndentGuides({ depth }) {
4082
4102
  if (depth === 0) return null;
4083
- return /* @__PURE__ */ jsx(Fragment, { children: Array.from({ length: depth }, (_, level) => /* @__PURE__ */ jsx("span", {
4103
+ return /* @__PURE__ */ jsx(Fragment$1, { children: Array.from({ length: depth }, (_, level) => /* @__PURE__ */ jsx("span", {
4084
4104
  "aria-hidden": "true",
4085
4105
  className: "pointer-events-none absolute inset-y-0 w-px bg-(--theme-border)/60",
4086
4106
  style: { left: GUIDE_OFFSET + level * INDENT_PER_LEVEL }
@@ -4092,7 +4112,7 @@ function RowIcons({ isDirectory, isExpanded, customIcon }) {
4092
4112
  children: customIcon
4093
4113
  });
4094
4114
  const folder = isExpanded ? /* @__PURE__ */ jsx(FolderOpenIcon, { className: "h-4 w-4 mr-1 text-sky-500" }) : /* @__PURE__ */ jsx(FolderIcon, { className: "h-4 w-4 mr-1 text-sky-500/80" });
4095
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(AngleRightIcon, { className: cx("h-3.5 w-3.5 shrink-0 theme-muted-text transition-transform duration-150", isExpanded && "rotate-90") }), customIcon ?? folder] });
4115
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(AngleRightIcon, { className: cx("h-3.5 w-3.5 shrink-0 theme-muted-text transition-transform duration-150", isExpanded && "rotate-90") }), customIcon ?? folder] });
4096
4116
  }
4097
4117
  /** Sized by the slot, not by its contents: react-loading-skeleton wraps each bar
4098
4118
  * in an inline span, so a bar occupies a line box taller than its `height` and
@@ -4276,7 +4296,7 @@ function useTreePitchAudit(scrollEl, rows, items) {
4276
4296
  //#region src/file-explorer/FileExplorer.tsx
4277
4297
  function UriCard({ value }) {
4278
4298
  const t = useStrings().fileExplorer;
4279
- const scheme = value.match(/^([a-z0-9+.-]+):\/\//i)?.[1] ?? "";
4299
+ const scheme = getUriScheme(value);
4280
4300
  return /* @__PURE__ */ jsxs(CopyToClipboard, {
4281
4301
  text: value,
4282
4302
  showIcon: false,
@@ -4344,14 +4364,13 @@ function TreeFrame({ treeOnly, sidebarWidth, onWidthChange, children }) {
4344
4364
  });
4345
4365
  }
4346
4366
  function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy = false, groups = [], groupsLoading = false, selectedPath, onPathChange, getProviderAndClient, treeOnly = false, initialExpandedPaths, onExpandedPathsChange }) {
4367
+ const copySubmenu = useCopySubmenu();
4347
4368
  const t = useStrings().fileExplorer;
4348
4369
  const notify = useNotify();
4349
4370
  const slots = useSlots();
4350
4371
  const fileExplorerContext = useFileExplorer();
4351
- const [localStorages, setLocalStorages] = useState(storages);
4352
4372
  const [listings, setListings] = useState(() => /* @__PURE__ */ new Map());
4353
- const fetchesInFlightRef = useRef(/* @__PURE__ */ new Set());
4354
- const listAttemptsRef = useRef(/* @__PURE__ */ new Set());
4373
+ const fetchesInFlightRef = useRef(/* @__PURE__ */ new Map());
4355
4374
  const incompleteDirsRef = useRef(/* @__PURE__ */ new Set());
4356
4375
  const incompleteDirs = useMemo(() => {
4357
4376
  const next = incompletePaths(listings);
@@ -4360,13 +4379,15 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4360
4379
  incompleteDirsRef.current = next;
4361
4380
  return next;
4362
4381
  }, [listings]);
4363
- const { treeMap, parentToChildrenMap } = useMemo(() => {
4364
- return buildTreeFromUserStorages(localStorages, {
4365
- forceUserHierarchy: showUserHierarchy,
4366
- unsortedParents: incompleteDirs
4367
- });
4368
- }, [
4369
- localStorages,
4382
+ const [buildTree] = useState(createTreeBuilder);
4383
+ const { treeMap, parentToChildrenMap, rootNodes } = useMemo(() => buildTree(storages, {
4384
+ listings,
4385
+ forceUserHierarchy: showUserHierarchy,
4386
+ unsortedParents: incompleteDirs
4387
+ }), [
4388
+ buildTree,
4389
+ storages,
4390
+ listings,
4370
4391
  showUserHierarchy,
4371
4392
  incompleteDirs
4372
4393
  ]);
@@ -4427,11 +4448,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4427
4448
  const isUploadSizeExceeded = (size) => {
4428
4449
  return size > MAX_UPLOAD_SIZE;
4429
4450
  };
4430
- const rootNodes = useMemo(() => {
4431
- const childPaths = /* @__PURE__ */ new Set();
4432
- for (const children of Object.values(parentToChildrenMap)) for (const child of children) childPaths.add(child.path);
4433
- return Object.values(treeMap).filter((node) => node.type === "directory" && node.path.endsWith("/") && !childPaths.has(node.path));
4434
- }, [treeMap, parentToChildrenMap]);
4435
4451
  const handleToggleExpand = useCallback((path) => {
4436
4452
  setExpandedPaths((prev) => {
4437
4453
  const next = new Set(prev);
@@ -4443,7 +4459,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4443
4459
  setExpandedPaths(new Set([""]));
4444
4460
  }, []);
4445
4461
  const getStorageProviderAndClient = async (storageId) => {
4446
- const storage = localStorages.find((s) => s.id === storageId);
4462
+ const storage = storages.find((s) => s.id === storageId);
4447
4463
  if (!storage) {
4448
4464
  notify.error(t.messages.storageUnavailable);
4449
4465
  return;
@@ -4473,17 +4489,12 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4473
4489
  const silent = !more && (options?.silent ?? false);
4474
4490
  const nodeStorageId = node.storageId ?? "";
4475
4491
  if (!options?.replace && fetchesInFlightRef.current.has(node.path)) return { corsError: false };
4476
- fetchesInFlightRef.current.add(node.path);
4492
+ const request = silent ? void 0 : {};
4493
+ if (request) fetchesInFlightRef.current.set(node.path, request);
4494
+ const owns = () => request !== void 0 && fetchesInFlightRef.current.get(node.path) === request;
4477
4495
  if (more) setListings((prev) => beginLoadingMore(prev, node.path));
4478
- else {
4479
- if (options?.replace) {
4480
- setListings((prev) => clearListingsUnder(prev, node.path));
4481
- forgetPathsUnder(listAttemptsRef.current, node.path);
4482
- }
4483
- if (!silent) setLoadingPaths((prev) => new Set(prev).add(node.path));
4484
- }
4496
+ else if (request) setLoadingPaths((prev) => new Set(prev).add(node.path));
4485
4497
  const cursor = more ? listings.get(node.path)?.cursor : void 0;
4486
- const pagesBefore = options?.replace ? 0 : listings.get(node.path)?.pagesLoaded ?? 0;
4487
4498
  try {
4488
4499
  const factory = await getStorageProviderAndClient(node.storageId);
4489
4500
  if (!factory) throw new Error("Failed to connect to storage provider.");
@@ -4493,21 +4504,17 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4493
4504
  const dir_path = node.root ? "" : node.relativePath ?? "";
4494
4505
  const input = provider.listDirectoryInput(storage, dir_path, cursor ? { cursor } : {});
4495
4506
  const objects = await client.listDirectory(input);
4496
- const newObjects = provider.convertDataToStorageObjects(objects);
4507
+ const entries = provider.convertDataToStorageObjects(objects);
4497
4508
  const nextCursor = provider.nextPageCursor?.(objects);
4498
- const updatedStorage = {
4499
- ...storage,
4500
- objects: nextObjects(storage.objects || [], newObjects, {
4501
- replace: options?.replace ?? false,
4502
- dirPath: dir_path
4503
- })
4504
- };
4505
- if (!silent) {
4506
- setLocalStorages((prev) => prev.map((s) => s.id === storage.id ? updatedStorage : s));
4507
- setListings((prev) => advanceListing(prev, node.path, nextCursor ? { cursor: nextCursor } : {}, pagesBefore));
4509
+ if (owns()) {
4510
+ const page = {
4511
+ entries,
4512
+ cursor: nextCursor
4513
+ };
4514
+ setListings((prev) => options?.replace ? replaceListing(prev, node.path, page) : advanceListing(prev, node.path, page));
4508
4515
  }
4509
4516
  }
4510
- if (!silent) setHasCorsIssue((prev) => {
4517
+ if (owns()) setHasCorsIssue((prev) => {
4511
4518
  if (!prev.has(nodeStorageId)) return prev;
4512
4519
  const next = new Map(prev);
4513
4520
  next.delete(nodeStorageId);
@@ -4515,26 +4522,23 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4515
4522
  });
4516
4523
  return { corsError: false };
4517
4524
  } catch (error) {
4518
- const errorMessage = error instanceof CorsError ? error.message || "CORS error" : error instanceof Error ? error.message : "Unknown error";
4519
- setListings((prev) => recordListingError(prev, node.path, errorMessage));
4520
- if (error instanceof CorsError) {
4521
- if (!silent) {
4522
- const message = `Failed to load contents for ${node?.displayName || node?.name}: ${errorMessage}`;
4523
- updateConnectionIssuesMessage((prev) => new Map(prev).set(nodeStorageId, message));
4524
- setHasCorsIssue((prev) => new Map(prev).set(nodeStorageId, true));
4525
- }
4526
- return { corsError: true };
4525
+ const isCorsError = error instanceof CorsError;
4526
+ const errorMessage = isCorsError ? error.message || "CORS error" : error instanceof Error ? error.message : "Unknown error";
4527
+ if (owns()) {
4528
+ setListings((prev) => recordListingError(options?.replace ? clearListingsUnder(prev, node.path) : prev, node.path, errorMessage));
4529
+ updateConnectionIssuesMessage((prev) => new Map(prev).set(nodeStorageId, `Failed to load contents for ${node?.displayName || node?.name}: ${errorMessage}`));
4530
+ if (isCorsError) setHasCorsIssue((prev) => new Map(prev).set(nodeStorageId, true));
4527
4531
  }
4528
- const message = `Failed to load contents for ${node?.displayName || node?.name}: ${errorMessage}`;
4529
- updateConnectionIssuesMessage((prev) => new Map(prev).set(nodeStorageId, message));
4530
- return { corsError: false };
4532
+ return { corsError: isCorsError };
4531
4533
  } finally {
4532
- fetchesInFlightRef.current.delete(node.path);
4533
- if (!more && !silent) setLoadingPaths((prev) => {
4534
- const next = new Set(prev);
4535
- next.delete(node.path);
4536
- return next;
4537
- });
4534
+ if (owns()) {
4535
+ fetchesInFlightRef.current.delete(node.path);
4536
+ if (!more) setLoadingPaths((prev) => {
4537
+ const next = new Set(prev);
4538
+ next.delete(node.path);
4539
+ return next;
4540
+ });
4541
+ }
4538
4542
  }
4539
4543
  });
4540
4544
  const handleSelectTreeItem = async (path, node) => {
@@ -4556,8 +4560,8 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4556
4560
  return next;
4557
4561
  });
4558
4562
  }, [normalizedSelectedPath]);
4559
- const [stuckPath, setStuckPath] = useState(null);
4560
- const hydrationStuck = stuckPath === normalizedSelectedPath;
4563
+ const [stuck, setStuck] = useState(null);
4564
+ const hydrationStuck = stuck?.target === normalizedSelectedPath;
4561
4565
  const hydrationPagesRef = useRef({
4562
4566
  path: "",
4563
4567
  pages: 0
@@ -4577,18 +4581,19 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4577
4581
  },
4578
4582
  listings,
4579
4583
  inFlight: fetchesInFlightRef.current,
4580
- attempted: listAttemptsRef.current,
4581
4584
  pagesPulled: hydrationPagesRef.current.pages
4582
4585
  });
4583
4586
  if (step.kind === "stuck") {
4584
- setStuckPath(normalizedSelectedPath);
4587
+ setStuck({
4588
+ target: normalizedSelectedPath,
4589
+ deepest: step.path
4590
+ });
4585
4591
  return;
4586
4592
  }
4587
4593
  if (step.kind === "done") return;
4588
4594
  const node = treeMap[step.path];
4589
4595
  if (!node) return;
4590
4596
  if (step.kind === "fetch") {
4591
- listAttemptsRef.current.add(step.path);
4592
4597
  fetchNodeChildren(node);
4593
4598
  return;
4594
4599
  }
@@ -4602,8 +4607,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4602
4607
  useEffect(() => {
4603
4608
  for (const path of expandedPaths) {
4604
4609
  const node = treeMap[path];
4605
- if (node?.type === "directory" && node.storageId && !isListed(listings, path) && !listAttemptsRef.current.has(path) && !fetchesInFlightRef.current.has(path)) {
4606
- listAttemptsRef.current.add(path);
4610
+ if (node?.type === "directory" && node.storageId && !listings.has(path) && !fetchesInFlightRef.current.has(path)) {
4607
4611
  fetchNodeChildren(node);
4608
4612
  return;
4609
4613
  }
@@ -4806,7 +4810,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4806
4810
  };
4807
4811
  const openCreateFolder = async (node) => {
4808
4812
  if (!isStorageDirectory(node)) return;
4809
- if (!parentToChildrenMap[node.path]) await fetchNodeChildren(node);
4813
+ if (!isListed(listings, node.path)) await fetchNodeChildren(node);
4810
4814
  setCreateFolderTarget(node);
4811
4815
  };
4812
4816
  const getCreateFolderActions = (node, storage) => {
@@ -4859,15 +4863,11 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4859
4863
  icon: isFile ? /* @__PURE__ */ jsx(FileTextIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {}),
4860
4864
  onSelect: () => handleSelectTreeItem(node.path, node)
4861
4865
  }];
4862
- const uriExtras = [{
4863
- label: "PW URI",
4864
- value: pwUri,
4866
+ const uriExtras = [pwUri, cspUri].filter((uri) => !!uri).map((value) => ({
4867
+ label: `${getUriScheme(value).toUpperCase()} URI`,
4868
+ value,
4865
4869
  icon: /* @__PURE__ */ jsx(LinkIcon, {})
4866
- }, {
4867
- label: "S3 URI",
4868
- value: cspUri,
4869
- icon: /* @__PURE__ */ jsx(LinkIcon, {})
4870
- }].filter((u) => u.value);
4870
+ }));
4871
4871
  return assembleRowMenu({
4872
4872
  lead,
4873
4873
  actions,
@@ -4921,7 +4921,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4921
4921
  else if (selectedNode && selectedNode.type === "file") filesToDownload = [selectedNode];
4922
4922
  if (filesToDownload.length === 0) return "";
4923
4923
  const commands = filesToDownload.map((node) => {
4924
- const storage = localStorages.find((s) => s.id === node.storageId);
4924
+ const storage = storages.find((s) => s.id === node.storageId);
4925
4925
  if (!storage?.user || !storage.name) return "";
4926
4926
  return `pw buckets cp ${shellSingleQuote(getPwUri(storage.user, storage.name, getObjectKeyFromNode(node)))} ${shellSingleQuote(`./${node.name}`)}`;
4927
4927
  });
@@ -5053,7 +5053,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5053
5053
  notify.error(t.messages.uploadUnsupported);
5054
5054
  return;
5055
5055
  }
5056
- const sessionId = fileExplorerContext.addUploadSession(storageName, cleanTargetPath, pendingUpload.uploadNodes, client, provider, targetNode, fetchNodeChildren);
5056
+ const sessionId = fileExplorerContext.addUploadSession(storageName, cleanTargetPath, pendingUpload.uploadNodes, client, provider, targetNode, (node) => fetchNodeChildren(node, { replace: true }));
5057
5057
  const sessionQueueIndex = fileExplorerContext.getUploadQueueIndex(sessionId);
5058
5058
  if (sessionQueueIndex === -1) {
5059
5059
  notify.update(sessionId, {
@@ -5181,12 +5181,13 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5181
5181
  children: !selectedNode && normalizedSelectedPath ? hydrationStuck ? /* @__PURE__ */ jsx(NotFoundPreview, {
5182
5182
  path: normalizedSelectedPath,
5183
5183
  onRetry: () => {
5184
- listAttemptsRef.current.clear();
5184
+ const deepest = stuck?.deepest;
5185
5185
  hydrationPagesRef.current = {
5186
5186
  path: "",
5187
5187
  pages: 0
5188
5188
  };
5189
- setStuckPath(null);
5189
+ setStuck(null);
5190
+ if (deepest) setListings((prev) => hasMore(prev, deepest) ? clearListingError(prev, deepest) : clearListingsUnder(prev, deepest));
5190
5191
  }
5191
5192
  }) : /* @__PURE__ */ jsx(ListSkeleton, {}) : !selectedNode && showUserHierarchy ? /* @__PURE__ */ jsxs("div", {
5192
5193
  className: "overflow-auto h-full",
@@ -5228,7 +5229,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5228
5229
  selectedStorage: null,
5229
5230
  onRefresh: handleRefresh
5230
5231
  })]
5231
- }) : !selectedNode ? /* @__PURE__ */ jsx(NoFileSelectedPreview, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
5232
+ }) : !selectedNode ? /* @__PURE__ */ jsx(NoFileSelectedPreview, {}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
5232
5233
  /* @__PURE__ */ jsxs("div", {
5233
5234
  className: "flex items-center justify-between gap-3 px-4 h-12 shrink-0 border-b theme-border",
5234
5235
  children: [renderBreadcrumb(), /* @__PURE__ */ jsxs("div", {
@@ -5320,7 +5321,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5320
5321
  className: "text-sm rounded-sm hover:theme-hover cursor-pointer w-full items-center px-2 py-1.5",
5321
5322
  onClick: handleDownload,
5322
5323
  children: "From browser"
5323
- }) }), selectedNodeSupportsPwUri && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, { className: "my-0.5 h-px theme-border" }), /* @__PURE__ */ jsx(MenuItem, { children: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
5324
+ }) }), selectedNodeSupportsPwUri && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(MenuSeparator, { className: "my-0.5 h-px theme-border" }), /* @__PURE__ */ jsx(MenuItem, { children: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
5324
5325
  className: "text-sm w-full text-center pointer-events-none px-2 py-1.5",
5325
5326
  children: "Using PW CLI"
5326
5327
  }), (() => {
@@ -5391,7 +5392,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5391
5392
  onSelectAll: handleBatchCheckbox
5392
5393
  })
5393
5394
  }), /* @__PURE__ */ jsx(NoDataAvailablePreview, {
5394
- selectedStorage: localStorages.find((s) => s.id === selectedNode.storageId) || null,
5395
+ selectedStorage: storages.find((s) => s.id === selectedNode.storageId) || null,
5395
5396
  canWrite: selectedNodeCanWrite,
5396
5397
  onRefresh: handleRefresh,
5397
5398
  error: hasCorsIssue.get(selectedNode.storageId ?? "") ? new CorsError(connectionIssuesMessage.get(selectedNode.storageId ?? "") || "") : null,
@@ -5424,7 +5425,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5424
5425
  })]
5425
5426
  }), effectiveTab === "preview" && previewable ? /* @__PURE__ */ jsx(FilePreviewInline, {
5426
5427
  node: selectedNode,
5427
- storage: localStorages.find((s) => s.id === selectedNode.storageId) ?? null,
5428
+ storage: storages.find((s) => s.id === selectedNode.storageId) ?? null,
5428
5429
  getPresignedUrl,
5429
5430
  onOpenFull: openPreview,
5430
5431
  onDownload: downloadNode,
@@ -5456,7 +5457,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5456
5457
  className: "tabular-nums",
5457
5458
  children: formatFileSize(selectedNode.size)
5458
5459
  }),
5459
- modifiedDt && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
5460
+ modifiedDt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
5460
5461
  "aria-hidden": true,
5461
5462
  children: "·"
5462
5463
  }), /* @__PURE__ */ jsxs("span", { children: ["Modified ", modifiedDt.toRelative()] })] })
@@ -5486,21 +5487,21 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5486
5487
  }), /* @__PURE__ */ jsxs("dl", {
5487
5488
  className: "grid grid-cols-[minmax(0,140px)_1fr] gap-x-6 gap-y-2 text-sm",
5488
5489
  children: [
5489
- typeof selectedNode.size === "number" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5490
+ typeof selectedNode.size === "number" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5490
5491
  className: "theme-muted-text",
5491
5492
  children: t.chrome.size
5492
5493
  }), /* @__PURE__ */ jsx("dd", {
5493
5494
  className: "tabular-nums",
5494
5495
  children: formatFileSize(selectedNode.size)
5495
5496
  })] }),
5496
- selectedNode.contentType && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5497
+ selectedNode.contentType && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5497
5498
  className: "theme-muted-text",
5498
5499
  children: "Content Type"
5499
5500
  }), /* @__PURE__ */ jsx("dd", {
5500
5501
  className: "font-mono text-xs",
5501
5502
  children: selectedNode.contentType
5502
5503
  })] }),
5503
- createdDt && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5504
+ createdDt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5504
5505
  className: "theme-muted-text",
5505
5506
  children: t.chrome.created
5506
5507
  }), /* @__PURE__ */ jsxs("dd", { children: [
@@ -5511,7 +5512,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5511
5512
  children: ["· ", createdDt.toRelative()]
5512
5513
  })
5513
5514
  ] })] }),
5514
- modifiedDt && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5515
+ modifiedDt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5515
5516
  className: "theme-muted-text",
5516
5517
  children: "Last Modified"
5517
5518
  }), /* @__PURE__ */ jsxs("dd", { children: [
@@ -5522,14 +5523,14 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5522
5523
  children: ["· ", modifiedDt.toRelative()]
5523
5524
  })
5524
5525
  ] })] }),
5525
- selectedNode.hash && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5526
+ selectedNode.hash && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5526
5527
  className: "theme-muted-text",
5527
5528
  children: t.chrome.hash
5528
5529
  }), /* @__PURE__ */ jsx("dd", {
5529
5530
  className: "font-mono text-xs break-all",
5530
5531
  children: selectedNode.hash.replace(/^"|"$/g, "")
5531
5532
  })] }),
5532
- selectedNode.storageClass && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5533
+ selectedNode.storageClass && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5533
5534
  className: "theme-muted-text",
5534
5535
  children: "Storage Class"
5535
5536
  }), /* @__PURE__ */ jsx("dd", {
@@ -5551,7 +5552,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5551
5552
  open: previewOpen,
5552
5553
  node: previewNode,
5553
5554
  siblings: previewSiblings,
5554
- storage: localStorages.find((s) => s.id === previewNode?.storageId) ?? null,
5555
+ storage: storages.find((s) => s.id === previewNode?.storageId) ?? null,
5555
5556
  getPresignedUrl,
5556
5557
  onNavigate: setPreviewNode,
5557
5558
  onClose: closePreview,
@@ -5658,7 +5659,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5658
5659
  open: accessDrawerOpen,
5659
5660
  setOpen: setAccessDrawerOpen,
5660
5661
  groups,
5661
- storage: localStorages.find((s) => s.id === selectedNode.storageId)
5662
+ storage: storages.find((s) => s.id === selectedNode.storageId)
5662
5663
  })
5663
5664
  ]
5664
5665
  });