@marimo-team/islands 0.23.10-dev3 → 0.23.10-dev30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-DmBropAy.js} +31 -31
- package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
- package/dist/{any-language-editor-DfdpyDv_.js → any-language-editor-DNmoSiWL.js} +20 -20
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
- package/dist/assets/worker-CC0Oul9k.js +73 -0
- package/dist/{chat-ui-C1tL1pML.js → chat-ui-D6oraHT2.js} +76 -76
- package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
- package/dist/{code-visibility-DfnO0DcH.js → code-visibility-wR7WSQ4c.js} +2166 -1292
- package/dist/{copy-BuQpJEzp.js → copy-UqRYxiOg.js} +33 -33
- package/dist/dist-7QfXoMdB.js +5 -0
- package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
- package/dist/dist-BEXXyZig.js +5 -0
- package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
- package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
- package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
- package/dist/dist-BYj57OV4.js +5 -0
- package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
- package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
- package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
- package/dist/dist-BlSvQzNr.js +5 -0
- package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
- package/dist/dist-CCBlxAgS.js +8 -0
- package/dist/dist-CIDTVIUf.js +5 -0
- package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
- package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
- package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
- package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
- package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
- package/dist/dist-CqQyhAM8.js +8 -0
- package/dist/dist-CwRu2Xzh.js +5 -0
- package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
- package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
- package/dist/dist-D8CDTVgf.js +6 -0
- package/dist/dist-D8DNB0nO.js +8 -0
- package/dist/dist-DL6N_q-A.js +5 -0
- package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
- package/dist/dist-DOFbNV_b.js +8 -0
- package/dist/dist-DPrYzMY0.js +6 -0
- package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
- package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
- package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
- package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
- package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
- package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
- package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
- package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
- package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
- package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
- package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
- package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
- package/dist/dist-cdmMjgsn.js +5 -0
- package/dist/dist-hT4QzYX-.js +1247 -0
- package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
- package/dist/{error-banner-5bz0L9hS.js → error-banner-Cc0I3C9e.js} +1 -1
- package/dist/esm-BaH2eg5-.js +1171 -0
- package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
- package/dist/{extends-BgdxCfYu.js → extends-D_hDsj6R.js} +4 -4
- package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
- package/dist/{glide-data-editor-BOmK9ETQ.js → glide-data-editor-Qhu8oCX-.js} +12 -12
- package/dist/{html-to-image-CNa5ok96.js → html-to-image-UEH5lFDZ.js} +2318 -2275
- package/dist/{input-_2sjvfne.js → input-CMYy4hzj.js} +187 -185
- package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
- package/dist/main.js +6941 -6913
- package/dist/{mermaid-lXOw5Py9.js → mermaid-zuLgJ8J8.js} +4 -4
- package/dist/{process-output-DKr4f1di.js → process-output-CyMLTogj.js} +3 -3
- package/dist/{reveal-component-UdMnCK5U.js → reveal-component-BjnkUAZ9.js} +697 -619
- package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
- package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
- package/dist/style.css +1 -1
- package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
- package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
- package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
- package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
- package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
- package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
- package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
- package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
- package/dist/{vega-component-Dq-SH463.js → vega-component-9h1ACS78.js} +8 -8
- package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
- package/package.json +3 -3
- package/src/components/data-table/TableBottomBar.tsx +1 -15
- package/src/components/data-table/TableTopBar.tsx +8 -13
- package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +154 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
- package/src/components/data-table/data-table.tsx +1 -1
- package/src/components/data-table/filter-by-values-picker.tsx +39 -17
- package/src/components/data-table/filter-pills.tsx +1 -1
- package/src/components/data-table/hover-tooltip/__tests__/content.test.ts +60 -0
- package/src/components/data-table/hover-tooltip/content.ts +44 -0
- package/src/components/data-table/hover-tooltip/hover-tooltip.tsx +55 -0
- package/src/components/data-table/hover-tooltip/use-table-hover-tooltip.ts +159 -0
- package/src/components/data-table/renderers.tsx +27 -43
- package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
- package/src/components/datasources/datasources.tsx +92 -3
- package/src/components/editor/cell/cell-context-menu.tsx +15 -2
- package/src/components/editor/cell/code/language-toggle.tsx +7 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
- package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
- package/src/components/editor/documentation.css +35 -0
- package/src/components/editor/file-tree/file-explorer.tsx +8 -18
- package/src/components/editor/file-tree/tree-actions.tsx +46 -1
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
- package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
- package/src/components/slides/minimap.tsx +127 -10
- package/src/components/slides/reveal-component.tsx +287 -61
- package/src/components/slides/slide-cell-view.tsx +26 -2
- package/src/components/slides/slide-form.tsx +26 -4
- package/src/components/storage/__tests__/storage-inspector.test.ts +53 -0
- package/src/components/storage/storage-inspector.tsx +68 -48
- package/src/components/ui/__tests__/use-toast.test.ts +75 -0
- package/src/components/ui/combobox.tsx +51 -32
- package/src/components/ui/reorderable-list.tsx +13 -0
- package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
- package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
- package/src/components/ui/select-core/index.ts +16 -0
- package/src/components/ui/select-core/option-row.tsx +33 -0
- package/src/components/ui/select-core/render-slot.ts +20 -0
- package/src/components/ui/select-core/select-list.tsx +248 -0
- package/src/components/ui/select-core/types.ts +44 -0
- package/src/components/ui/select-core/use-select-list.ts +347 -0
- package/src/components/ui/select-core/utils.ts +121 -0
- package/src/components/ui/use-toast.ts +33 -13
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -28
- package/src/core/cells/__tests__/cell.test.ts +29 -2
- package/src/core/cells/cell.ts +5 -1
- package/src/core/codemirror/go-to-definition/commands.ts +4 -3
- package/src/core/codemirror/language/languages/python.ts +2 -0
- package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
- package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
- package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
- package/src/core/datasets/data-source-connections.ts +2 -0
- package/src/core/network/__tests__/requests-static.test.ts +30 -0
- package/src/core/network/requests-static.ts +14 -10
- package/src/core/wasm/worker/bootstrap.ts +12 -4
- package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/layout/DownloadPlugin.tsx +1 -1
- package/src/utils/lazy.ts +6 -1
- package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
- package/dist/assets/worker-ip3AI_sN.js +0 -73
- package/dist/dist-0Fif7jnk.js +0 -5
- package/dist/dist-B5h_9sHB.js +0 -6
- package/dist/dist-B9M6R5ye.js +0 -5
- package/dist/dist-BCt3tnck.js +0 -8
- package/dist/dist-BUIJwMwn.js +0 -8
- package/dist/dist-BpquMd3k.js +0 -5
- package/dist/dist-BzJsqYfz.js +0 -5
- package/dist/dist-CA5ELXAf.js +0 -6
- package/dist/dist-CLBRs6Uv.js +0 -5
- package/dist/dist-CStVCMbq.js +0 -5
- package/dist/dist-CZRIEY3Y.js +0 -8
- package/dist/dist-CuUHbFD0.js +0 -5
- package/dist/dist-DV7Iabxb.js +0 -8
- package/dist/dist-DhHh0jLg.js +0 -1247
- package/dist/dist-DuEeHMvL.js +0 -5
- package/dist/esm-BfhQmZjp.js +0 -1171
- package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
- /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import type { StorageEntry } from "@/core/storage/types";
|
|
4
|
+
import { storageEntryKey } from "../storage-inspector";
|
|
5
|
+
|
|
6
|
+
function makeEntry(
|
|
7
|
+
overrides: Partial<StorageEntry> & { path: string },
|
|
8
|
+
): StorageEntry {
|
|
9
|
+
return {
|
|
10
|
+
kind: overrides.kind ?? "file",
|
|
11
|
+
lastModified: overrides.lastModified ?? null,
|
|
12
|
+
metadata: overrides.metadata ?? {},
|
|
13
|
+
path: overrides.path,
|
|
14
|
+
size: overrides.size ?? 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("storageEntryKey", () => {
|
|
19
|
+
it("prefers the backend id when present (e.g. Google Drive)", () => {
|
|
20
|
+
const entry = makeEntry({
|
|
21
|
+
path: "Data Resume.pdf",
|
|
22
|
+
metadata: { id: "drive-file-id-123" },
|
|
23
|
+
});
|
|
24
|
+
// Two files can share a path on Drive; the id keeps keys unique.
|
|
25
|
+
expect(storageEntryKey(entry, 0)).toBe("drive-file-id-123");
|
|
26
|
+
expect(storageEntryKey(entry, 4)).toBe("drive-file-id-123");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("falls back to path + index when there is no id", () => {
|
|
30
|
+
const entry = makeEntry({ path: "Data Resume.pdf" });
|
|
31
|
+
expect(storageEntryKey(entry, 0)).toBe("Data Resume.pdf::0");
|
|
32
|
+
expect(storageEntryKey(entry, 4)).toBe("Data Resume.pdf::4");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps duplicate-path entries unique via the index fallback", () => {
|
|
36
|
+
const entries = [
|
|
37
|
+
makeEntry({ path: "Data Resume.pdf" }),
|
|
38
|
+
makeEntry({ path: "Data Resume.pdf" }),
|
|
39
|
+
makeEntry({ path: "Data Resume.pdf" }),
|
|
40
|
+
];
|
|
41
|
+
const keys = entries.map((entry, index) => storageEntryKey(entry, index));
|
|
42
|
+
expect(new Set(keys).size).toBe(entries.length);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("ignores a non-string or empty id", () => {
|
|
46
|
+
expect(
|
|
47
|
+
storageEntryKey(makeEntry({ path: "a.pdf", metadata: { id: "" } }), 2),
|
|
48
|
+
).toBe("a.pdf::2");
|
|
49
|
+
expect(
|
|
50
|
+
storageEntryKey(makeEntry({ path: "a.pdf", metadata: { id: 5 } }), 2),
|
|
51
|
+
).toBe("a.pdf::2");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -94,15 +94,31 @@ function displayName(path: string): string {
|
|
|
94
94
|
return parts[parts.length - 1] || trimmed;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Stable, unique identity for an entry row. Prefer the
|
|
99
|
+
* backend's stable id when present and fall back to the list index
|
|
100
|
+
*/
|
|
101
|
+
export function storageEntryKey(entry: StorageEntry, index: number): string {
|
|
102
|
+
const id = entry.metadata?.id;
|
|
103
|
+
if (typeof id === "string" && id.length > 0) {
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
return `${entry.path}::${index}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SearchContext {
|
|
110
|
+
namespace: string;
|
|
111
|
+
searchValue: string;
|
|
112
|
+
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>;
|
|
113
|
+
}
|
|
114
|
+
|
|
97
115
|
/**
|
|
98
116
|
* Recursively check whether an entry (or any of its loaded descendants)
|
|
99
117
|
* matches the search query.
|
|
100
118
|
*/
|
|
101
119
|
function entryMatchesSearch(
|
|
102
120
|
entry: StorageEntry,
|
|
103
|
-
namespace:
|
|
104
|
-
searchValue: string,
|
|
105
|
-
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>,
|
|
121
|
+
{ namespace, searchValue, entriesByPath }: SearchContext,
|
|
106
122
|
): boolean {
|
|
107
123
|
const query = searchValue.toLowerCase();
|
|
108
124
|
|
|
@@ -115,7 +131,7 @@ function entryMatchesSearch(
|
|
|
115
131
|
const children = entriesByPath.get(storagePathKey(namespace, entry.path));
|
|
116
132
|
if (children) {
|
|
117
133
|
return children.some((child) =>
|
|
118
|
-
entryMatchesSearch(child, namespace, searchValue, entriesByPath),
|
|
134
|
+
entryMatchesSearch(child, { namespace, searchValue, entriesByPath }),
|
|
119
135
|
);
|
|
120
136
|
}
|
|
121
137
|
}
|
|
@@ -129,16 +145,12 @@ function entryMatchesSearch(
|
|
|
129
145
|
*/
|
|
130
146
|
function filterEntries(
|
|
131
147
|
entries: StorageEntry[],
|
|
132
|
-
|
|
133
|
-
searchValue: string,
|
|
134
|
-
entriesByPath: ReadonlyMap<StoragePathKey, StorageEntry[]>,
|
|
148
|
+
context: SearchContext,
|
|
135
149
|
): StorageEntry[] {
|
|
136
|
-
if (!searchValue.trim()) {
|
|
150
|
+
if (!context.searchValue.trim()) {
|
|
137
151
|
return entries;
|
|
138
152
|
}
|
|
139
|
-
return entries.filter((entry) =>
|
|
140
|
-
entryMatchesSearch(entry, namespace, searchValue, entriesByPath),
|
|
141
|
-
);
|
|
153
|
+
return entries.filter((entry) => entryMatchesSearch(entry, context));
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
/**
|
|
@@ -204,35 +216,39 @@ const StorageEntryChildren: React.FC<{
|
|
|
204
216
|
);
|
|
205
217
|
}
|
|
206
218
|
|
|
207
|
-
const filtered = filterEntries(
|
|
208
|
-
children,
|
|
219
|
+
const filtered = filterEntries(children, {
|
|
209
220
|
namespace,
|
|
210
221
|
searchValue,
|
|
211
222
|
entriesByPath,
|
|
212
|
-
);
|
|
223
|
+
});
|
|
213
224
|
|
|
214
225
|
return (
|
|
215
226
|
<>
|
|
216
|
-
{filtered.map((child) =>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
{filtered.map((child) => {
|
|
228
|
+
const rowKey = storageEntryKey(child, children.indexOf(child));
|
|
229
|
+
return (
|
|
230
|
+
<StorageEntryRow
|
|
231
|
+
key={rowKey}
|
|
232
|
+
rowKey={rowKey}
|
|
233
|
+
entry={child}
|
|
234
|
+
namespace={namespace}
|
|
235
|
+
protocol={protocol}
|
|
236
|
+
rootPath={rootPath}
|
|
237
|
+
backendType={backendType}
|
|
238
|
+
depth={depth}
|
|
239
|
+
locale={locale}
|
|
240
|
+
searchValue={searchValue}
|
|
241
|
+
onOpenFile={onOpenFile}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
})}
|
|
230
245
|
</>
|
|
231
246
|
);
|
|
232
247
|
};
|
|
233
248
|
|
|
234
249
|
const StorageEntryRow: React.FC<{
|
|
235
250
|
entry: StorageEntry;
|
|
251
|
+
rowKey: string;
|
|
236
252
|
namespace: string;
|
|
237
253
|
protocol: string;
|
|
238
254
|
rootPath: string;
|
|
@@ -243,6 +259,7 @@ const StorageEntryRow: React.FC<{
|
|
|
243
259
|
onOpenFile: (info: OpenFileInfo) => void;
|
|
244
260
|
}> = ({
|
|
245
261
|
entry,
|
|
262
|
+
rowKey,
|
|
246
263
|
namespace,
|
|
247
264
|
protocol,
|
|
248
265
|
rootPath,
|
|
@@ -271,7 +288,7 @@ const StorageEntryRow: React.FC<{
|
|
|
271
288
|
!!entriesByPath
|
|
272
289
|
.get(storagePathKey(namespace, entry.path))
|
|
273
290
|
?.some((child) =>
|
|
274
|
-
entryMatchesSearch(child, namespace, searchValue, entriesByPath),
|
|
291
|
+
entryMatchesSearch(child, { namespace, searchValue, entriesByPath }),
|
|
275
292
|
);
|
|
276
293
|
|
|
277
294
|
// Folder is shown expanded by manual toggle OR by search auto-expand
|
|
@@ -312,7 +329,7 @@ const StorageEntryRow: React.FC<{
|
|
|
312
329
|
isDir && "font-medium",
|
|
313
330
|
)}
|
|
314
331
|
style={indentStyle(depth)}
|
|
315
|
-
value={`${namespace}:${
|
|
332
|
+
value={`${namespace}:${rowKey}`}
|
|
316
333
|
onSelect={() => {
|
|
317
334
|
if (isDir) {
|
|
318
335
|
setIsExpanded(!effectiveExpanded);
|
|
@@ -458,12 +475,11 @@ const StorageNamespaceSection: React.FC<{
|
|
|
458
475
|
|
|
459
476
|
// While loading, fall back to initial entries from the namespace notification
|
|
460
477
|
const entries = isPending ? namespace.storageEntries : fetchedEntries;
|
|
461
|
-
const filtered = filterEntries(
|
|
462
|
-
|
|
463
|
-
namespaceName,
|
|
478
|
+
const filtered = filterEntries(entries, {
|
|
479
|
+
namespace: namespaceName,
|
|
464
480
|
searchValue,
|
|
465
481
|
entriesByPath,
|
|
466
|
-
);
|
|
482
|
+
});
|
|
467
483
|
|
|
468
484
|
return (
|
|
469
485
|
<>
|
|
@@ -525,20 +541,24 @@ const StorageNamespaceSection: React.FC<{
|
|
|
525
541
|
No matches
|
|
526
542
|
</div>
|
|
527
543
|
)}
|
|
528
|
-
{filtered.map((entry) =>
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
544
|
+
{filtered.map((entry) => {
|
|
545
|
+
const rowKey = storageEntryKey(entry, entries.indexOf(entry));
|
|
546
|
+
return (
|
|
547
|
+
<StorageEntryRow
|
|
548
|
+
key={rowKey}
|
|
549
|
+
rowKey={rowKey}
|
|
550
|
+
entry={entry}
|
|
551
|
+
namespace={namespaceName}
|
|
552
|
+
protocol={namespace.protocol}
|
|
553
|
+
rootPath={namespace.rootPath}
|
|
554
|
+
backendType={namespace.backendType}
|
|
555
|
+
depth={1}
|
|
556
|
+
locale={locale}
|
|
557
|
+
searchValue={searchValue}
|
|
558
|
+
onOpenFile={onOpenFile}
|
|
559
|
+
/>
|
|
560
|
+
);
|
|
561
|
+
})}
|
|
542
562
|
</>
|
|
543
563
|
)}
|
|
544
564
|
</>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { toast, useToast } from "@/components/ui/use-toast";
|
|
5
|
+
|
|
6
|
+
describe("toast once", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
const { result } = renderHook(() => useToast());
|
|
13
|
+
act(() => {
|
|
14
|
+
result.current.dismiss();
|
|
15
|
+
vi.runAllTimers();
|
|
16
|
+
});
|
|
17
|
+
vi.useRealTimers();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("shows a once-toast a single time per session, even after removal", () => {
|
|
21
|
+
const { result } = renderHook(() => useToast());
|
|
22
|
+
|
|
23
|
+
act(() => {
|
|
24
|
+
toast({ id: "static-notebook", once: true, title: "Static notebook" });
|
|
25
|
+
});
|
|
26
|
+
expect(result.current.toasts).toHaveLength(1);
|
|
27
|
+
|
|
28
|
+
act(() => {
|
|
29
|
+
result.current.dismiss("static-notebook");
|
|
30
|
+
vi.advanceTimersByTime(10_000);
|
|
31
|
+
});
|
|
32
|
+
expect(result.current.toasts).toHaveLength(0);
|
|
33
|
+
|
|
34
|
+
act(() => {
|
|
35
|
+
toast({ id: "static-notebook", once: true, title: "Static notebook" });
|
|
36
|
+
});
|
|
37
|
+
expect(result.current.toasts).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("does not suppress normal (non-once) toasts", () => {
|
|
41
|
+
const { result } = renderHook(() => useToast());
|
|
42
|
+
|
|
43
|
+
act(() => {
|
|
44
|
+
toast({ id: "normal", title: "First" });
|
|
45
|
+
});
|
|
46
|
+
act(() => {
|
|
47
|
+
result.current.dismiss("normal");
|
|
48
|
+
vi.advanceTimersByTime(10_000);
|
|
49
|
+
});
|
|
50
|
+
expect(result.current.toasts).toHaveLength(0);
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
toast({ id: "normal", title: "First" });
|
|
54
|
+
});
|
|
55
|
+
expect(result.current.toasts).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("does not dedupe a once-toast without a stable id", () => {
|
|
59
|
+
const { result } = renderHook(() => useToast());
|
|
60
|
+
|
|
61
|
+
act(() => {
|
|
62
|
+
toast({ once: true, title: "No id" });
|
|
63
|
+
});
|
|
64
|
+
act(() => {
|
|
65
|
+
result.current.dismiss();
|
|
66
|
+
vi.advanceTimersByTime(10_000);
|
|
67
|
+
});
|
|
68
|
+
expect(result.current.toasts).toHaveLength(0);
|
|
69
|
+
|
|
70
|
+
act(() => {
|
|
71
|
+
toast({ once: true, title: "No id" });
|
|
72
|
+
});
|
|
73
|
+
expect(result.current.toasts).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
4
4
|
import { Check, ChevronDownIcon, XCircle } from "lucide-react";
|
|
5
|
-
import React, { createContext } from "react";
|
|
5
|
+
import React, { createContext, useCallback, useMemo } from "react";
|
|
6
6
|
import { cn } from "../../utils/cn";
|
|
7
7
|
import { Functions } from "../../utils/functions";
|
|
8
8
|
import { Badge } from "./badge";
|
|
@@ -40,6 +40,8 @@ interface ComboboxCommonProps<TValue> {
|
|
|
40
40
|
id?: string;
|
|
41
41
|
keepPopoverOpenOnSelect?: boolean;
|
|
42
42
|
disabled?: boolean;
|
|
43
|
+
/** Override the trigger contents with a node (e.g. a compact chip summary). */
|
|
44
|
+
renderValue?: (value: TValue[] | TValue | null) => React.ReactNode;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
type ComboboxFilterProps =
|
|
@@ -97,6 +99,7 @@ export const Combobox = <TValue,>({
|
|
|
97
99
|
keepPopoverOpenOnSelect,
|
|
98
100
|
id,
|
|
99
101
|
disabled = false,
|
|
102
|
+
renderValue,
|
|
100
103
|
...rest
|
|
101
104
|
}: ComboboxProps<TValue>) => {
|
|
102
105
|
const [open = false, setOpen] = useControllableState({
|
|
@@ -112,39 +115,45 @@ export const Combobox = <TValue,>({
|
|
|
112
115
|
},
|
|
113
116
|
});
|
|
114
117
|
|
|
115
|
-
const isSelected = (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
const isSelected = useCallback(
|
|
119
|
+
(selectedValue: unknown) => {
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
return value.includes(selectedValue as TValue);
|
|
122
|
+
}
|
|
123
|
+
return value === selectedValue;
|
|
124
|
+
},
|
|
125
|
+
[value],
|
|
126
|
+
);
|
|
121
127
|
|
|
122
|
-
const handleSelect = (
|
|
123
|
-
|
|
128
|
+
const handleSelect = useCallback(
|
|
129
|
+
(selectedValue: unknown) => {
|
|
130
|
+
let newValue: TValue | TValue[] | null = selectedValue as TValue;
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
if (multiple) {
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
if (value.includes(newValue)) {
|
|
135
|
+
const newArr = value.filter((val) => val !== selectedValue);
|
|
136
|
+
newValue = newArr.length > 0 ? newArr : [];
|
|
137
|
+
} else {
|
|
138
|
+
newValue = [...value, newValue];
|
|
139
|
+
}
|
|
130
140
|
} else {
|
|
131
|
-
newValue = [
|
|
141
|
+
newValue = [newValue];
|
|
132
142
|
}
|
|
133
|
-
} else {
|
|
134
|
-
newValue =
|
|
143
|
+
} else if (value === selectedValue) {
|
|
144
|
+
newValue = null;
|
|
135
145
|
}
|
|
136
|
-
} else if (value === selectedValue) {
|
|
137
|
-
newValue = null;
|
|
138
|
-
}
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
setValue(newValue);
|
|
148
|
+
const keepOpen = keepPopoverOpenOnSelect ?? multiple;
|
|
149
|
+
if (!keepOpen) {
|
|
150
|
+
setOpen(false);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[keepPopoverOpenOnSelect, multiple, value, setValue, setOpen],
|
|
154
|
+
);
|
|
146
155
|
|
|
147
|
-
const
|
|
156
|
+
const renderValueLabel = (): string => {
|
|
148
157
|
// If we show chips, we don't want to change the placeholder
|
|
149
158
|
if (multiple && chips && placeholder) {
|
|
150
159
|
return placeholder;
|
|
@@ -168,6 +177,14 @@ export const Combobox = <TValue,>({
|
|
|
168
177
|
return placeholder ?? "--";
|
|
169
178
|
};
|
|
170
179
|
|
|
180
|
+
const comboboxContextValue: ComboboxContextValue = useMemo(
|
|
181
|
+
() => ({
|
|
182
|
+
isSelected,
|
|
183
|
+
onSelect: handleSelect,
|
|
184
|
+
}),
|
|
185
|
+
[isSelected, handleSelect],
|
|
186
|
+
);
|
|
187
|
+
|
|
171
188
|
return (
|
|
172
189
|
<div className={cn("relative")} {...rest}>
|
|
173
190
|
<Popover
|
|
@@ -191,7 +208,9 @@ export const Combobox = <TValue,>({
|
|
|
191
208
|
aria-expanded={open}
|
|
192
209
|
aria-disabled={disabled}
|
|
193
210
|
>
|
|
194
|
-
<span className="truncate flex-1 min-w-0">
|
|
211
|
+
<span className="truncate flex-1 min-w-0">
|
|
212
|
+
{renderValue ? renderValue(value ?? null) : renderValueLabel()}
|
|
213
|
+
</span>
|
|
195
214
|
<ChevronDownIcon className="ml-3 w-4 h-4 opacity-50 shrink-0" />
|
|
196
215
|
</button>
|
|
197
216
|
</PopoverTrigger>
|
|
@@ -209,7 +228,7 @@ export const Combobox = <TValue,>({
|
|
|
209
228
|
/>
|
|
210
229
|
<CommandList className="max-h-60 py-.5">
|
|
211
230
|
<CommandEmpty>{emptyState}</CommandEmpty>
|
|
212
|
-
<ComboboxContext value={
|
|
231
|
+
<ComboboxContext value={comboboxContextValue}>
|
|
213
232
|
{children}
|
|
214
233
|
</ComboboxContext>
|
|
215
234
|
</CommandList>
|
|
@@ -277,12 +296,14 @@ export const ComboboxItem = React.forwardRef(
|
|
|
277
296
|
? value.value
|
|
278
297
|
: String(value);
|
|
279
298
|
const context = React.use(ComboboxContext);
|
|
299
|
+
const isOptionSelected = context.isSelected(value);
|
|
280
300
|
|
|
281
301
|
return (
|
|
282
302
|
<CommandItem
|
|
283
303
|
ref={ref}
|
|
284
304
|
className={cn("pl-6 m-1 py-1", className)}
|
|
285
305
|
role="option"
|
|
306
|
+
aria-selected={isOptionSelected}
|
|
286
307
|
value={valueAsString}
|
|
287
308
|
disabled={disabled}
|
|
288
309
|
onSelect={() => {
|
|
@@ -290,9 +311,7 @@ export const ComboboxItem = React.forwardRef(
|
|
|
290
311
|
onSelect?.(value);
|
|
291
312
|
}}
|
|
292
313
|
>
|
|
293
|
-
{
|
|
294
|
-
<Check className="absolute left-1 h-4 w-4" />
|
|
295
|
-
)}
|
|
314
|
+
{isOptionSelected && <Check className="absolute left-1 h-4 w-4" />}
|
|
296
315
|
{children}
|
|
297
316
|
</CommandItem>
|
|
298
317
|
);
|
|
@@ -57,6 +57,12 @@ export interface ReorderableListProps<T> {
|
|
|
57
57
|
* Callback when an item is clicked
|
|
58
58
|
*/
|
|
59
59
|
onAction?: (item: T) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Fired when an item is hovered or focused. Useful for preloading the
|
|
62
|
+
* resource the item points to before it is activated. Attached to the
|
|
63
|
+
* focusable list item so it works for both pointer and keyboard users.
|
|
64
|
+
*/
|
|
65
|
+
onItemPreloadHint?: (item: T) => void;
|
|
60
66
|
/**
|
|
61
67
|
* All available items that can be added to the list
|
|
62
68
|
*/
|
|
@@ -142,6 +148,7 @@ export const ReorderableList = <T extends object>({
|
|
|
142
148
|
ariaLabel = "Reorderable list",
|
|
143
149
|
className,
|
|
144
150
|
crossListDrag,
|
|
151
|
+
onItemPreloadHint,
|
|
145
152
|
}: ReorderableListProps<T>) => {
|
|
146
153
|
const mimeType = crossListDrag
|
|
147
154
|
? getDragMimeType(crossListDrag.dragType)
|
|
@@ -294,6 +301,12 @@ export const ReorderableList = <T extends object>({
|
|
|
294
301
|
key={getKey(item)}
|
|
295
302
|
id={getKey(item)}
|
|
296
303
|
className="active:cursor-grabbing data-[dragging]:opacity-60 outline-none"
|
|
304
|
+
onHoverStart={
|
|
305
|
+
onItemPreloadHint ? () => onItemPreloadHint(item) : undefined
|
|
306
|
+
}
|
|
307
|
+
onFocus={
|
|
308
|
+
onItemPreloadHint ? () => onItemPreloadHint(item) : undefined
|
|
309
|
+
}
|
|
297
310
|
>
|
|
298
311
|
{renderItem(item)}
|
|
299
312
|
</ListBoxItem>
|