@marimo-team/islands 0.23.10-dev5 → 0.23.10-dev50
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-Z3RP7Vmm.js} +31 -31
- package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
- package/dist/{any-language-editor-DfdpyDv_.js → any-language-editor-CAgFD4Kd.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-1rLrACXD.js → chat-ui-BxwraqS-.js} +77 -77
- package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
- package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
- package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
- package/dist/{code-visibility-Dek9JNl9.js → code-visibility-DKAnHkUo.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-CJXYJ6Sb.js} +11 -6
- 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/files/wasm-intro.py +11 -8
- package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
- package/dist/{glide-data-editor-BOmK9ETQ.js → glide-data-editor-BPkCPs7L.js} +255 -255
- package/dist/{html-to-image-CnBdHeLh.js → html-to-image-DXSdAI_Y.js} +2338 -2279
- package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
- package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
- package/dist/main.js +7631 -7559
- package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
- package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
- package/dist/{process-output-vFgqkyUT.js → process-output-uozPQhBl.js} +3 -3
- package/dist/{reveal-component-C8AmnVfj.js → reveal-component-BxE_9kae.js} +698 -620
- 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-DGPUhbDs.js} +8 -8
- package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
- package/package.json +4 -3
- package/src/components/chat/chat-display.tsx +2 -2
- 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/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/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
- package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
- package/src/components/editor/connections/storage/as-code.ts +20 -6
- 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/markdown/__tests__/markdown-renderer.test.tsx +43 -0
- package/src/components/markdown/markdown-renderer.tsx +24 -1
- 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 +115 -44
- package/src/components/ui/__tests__/use-toast.test.ts +75 -0
- package/src/components/ui/combobox.tsx +51 -32
- package/src/components/ui/number-field.tsx +5 -1
- 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/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/storage/__tests__/state.test.ts +49 -0
- package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
- package/src/core/storage/state.ts +124 -13
- package/src/core/storage/types.ts +6 -0
- package/src/core/wasm/worker/bootstrap.ts +12 -4
- package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
- package/src/plugins/core/registerReactComponent.tsx +42 -10
- package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/SliderPlugin.tsx +132 -18
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
- package/src/plugins/layout/DownloadPlugin.tsx +1 -1
- package/src/utils/__tests__/numbers.test.ts +20 -0
- package/src/utils/errors.ts +9 -0
- package/src/utils/lazy.ts +6 -1
- package/src/utils/numbers.ts +27 -0
- 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,18 +145,41 @@ 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
|
|
|
156
|
+
const LoadMoreStorageEntries: React.FC<{
|
|
157
|
+
depth: number;
|
|
158
|
+
isLoading: boolean;
|
|
159
|
+
error?: Error;
|
|
160
|
+
onLoadMore: () => void;
|
|
161
|
+
}> = ({ depth, isLoading, error, onLoadMore }) => {
|
|
162
|
+
return (
|
|
163
|
+
<div className="py-px text-xs" style={indentStyle(depth)}>
|
|
164
|
+
<Button
|
|
165
|
+
variant="text"
|
|
166
|
+
size="xs"
|
|
167
|
+
className="h-6 px-0 hover:text-blue-600"
|
|
168
|
+
disabled={isLoading}
|
|
169
|
+
onClick={onLoadMore}
|
|
170
|
+
>
|
|
171
|
+
{isLoading && <LoaderCircle className="h-3 w-3 mr-1 animate-spin" />}
|
|
172
|
+
{isLoading ? "Loading..." : "Load more"}
|
|
173
|
+
</Button>
|
|
174
|
+
{error && (
|
|
175
|
+
<span className="ml-2 text-destructive">
|
|
176
|
+
Failed to load: {error.message}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
144
183
|
/**
|
|
145
184
|
* Lazily loaded children of a directory entry.
|
|
146
185
|
* Caches fetched entries in the Jotai store so re-expanding doesn't re-fetch.
|
|
@@ -171,6 +210,10 @@ const StorageEntryChildren: React.FC<{
|
|
|
171
210
|
entries: children,
|
|
172
211
|
isPending,
|
|
173
212
|
error,
|
|
213
|
+
hasMore,
|
|
214
|
+
loadMore,
|
|
215
|
+
isLoadingMore,
|
|
216
|
+
loadMoreError,
|
|
174
217
|
} = useStorageEntries(namespace, prefix);
|
|
175
218
|
|
|
176
219
|
if (isPending) {
|
|
@@ -204,35 +247,47 @@ const StorageEntryChildren: React.FC<{
|
|
|
204
247
|
);
|
|
205
248
|
}
|
|
206
249
|
|
|
207
|
-
const filtered = filterEntries(
|
|
208
|
-
children,
|
|
250
|
+
const filtered = filterEntries(children, {
|
|
209
251
|
namespace,
|
|
210
252
|
searchValue,
|
|
211
253
|
entriesByPath,
|
|
212
|
-
);
|
|
254
|
+
});
|
|
213
255
|
|
|
214
256
|
return (
|
|
215
257
|
<>
|
|
216
|
-
{filtered.map((child) =>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
{filtered.map((child) => {
|
|
259
|
+
const rowKey = storageEntryKey(child, children.indexOf(child));
|
|
260
|
+
return (
|
|
261
|
+
<StorageEntryRow
|
|
262
|
+
key={rowKey}
|
|
263
|
+
rowKey={rowKey}
|
|
264
|
+
entry={child}
|
|
265
|
+
namespace={namespace}
|
|
266
|
+
protocol={protocol}
|
|
267
|
+
rootPath={rootPath}
|
|
268
|
+
backendType={backendType}
|
|
269
|
+
depth={depth}
|
|
270
|
+
locale={locale}
|
|
271
|
+
searchValue={searchValue}
|
|
272
|
+
onOpenFile={onOpenFile}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
})}
|
|
276
|
+
{hasMore && (
|
|
277
|
+
<LoadMoreStorageEntries
|
|
224
278
|
depth={depth}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
279
|
+
isLoading={isLoadingMore}
|
|
280
|
+
error={loadMoreError}
|
|
281
|
+
onLoadMore={loadMore}
|
|
228
282
|
/>
|
|
229
|
-
)
|
|
283
|
+
)}
|
|
230
284
|
</>
|
|
231
285
|
);
|
|
232
286
|
};
|
|
233
287
|
|
|
234
288
|
const StorageEntryRow: React.FC<{
|
|
235
289
|
entry: StorageEntry;
|
|
290
|
+
rowKey: string;
|
|
236
291
|
namespace: string;
|
|
237
292
|
protocol: string;
|
|
238
293
|
rootPath: string;
|
|
@@ -243,6 +298,7 @@ const StorageEntryRow: React.FC<{
|
|
|
243
298
|
onOpenFile: (info: OpenFileInfo) => void;
|
|
244
299
|
}> = ({
|
|
245
300
|
entry,
|
|
301
|
+
rowKey,
|
|
246
302
|
namespace,
|
|
247
303
|
protocol,
|
|
248
304
|
rootPath,
|
|
@@ -271,7 +327,7 @@ const StorageEntryRow: React.FC<{
|
|
|
271
327
|
!!entriesByPath
|
|
272
328
|
.get(storagePathKey(namespace, entry.path))
|
|
273
329
|
?.some((child) =>
|
|
274
|
-
entryMatchesSearch(child, namespace, searchValue, entriesByPath),
|
|
330
|
+
entryMatchesSearch(child, { namespace, searchValue, entriesByPath }),
|
|
275
331
|
);
|
|
276
332
|
|
|
277
333
|
// Folder is shown expanded by manual toggle OR by search auto-expand
|
|
@@ -312,7 +368,7 @@ const StorageEntryRow: React.FC<{
|
|
|
312
368
|
isDir && "font-medium",
|
|
313
369
|
)}
|
|
314
370
|
style={indentStyle(depth)}
|
|
315
|
-
value={`${namespace}:${
|
|
371
|
+
value={`${namespace}:${rowKey}`}
|
|
316
372
|
onSelect={() => {
|
|
317
373
|
if (isDir) {
|
|
318
374
|
setIsExpanded(!effectiveExpanded);
|
|
@@ -444,6 +500,10 @@ const StorageNamespaceSection: React.FC<{
|
|
|
444
500
|
entries: fetchedEntries,
|
|
445
501
|
isPending,
|
|
446
502
|
error,
|
|
503
|
+
hasMore,
|
|
504
|
+
loadMore,
|
|
505
|
+
isLoadingMore,
|
|
506
|
+
loadMoreError,
|
|
447
507
|
refetch,
|
|
448
508
|
} = useStorageEntries(namespaceName);
|
|
449
509
|
|
|
@@ -458,12 +518,11 @@ const StorageNamespaceSection: React.FC<{
|
|
|
458
518
|
|
|
459
519
|
// While loading, fall back to initial entries from the namespace notification
|
|
460
520
|
const entries = isPending ? namespace.storageEntries : fetchedEntries;
|
|
461
|
-
const filtered = filterEntries(
|
|
462
|
-
|
|
463
|
-
namespaceName,
|
|
521
|
+
const filtered = filterEntries(entries, {
|
|
522
|
+
namespace: namespaceName,
|
|
464
523
|
searchValue,
|
|
465
524
|
entriesByPath,
|
|
466
|
-
);
|
|
525
|
+
});
|
|
467
526
|
|
|
468
527
|
return (
|
|
469
528
|
<>
|
|
@@ -525,20 +584,32 @@ const StorageNamespaceSection: React.FC<{
|
|
|
525
584
|
No matches
|
|
526
585
|
</div>
|
|
527
586
|
)}
|
|
528
|
-
{filtered.map((entry) =>
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
587
|
+
{filtered.map((entry) => {
|
|
588
|
+
const rowKey = storageEntryKey(entry, entries.indexOf(entry));
|
|
589
|
+
return (
|
|
590
|
+
<StorageEntryRow
|
|
591
|
+
key={rowKey}
|
|
592
|
+
rowKey={rowKey}
|
|
593
|
+
entry={entry}
|
|
594
|
+
namespace={namespaceName}
|
|
595
|
+
protocol={namespace.protocol}
|
|
596
|
+
rootPath={namespace.rootPath}
|
|
597
|
+
backendType={namespace.backendType}
|
|
598
|
+
depth={1}
|
|
599
|
+
locale={locale}
|
|
600
|
+
searchValue={searchValue}
|
|
601
|
+
onOpenFile={onOpenFile}
|
|
602
|
+
/>
|
|
603
|
+
);
|
|
604
|
+
})}
|
|
605
|
+
{hasMore && (
|
|
606
|
+
<LoadMoreStorageEntries
|
|
536
607
|
depth={1}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
608
|
+
isLoading={isLoadingMore}
|
|
609
|
+
error={loadMoreError}
|
|
610
|
+
onLoadMore={loadMore}
|
|
540
611
|
/>
|
|
541
|
-
)
|
|
612
|
+
)}
|
|
542
613
|
</>
|
|
543
614
|
)}
|
|
544
615
|
</>
|
|
@@ -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
|
);
|
|
@@ -25,7 +25,10 @@ export interface NumberFieldProps extends AriaNumberFieldProps {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
28
|
-
(
|
|
28
|
+
(
|
|
29
|
+
{ placeholder, variant = "default", onInputText, formatOptions, ...props },
|
|
30
|
+
ref,
|
|
31
|
+
) => {
|
|
29
32
|
const { locale } = useLocale();
|
|
30
33
|
return (
|
|
31
34
|
<AriaNumberField
|
|
@@ -33,6 +36,7 @@ export const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
33
36
|
formatOptions={{
|
|
34
37
|
minimumFractionDigits: 0,
|
|
35
38
|
maximumFractionDigits: maxFractionalDigits(locale),
|
|
39
|
+
...formatOptions,
|
|
36
40
|
}}
|
|
37
41
|
>
|
|
38
42
|
<div
|
|
@@ -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>
|