Skip to content

Commit dee6c0b

Browse files
claude[bot]claude
andauthored
fix(trees): allow resetPaths with preparedInput and no paths (#917)
* fix(trees): allow resetPaths with preparedInput and no paths The public types required `paths` on `resetPaths` even when the caller supplied `preparedInput`, although `resolveFileTreeInput` already short-circuits when `paths == null && preparedInput != null`. Callers using `preparePresortedFileTreeInput` were forced to pass a dummy `paths` list, which `PathStore.preparePaths` then re-parsed and re-sorted — the exact work the presorted API exists to skip. Add a preparedInput-only overload to `resetPaths` (on both the public `FileTreeMutationHandle` and the `FileTreeController`/`FileTree` runtimes) so exactly one of `paths` or `preparedInput` is required; passing neither is a type error. Runtime already supported the branch; the implementation now normalizes the single-argument options form. Fixes #670 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDXG4jRZT8sPS1YBSes2pR * style(trees): wrap long resetPaths delegate line for oxfmt CI root:format-check (oxfmt) wrapped the single-arg resetPaths delegate call that exceeded the print width. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDXG4jRZT8sPS1YBSes2pR --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e58bc4b commit dee6c0b

5 files changed

Lines changed: 105 additions & 6 deletions

File tree

packages/trees/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export type {
7474
FileTreeRenderProps,
7575
FileTreeResetEvent,
7676
FileTreeResetOptions,
77+
FileTreeResetPreparedOptions,
7778
FileTreeRowDecoration,
7879
FileTreeRowDecorationContext,
7980
FileTreeRowDecorationRenderer,

packages/trees/src/model/FileTreeController.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import type {
5555
FileTreeRenamingConfig,
5656
FileTreeResetEvent,
5757
FileTreeResetOptions,
58+
FileTreeResetPreparedOptions,
5859
FileTreeScrollOffset,
5960
FileTreeScrollToPathOptions,
6061
FileTreeSearchMode,
@@ -1252,8 +1253,20 @@ export class FileTreeController
12521253
*/
12531254
public resetPaths(
12541255
paths: readonly string[],
1255-
options: FileTreeResetOptions = {}
1256+
options?: FileTreeResetOptions
1257+
): void;
1258+
public resetPaths(options: FileTreeResetPreparedOptions): void;
1259+
public resetPaths(
1260+
pathsOrOptions: readonly string[] | FileTreeResetPreparedOptions,
1261+
maybeOptions: FileTreeResetOptions = {}
12561262
): void {
1263+
const usingPreparedOnlyOverload = !Array.isArray(pathsOrOptions);
1264+
const paths = usingPreparedOnlyOverload
1265+
? undefined
1266+
: (pathsOrOptions as readonly string[]);
1267+
const options: FileTreeResetOptions = usingPreparedOnlyOverload
1268+
? (pathsOrOptions as FileTreeResetPreparedOptions)
1269+
: maybeOptions;
12571270
const previousPathCount = this.#store.list().length;
12581271
const previousVisibleCount = this.#visibleCount;
12591272
const resolvedInput = resolveFileTreeInput(

packages/trees/src/model/publicTypes.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,16 +323,27 @@ export type FileTreeMutationEventForType<
323323
? FileTreeMutationEvent
324324
: Extract<FileTreeMutationEvent, { operation: TType }>;
325325

326-
export interface FileTreeResetOptions {
326+
interface FileTreeResetBehaviorOptions {
327327
// When provided, replaces the baseline expansion set stored at construction
328328
// time. Useful when the caller is swapping in a dramatically different path
329329
// list (e.g. upgrading from an SSR preview to a full dataset) and wants the
330330
// fresh store to start with expansion state that reflects the new paths.
331331
initialExpandedPaths?: readonly FileTreePublicId[];
332-
// Must describe the same path list passed to resetPaths(paths, ...).
333-
preparedInput?: FileTreePreparedInput;
334332
}
335333

334+
export type FileTreeResetOptions = FileTreeResetBehaviorOptions & {
335+
// When omitted, the raw `paths` argument describes the tree. When provided,
336+
// it must describe the same path list passed to resetPaths(paths, ...).
337+
preparedInput?: FileTreePreparedInput;
338+
};
339+
340+
// Options shape for the preparedInput-only `resetPaths` overload. `paths` may be
341+
// omitted entirely because `preparedInput` already carries the canonical path
342+
// list, so the runtime skips the parse + sort that raw `paths` would require.
343+
export type FileTreeResetPreparedOptions = FileTreeResetBehaviorOptions & {
344+
preparedInput: FileTreePreparedInput;
345+
};
346+
336347
export interface FileTreeMutationHandle {
337348
add(path: FileTreePublicId): void;
338349
batch(operations: readonly FileTreeBatchOperation[]): void;
@@ -346,10 +357,14 @@ export interface FileTreeMutationHandle {
346357
handler: (event: FileTreeMutationEventForType<TType>) => void
347358
): () => void;
348359
remove(path: FileTreePublicId, options?: FileTreeRemoveOptions): void;
360+
// Exactly one of `paths` or `preparedInput` is required. Pass raw `paths`
361+
// (optionally with a matching `preparedInput`), or pass `preparedInput`
362+
// alone to skip the parse + sort that raw `paths` would otherwise require.
349363
resetPaths(
350364
paths: readonly FileTreePublicId[],
351365
options?: FileTreeResetOptions
352366
): void;
367+
resetPaths(options: FileTreeResetPreparedOptions): void;
353368
}
354369

355370
export type FileTreeListener = () => void;

packages/trees/src/render/FileTree.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import type {
4343
FileTreeRemoveOptions,
4444
FileTreeRenderProps,
4545
FileTreeResetOptions,
46+
FileTreeResetPreparedOptions,
4647
FileTreeRowDecorationRenderer,
4748
FileTreeScrollToPathOptions,
4849
FileTreeSearchSessionHandle,
@@ -438,8 +439,19 @@ export class FileTree
438439
public resetPaths(
439440
paths: readonly string[],
440441
options?: FileTreeResetOptions
442+
): void;
443+
public resetPaths(options: FileTreeResetPreparedOptions): void;
444+
public resetPaths(
445+
pathsOrOptions: readonly string[] | FileTreeResetPreparedOptions,
446+
options?: FileTreeResetOptions
441447
): void {
442-
this.#controller.resetPaths(paths, options);
448+
if (Array.isArray(pathsOrOptions)) {
449+
this.#controller.resetPaths(pathsOrOptions as readonly string[], options);
450+
} else {
451+
this.#controller.resetPaths(
452+
pathsOrOptions as FileTreeResetPreparedOptions
453+
);
454+
}
443455
}
444456

445457
// Deliberately rerenders even when the same object reference is passed again.

packages/trees/test/file-tree-dynamic-files-mutation-api.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { describe, expect, test } from 'bun:test';
22
import { JSDOM } from 'jsdom';
33

4-
import { prepareFileTreeInput } from '../src/index';
4+
import {
5+
prepareFileTreeInput,
6+
preparePresortedFileTreeInput,
7+
} from '../src/index';
58
import { flushDom, installDom } from './helpers/dom';
69
import { loadFileTree, loadFileTreeController } from './helpers/loadFileTree';
710

@@ -141,6 +144,61 @@ describe('file-tree dynamic files / mutation API', () => {
141144
controller.destroy();
142145
});
143146

147+
test('resetPaths accepts a preparedInput-only call with no raw paths', async () => {
148+
const FileTreeController = await loadFileTreeController();
149+
150+
// Server-canonical (already sorted) folder paths, the scale-oriented input
151+
// that preparePresortedFileTreeInput exists to fast-path.
152+
const folderPaths = ['src/', 'src/index.ts', 'README.md'] as const;
153+
const controller = new FileTreeController({
154+
flattenEmptyDirectories: false,
155+
initialExpansion: 'open',
156+
paths: ['README.md'],
157+
});
158+
159+
// The preparedInput-only overload: no dummy `paths` argument required.
160+
controller.resetPaths({
161+
preparedInput: preparePresortedFileTreeInput([...folderPaths]),
162+
});
163+
164+
expect(controller.getVisibleRows(0, 3).map((row) => row.path)).toEqual([
165+
'src/',
166+
'src/index.ts',
167+
'README.md',
168+
]);
169+
170+
controller.destroy();
171+
});
172+
173+
test('resetPaths overloads type-check as documented (compile-only assertions)', async () => {
174+
const FileTreeController = await loadFileTreeController();
175+
176+
const controller = new FileTreeController({
177+
flattenEmptyDirectories: false,
178+
initialExpansion: 'open',
179+
paths: ['README.md'],
180+
});
181+
182+
const assertOverloadTyping = (): void => {
183+
// Passing only preparedInput type-checks: `paths` is no longer required.
184+
controller.resetPaths({
185+
preparedInput: prepareFileTreeInput(['README.md']),
186+
});
187+
188+
// Passing only raw paths still type-checks.
189+
controller.resetPaths(['README.md']);
190+
191+
// @ts-expect-error neither paths nor preparedInput is supplied
192+
controller.resetPaths({});
193+
194+
// @ts-expect-error neither paths nor preparedInput is supplied
195+
controller.resetPaths();
196+
};
197+
void assertOverloadTyping;
198+
199+
controller.destroy();
200+
});
201+
144202
test('typed onMutation listeners stay filtered while subscribe still tracks non-mutation rerenders', async () => {
145203
const FileTreeController = await loadFileTreeController();
146204

0 commit comments

Comments
 (0)