react-arborist 3.10.4 → 3.10.5
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/main/interfaces/tree-api.d.ts +33 -20
- package/dist/main/interfaces/tree-api.js +48 -23
- package/dist/main/interfaces/tree-api.test.js +48 -0
- package/dist/main/types/handlers.d.ts +1 -1
- package/dist/module/interfaces/tree-api.d.ts +33 -20
- package/dist/module/interfaces/tree-api.js +48 -23
- package/dist/module/interfaces/tree-api.test.js +48 -0
- package/dist/module/types/handlers.d.ts +1 -1
- package/package.json +1 -1
- package/src/interfaces/tree-api.test.ts +46 -0
- package/src/interfaces/tree-api.ts +68 -41
- package/src/types/handlers.ts +3 -1
|
@@ -149,6 +149,19 @@ export declare class TreeApi<T> {
|
|
|
149
149
|
get matchFn(): (node: NodeApi<T>) => boolean;
|
|
150
150
|
accessChildren(data: T): readonly T[] | null;
|
|
151
151
|
accessId(data: T): string;
|
|
152
|
+
/**
|
|
153
|
+
* Resolve an identifier to a node id. Public methods accept an id string, a
|
|
154
|
+
* NodeApi, or the raw row data; this is the one place that turns any of those
|
|
155
|
+
* into the string id used internally. Raw data is run through the configured
|
|
156
|
+
* `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
|
|
157
|
+
* just where nodes were built. A NodeApi already carries its accessor-derived
|
|
158
|
+
* `id`, so it is used directly rather than re-accessed (the accessor reads the
|
|
159
|
+
* underlying data, which a NodeApi does not expose under that key). Unlike
|
|
160
|
+
* `accessId`, an unresolved id comes back as `undefined` rather than throwing,
|
|
161
|
+
* preserving the previous behavior of the `id`-only lookup.
|
|
162
|
+
*/
|
|
163
|
+
identify(identity: string | IdObj | T): string;
|
|
164
|
+
identifyNull(identity: Identity | T): string | null;
|
|
152
165
|
get firstNode(): NodeApi<T>;
|
|
153
166
|
get lastNode(): NodeApi<T>;
|
|
154
167
|
get focusedNode(): NodeApi<T> | null;
|
|
@@ -158,7 +171,7 @@ export declare class TreeApi<T> {
|
|
|
158
171
|
get(id: string | null): NodeApi<T> | null;
|
|
159
172
|
at(index: number): NodeApi<T> | null;
|
|
160
173
|
nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
|
|
161
|
-
indexOf(id: Identity): number | null;
|
|
174
|
+
indexOf(id: Identity | T): number | null;
|
|
162
175
|
get editingId(): string | null;
|
|
163
176
|
createInternal(): Promise<void>;
|
|
164
177
|
createLeaf(): Promise<void>;
|
|
@@ -167,36 +180,36 @@ export declare class TreeApi<T> {
|
|
|
167
180
|
parentId?: null | string;
|
|
168
181
|
index?: null | number;
|
|
169
182
|
}): Promise<void>;
|
|
170
|
-
delete(node: Identity | string
|
|
171
|
-
edit(node: string | IdObj): Promise<EditResult>;
|
|
172
|
-
submit(identity: Identity, value: string): Promise<void>;
|
|
183
|
+
delete(node: Identity | T | (string | IdObj | T)[]): Promise<void>;
|
|
184
|
+
edit(node: string | IdObj | T): Promise<EditResult>;
|
|
185
|
+
submit(identity: Identity | T, value: string): Promise<void>;
|
|
173
186
|
reset(): void;
|
|
174
|
-
activate(id: Identity): void;
|
|
187
|
+
activate(id: Identity | T): void;
|
|
175
188
|
private resolveEdit;
|
|
176
189
|
get selectedIds(): Set<string>;
|
|
177
190
|
get selectedNodes(): NodeApi<T>[];
|
|
178
|
-
focus(node: Identity, opts?: {
|
|
191
|
+
focus(node: Identity | T, opts?: {
|
|
179
192
|
scroll?: boolean;
|
|
180
193
|
}): void;
|
|
181
194
|
pageUp(): void;
|
|
182
195
|
pageDown(): void;
|
|
183
|
-
select(node: Identity, opts?: {
|
|
196
|
+
select(node: Identity | T, opts?: {
|
|
184
197
|
align?: Align;
|
|
185
198
|
focus?: boolean;
|
|
186
199
|
}): void;
|
|
187
|
-
deselect(node: Identity): void;
|
|
188
|
-
selectMulti(identity: Identity, opts?: {
|
|
200
|
+
deselect(node: Identity | T): void;
|
|
201
|
+
selectMulti(identity: Identity | T, opts?: {
|
|
189
202
|
align?: Align;
|
|
190
203
|
focus?: boolean;
|
|
191
204
|
}): void;
|
|
192
|
-
selectContiguous(identity: Identity): void;
|
|
205
|
+
selectContiguous(identity: Identity | T): void;
|
|
193
206
|
deselectAll(): void;
|
|
194
207
|
selectAll(): void;
|
|
195
208
|
private filterSelectableNodes;
|
|
196
209
|
setSelection(args: {
|
|
197
|
-
ids: (IdObj | string)[] | null;
|
|
198
|
-
anchor: Identity;
|
|
199
|
-
mostRecent: Identity;
|
|
210
|
+
ids: (IdObj | string | T)[] | null;
|
|
211
|
+
anchor: Identity | T;
|
|
212
|
+
mostRecent: Identity | T;
|
|
200
213
|
}): void;
|
|
201
214
|
get cursorParentId(): string | null;
|
|
202
215
|
get cursorOverFolder(): boolean;
|
|
@@ -208,14 +221,14 @@ export declare class TreeApi<T> {
|
|
|
208
221
|
drop(): void;
|
|
209
222
|
hideCursor(): void;
|
|
210
223
|
showCursor(cursor: Cursor): void;
|
|
211
|
-
open(identity: Identity, redraw?: boolean): void;
|
|
212
|
-
close(identity: Identity, redraw?: boolean): void;
|
|
213
|
-
toggle(identity: Identity): void;
|
|
214
|
-
openParents(identity: Identity): void;
|
|
224
|
+
open(identity: Identity | T, redraw?: boolean): void;
|
|
225
|
+
close(identity: Identity | T, redraw?: boolean): void;
|
|
226
|
+
toggle(identity: Identity | T): void;
|
|
227
|
+
openParents(identity: Identity | T): void;
|
|
215
228
|
openSiblings(node: NodeApi<T>): void;
|
|
216
229
|
openAll(): void;
|
|
217
230
|
closeAll(): void;
|
|
218
|
-
scrollTo(identity: Identity, align?: Align): Promise<void> | undefined;
|
|
231
|
+
scrollTo(identity: Identity | T, align?: Align): Promise<void> | undefined;
|
|
219
232
|
get isEditing(): boolean;
|
|
220
233
|
get isFiltered(): boolean;
|
|
221
234
|
get hasFocus(): boolean;
|
|
@@ -228,10 +241,10 @@ export declare class TreeApi<T> {
|
|
|
228
241
|
isDraggable(data: T): boolean;
|
|
229
242
|
isSelectable(data: T): boolean;
|
|
230
243
|
private isActionPossible;
|
|
231
|
-
isDragging(node: Identity): boolean;
|
|
244
|
+
isDragging(node: Identity | T): boolean;
|
|
232
245
|
isFocused(id: string): boolean;
|
|
233
246
|
isMatch(node: NodeApi<T>): boolean;
|
|
234
|
-
willReceiveDrop(node: Identity): boolean;
|
|
247
|
+
willReceiveDrop(node: Identity | T): boolean;
|
|
235
248
|
onFocus(): void;
|
|
236
249
|
onBlur(): void;
|
|
237
250
|
onItemsRendered(args: ListOnItemsRenderedProps): void;
|
|
@@ -37,6 +37,7 @@ const utils = __importStar(require("../utils"));
|
|
|
37
37
|
const default_cursor_1 = require("../components/default-cursor");
|
|
38
38
|
const default_row_1 = require("../components/default-row");
|
|
39
39
|
const default_node_1 = require("../components/default-node");
|
|
40
|
+
const node_api_1 = require("./node-api");
|
|
40
41
|
const edit_slice_1 = require("../state/edit-slice");
|
|
41
42
|
const focus_slice_1 = require("../state/focus-slice");
|
|
42
43
|
const create_root_1 = require("../data/create-root");
|
|
@@ -47,7 +48,7 @@ const default_drag_preview_1 = require("../components/default-drag-preview");
|
|
|
47
48
|
const default_container_1 = require("../components/default-container");
|
|
48
49
|
const create_list_1 = require("../data/create-list");
|
|
49
50
|
const create_index_1 = require("../data/create-index");
|
|
50
|
-
const { safeRun
|
|
51
|
+
const { safeRun } = utils;
|
|
51
52
|
class TreeApi {
|
|
52
53
|
constructor(store, props, list, listEl) {
|
|
53
54
|
this.store = store;
|
|
@@ -189,6 +190,30 @@ class TreeApi {
|
|
|
189
190
|
throw new Error("Data must contain an 'id' property or props.idAccessor must return a string");
|
|
190
191
|
return id;
|
|
191
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Resolve an identifier to a node id. Public methods accept an id string, a
|
|
195
|
+
* NodeApi, or the raw row data; this is the one place that turns any of those
|
|
196
|
+
* into the string id used internally. Raw data is run through the configured
|
|
197
|
+
* `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
|
|
198
|
+
* just where nodes were built. A NodeApi already carries its accessor-derived
|
|
199
|
+
* `id`, so it is used directly rather than re-accessed (the accessor reads the
|
|
200
|
+
* underlying data, which a NodeApi does not expose under that key). Unlike
|
|
201
|
+
* `accessId`, an unresolved id comes back as `undefined` rather than throwing,
|
|
202
|
+
* preserving the previous behavior of the `id`-only lookup.
|
|
203
|
+
*/
|
|
204
|
+
identify(identity) {
|
|
205
|
+
if (typeof identity === "string")
|
|
206
|
+
return identity;
|
|
207
|
+
if (identity instanceof node_api_1.NodeApi)
|
|
208
|
+
return identity.id;
|
|
209
|
+
const get = this.props.idAccessor || "id";
|
|
210
|
+
return utils.access(identity, get);
|
|
211
|
+
}
|
|
212
|
+
identifyNull(identity) {
|
|
213
|
+
if (identity === null || identity === undefined)
|
|
214
|
+
return null;
|
|
215
|
+
return this.identify(identity);
|
|
216
|
+
}
|
|
192
217
|
/* Node Access */
|
|
193
218
|
get firstNode() {
|
|
194
219
|
var _a;
|
|
@@ -244,7 +269,7 @@ class TreeApi {
|
|
|
244
269
|
return this.visibleNodes.slice(start, end + 1);
|
|
245
270
|
}
|
|
246
271
|
indexOf(id) {
|
|
247
|
-
const key =
|
|
272
|
+
const key = this.identifyNull(id);
|
|
248
273
|
if (!key)
|
|
249
274
|
return null;
|
|
250
275
|
return this.idToIndex[key];
|
|
@@ -287,7 +312,7 @@ class TreeApi {
|
|
|
287
312
|
if (!node)
|
|
288
313
|
return;
|
|
289
314
|
const idents = Array.isArray(node) ? node : [node];
|
|
290
|
-
const ids = idents.map(identify);
|
|
315
|
+
const ids = idents.map((i) => this.identify(i));
|
|
291
316
|
const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
|
|
292
317
|
/* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
|
|
293
318
|
const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
|
|
@@ -297,7 +322,7 @@ class TreeApi {
|
|
|
297
322
|
}
|
|
298
323
|
edit(node) {
|
|
299
324
|
var _a, _b;
|
|
300
|
-
const id = identify(node);
|
|
325
|
+
const id = this.identify(node);
|
|
301
326
|
this.resolveEdit({ cancelled: true });
|
|
302
327
|
this.scrollTo(id);
|
|
303
328
|
this.dispatch((0, edit_slice_1.edit)(id));
|
|
@@ -311,7 +336,7 @@ class TreeApi {
|
|
|
311
336
|
var _a, _b;
|
|
312
337
|
if (!identity)
|
|
313
338
|
return;
|
|
314
|
-
const id = identify(identity);
|
|
339
|
+
const id = this.identify(identity);
|
|
315
340
|
yield safeRun(this.props.onRename, {
|
|
316
341
|
id,
|
|
317
342
|
name: value,
|
|
@@ -330,7 +355,7 @@ class TreeApi {
|
|
|
330
355
|
setTimeout(() => this.onFocus()); // Return focus to element;
|
|
331
356
|
}
|
|
332
357
|
activate(id) {
|
|
333
|
-
const node = this.get(identifyNull(id));
|
|
358
|
+
const node = this.get(this.identifyNull(id));
|
|
334
359
|
if (!node)
|
|
335
360
|
return;
|
|
336
361
|
safeRun(this.props.onActivate, node);
|
|
@@ -364,7 +389,7 @@ class TreeApi {
|
|
|
364
389
|
this.select(node);
|
|
365
390
|
}
|
|
366
391
|
else {
|
|
367
|
-
this.dispatch((0, focus_slice_1.focus)(identify(node)));
|
|
392
|
+
this.dispatch((0, focus_slice_1.focus)(this.identify(node)));
|
|
368
393
|
if (opts.scroll !== false)
|
|
369
394
|
this.scrollTo(node);
|
|
370
395
|
if (this.focusedNode)
|
|
@@ -404,7 +429,7 @@ class TreeApi {
|
|
|
404
429
|
if (!node)
|
|
405
430
|
return;
|
|
406
431
|
const changeFocus = opts.focus !== false;
|
|
407
|
-
const id = identify(node);
|
|
432
|
+
const id = this.identify(node);
|
|
408
433
|
if (changeFocus)
|
|
409
434
|
this.dispatch((0, focus_slice_1.focus)(id));
|
|
410
435
|
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
@@ -422,12 +447,12 @@ class TreeApi {
|
|
|
422
447
|
deselect(node) {
|
|
423
448
|
if (!node)
|
|
424
449
|
return;
|
|
425
|
-
const id = identify(node);
|
|
450
|
+
const id = this.identify(node);
|
|
426
451
|
this.dispatch(selection_slice_1.actions.remove(id));
|
|
427
452
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
428
453
|
}
|
|
429
454
|
selectMulti(identity, opts = {}) {
|
|
430
|
-
const node = this.get(identifyNull(identity));
|
|
455
|
+
const node = this.get(this.identifyNull(identity));
|
|
431
456
|
if (!node)
|
|
432
457
|
return;
|
|
433
458
|
const changeFocus = opts.focus !== false;
|
|
@@ -448,11 +473,11 @@ class TreeApi {
|
|
|
448
473
|
var _a;
|
|
449
474
|
if (!identity)
|
|
450
475
|
return;
|
|
451
|
-
const id = identify(identity);
|
|
476
|
+
const id = this.identify(identity);
|
|
452
477
|
this.dispatch((0, focus_slice_1.focus)(id));
|
|
453
478
|
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
454
479
|
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
455
|
-
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
|
|
480
|
+
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, this.identifyNull(id)));
|
|
456
481
|
this.dispatch(selection_slice_1.actions.remove(this.nodesBetween(anchor, mostRecent)));
|
|
457
482
|
this.dispatch(selection_slice_1.actions.add(selectableNodes));
|
|
458
483
|
this.dispatch(selection_slice_1.actions.mostRecent(id));
|
|
@@ -481,14 +506,14 @@ class TreeApi {
|
|
|
481
506
|
}
|
|
482
507
|
filterSelectableNodes(nodes) {
|
|
483
508
|
return nodes
|
|
484
|
-
.map((n) => this.get(identify(n)))
|
|
509
|
+
.map((n) => this.get(this.identify(n)))
|
|
485
510
|
.filter((n) => !!n && n.isSelectable);
|
|
486
511
|
}
|
|
487
512
|
setSelection(args) {
|
|
488
513
|
var _a;
|
|
489
|
-
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
|
|
490
|
-
const anchor = identifyNull(args.anchor);
|
|
491
|
-
const mostRecent = identifyNull(args.mostRecent);
|
|
514
|
+
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map((i) => this.identify(i)));
|
|
515
|
+
const anchor = this.identifyNull(args.anchor);
|
|
516
|
+
const mostRecent = this.identifyNull(args.mostRecent);
|
|
492
517
|
this.dispatch(selection_slice_1.actions.set({ ids, anchor, mostRecent }));
|
|
493
518
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
494
519
|
}
|
|
@@ -571,7 +596,7 @@ class TreeApi {
|
|
|
571
596
|
/* Visibility */
|
|
572
597
|
open(identity, redraw = true) {
|
|
573
598
|
var _a, _b;
|
|
574
|
-
const id = identifyNull(identity);
|
|
599
|
+
const id = this.identifyNull(identity);
|
|
575
600
|
if (!id)
|
|
576
601
|
return;
|
|
577
602
|
if (this.isOpen(id))
|
|
@@ -583,7 +608,7 @@ class TreeApi {
|
|
|
583
608
|
}
|
|
584
609
|
close(identity, redraw = true) {
|
|
585
610
|
var _a, _b;
|
|
586
|
-
const id = identifyNull(identity);
|
|
611
|
+
const id = this.identifyNull(identity);
|
|
587
612
|
if (!id)
|
|
588
613
|
return;
|
|
589
614
|
if (!this.isOpen(id))
|
|
@@ -594,13 +619,13 @@ class TreeApi {
|
|
|
594
619
|
safeRun(this.props.onToggle, id);
|
|
595
620
|
}
|
|
596
621
|
toggle(identity) {
|
|
597
|
-
const id = identifyNull(identity);
|
|
622
|
+
const id = this.identifyNull(identity);
|
|
598
623
|
if (!id)
|
|
599
624
|
return;
|
|
600
625
|
return this.isOpen(id) ? this.close(id) : this.open(id);
|
|
601
626
|
}
|
|
602
627
|
openParents(identity) {
|
|
603
|
-
const id = identifyNull(identity);
|
|
628
|
+
const id = this.identifyNull(identity);
|
|
604
629
|
if (!id)
|
|
605
630
|
return;
|
|
606
631
|
const node = utils.dfs(this.root, id);
|
|
@@ -648,7 +673,7 @@ class TreeApi {
|
|
|
648
673
|
scrollTo(identity, align = "smart") {
|
|
649
674
|
if (!identity)
|
|
650
675
|
return;
|
|
651
|
-
const id = identify(identity);
|
|
676
|
+
const id = this.identify(identity);
|
|
652
677
|
this.openParents(id);
|
|
653
678
|
return utils
|
|
654
679
|
.waitFor(() => id in this.idToIndex)
|
|
@@ -715,7 +740,7 @@ class TreeApi {
|
|
|
715
740
|
return !utils.access(data, disabler);
|
|
716
741
|
}
|
|
717
742
|
isDragging(node) {
|
|
718
|
-
const id = identifyNull(node);
|
|
743
|
+
const id = this.identifyNull(node);
|
|
719
744
|
if (!id)
|
|
720
745
|
return false;
|
|
721
746
|
return this.state.nodes.drag.id === id;
|
|
@@ -727,7 +752,7 @@ class TreeApi {
|
|
|
727
752
|
return this.matchFn(node);
|
|
728
753
|
}
|
|
729
754
|
willReceiveDrop(node) {
|
|
730
|
-
const id = identifyNull(node);
|
|
755
|
+
const id = this.identifyNull(node);
|
|
731
756
|
if (!id)
|
|
732
757
|
return false;
|
|
733
758
|
const { destinationParentId, destinationIndex } = this.state.nodes.drag;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
12
|
const redux_1 = require("redux");
|
|
4
13
|
const root_reducer_1 = require("../state/root-reducer");
|
|
@@ -38,6 +47,45 @@ describe("tree.drop() fires onMove (#313)", () => {
|
|
|
38
47
|
expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ parentId: "folder", index: 0 }));
|
|
39
48
|
});
|
|
40
49
|
});
|
|
50
|
+
describe("custom idAccessor is honored when methods receive raw data (#347)", () => {
|
|
51
|
+
const uuidData = [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }];
|
|
52
|
+
test("select(data) resolves the id through idAccessor", () => {
|
|
53
|
+
const onSelect = jest.fn();
|
|
54
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onSelect });
|
|
55
|
+
api.select(uuidData[1]);
|
|
56
|
+
expect(api.selectedIds.has("b")).toBe(true);
|
|
57
|
+
expect(api.selectedNodes.map((n) => n.id)).toEqual(["b"]);
|
|
58
|
+
});
|
|
59
|
+
test("focus(data) resolves the id through idAccessor", () => {
|
|
60
|
+
var _a;
|
|
61
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid" });
|
|
62
|
+
api.focus(uuidData[2]);
|
|
63
|
+
expect((_a = api.focusedNode) === null || _a === void 0 ? void 0 : _a.id).toBe("c");
|
|
64
|
+
});
|
|
65
|
+
test("delete(data) passes the accessor-derived id to onDelete", () => {
|
|
66
|
+
const onDelete = jest.fn();
|
|
67
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onDelete });
|
|
68
|
+
api.delete(uuidData[0]);
|
|
69
|
+
expect(onDelete).toHaveBeenCalledWith(expect.objectContaining({ ids: ["a"] }));
|
|
70
|
+
});
|
|
71
|
+
test("create() focuses the new node by its accessor-derived id", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
// create() passes the raw row data returned by onCreate straight to
|
|
73
|
+
// focus/edit/select; before the fix these read `.id` and lost the node.
|
|
74
|
+
const onCreate = () => ({ uuid: "new" });
|
|
75
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onCreate });
|
|
76
|
+
yield api.create();
|
|
77
|
+
expect(api.state.nodes.focus.id).toBe("new");
|
|
78
|
+
}));
|
|
79
|
+
test("a function idAccessor is honored too", () => {
|
|
80
|
+
const fnData = [{ meta: { key: "x" } }, { meta: { key: "y" } }];
|
|
81
|
+
const api = setupApi({
|
|
82
|
+
data: fnData,
|
|
83
|
+
idAccessor: (d) => d.meta.key,
|
|
84
|
+
});
|
|
85
|
+
api.select(fnData[1]);
|
|
86
|
+
expect(api.selectedIds.has("y")).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
41
89
|
test("rowHeight defaults to 24", () => {
|
|
42
90
|
const api = setupApi({});
|
|
43
91
|
expect(api.rowHeight).toBe(24);
|
|
@@ -5,7 +5,7 @@ export type CreateHandler<T> = (args: {
|
|
|
5
5
|
parentNode: NodeApi<T> | null;
|
|
6
6
|
index: number;
|
|
7
7
|
type: "internal" | "leaf";
|
|
8
|
-
}) => (IdObj | null) | Promise<IdObj | null>;
|
|
8
|
+
}) => (T | IdObj | null) | Promise<T | IdObj | null>;
|
|
9
9
|
export type MoveHandler<T> = (args: {
|
|
10
10
|
dragIds: string[];
|
|
11
11
|
dragNodes: NodeApi<T>[];
|
|
@@ -149,6 +149,19 @@ export declare class TreeApi<T> {
|
|
|
149
149
|
get matchFn(): (node: NodeApi<T>) => boolean;
|
|
150
150
|
accessChildren(data: T): readonly T[] | null;
|
|
151
151
|
accessId(data: T): string;
|
|
152
|
+
/**
|
|
153
|
+
* Resolve an identifier to a node id. Public methods accept an id string, a
|
|
154
|
+
* NodeApi, or the raw row data; this is the one place that turns any of those
|
|
155
|
+
* into the string id used internally. Raw data is run through the configured
|
|
156
|
+
* `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
|
|
157
|
+
* just where nodes were built. A NodeApi already carries its accessor-derived
|
|
158
|
+
* `id`, so it is used directly rather than re-accessed (the accessor reads the
|
|
159
|
+
* underlying data, which a NodeApi does not expose under that key). Unlike
|
|
160
|
+
* `accessId`, an unresolved id comes back as `undefined` rather than throwing,
|
|
161
|
+
* preserving the previous behavior of the `id`-only lookup.
|
|
162
|
+
*/
|
|
163
|
+
identify(identity: string | IdObj | T): string;
|
|
164
|
+
identifyNull(identity: Identity | T): string | null;
|
|
152
165
|
get firstNode(): NodeApi<T>;
|
|
153
166
|
get lastNode(): NodeApi<T>;
|
|
154
167
|
get focusedNode(): NodeApi<T> | null;
|
|
@@ -158,7 +171,7 @@ export declare class TreeApi<T> {
|
|
|
158
171
|
get(id: string | null): NodeApi<T> | null;
|
|
159
172
|
at(index: number): NodeApi<T> | null;
|
|
160
173
|
nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
|
|
161
|
-
indexOf(id: Identity): number | null;
|
|
174
|
+
indexOf(id: Identity | T): number | null;
|
|
162
175
|
get editingId(): string | null;
|
|
163
176
|
createInternal(): Promise<void>;
|
|
164
177
|
createLeaf(): Promise<void>;
|
|
@@ -167,36 +180,36 @@ export declare class TreeApi<T> {
|
|
|
167
180
|
parentId?: null | string;
|
|
168
181
|
index?: null | number;
|
|
169
182
|
}): Promise<void>;
|
|
170
|
-
delete(node: Identity | string
|
|
171
|
-
edit(node: string | IdObj): Promise<EditResult>;
|
|
172
|
-
submit(identity: Identity, value: string): Promise<void>;
|
|
183
|
+
delete(node: Identity | T | (string | IdObj | T)[]): Promise<void>;
|
|
184
|
+
edit(node: string | IdObj | T): Promise<EditResult>;
|
|
185
|
+
submit(identity: Identity | T, value: string): Promise<void>;
|
|
173
186
|
reset(): void;
|
|
174
|
-
activate(id: Identity): void;
|
|
187
|
+
activate(id: Identity | T): void;
|
|
175
188
|
private resolveEdit;
|
|
176
189
|
get selectedIds(): Set<string>;
|
|
177
190
|
get selectedNodes(): NodeApi<T>[];
|
|
178
|
-
focus(node: Identity, opts?: {
|
|
191
|
+
focus(node: Identity | T, opts?: {
|
|
179
192
|
scroll?: boolean;
|
|
180
193
|
}): void;
|
|
181
194
|
pageUp(): void;
|
|
182
195
|
pageDown(): void;
|
|
183
|
-
select(node: Identity, opts?: {
|
|
196
|
+
select(node: Identity | T, opts?: {
|
|
184
197
|
align?: Align;
|
|
185
198
|
focus?: boolean;
|
|
186
199
|
}): void;
|
|
187
|
-
deselect(node: Identity): void;
|
|
188
|
-
selectMulti(identity: Identity, opts?: {
|
|
200
|
+
deselect(node: Identity | T): void;
|
|
201
|
+
selectMulti(identity: Identity | T, opts?: {
|
|
189
202
|
align?: Align;
|
|
190
203
|
focus?: boolean;
|
|
191
204
|
}): void;
|
|
192
|
-
selectContiguous(identity: Identity): void;
|
|
205
|
+
selectContiguous(identity: Identity | T): void;
|
|
193
206
|
deselectAll(): void;
|
|
194
207
|
selectAll(): void;
|
|
195
208
|
private filterSelectableNodes;
|
|
196
209
|
setSelection(args: {
|
|
197
|
-
ids: (IdObj | string)[] | null;
|
|
198
|
-
anchor: Identity;
|
|
199
|
-
mostRecent: Identity;
|
|
210
|
+
ids: (IdObj | string | T)[] | null;
|
|
211
|
+
anchor: Identity | T;
|
|
212
|
+
mostRecent: Identity | T;
|
|
200
213
|
}): void;
|
|
201
214
|
get cursorParentId(): string | null;
|
|
202
215
|
get cursorOverFolder(): boolean;
|
|
@@ -208,14 +221,14 @@ export declare class TreeApi<T> {
|
|
|
208
221
|
drop(): void;
|
|
209
222
|
hideCursor(): void;
|
|
210
223
|
showCursor(cursor: Cursor): void;
|
|
211
|
-
open(identity: Identity, redraw?: boolean): void;
|
|
212
|
-
close(identity: Identity, redraw?: boolean): void;
|
|
213
|
-
toggle(identity: Identity): void;
|
|
214
|
-
openParents(identity: Identity): void;
|
|
224
|
+
open(identity: Identity | T, redraw?: boolean): void;
|
|
225
|
+
close(identity: Identity | T, redraw?: boolean): void;
|
|
226
|
+
toggle(identity: Identity | T): void;
|
|
227
|
+
openParents(identity: Identity | T): void;
|
|
215
228
|
openSiblings(node: NodeApi<T>): void;
|
|
216
229
|
openAll(): void;
|
|
217
230
|
closeAll(): void;
|
|
218
|
-
scrollTo(identity: Identity, align?: Align): Promise<void> | undefined;
|
|
231
|
+
scrollTo(identity: Identity | T, align?: Align): Promise<void> | undefined;
|
|
219
232
|
get isEditing(): boolean;
|
|
220
233
|
get isFiltered(): boolean;
|
|
221
234
|
get hasFocus(): boolean;
|
|
@@ -228,10 +241,10 @@ export declare class TreeApi<T> {
|
|
|
228
241
|
isDraggable(data: T): boolean;
|
|
229
242
|
isSelectable(data: T): boolean;
|
|
230
243
|
private isActionPossible;
|
|
231
|
-
isDragging(node: Identity): boolean;
|
|
244
|
+
isDragging(node: Identity | T): boolean;
|
|
232
245
|
isFocused(id: string): boolean;
|
|
233
246
|
isMatch(node: NodeApi<T>): boolean;
|
|
234
|
-
willReceiveDrop(node: Identity): boolean;
|
|
247
|
+
willReceiveDrop(node: Identity | T): boolean;
|
|
235
248
|
onFocus(): void;
|
|
236
249
|
onBlur(): void;
|
|
237
250
|
onItemsRendered(args: ListOnItemsRenderedProps): void;
|
|
@@ -11,6 +11,7 @@ import * as utils from "../utils";
|
|
|
11
11
|
import { DefaultCursor } from "../components/default-cursor";
|
|
12
12
|
import { DefaultRow } from "../components/default-row";
|
|
13
13
|
import { DefaultNode } from "../components/default-node";
|
|
14
|
+
import { NodeApi } from "./node-api";
|
|
14
15
|
import { edit } from "../state/edit-slice";
|
|
15
16
|
import { focus, treeBlur } from "../state/focus-slice";
|
|
16
17
|
import { createRoot, ROOT_ID } from "../data/create-root";
|
|
@@ -21,7 +22,7 @@ import { DefaultDragPreview } from "../components/default-drag-preview";
|
|
|
21
22
|
import { DefaultContainer } from "../components/default-container";
|
|
22
23
|
import { createList } from "../data/create-list";
|
|
23
24
|
import { createIndex } from "../data/create-index";
|
|
24
|
-
const { safeRun
|
|
25
|
+
const { safeRun } = utils;
|
|
25
26
|
export class TreeApi {
|
|
26
27
|
constructor(store, props, list, listEl) {
|
|
27
28
|
this.store = store;
|
|
@@ -163,6 +164,30 @@ export class TreeApi {
|
|
|
163
164
|
throw new Error("Data must contain an 'id' property or props.idAccessor must return a string");
|
|
164
165
|
return id;
|
|
165
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve an identifier to a node id. Public methods accept an id string, a
|
|
169
|
+
* NodeApi, or the raw row data; this is the one place that turns any of those
|
|
170
|
+
* into the string id used internally. Raw data is run through the configured
|
|
171
|
+
* `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
|
|
172
|
+
* just where nodes were built. A NodeApi already carries its accessor-derived
|
|
173
|
+
* `id`, so it is used directly rather than re-accessed (the accessor reads the
|
|
174
|
+
* underlying data, which a NodeApi does not expose under that key). Unlike
|
|
175
|
+
* `accessId`, an unresolved id comes back as `undefined` rather than throwing,
|
|
176
|
+
* preserving the previous behavior of the `id`-only lookup.
|
|
177
|
+
*/
|
|
178
|
+
identify(identity) {
|
|
179
|
+
if (typeof identity === "string")
|
|
180
|
+
return identity;
|
|
181
|
+
if (identity instanceof NodeApi)
|
|
182
|
+
return identity.id;
|
|
183
|
+
const get = this.props.idAccessor || "id";
|
|
184
|
+
return utils.access(identity, get);
|
|
185
|
+
}
|
|
186
|
+
identifyNull(identity) {
|
|
187
|
+
if (identity === null || identity === undefined)
|
|
188
|
+
return null;
|
|
189
|
+
return this.identify(identity);
|
|
190
|
+
}
|
|
166
191
|
/* Node Access */
|
|
167
192
|
get firstNode() {
|
|
168
193
|
var _a;
|
|
@@ -218,7 +243,7 @@ export class TreeApi {
|
|
|
218
243
|
return this.visibleNodes.slice(start, end + 1);
|
|
219
244
|
}
|
|
220
245
|
indexOf(id) {
|
|
221
|
-
const key =
|
|
246
|
+
const key = this.identifyNull(id);
|
|
222
247
|
if (!key)
|
|
223
248
|
return null;
|
|
224
249
|
return this.idToIndex[key];
|
|
@@ -261,7 +286,7 @@ export class TreeApi {
|
|
|
261
286
|
if (!node)
|
|
262
287
|
return;
|
|
263
288
|
const idents = Array.isArray(node) ? node : [node];
|
|
264
|
-
const ids = idents.map(identify);
|
|
289
|
+
const ids = idents.map((i) => this.identify(i));
|
|
265
290
|
const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
|
|
266
291
|
/* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
|
|
267
292
|
const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
|
|
@@ -271,7 +296,7 @@ export class TreeApi {
|
|
|
271
296
|
}
|
|
272
297
|
edit(node) {
|
|
273
298
|
var _a, _b;
|
|
274
|
-
const id = identify(node);
|
|
299
|
+
const id = this.identify(node);
|
|
275
300
|
this.resolveEdit({ cancelled: true });
|
|
276
301
|
this.scrollTo(id);
|
|
277
302
|
this.dispatch(edit(id));
|
|
@@ -285,7 +310,7 @@ export class TreeApi {
|
|
|
285
310
|
var _a, _b;
|
|
286
311
|
if (!identity)
|
|
287
312
|
return;
|
|
288
|
-
const id = identify(identity);
|
|
313
|
+
const id = this.identify(identity);
|
|
289
314
|
yield safeRun(this.props.onRename, {
|
|
290
315
|
id,
|
|
291
316
|
name: value,
|
|
@@ -304,7 +329,7 @@ export class TreeApi {
|
|
|
304
329
|
setTimeout(() => this.onFocus()); // Return focus to element;
|
|
305
330
|
}
|
|
306
331
|
activate(id) {
|
|
307
|
-
const node = this.get(identifyNull(id));
|
|
332
|
+
const node = this.get(this.identifyNull(id));
|
|
308
333
|
if (!node)
|
|
309
334
|
return;
|
|
310
335
|
safeRun(this.props.onActivate, node);
|
|
@@ -338,7 +363,7 @@ export class TreeApi {
|
|
|
338
363
|
this.select(node);
|
|
339
364
|
}
|
|
340
365
|
else {
|
|
341
|
-
this.dispatch(focus(identify(node)));
|
|
366
|
+
this.dispatch(focus(this.identify(node)));
|
|
342
367
|
if (opts.scroll !== false)
|
|
343
368
|
this.scrollTo(node);
|
|
344
369
|
if (this.focusedNode)
|
|
@@ -378,7 +403,7 @@ export class TreeApi {
|
|
|
378
403
|
if (!node)
|
|
379
404
|
return;
|
|
380
405
|
const changeFocus = opts.focus !== false;
|
|
381
|
-
const id = identify(node);
|
|
406
|
+
const id = this.identify(node);
|
|
382
407
|
if (changeFocus)
|
|
383
408
|
this.dispatch(focus(id));
|
|
384
409
|
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
@@ -396,12 +421,12 @@ export class TreeApi {
|
|
|
396
421
|
deselect(node) {
|
|
397
422
|
if (!node)
|
|
398
423
|
return;
|
|
399
|
-
const id = identify(node);
|
|
424
|
+
const id = this.identify(node);
|
|
400
425
|
this.dispatch(selection.remove(id));
|
|
401
426
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
402
427
|
}
|
|
403
428
|
selectMulti(identity, opts = {}) {
|
|
404
|
-
const node = this.get(identifyNull(identity));
|
|
429
|
+
const node = this.get(this.identifyNull(identity));
|
|
405
430
|
if (!node)
|
|
406
431
|
return;
|
|
407
432
|
const changeFocus = opts.focus !== false;
|
|
@@ -422,11 +447,11 @@ export class TreeApi {
|
|
|
422
447
|
var _a;
|
|
423
448
|
if (!identity)
|
|
424
449
|
return;
|
|
425
|
-
const id = identify(identity);
|
|
450
|
+
const id = this.identify(identity);
|
|
426
451
|
this.dispatch(focus(id));
|
|
427
452
|
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
428
453
|
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
429
|
-
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
|
|
454
|
+
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, this.identifyNull(id)));
|
|
430
455
|
this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
|
|
431
456
|
this.dispatch(selection.add(selectableNodes));
|
|
432
457
|
this.dispatch(selection.mostRecent(id));
|
|
@@ -455,14 +480,14 @@ export class TreeApi {
|
|
|
455
480
|
}
|
|
456
481
|
filterSelectableNodes(nodes) {
|
|
457
482
|
return nodes
|
|
458
|
-
.map((n) => this.get(identify(n)))
|
|
483
|
+
.map((n) => this.get(this.identify(n)))
|
|
459
484
|
.filter((n) => !!n && n.isSelectable);
|
|
460
485
|
}
|
|
461
486
|
setSelection(args) {
|
|
462
487
|
var _a;
|
|
463
|
-
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
|
|
464
|
-
const anchor = identifyNull(args.anchor);
|
|
465
|
-
const mostRecent = identifyNull(args.mostRecent);
|
|
488
|
+
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map((i) => this.identify(i)));
|
|
489
|
+
const anchor = this.identifyNull(args.anchor);
|
|
490
|
+
const mostRecent = this.identifyNull(args.mostRecent);
|
|
466
491
|
this.dispatch(selection.set({ ids, anchor, mostRecent }));
|
|
467
492
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
468
493
|
}
|
|
@@ -545,7 +570,7 @@ export class TreeApi {
|
|
|
545
570
|
/* Visibility */
|
|
546
571
|
open(identity, redraw = true) {
|
|
547
572
|
var _a, _b;
|
|
548
|
-
const id = identifyNull(identity);
|
|
573
|
+
const id = this.identifyNull(identity);
|
|
549
574
|
if (!id)
|
|
550
575
|
return;
|
|
551
576
|
if (this.isOpen(id))
|
|
@@ -557,7 +582,7 @@ export class TreeApi {
|
|
|
557
582
|
}
|
|
558
583
|
close(identity, redraw = true) {
|
|
559
584
|
var _a, _b;
|
|
560
|
-
const id = identifyNull(identity);
|
|
585
|
+
const id = this.identifyNull(identity);
|
|
561
586
|
if (!id)
|
|
562
587
|
return;
|
|
563
588
|
if (!this.isOpen(id))
|
|
@@ -568,13 +593,13 @@ export class TreeApi {
|
|
|
568
593
|
safeRun(this.props.onToggle, id);
|
|
569
594
|
}
|
|
570
595
|
toggle(identity) {
|
|
571
|
-
const id = identifyNull(identity);
|
|
596
|
+
const id = this.identifyNull(identity);
|
|
572
597
|
if (!id)
|
|
573
598
|
return;
|
|
574
599
|
return this.isOpen(id) ? this.close(id) : this.open(id);
|
|
575
600
|
}
|
|
576
601
|
openParents(identity) {
|
|
577
|
-
const id = identifyNull(identity);
|
|
602
|
+
const id = this.identifyNull(identity);
|
|
578
603
|
if (!id)
|
|
579
604
|
return;
|
|
580
605
|
const node = utils.dfs(this.root, id);
|
|
@@ -622,7 +647,7 @@ export class TreeApi {
|
|
|
622
647
|
scrollTo(identity, align = "smart") {
|
|
623
648
|
if (!identity)
|
|
624
649
|
return;
|
|
625
|
-
const id = identify(identity);
|
|
650
|
+
const id = this.identify(identity);
|
|
626
651
|
this.openParents(id);
|
|
627
652
|
return utils
|
|
628
653
|
.waitFor(() => id in this.idToIndex)
|
|
@@ -689,7 +714,7 @@ export class TreeApi {
|
|
|
689
714
|
return !utils.access(data, disabler);
|
|
690
715
|
}
|
|
691
716
|
isDragging(node) {
|
|
692
|
-
const id = identifyNull(node);
|
|
717
|
+
const id = this.identifyNull(node);
|
|
693
718
|
if (!id)
|
|
694
719
|
return false;
|
|
695
720
|
return this.state.nodes.drag.id === id;
|
|
@@ -701,7 +726,7 @@ export class TreeApi {
|
|
|
701
726
|
return this.matchFn(node);
|
|
702
727
|
}
|
|
703
728
|
willReceiveDrop(node) {
|
|
704
|
-
const id = identifyNull(node);
|
|
729
|
+
const id = this.identifyNull(node);
|
|
705
730
|
if (!id)
|
|
706
731
|
return false;
|
|
707
732
|
const { destinationParentId, destinationIndex } = this.state.nodes.drag;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
1
10
|
import { createStore } from "redux";
|
|
2
11
|
import { rootReducer } from "../state/root-reducer";
|
|
3
12
|
import { actions as dnd } from "../state/dnd-slice";
|
|
@@ -36,6 +45,45 @@ describe("tree.drop() fires onMove (#313)", () => {
|
|
|
36
45
|
expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ parentId: "folder", index: 0 }));
|
|
37
46
|
});
|
|
38
47
|
});
|
|
48
|
+
describe("custom idAccessor is honored when methods receive raw data (#347)", () => {
|
|
49
|
+
const uuidData = [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }];
|
|
50
|
+
test("select(data) resolves the id through idAccessor", () => {
|
|
51
|
+
const onSelect = jest.fn();
|
|
52
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onSelect });
|
|
53
|
+
api.select(uuidData[1]);
|
|
54
|
+
expect(api.selectedIds.has("b")).toBe(true);
|
|
55
|
+
expect(api.selectedNodes.map((n) => n.id)).toEqual(["b"]);
|
|
56
|
+
});
|
|
57
|
+
test("focus(data) resolves the id through idAccessor", () => {
|
|
58
|
+
var _a;
|
|
59
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid" });
|
|
60
|
+
api.focus(uuidData[2]);
|
|
61
|
+
expect((_a = api.focusedNode) === null || _a === void 0 ? void 0 : _a.id).toBe("c");
|
|
62
|
+
});
|
|
63
|
+
test("delete(data) passes the accessor-derived id to onDelete", () => {
|
|
64
|
+
const onDelete = jest.fn();
|
|
65
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onDelete });
|
|
66
|
+
api.delete(uuidData[0]);
|
|
67
|
+
expect(onDelete).toHaveBeenCalledWith(expect.objectContaining({ ids: ["a"] }));
|
|
68
|
+
});
|
|
69
|
+
test("create() focuses the new node by its accessor-derived id", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
// create() passes the raw row data returned by onCreate straight to
|
|
71
|
+
// focus/edit/select; before the fix these read `.id` and lost the node.
|
|
72
|
+
const onCreate = () => ({ uuid: "new" });
|
|
73
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onCreate });
|
|
74
|
+
yield api.create();
|
|
75
|
+
expect(api.state.nodes.focus.id).toBe("new");
|
|
76
|
+
}));
|
|
77
|
+
test("a function idAccessor is honored too", () => {
|
|
78
|
+
const fnData = [{ meta: { key: "x" } }, { meta: { key: "y" } }];
|
|
79
|
+
const api = setupApi({
|
|
80
|
+
data: fnData,
|
|
81
|
+
idAccessor: (d) => d.meta.key,
|
|
82
|
+
});
|
|
83
|
+
api.select(fnData[1]);
|
|
84
|
+
expect(api.selectedIds.has("y")).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
39
87
|
test("rowHeight defaults to 24", () => {
|
|
40
88
|
const api = setupApi({});
|
|
41
89
|
expect(api.rowHeight).toBe(24);
|
|
@@ -5,7 +5,7 @@ export type CreateHandler<T> = (args: {
|
|
|
5
5
|
parentNode: NodeApi<T> | null;
|
|
6
6
|
index: number;
|
|
7
7
|
type: "internal" | "leaf";
|
|
8
|
-
}) => (IdObj | null) | Promise<IdObj | null>;
|
|
8
|
+
}) => (T | IdObj | null) | Promise<T | IdObj | null>;
|
|
9
9
|
export type MoveHandler<T> = (args: {
|
|
10
10
|
dragIds: string[];
|
|
11
11
|
dragNodes: NodeApi<T>[];
|
package/package.json
CHANGED
|
@@ -47,6 +47,52 @@ describe("tree.drop() fires onMove (#313)", () => {
|
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
describe("custom idAccessor is honored when methods receive raw data (#347)", () => {
|
|
51
|
+
const uuidData = [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }];
|
|
52
|
+
|
|
53
|
+
test("select(data) resolves the id through idAccessor", () => {
|
|
54
|
+
const onSelect = jest.fn();
|
|
55
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onSelect });
|
|
56
|
+
api.select(uuidData[1]);
|
|
57
|
+
expect(api.selectedIds.has("b")).toBe(true);
|
|
58
|
+
expect(api.selectedNodes.map((n) => n.id)).toEqual(["b"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("focus(data) resolves the id through idAccessor", () => {
|
|
62
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid" });
|
|
63
|
+
api.focus(uuidData[2]);
|
|
64
|
+
expect(api.focusedNode?.id).toBe("c");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("delete(data) passes the accessor-derived id to onDelete", () => {
|
|
68
|
+
const onDelete = jest.fn();
|
|
69
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onDelete });
|
|
70
|
+
api.delete(uuidData[0]);
|
|
71
|
+
expect(onDelete).toHaveBeenCalledWith(
|
|
72
|
+
expect.objectContaining({ ids: ["a"] }),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("create() focuses the new node by its accessor-derived id", async () => {
|
|
77
|
+
// create() passes the raw row data returned by onCreate straight to
|
|
78
|
+
// focus/edit/select; before the fix these read `.id` and lost the node.
|
|
79
|
+
const onCreate = () => ({ uuid: "new" });
|
|
80
|
+
const api = setupApi({ data: uuidData, idAccessor: "uuid", onCreate });
|
|
81
|
+
await api.create();
|
|
82
|
+
expect(api.state.nodes.focus.id).toBe("new");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a function idAccessor is honored too", () => {
|
|
86
|
+
const fnData = [{ meta: { key: "x" } }, { meta: { key: "y" } }];
|
|
87
|
+
const api = setupApi({
|
|
88
|
+
data: fnData,
|
|
89
|
+
idAccessor: (d: any) => d.meta.key,
|
|
90
|
+
});
|
|
91
|
+
api.select(fnData[1]);
|
|
92
|
+
expect(api.selectedIds.has("y")).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
50
96
|
test("rowHeight defaults to 24", () => {
|
|
51
97
|
const api = setupApi({});
|
|
52
98
|
expect(api.rowHeight).toBe(24);
|
|
@@ -22,7 +22,7 @@ import { Store } from "redux";
|
|
|
22
22
|
import { createList } from "../data/create-list";
|
|
23
23
|
import { createIndex } from "../data/create-index";
|
|
24
24
|
|
|
25
|
-
const { safeRun
|
|
25
|
+
const { safeRun } = utils;
|
|
26
26
|
export class TreeApi<T> {
|
|
27
27
|
static editPromise: null | ((args: EditResult) => void);
|
|
28
28
|
root: NodeApi<T>;
|
|
@@ -187,6 +187,29 @@ export class TreeApi<T> {
|
|
|
187
187
|
return id;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Resolve an identifier to a node id. Public methods accept an id string, a
|
|
192
|
+
* NodeApi, or the raw row data; this is the one place that turns any of those
|
|
193
|
+
* into the string id used internally. Raw data is run through the configured
|
|
194
|
+
* `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
|
|
195
|
+
* just where nodes were built. A NodeApi already carries its accessor-derived
|
|
196
|
+
* `id`, so it is used directly rather than re-accessed (the accessor reads the
|
|
197
|
+
* underlying data, which a NodeApi does not expose under that key). Unlike
|
|
198
|
+
* `accessId`, an unresolved id comes back as `undefined` rather than throwing,
|
|
199
|
+
* preserving the previous behavior of the `id`-only lookup.
|
|
200
|
+
*/
|
|
201
|
+
identify(identity: string | IdObj | T): string {
|
|
202
|
+
if (typeof identity === "string") return identity;
|
|
203
|
+
if (identity instanceof NodeApi) return identity.id;
|
|
204
|
+
const get = this.props.idAccessor || "id";
|
|
205
|
+
return utils.access<string>(identity, get);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
identifyNull(identity: Identity | T): string | null {
|
|
209
|
+
if (identity === null || identity === undefined) return null;
|
|
210
|
+
return this.identify(identity);
|
|
211
|
+
}
|
|
212
|
+
|
|
190
213
|
/* Node Access */
|
|
191
214
|
|
|
192
215
|
get firstNode() {
|
|
@@ -237,8 +260,8 @@ export class TreeApi<T> {
|
|
|
237
260
|
return this.visibleNodes.slice(start, end + 1);
|
|
238
261
|
}
|
|
239
262
|
|
|
240
|
-
indexOf(id: Identity) {
|
|
241
|
-
const key =
|
|
263
|
+
indexOf(id: Identity | T) {
|
|
264
|
+
const key = this.identifyNull(id);
|
|
242
265
|
if (!key) return null;
|
|
243
266
|
return this.idToIndex[key];
|
|
244
267
|
}
|
|
@@ -284,10 +307,10 @@ export class TreeApi<T> {
|
|
|
284
307
|
}
|
|
285
308
|
}
|
|
286
309
|
|
|
287
|
-
async delete(node: Identity | string
|
|
310
|
+
async delete(node: Identity | T | (string | IdObj | T)[]) {
|
|
288
311
|
if (!node) return;
|
|
289
312
|
const idents = Array.isArray(node) ? node : [node];
|
|
290
|
-
const ids = idents.map(identify);
|
|
313
|
+
const ids = idents.map((i) => this.identify(i));
|
|
291
314
|
const nodes = ids.map((id) => this.get(id)!).filter((n) => !!n);
|
|
292
315
|
/* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
|
|
293
316
|
const fromIndex = nodes.length ? Math.min(...nodes.map((n) => n.rowIndex ?? 0)) : 0;
|
|
@@ -295,8 +318,8 @@ export class TreeApi<T> {
|
|
|
295
318
|
this.redrawList(fromIndex);
|
|
296
319
|
}
|
|
297
320
|
|
|
298
|
-
edit(node: string | IdObj): Promise<EditResult> {
|
|
299
|
-
const id = identify(node);
|
|
321
|
+
edit(node: string | IdObj | T): Promise<EditResult> {
|
|
322
|
+
const id = this.identify(node);
|
|
300
323
|
this.resolveEdit({ cancelled: true });
|
|
301
324
|
this.scrollTo(id);
|
|
302
325
|
this.dispatch(edit(id));
|
|
@@ -306,9 +329,9 @@ export class TreeApi<T> {
|
|
|
306
329
|
});
|
|
307
330
|
}
|
|
308
331
|
|
|
309
|
-
async submit(identity: Identity, value: string) {
|
|
332
|
+
async submit(identity: Identity | T, value: string) {
|
|
310
333
|
if (!identity) return;
|
|
311
|
-
const id = identify(identity);
|
|
334
|
+
const id = this.identify(identity);
|
|
312
335
|
await safeRun(this.props.onRename, {
|
|
313
336
|
id,
|
|
314
337
|
name: value,
|
|
@@ -327,8 +350,8 @@ export class TreeApi<T> {
|
|
|
327
350
|
setTimeout(() => this.onFocus()); // Return focus to element;
|
|
328
351
|
}
|
|
329
352
|
|
|
330
|
-
activate(id: Identity) {
|
|
331
|
-
const node = this.get(identifyNull(id));
|
|
353
|
+
activate(id: Identity | T) {
|
|
354
|
+
const node = this.get(this.identifyNull(id));
|
|
332
355
|
if (!node) return;
|
|
333
356
|
safeRun(this.props.onActivate, node);
|
|
334
357
|
}
|
|
@@ -354,7 +377,7 @@ export class TreeApi<T> {
|
|
|
354
377
|
return nodes;
|
|
355
378
|
}
|
|
356
379
|
|
|
357
|
-
focus(node: Identity, opts: { scroll?: boolean } = {}) {
|
|
380
|
+
focus(node: Identity | T, opts: { scroll?: boolean } = {}) {
|
|
358
381
|
if (!node) return;
|
|
359
382
|
/* Focus is responsible for scrolling, while selection is
|
|
360
383
|
* responsible for focus. If selectionFollowsFocus, then
|
|
@@ -362,7 +385,7 @@ export class TreeApi<T> {
|
|
|
362
385
|
if (this.props.selectionFollowsFocus) {
|
|
363
386
|
this.select(node);
|
|
364
387
|
} else {
|
|
365
|
-
this.dispatch(focus(identify(node)));
|
|
388
|
+
this.dispatch(focus(this.identify(node)));
|
|
366
389
|
if (opts.scroll !== false) this.scrollTo(node);
|
|
367
390
|
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
368
391
|
}
|
|
@@ -394,10 +417,10 @@ export class TreeApi<T> {
|
|
|
394
417
|
this.focus(this.at(index));
|
|
395
418
|
}
|
|
396
419
|
|
|
397
|
-
select(node: Identity, opts: { align?: Align; focus?: boolean } = {}) {
|
|
420
|
+
select(node: Identity | T, opts: { align?: Align; focus?: boolean } = {}) {
|
|
398
421
|
if (!node) return;
|
|
399
422
|
const changeFocus = opts.focus !== false;
|
|
400
|
-
const id = identify(node);
|
|
423
|
+
const id = this.identify(node);
|
|
401
424
|
if (changeFocus) this.dispatch(focus(id));
|
|
402
425
|
if (this.get(id)?.isSelectable) {
|
|
403
426
|
this.setSelection({
|
|
@@ -412,15 +435,15 @@ export class TreeApi<T> {
|
|
|
412
435
|
}
|
|
413
436
|
}
|
|
414
437
|
|
|
415
|
-
deselect(node: Identity) {
|
|
438
|
+
deselect(node: Identity | T) {
|
|
416
439
|
if (!node) return;
|
|
417
|
-
const id = identify(node);
|
|
440
|
+
const id = this.identify(node);
|
|
418
441
|
this.dispatch(selection.remove(id));
|
|
419
442
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
420
443
|
}
|
|
421
444
|
|
|
422
|
-
selectMulti(identity: Identity, opts: { align?: Align; focus?: boolean } = {}) {
|
|
423
|
-
const node = this.get(identifyNull(identity));
|
|
445
|
+
selectMulti(identity: Identity | T, opts: { align?: Align; focus?: boolean } = {}) {
|
|
446
|
+
const node = this.get(this.identifyNull(identity));
|
|
424
447
|
if (!node) return;
|
|
425
448
|
const changeFocus = opts.focus !== false;
|
|
426
449
|
if (changeFocus) this.dispatch(focus(node.id));
|
|
@@ -436,14 +459,14 @@ export class TreeApi<T> {
|
|
|
436
459
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
437
460
|
}
|
|
438
461
|
|
|
439
|
-
selectContiguous(identity: Identity) {
|
|
462
|
+
selectContiguous(identity: Identity | T) {
|
|
440
463
|
if (!identity) return;
|
|
441
|
-
const id = identify(identity);
|
|
464
|
+
const id = this.identify(identity);
|
|
442
465
|
this.dispatch(focus(id));
|
|
443
466
|
if (this.get(id)?.isSelectable) {
|
|
444
467
|
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
445
468
|
const selectableNodes = this.filterSelectableNodes(
|
|
446
|
-
this.nodesBetween(anchor, identifyNull(id)),
|
|
469
|
+
this.nodesBetween(anchor, this.identifyNull(id)),
|
|
447
470
|
);
|
|
448
471
|
this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
|
|
449
472
|
this.dispatch(selection.add(selectableNodes));
|
|
@@ -473,14 +496,18 @@ export class TreeApi<T> {
|
|
|
473
496
|
|
|
474
497
|
private filterSelectableNodes(nodes: (IdObj | string)[]) {
|
|
475
498
|
return nodes
|
|
476
|
-
.map((n) => this.get(identify(n)))
|
|
499
|
+
.map((n) => this.get(this.identify(n)))
|
|
477
500
|
.filter((n): n is NodeApi<T> => !!n && n.isSelectable);
|
|
478
501
|
}
|
|
479
502
|
|
|
480
|
-
setSelection(args: {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
503
|
+
setSelection(args: {
|
|
504
|
+
ids: (IdObj | string | T)[] | null;
|
|
505
|
+
anchor: Identity | T;
|
|
506
|
+
mostRecent: Identity | T;
|
|
507
|
+
}) {
|
|
508
|
+
const ids = new Set(args.ids?.map((i) => this.identify(i)));
|
|
509
|
+
const anchor = this.identifyNull(args.anchor);
|
|
510
|
+
const mostRecent = this.identifyNull(args.mostRecent);
|
|
484
511
|
this.dispatch(selection.set({ ids, anchor, mostRecent }));
|
|
485
512
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
486
513
|
}
|
|
@@ -568,8 +595,8 @@ export class TreeApi<T> {
|
|
|
568
595
|
|
|
569
596
|
/* Visibility */
|
|
570
597
|
|
|
571
|
-
open(identity: Identity, redraw: boolean = true) {
|
|
572
|
-
const id = identifyNull(identity);
|
|
598
|
+
open(identity: Identity | T, redraw: boolean = true) {
|
|
599
|
+
const id = this.identifyNull(identity);
|
|
573
600
|
if (!id) return;
|
|
574
601
|
if (this.isOpen(id)) return;
|
|
575
602
|
this.dispatch(visibility.open(id, this.isFiltered));
|
|
@@ -577,8 +604,8 @@ export class TreeApi<T> {
|
|
|
577
604
|
safeRun(this.props.onToggle, id);
|
|
578
605
|
}
|
|
579
606
|
|
|
580
|
-
close(identity: Identity, redraw: boolean = true) {
|
|
581
|
-
const id = identifyNull(identity);
|
|
607
|
+
close(identity: Identity | T, redraw: boolean = true) {
|
|
608
|
+
const id = this.identifyNull(identity);
|
|
582
609
|
if (!id) return;
|
|
583
610
|
if (!this.isOpen(id)) return;
|
|
584
611
|
this.dispatch(visibility.close(id, this.isFiltered));
|
|
@@ -586,14 +613,14 @@ export class TreeApi<T> {
|
|
|
586
613
|
safeRun(this.props.onToggle, id);
|
|
587
614
|
}
|
|
588
615
|
|
|
589
|
-
toggle(identity: Identity) {
|
|
590
|
-
const id = identifyNull(identity);
|
|
616
|
+
toggle(identity: Identity | T) {
|
|
617
|
+
const id = this.identifyNull(identity);
|
|
591
618
|
if (!id) return;
|
|
592
619
|
return this.isOpen(id) ? this.close(id) : this.open(id);
|
|
593
620
|
}
|
|
594
621
|
|
|
595
|
-
openParents(identity: Identity) {
|
|
596
|
-
const id = identifyNull(identity);
|
|
622
|
+
openParents(identity: Identity | T) {
|
|
623
|
+
const id = this.identifyNull(identity);
|
|
597
624
|
if (!id) return;
|
|
598
625
|
const node = utils.dfs(this.root, id);
|
|
599
626
|
let parent = node?.parent;
|
|
@@ -638,9 +665,9 @@ export class TreeApi<T> {
|
|
|
638
665
|
|
|
639
666
|
/* Scrolling */
|
|
640
667
|
|
|
641
|
-
scrollTo(identity: Identity, align: Align = "smart") {
|
|
668
|
+
scrollTo(identity: Identity | T, align: Align = "smart") {
|
|
642
669
|
if (!identity) return;
|
|
643
|
-
const id = identify(identity);
|
|
670
|
+
const id = this.identify(identity);
|
|
644
671
|
this.openParents(id);
|
|
645
672
|
return utils
|
|
646
673
|
.waitFor(() => id in this.idToIndex)
|
|
@@ -712,8 +739,8 @@ export class TreeApi<T> {
|
|
|
712
739
|
return !utils.access(data, disabler);
|
|
713
740
|
}
|
|
714
741
|
|
|
715
|
-
isDragging(node: Identity) {
|
|
716
|
-
const id = identifyNull(node);
|
|
742
|
+
isDragging(node: Identity | T) {
|
|
743
|
+
const id = this.identifyNull(node);
|
|
717
744
|
if (!id) return false;
|
|
718
745
|
return this.state.nodes.drag.id === id;
|
|
719
746
|
}
|
|
@@ -726,8 +753,8 @@ export class TreeApi<T> {
|
|
|
726
753
|
return this.matchFn(node);
|
|
727
754
|
}
|
|
728
755
|
|
|
729
|
-
willReceiveDrop(node: Identity) {
|
|
730
|
-
const id = identifyNull(node);
|
|
756
|
+
willReceiveDrop(node: Identity | T) {
|
|
757
|
+
const id = this.identifyNull(node);
|
|
731
758
|
if (!id) return false;
|
|
732
759
|
const { destinationParentId, destinationIndex } = this.state.nodes.drag;
|
|
733
760
|
return id === destinationParentId && destinationIndex === null;
|
package/src/types/handlers.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { NodeApi } from "../interfaces/node-api";
|
|
2
2
|
import { IdObj } from "./utils";
|
|
3
3
|
|
|
4
|
+
// Returns the newly created row data, whose id is read via idAccessor. `IdObj`
|
|
5
|
+
// is kept for back-compat with handlers that return a bare `{ id }` (#347).
|
|
4
6
|
export type CreateHandler<T> = (args: {
|
|
5
7
|
parentId: string | null;
|
|
6
8
|
parentNode: NodeApi<T> | null;
|
|
7
9
|
index: number;
|
|
8
10
|
type: "internal" | "leaf";
|
|
9
|
-
}) => (IdObj | null) | Promise<IdObj | null>;
|
|
11
|
+
}) => (T | IdObj | null) | Promise<T | IdObj | null>;
|
|
10
12
|
|
|
11
13
|
export type MoveHandler<T> = (args: {
|
|
12
14
|
dragIds: string[];
|