@parallelworks/ui 0.15.0 → 0.17.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 (38) hide show
  1. package/dist/chunks/{CodePreviewEditor-DuMZMA_k.js → CodePreviewEditor-UAtnpmOV.js} +1 -1
  2. package/dist/chunks/{CreateModal-B7smkHkv.js → CreateModal-BbV5cTj7.js} +3 -3
  3. package/dist/chunks/{Monaco-BXDH2jVj.js → Monaco-DVyIpyv4.js} +485 -172
  4. package/dist/chunks/{PageHeader-CoZWF0pJ.js → PageHeader-MxuOs9cH.js} +1 -1
  5. package/dist/chunks/{Provider-CbFVFB7O.js → Provider-DHmZ3v9i.js} +28 -2
  6. package/dist/chunks/{StatusBadge-zvEtK16a.js → StatusBadge-C1Blx_cW.js} +23 -6
  7. package/dist/chunks/{SwitchToggle-3V836Cn7.js → SwitchToggle-BRQoEwLZ.js} +7 -4
  8. package/dist/chunks/{Table-CutP1bRt.js → Table-C6JdFFiJ.js} +1 -1
  9. package/dist/chunks/{editor-Ko3zX22Y.js → editor-mNznodJN.js} +2 -2
  10. package/dist/chunks/{list-CduoUsMW.js → list-7k0eaI5N.js} +21 -19
  11. package/dist/components/Button.d.ts +2 -1
  12. package/dist/components/Provider.d.ts +57 -2
  13. package/dist/components/SectionHeader.d.ts +3 -1
  14. package/dist/components/StatusBadge.d.ts +7 -2
  15. package/dist/components/relativeTime.d.ts +15 -0
  16. package/dist/editor/index.js +1 -1
  17. package/dist/editor/usesGitlab.d.ts +97 -0
  18. package/dist/file-explorer/index.js +253 -253
  19. package/dist/file-explorer/lib/paging.d.ts +27 -28
  20. package/dist/file-explorer/lib/types.d.ts +0 -1
  21. package/dist/file-explorer/lib/utils.d.ts +16 -5
  22. package/dist/form/index.js +5 -5
  23. package/dist/form/types/workflow-schema.d.ts +38 -0
  24. package/dist/index.d.ts +4 -2
  25. package/dist/index.js +42 -11
  26. package/dist/list/ListPage.d.ts +3 -1
  27. package/dist/list/ListPager.d.ts +3 -1
  28. package/dist/list/ListTable.d.ts +2 -1
  29. package/dist/list/index.js +2 -2
  30. package/dist/list/listView.d.ts +1 -1
  31. package/dist/logviewer/index.js +1 -1
  32. package/dist/styles/theme.css +14 -12
  33. package/dist/styles.css +1 -1
  34. package/dist/workflow/DependencyGraph.d.ts +2 -0
  35. package/dist/workflow/index.d.ts +1 -0
  36. package/dist/workflow/index.js +3 -2
  37. package/dist/workflow/types.d.ts +5 -0
  38. package/package.json +4 -4
@@ -1,21 +1,21 @@
1
1
  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";
2
+ import { c as ConfirmModal, f as Markdown, h as Callout, m as CopyToClipboard, n as CreateModal, p as CopyCodeBlock } from "../chunks/CreateModal-BbV5cTj7.js";
3
+ import { i as useNotify, l as useSlots, o as useProvisionCorsRules, u as useStrings } from "../chunks/Provider-DHmZ3v9i.js";
4
4
  import { n as TOOLTIP_ID, r as TooltipInfo } from "../chunks/Tooltip-n9w7Tkt1.js";
5
- import { t as StatusBadge } from "../chunks/StatusBadge-zvEtK16a.js";
5
+ import { t as StatusBadge } from "../chunks/StatusBadge-C1Blx_cW.js";
6
6
  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";
7
+ import { _ as useRowMenuActive, d as assembleRowMenu, f as copySubmenu, g as useRowMenu, u as MoreButton } from "../chunks/PageHeader-MxuOs9cH.js";
8
+ import { n as Table } from "../chunks/Table-C6JdFFiJ.js";
9
+ import { C as ListSkeleton, E as RowSelectCheckbox, O as listTableFixedProps, k as listTableProps, x as ListActionsHeader } from "../chunks/list-7k0eaI5N.js";
10
10
  import cx from "classnames";
11
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
12
  import { Suspense, createContext, lazy, memo, use, useCallback, useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState, useTransition } from "react";
13
+ import { useLocalStorage } from "usehooks-ts";
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
  }
@@ -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-UAtnpmOV.js"));
2541
2561
  var PREVIEW_MARKDOWN_CONTROLS = {
2542
2562
  table: { fullscreen: false },
2543
2563
  mermaid: { fullscreen: false }
@@ -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;
@@ -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,
@@ -4348,10 +4368,8 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4348
4368
  const notify = useNotify();
4349
4369
  const slots = useSlots();
4350
4370
  const fileExplorerContext = useFileExplorer();
4351
- const [localStorages, setLocalStorages] = useState(storages);
4352
4371
  const [listings, setListings] = useState(() => /* @__PURE__ */ new Map());
4353
- const fetchesInFlightRef = useRef(/* @__PURE__ */ new Set());
4354
- const listAttemptsRef = useRef(/* @__PURE__ */ new Set());
4372
+ const fetchesInFlightRef = useRef(/* @__PURE__ */ new Map());
4355
4373
  const incompleteDirsRef = useRef(/* @__PURE__ */ new Set());
4356
4374
  const incompleteDirs = useMemo(() => {
4357
4375
  const next = incompletePaths(listings);
@@ -4360,13 +4378,15 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4360
4378
  incompleteDirsRef.current = next;
4361
4379
  return next;
4362
4380
  }, [listings]);
4363
- const { treeMap, parentToChildrenMap } = useMemo(() => {
4364
- return buildTreeFromUserStorages(localStorages, {
4365
- forceUserHierarchy: showUserHierarchy,
4366
- unsortedParents: incompleteDirs
4367
- });
4368
- }, [
4369
- localStorages,
4381
+ const [buildTree] = useState(createTreeBuilder);
4382
+ const { treeMap, parentToChildrenMap, rootNodes } = useMemo(() => buildTree(storages, {
4383
+ listings,
4384
+ forceUserHierarchy: showUserHierarchy,
4385
+ unsortedParents: incompleteDirs
4386
+ }), [
4387
+ buildTree,
4388
+ storages,
4389
+ listings,
4370
4390
  showUserHierarchy,
4371
4391
  incompleteDirs
4372
4392
  ]);
@@ -4427,11 +4447,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4427
4447
  const isUploadSizeExceeded = (size) => {
4428
4448
  return size > MAX_UPLOAD_SIZE;
4429
4449
  };
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
4450
  const handleToggleExpand = useCallback((path) => {
4436
4451
  setExpandedPaths((prev) => {
4437
4452
  const next = new Set(prev);
@@ -4443,7 +4458,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4443
4458
  setExpandedPaths(new Set([""]));
4444
4459
  }, []);
4445
4460
  const getStorageProviderAndClient = async (storageId) => {
4446
- const storage = localStorages.find((s) => s.id === storageId);
4461
+ const storage = storages.find((s) => s.id === storageId);
4447
4462
  if (!storage) {
4448
4463
  notify.error(t.messages.storageUnavailable);
4449
4464
  return;
@@ -4473,17 +4488,12 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4473
4488
  const silent = !more && (options?.silent ?? false);
4474
4489
  const nodeStorageId = node.storageId ?? "";
4475
4490
  if (!options?.replace && fetchesInFlightRef.current.has(node.path)) return { corsError: false };
4476
- fetchesInFlightRef.current.add(node.path);
4491
+ const request = silent ? void 0 : {};
4492
+ if (request) fetchesInFlightRef.current.set(node.path, request);
4493
+ const owns = () => request !== void 0 && fetchesInFlightRef.current.get(node.path) === request;
4477
4494
  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
- }
4495
+ else if (request) setLoadingPaths((prev) => new Set(prev).add(node.path));
4485
4496
  const cursor = more ? listings.get(node.path)?.cursor : void 0;
4486
- const pagesBefore = options?.replace ? 0 : listings.get(node.path)?.pagesLoaded ?? 0;
4487
4497
  try {
4488
4498
  const factory = await getStorageProviderAndClient(node.storageId);
4489
4499
  if (!factory) throw new Error("Failed to connect to storage provider.");
@@ -4493,21 +4503,17 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4493
4503
  const dir_path = node.root ? "" : node.relativePath ?? "";
4494
4504
  const input = provider.listDirectoryInput(storage, dir_path, cursor ? { cursor } : {});
4495
4505
  const objects = await client.listDirectory(input);
4496
- const newObjects = provider.convertDataToStorageObjects(objects);
4506
+ const entries = provider.convertDataToStorageObjects(objects);
4497
4507
  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));
4508
+ if (owns()) {
4509
+ const page = {
4510
+ entries,
4511
+ cursor: nextCursor
4512
+ };
4513
+ setListings((prev) => options?.replace ? replaceListing(prev, node.path, page) : advanceListing(prev, node.path, page));
4508
4514
  }
4509
4515
  }
4510
- if (!silent) setHasCorsIssue((prev) => {
4516
+ if (owns()) setHasCorsIssue((prev) => {
4511
4517
  if (!prev.has(nodeStorageId)) return prev;
4512
4518
  const next = new Map(prev);
4513
4519
  next.delete(nodeStorageId);
@@ -4515,26 +4521,23 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4515
4521
  });
4516
4522
  return { corsError: false };
4517
4523
  } 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 };
4524
+ const isCorsError = error instanceof CorsError;
4525
+ const errorMessage = isCorsError ? error.message || "CORS error" : error instanceof Error ? error.message : "Unknown error";
4526
+ if (owns()) {
4527
+ setListings((prev) => recordListingError(options?.replace ? clearListingsUnder(prev, node.path) : prev, node.path, errorMessage));
4528
+ updateConnectionIssuesMessage((prev) => new Map(prev).set(nodeStorageId, `Failed to load contents for ${node?.displayName || node?.name}: ${errorMessage}`));
4529
+ if (isCorsError) setHasCorsIssue((prev) => new Map(prev).set(nodeStorageId, true));
4527
4530
  }
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 };
4531
+ return { corsError: isCorsError };
4531
4532
  } 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
- });
4533
+ if (owns()) {
4534
+ fetchesInFlightRef.current.delete(node.path);
4535
+ if (!more) setLoadingPaths((prev) => {
4536
+ const next = new Set(prev);
4537
+ next.delete(node.path);
4538
+ return next;
4539
+ });
4540
+ }
4538
4541
  }
4539
4542
  });
4540
4543
  const handleSelectTreeItem = async (path, node) => {
@@ -4556,8 +4559,8 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4556
4559
  return next;
4557
4560
  });
4558
4561
  }, [normalizedSelectedPath]);
4559
- const [stuckPath, setStuckPath] = useState(null);
4560
- const hydrationStuck = stuckPath === normalizedSelectedPath;
4562
+ const [stuck, setStuck] = useState(null);
4563
+ const hydrationStuck = stuck?.target === normalizedSelectedPath;
4561
4564
  const hydrationPagesRef = useRef({
4562
4565
  path: "",
4563
4566
  pages: 0
@@ -4577,18 +4580,19 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4577
4580
  },
4578
4581
  listings,
4579
4582
  inFlight: fetchesInFlightRef.current,
4580
- attempted: listAttemptsRef.current,
4581
4583
  pagesPulled: hydrationPagesRef.current.pages
4582
4584
  });
4583
4585
  if (step.kind === "stuck") {
4584
- setStuckPath(normalizedSelectedPath);
4586
+ setStuck({
4587
+ target: normalizedSelectedPath,
4588
+ deepest: step.path
4589
+ });
4585
4590
  return;
4586
4591
  }
4587
4592
  if (step.kind === "done") return;
4588
4593
  const node = treeMap[step.path];
4589
4594
  if (!node) return;
4590
4595
  if (step.kind === "fetch") {
4591
- listAttemptsRef.current.add(step.path);
4592
4596
  fetchNodeChildren(node);
4593
4597
  return;
4594
4598
  }
@@ -4602,8 +4606,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4602
4606
  useEffect(() => {
4603
4607
  for (const path of expandedPaths) {
4604
4608
  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);
4609
+ if (node?.type === "directory" && node.storageId && !listings.has(path) && !fetchesInFlightRef.current.has(path)) {
4607
4610
  fetchNodeChildren(node);
4608
4611
  return;
4609
4612
  }
@@ -4806,7 +4809,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4806
4809
  };
4807
4810
  const openCreateFolder = async (node) => {
4808
4811
  if (!isStorageDirectory(node)) return;
4809
- if (!parentToChildrenMap[node.path]) await fetchNodeChildren(node);
4812
+ if (!isListed(listings, node.path)) await fetchNodeChildren(node);
4810
4813
  setCreateFolderTarget(node);
4811
4814
  };
4812
4815
  const getCreateFolderActions = (node, storage) => {
@@ -4859,15 +4862,11 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4859
4862
  icon: isFile ? /* @__PURE__ */ jsx(FileTextIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {}),
4860
4863
  onSelect: () => handleSelectTreeItem(node.path, node)
4861
4864
  }];
4862
- const uriExtras = [{
4863
- label: "PW URI",
4864
- value: pwUri,
4865
+ const uriExtras = [pwUri, cspUri].filter((uri) => !!uri).map((value) => ({
4866
+ label: `${getUriScheme(value).toUpperCase()} URI`,
4867
+ value,
4865
4868
  icon: /* @__PURE__ */ jsx(LinkIcon, {})
4866
- }, {
4867
- label: "S3 URI",
4868
- value: cspUri,
4869
- icon: /* @__PURE__ */ jsx(LinkIcon, {})
4870
- }].filter((u) => u.value);
4869
+ }));
4871
4870
  return assembleRowMenu({
4872
4871
  lead,
4873
4872
  actions,
@@ -4921,7 +4920,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4921
4920
  else if (selectedNode && selectedNode.type === "file") filesToDownload = [selectedNode];
4922
4921
  if (filesToDownload.length === 0) return "";
4923
4922
  const commands = filesToDownload.map((node) => {
4924
- const storage = localStorages.find((s) => s.id === node.storageId);
4923
+ const storage = storages.find((s) => s.id === node.storageId);
4925
4924
  if (!storage?.user || !storage.name) return "";
4926
4925
  return `pw buckets cp ${shellSingleQuote(getPwUri(storage.user, storage.name, getObjectKeyFromNode(node)))} ${shellSingleQuote(`./${node.name}`)}`;
4927
4926
  });
@@ -5053,7 +5052,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5053
5052
  notify.error(t.messages.uploadUnsupported);
5054
5053
  return;
5055
5054
  }
5056
- const sessionId = fileExplorerContext.addUploadSession(storageName, cleanTargetPath, pendingUpload.uploadNodes, client, provider, targetNode, fetchNodeChildren);
5055
+ const sessionId = fileExplorerContext.addUploadSession(storageName, cleanTargetPath, pendingUpload.uploadNodes, client, provider, targetNode, (node) => fetchNodeChildren(node, { replace: true }));
5057
5056
  const sessionQueueIndex = fileExplorerContext.getUploadQueueIndex(sessionId);
5058
5057
  if (sessionQueueIndex === -1) {
5059
5058
  notify.update(sessionId, {
@@ -5181,12 +5180,13 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5181
5180
  children: !selectedNode && normalizedSelectedPath ? hydrationStuck ? /* @__PURE__ */ jsx(NotFoundPreview, {
5182
5181
  path: normalizedSelectedPath,
5183
5182
  onRetry: () => {
5184
- listAttemptsRef.current.clear();
5183
+ const deepest = stuck?.deepest;
5185
5184
  hydrationPagesRef.current = {
5186
5185
  path: "",
5187
5186
  pages: 0
5188
5187
  };
5189
- setStuckPath(null);
5188
+ setStuck(null);
5189
+ if (deepest) setListings((prev) => hasMore(prev, deepest) ? clearListingError(prev, deepest) : clearListingsUnder(prev, deepest));
5190
5190
  }
5191
5191
  }) : /* @__PURE__ */ jsx(ListSkeleton, {}) : !selectedNode && showUserHierarchy ? /* @__PURE__ */ jsxs("div", {
5192
5192
  className: "overflow-auto h-full",
@@ -5391,7 +5391,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5391
5391
  onSelectAll: handleBatchCheckbox
5392
5392
  })
5393
5393
  }), /* @__PURE__ */ jsx(NoDataAvailablePreview, {
5394
- selectedStorage: localStorages.find((s) => s.id === selectedNode.storageId) || null,
5394
+ selectedStorage: storages.find((s) => s.id === selectedNode.storageId) || null,
5395
5395
  canWrite: selectedNodeCanWrite,
5396
5396
  onRefresh: handleRefresh,
5397
5397
  error: hasCorsIssue.get(selectedNode.storageId ?? "") ? new CorsError(connectionIssuesMessage.get(selectedNode.storageId ?? "") || "") : null,
@@ -5424,7 +5424,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5424
5424
  })]
5425
5425
  }), effectiveTab === "preview" && previewable ? /* @__PURE__ */ jsx(FilePreviewInline, {
5426
5426
  node: selectedNode,
5427
- storage: localStorages.find((s) => s.id === selectedNode.storageId) ?? null,
5427
+ storage: storages.find((s) => s.id === selectedNode.storageId) ?? null,
5428
5428
  getPresignedUrl,
5429
5429
  onOpenFull: openPreview,
5430
5430
  onDownload: downloadNode,
@@ -5551,7 +5551,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5551
5551
  open: previewOpen,
5552
5552
  node: previewNode,
5553
5553
  siblings: previewSiblings,
5554
- storage: localStorages.find((s) => s.id === previewNode?.storageId) ?? null,
5554
+ storage: storages.find((s) => s.id === previewNode?.storageId) ?? null,
5555
5555
  getPresignedUrl,
5556
5556
  onNavigate: setPreviewNode,
5557
5557
  onClose: closePreview,
@@ -5658,7 +5658,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5658
5658
  open: accessDrawerOpen,
5659
5659
  setOpen: setAccessDrawerOpen,
5660
5660
  groups,
5661
- storage: localStorages.find((s) => s.id === selectedNode.storageId)
5661
+ storage: storages.find((s) => s.id === selectedNode.storageId)
5662
5662
  })
5663
5663
  ]
5664
5664
  });