@headless-tree/core 0.0.10 → 0.0.11
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/CHANGELOG.md +6 -0
- package/lib/cjs/core/build-proxified-instance.d.ts +2 -0
- package/lib/cjs/core/build-proxified-instance.js +58 -0
- package/lib/cjs/core/build-static-instance.d.ts +2 -0
- package/lib/cjs/core/build-static-instance.js +27 -0
- package/lib/cjs/core/create-tree.js +55 -36
- package/lib/cjs/features/async-data-loader/feature.js +37 -23
- package/lib/cjs/features/async-data-loader/types.d.ts +2 -1
- package/lib/cjs/features/drag-and-drop/feature.js +64 -32
- package/lib/cjs/features/drag-and-drop/types.d.ts +13 -4
- package/lib/cjs/features/drag-and-drop/utils.d.ts +1 -2
- package/lib/cjs/features/drag-and-drop/utils.js +140 -37
- package/lib/cjs/features/expand-all/feature.js +12 -6
- package/lib/cjs/features/main/types.d.ts +8 -2
- package/lib/cjs/features/renaming/feature.js +33 -18
- package/lib/cjs/features/renaming/types.d.ts +1 -1
- package/lib/cjs/features/search/feature.js +38 -24
- package/lib/cjs/features/search/types.d.ts +0 -1
- package/lib/cjs/features/selection/feature.js +23 -14
- package/lib/cjs/features/sync-data-loader/feature.js +7 -2
- package/lib/cjs/features/tree/feature.d.ts +2 -1
- package/lib/cjs/features/tree/feature.js +85 -63
- package/lib/cjs/features/tree/types.d.ts +5 -3
- package/lib/cjs/index.d.ts +3 -1
- package/lib/cjs/index.js +2 -1
- package/lib/cjs/test-utils/test-tree-do.d.ts +23 -0
- package/lib/cjs/test-utils/test-tree-do.js +99 -0
- package/lib/cjs/test-utils/test-tree-expect.d.ts +15 -0
- package/lib/cjs/test-utils/test-tree-expect.js +62 -0
- package/lib/cjs/test-utils/test-tree.d.ts +47 -0
- package/lib/cjs/test-utils/test-tree.js +195 -0
- package/lib/cjs/types/core.d.ts +31 -15
- package/lib/cjs/utilities/errors.d.ts +1 -0
- package/lib/cjs/utilities/errors.js +5 -0
- package/lib/cjs/utilities/insert-items-at-target.js +10 -3
- package/lib/cjs/utilities/remove-items-from-parents.js +14 -8
- package/lib/cjs/utils.d.ts +3 -3
- package/lib/cjs/utils.js +6 -6
- package/lib/esm/core/build-proxified-instance.d.ts +2 -0
- package/lib/esm/core/build-proxified-instance.js +54 -0
- package/lib/esm/core/build-static-instance.d.ts +2 -0
- package/lib/esm/core/build-static-instance.js +23 -0
- package/lib/esm/core/create-tree.js +55 -36
- package/lib/esm/features/async-data-loader/feature.js +37 -23
- package/lib/esm/features/async-data-loader/types.d.ts +2 -1
- package/lib/esm/features/drag-and-drop/feature.js +64 -32
- package/lib/esm/features/drag-and-drop/types.d.ts +13 -4
- package/lib/esm/features/drag-and-drop/utils.d.ts +1 -2
- package/lib/esm/features/drag-and-drop/utils.js +138 -34
- package/lib/esm/features/expand-all/feature.js +12 -6
- package/lib/esm/features/main/types.d.ts +8 -2
- package/lib/esm/features/renaming/feature.js +33 -18
- package/lib/esm/features/renaming/types.d.ts +1 -1
- package/lib/esm/features/search/feature.js +38 -24
- package/lib/esm/features/search/types.d.ts +0 -1
- package/lib/esm/features/selection/feature.js +23 -14
- package/lib/esm/features/sync-data-loader/feature.js +7 -2
- package/lib/esm/features/tree/feature.d.ts +2 -1
- package/lib/esm/features/tree/feature.js +86 -64
- package/lib/esm/features/tree/types.d.ts +5 -3
- package/lib/esm/index.d.ts +3 -1
- package/lib/esm/index.js +2 -1
- package/lib/esm/test-utils/test-tree-do.d.ts +23 -0
- package/lib/esm/test-utils/test-tree-do.js +95 -0
- package/lib/esm/test-utils/test-tree-expect.d.ts +15 -0
- package/lib/esm/test-utils/test-tree-expect.js +58 -0
- package/lib/esm/test-utils/test-tree.d.ts +47 -0
- package/lib/esm/test-utils/test-tree.js +191 -0
- package/lib/esm/types/core.d.ts +31 -15
- package/lib/esm/utilities/errors.d.ts +1 -0
- package/lib/esm/utilities/errors.js +1 -0
- package/lib/esm/utilities/insert-items-at-target.js +10 -3
- package/lib/esm/utilities/remove-items-from-parents.js +14 -8
- package/lib/esm/utils.d.ts +3 -3
- package/lib/esm/utils.js +3 -3
- package/package.json +7 -3
- package/src/core/build-proxified-instance.ts +115 -0
- package/src/core/build-static-instance.ts +28 -0
- package/src/core/create-tree.ts +60 -62
- package/src/features/async-data-loader/async-data-loader.spec.ts +143 -0
- package/src/features/async-data-loader/feature.ts +33 -31
- package/src/features/async-data-loader/types.ts +3 -1
- package/src/features/drag-and-drop/drag-and-drop.spec.ts +716 -0
- package/src/features/drag-and-drop/feature.ts +109 -85
- package/src/features/drag-and-drop/types.ts +21 -7
- package/src/features/drag-and-drop/utils.ts +196 -55
- package/src/features/expand-all/expand-all.spec.ts +52 -0
- package/src/features/expand-all/feature.ts +8 -12
- package/src/features/hotkeys-core/feature.ts +1 -1
- package/src/features/main/types.ts +14 -1
- package/src/features/renaming/feature.ts +30 -29
- package/src/features/renaming/renaming.spec.ts +125 -0
- package/src/features/renaming/types.ts +1 -1
- package/src/features/search/feature.ts +34 -38
- package/src/features/search/search.spec.ts +115 -0
- package/src/features/search/types.ts +0 -1
- package/src/features/selection/feature.ts +29 -30
- package/src/features/selection/selection.spec.ts +220 -0
- package/src/features/sync-data-loader/feature.ts +8 -11
- package/src/features/tree/feature.ts +82 -87
- package/src/features/tree/tree.spec.ts +515 -0
- package/src/features/tree/types.ts +5 -3
- package/src/index.ts +4 -1
- package/src/test-utils/test-tree-do.ts +136 -0
- package/src/test-utils/test-tree-expect.ts +86 -0
- package/src/test-utils/test-tree.ts +217 -0
- package/src/types/core.ts +92 -33
- package/src/utilities/errors.ts +2 -0
- package/src/utilities/insert-items-at-target.ts +10 -3
- package/src/utilities/remove-items-from-parents.ts +15 -10
- package/src/utils.spec.ts +89 -0
- package/src/utils.ts +6 -6
- package/tsconfig.json +1 -0
- package/vitest.config.ts +6 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { FeatureImplementation } from "../types/core";
|
|
2
|
+
import { InstanceBuilder, InstanceTypeMap } from "../features/main/types";
|
|
3
|
+
import { throwError } from "../utilities/errors";
|
|
4
|
+
|
|
5
|
+
const noop = () => {};
|
|
6
|
+
|
|
7
|
+
const findPrevInstanceMethod = (
|
|
8
|
+
features: FeatureImplementation[],
|
|
9
|
+
instanceType: keyof InstanceTypeMap,
|
|
10
|
+
methodKey: string,
|
|
11
|
+
featureSearchIndex: number,
|
|
12
|
+
) => {
|
|
13
|
+
for (let i = featureSearchIndex; i >= 0; i--) {
|
|
14
|
+
const feature = features[i];
|
|
15
|
+
const itemInstanceMethod = feature[instanceType]?.[methodKey];
|
|
16
|
+
if (itemInstanceMethod) {
|
|
17
|
+
return i;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const invokeInstanceMethod = (
|
|
24
|
+
features: FeatureImplementation[],
|
|
25
|
+
instanceType: keyof InstanceTypeMap,
|
|
26
|
+
opts: any,
|
|
27
|
+
methodKey: string,
|
|
28
|
+
featureIndex: number,
|
|
29
|
+
args: any[],
|
|
30
|
+
) => {
|
|
31
|
+
const prevIndex = findPrevInstanceMethod(
|
|
32
|
+
features,
|
|
33
|
+
instanceType,
|
|
34
|
+
methodKey,
|
|
35
|
+
featureIndex - 1,
|
|
36
|
+
);
|
|
37
|
+
const itemInstanceMethod = features[featureIndex][instanceType]?.[methodKey]!;
|
|
38
|
+
return itemInstanceMethod(
|
|
39
|
+
{
|
|
40
|
+
...opts,
|
|
41
|
+
prev:
|
|
42
|
+
prevIndex !== null
|
|
43
|
+
? (...newArgs) =>
|
|
44
|
+
invokeInstanceMethod(
|
|
45
|
+
features,
|
|
46
|
+
instanceType,
|
|
47
|
+
opts,
|
|
48
|
+
methodKey,
|
|
49
|
+
prevIndex,
|
|
50
|
+
newArgs,
|
|
51
|
+
)
|
|
52
|
+
: null,
|
|
53
|
+
},
|
|
54
|
+
...args,
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const buildProxiedInstance: InstanceBuilder = (
|
|
59
|
+
features,
|
|
60
|
+
instanceType,
|
|
61
|
+
buildOpts,
|
|
62
|
+
) => {
|
|
63
|
+
// demo with prototypes: https://jsfiddle.net/bgenc58r/
|
|
64
|
+
const opts = {};
|
|
65
|
+
const item = new Proxy(
|
|
66
|
+
{},
|
|
67
|
+
{
|
|
68
|
+
has(target, key: string | symbol) {
|
|
69
|
+
if (typeof key === "symbol") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (key === "toJSON") {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const hasInstanceMethod = findPrevInstanceMethod(
|
|
76
|
+
features,
|
|
77
|
+
instanceType,
|
|
78
|
+
key,
|
|
79
|
+
features.length - 1,
|
|
80
|
+
);
|
|
81
|
+
return Boolean(hasInstanceMethod);
|
|
82
|
+
},
|
|
83
|
+
get(target, key: string | symbol) {
|
|
84
|
+
if (typeof key === "symbol") {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (key === "toJSON") {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
return (...args: any[]) => {
|
|
91
|
+
const featureIndex = findPrevInstanceMethod(
|
|
92
|
+
features,
|
|
93
|
+
instanceType,
|
|
94
|
+
key,
|
|
95
|
+
features.length - 1,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (featureIndex === null) {
|
|
99
|
+
throw throwError(`feature missing for method ${key}`);
|
|
100
|
+
}
|
|
101
|
+
return invokeInstanceMethod(
|
|
102
|
+
features,
|
|
103
|
+
instanceType,
|
|
104
|
+
opts,
|
|
105
|
+
key,
|
|
106
|
+
featureIndex,
|
|
107
|
+
args,
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
Object.assign(opts, buildOpts(item));
|
|
114
|
+
return [item as any, noop];
|
|
115
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable no-continue,no-labels,no-extra-label */
|
|
2
|
+
|
|
3
|
+
import { InstanceBuilder } from "../features/main/types";
|
|
4
|
+
|
|
5
|
+
export const buildStaticInstance: InstanceBuilder = (
|
|
6
|
+
features,
|
|
7
|
+
instanceType,
|
|
8
|
+
buildOpts,
|
|
9
|
+
) => {
|
|
10
|
+
const instance: any = {};
|
|
11
|
+
const finalize = () => {
|
|
12
|
+
const opts = buildOpts(instance);
|
|
13
|
+
featureLoop: for (let i = 0; i < features.length; i++) {
|
|
14
|
+
// Loop goes in forward order, because later features overwrite previous ones
|
|
15
|
+
// TODO loop order correct? I think so...
|
|
16
|
+
const definition = features[i][instanceType];
|
|
17
|
+
if (!definition) continue featureLoop;
|
|
18
|
+
methodLoop: for (const [key, method] of Object.entries(definition)) {
|
|
19
|
+
if (!method) continue methodLoop;
|
|
20
|
+
const prev = instance[key];
|
|
21
|
+
instance[key] = (...args: any[]) => {
|
|
22
|
+
return method({ ...opts, prev }, ...args);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return [instance as any, finalize];
|
|
28
|
+
};
|
package/src/core/create-tree.ts
CHANGED
|
@@ -5,32 +5,13 @@ import {
|
|
|
5
5
|
TreeConfig,
|
|
6
6
|
TreeInstance,
|
|
7
7
|
TreeState,
|
|
8
|
+
Updater,
|
|
8
9
|
} from "../types/core";
|
|
9
10
|
import { MainFeatureDef } from "../features/main/types";
|
|
10
11
|
import { treeFeature } from "../features/tree/feature";
|
|
11
12
|
import { ItemMeta } from "../features/tree/types";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
features: FeatureImplementation[],
|
|
15
|
-
tree: TreeInstance<any>,
|
|
16
|
-
itemId: string,
|
|
17
|
-
) => {
|
|
18
|
-
const itemInstance = {} as ItemInstance<any>;
|
|
19
|
-
for (const feature of features) {
|
|
20
|
-
Object.assign(
|
|
21
|
-
// TODO dont run createItemInstance, but assign prototype objects instead?
|
|
22
|
-
// https://jsfiddle.net/bgenc58r/
|
|
23
|
-
itemInstance,
|
|
24
|
-
feature.createItemInstance?.(
|
|
25
|
-
{ ...itemInstance },
|
|
26
|
-
itemInstance,
|
|
27
|
-
tree,
|
|
28
|
-
itemId,
|
|
29
|
-
) ?? {},
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
return itemInstance;
|
|
33
|
-
};
|
|
13
|
+
import { buildStaticInstance } from "./build-static-instance";
|
|
14
|
+
import { throwError } from "../utilities/errors";
|
|
34
15
|
|
|
35
16
|
const verifyFeatures = (features: FeatureImplementation[] | undefined) => {
|
|
36
17
|
const loadedFeatures = features?.map((feature) => feature.key);
|
|
@@ -39,7 +20,7 @@ const verifyFeatures = (features: FeatureImplementation[] | undefined) => {
|
|
|
39
20
|
(dep) => !loadedFeatures?.includes(dep),
|
|
40
21
|
);
|
|
41
22
|
if (missingDependency) {
|
|
42
|
-
throw
|
|
23
|
+
throw throwError(`${feature.key} needs ${missingDependency}`);
|
|
43
24
|
}
|
|
44
25
|
}
|
|
45
26
|
};
|
|
@@ -60,25 +41,32 @@ const sortFeatures = (features: FeatureImplementation[] = []) =>
|
|
|
60
41
|
export const createTree = <T>(
|
|
61
42
|
initialConfig: TreeConfig<T>,
|
|
62
43
|
): TreeInstance<T> => {
|
|
63
|
-
const
|
|
64
|
-
|
|
44
|
+
const buildInstance = initialConfig.instanceBuilder ?? buildStaticInstance;
|
|
65
45
|
const additionalFeatures = [
|
|
66
46
|
treeFeature,
|
|
67
47
|
...sortFeatures(initialConfig.features),
|
|
68
48
|
];
|
|
69
49
|
verifyFeatures(additionalFeatures);
|
|
50
|
+
const features = [...additionalFeatures];
|
|
51
|
+
|
|
52
|
+
const [treeInstance, finalizeTree] = buildInstance(
|
|
53
|
+
features,
|
|
54
|
+
"treeInstance",
|
|
55
|
+
(tree) => ({ tree }),
|
|
56
|
+
);
|
|
70
57
|
|
|
71
58
|
let state = additionalFeatures.reduce(
|
|
72
59
|
(acc, feature) => feature.getInitialState?.(acc, treeInstance) ?? acc,
|
|
73
60
|
initialConfig.initialState ?? initialConfig.state ?? {},
|
|
74
61
|
) as TreeState<T>;
|
|
75
62
|
let config = additionalFeatures.reduce(
|
|
76
|
-
(acc, feature) =>
|
|
63
|
+
(acc, feature) =>
|
|
64
|
+
(feature.getDefaultConfig?.(acc, treeInstance) as TreeConfig<T>) ?? acc,
|
|
77
65
|
initialConfig,
|
|
78
66
|
) as TreeConfig<T>;
|
|
79
67
|
const stateHandlerNames = additionalFeatures.reduce(
|
|
80
68
|
(acc, feature) => ({ ...acc, ...feature.stateHandlerNames }),
|
|
81
|
-
{} as Record<string,
|
|
69
|
+
{} as Record<string, keyof TreeConfig<T>>,
|
|
82
70
|
);
|
|
83
71
|
|
|
84
72
|
let treeElement: HTMLElement | undefined | null;
|
|
@@ -92,16 +80,17 @@ export const createTree = <T>(
|
|
|
92
80
|
|
|
93
81
|
const hotkeyPresets = {} as HotkeysConfig<T>;
|
|
94
82
|
|
|
95
|
-
const rebuildItemMeta = (
|
|
83
|
+
const rebuildItemMeta = () => {
|
|
96
84
|
// TODO can we find a way to only run this for the changed substructure?
|
|
97
85
|
itemInstances = [];
|
|
98
86
|
itemMetaMap = {};
|
|
99
87
|
|
|
100
|
-
const rootInstance =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
config.rootItemId,
|
|
88
|
+
const [rootInstance, finalizeRootInstance] = buildInstance(
|
|
89
|
+
features,
|
|
90
|
+
"itemInstance",
|
|
91
|
+
(item) => ({ item, tree: treeInstance, itemId: config.rootItemId }),
|
|
104
92
|
);
|
|
93
|
+
finalizeRootInstance();
|
|
105
94
|
itemInstancesMap[config.rootItemId] = rootInstance;
|
|
106
95
|
itemMetaMap[config.rootItemId] = {
|
|
107
96
|
itemId: config.rootItemId,
|
|
@@ -115,11 +104,16 @@ export const createTree = <T>(
|
|
|
115
104
|
for (const item of treeInstance.getItemsMeta()) {
|
|
116
105
|
itemMetaMap[item.itemId] = item;
|
|
117
106
|
if (!itemInstancesMap[item.itemId]) {
|
|
118
|
-
const instance =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
const [instance, finalizeInstance] = buildInstance(
|
|
108
|
+
features,
|
|
109
|
+
"itemInstance",
|
|
110
|
+
(instance) => ({
|
|
111
|
+
item: instance,
|
|
112
|
+
tree: treeInstance,
|
|
113
|
+
itemId: item.itemId,
|
|
114
|
+
}),
|
|
122
115
|
);
|
|
116
|
+
finalizeInstance();
|
|
123
117
|
itemInstancesMap[item.itemId] = instance;
|
|
124
118
|
itemInstances.push(instance);
|
|
125
119
|
} else {
|
|
@@ -140,34 +134,41 @@ export const createTree = <T>(
|
|
|
140
134
|
MainFeatureDef<T>
|
|
141
135
|
> = {
|
|
142
136
|
key: "main",
|
|
143
|
-
|
|
144
|
-
...prev,
|
|
137
|
+
treeInstance: {
|
|
145
138
|
getState: () => state,
|
|
146
|
-
setState: (updater) => {
|
|
139
|
+
setState: ({}, updater) => {
|
|
147
140
|
// Not necessary, since I think the subupdate below keeps the state fresh anyways?
|
|
148
141
|
// state = typeof updater === "function" ? updater(state) : updater;
|
|
149
|
-
config.setState?.(state);
|
|
142
|
+
config.setState?.(state); // TODO this cant be right... This doesnt allow external state updates
|
|
150
143
|
},
|
|
151
|
-
applySubStateUpdate:
|
|
144
|
+
applySubStateUpdate: <K extends keyof TreeState<any>>(
|
|
145
|
+
{},
|
|
146
|
+
stateName: K,
|
|
147
|
+
updater: Updater<TreeState<T>[K]>,
|
|
148
|
+
) => {
|
|
152
149
|
state[stateName] =
|
|
153
150
|
typeof updater === "function" ? updater(state[stateName]) : updater;
|
|
154
|
-
config[
|
|
151
|
+
const externalStateSetter = config[
|
|
152
|
+
stateHandlerNames[stateName]
|
|
153
|
+
] as Function;
|
|
154
|
+
externalStateSetter?.(state[stateName]);
|
|
155
155
|
},
|
|
156
|
+
// TODO rebuildSubTree: (itemId: string) => void;
|
|
156
157
|
rebuildTree: () => {
|
|
157
|
-
rebuildItemMeta(
|
|
158
|
+
rebuildItemMeta();
|
|
158
159
|
config.setState?.(state);
|
|
159
160
|
},
|
|
160
161
|
getConfig: () => config,
|
|
161
|
-
setConfig: (updater) => {
|
|
162
|
+
setConfig: (_, updater) => {
|
|
162
163
|
config = typeof updater === "function" ? updater(config) : updater;
|
|
163
164
|
|
|
164
165
|
if (config.state) {
|
|
165
166
|
state = { ...state, ...config.state };
|
|
166
167
|
}
|
|
167
168
|
},
|
|
168
|
-
getItemInstance: (itemId) => itemInstancesMap[itemId],
|
|
169
|
+
getItemInstance: ({}, itemId) => itemInstancesMap[itemId],
|
|
169
170
|
getItems: () => itemInstances,
|
|
170
|
-
registerElement: (element) => {
|
|
171
|
+
registerElement: ({}, element) => {
|
|
171
172
|
if (treeElement === element) {
|
|
172
173
|
return;
|
|
173
174
|
}
|
|
@@ -186,10 +187,10 @@ export const createTree = <T>(
|
|
|
186
187
|
getElement: () => treeElement,
|
|
187
188
|
getDataRef: () => treeDataRef,
|
|
188
189
|
getHotkeyPresets: () => hotkeyPresets,
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
registerElement: (element) => {
|
|
190
|
+
},
|
|
191
|
+
itemInstance: {
|
|
192
|
+
// TODO just change to a getRef method that memoizes, maybe as part of getProps
|
|
193
|
+
registerElement: ({ itemId, item }, element) => {
|
|
193
194
|
if (itemElementsMap[itemId] === element) {
|
|
194
195
|
return;
|
|
195
196
|
}
|
|
@@ -197,33 +198,30 @@ export const createTree = <T>(
|
|
|
197
198
|
const oldElement = itemElementsMap[itemId];
|
|
198
199
|
if (oldElement && !element) {
|
|
199
200
|
eachFeature((feature) =>
|
|
200
|
-
feature.onItemUnmount?.(
|
|
201
|
+
feature.onItemUnmount?.(item, oldElement!, treeInstance),
|
|
201
202
|
);
|
|
202
203
|
} else if (!oldElement && element) {
|
|
203
204
|
eachFeature((feature) =>
|
|
204
|
-
feature.onItemMount?.(
|
|
205
|
+
feature.onItemMount?.(item, element!, treeInstance),
|
|
205
206
|
);
|
|
206
207
|
}
|
|
207
208
|
itemElementsMap[itemId] = element;
|
|
208
209
|
},
|
|
209
|
-
getElement: () => itemElementsMap[itemId],
|
|
210
|
+
getElement: ({ itemId }) => itemElementsMap[itemId],
|
|
210
211
|
// eslint-disable-next-line no-return-assign
|
|
211
|
-
getDataRef: () => (itemDataRefs[itemId] ??= { current: {} }),
|
|
212
|
-
getItemMeta: () => itemMetaMap[itemId],
|
|
213
|
-
}
|
|
212
|
+
getDataRef: ({ itemId }) => (itemDataRefs[itemId] ??= { current: {} }),
|
|
213
|
+
getItemMeta: ({ itemId }) => itemMetaMap[itemId],
|
|
214
|
+
},
|
|
214
215
|
};
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
features.unshift(mainFeature);
|
|
217
218
|
|
|
218
219
|
for (const feature of features) {
|
|
219
|
-
Object.assign(
|
|
220
|
-
treeInstance,
|
|
221
|
-
feature.createTreeInstance?.({ ...treeInstance }, treeInstance) ?? {},
|
|
222
|
-
);
|
|
223
220
|
Object.assign(hotkeyPresets, feature.hotkeys ?? {});
|
|
224
221
|
}
|
|
225
222
|
|
|
226
|
-
|
|
223
|
+
finalizeTree();
|
|
224
|
+
rebuildItemMeta();
|
|
227
225
|
|
|
228
226
|
return treeInstance;
|
|
229
227
|
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { TestTree } from "../../test-utils/test-tree";
|
|
3
|
+
import { asyncDataLoaderFeature } from "./feature";
|
|
4
|
+
|
|
5
|
+
const tree = TestTree.default({}).withFeatures(asyncDataLoaderFeature);
|
|
6
|
+
|
|
7
|
+
describe("core-feature/selections", () => {
|
|
8
|
+
tree.resetBeforeEach();
|
|
9
|
+
|
|
10
|
+
describe("loading of items", () => {
|
|
11
|
+
it("has initial items", () => {
|
|
12
|
+
tree.expect.hasChildren("x", ["x1", "x2", "x3", "x4"]);
|
|
13
|
+
tree.expect.hasChildren("x1", ["x11", "x12", "x13", "x14"]);
|
|
14
|
+
tree.expect.hasChildren("x11", ["x111", "x112", "x113", "x114"]);
|
|
15
|
+
tree.expect.hasChildren("x12", []);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it.skip("has loading items after expanding", async () => {
|
|
19
|
+
tree.do.selectItem("x12");
|
|
20
|
+
await TestTree.resolveAsyncLoaders();
|
|
21
|
+
// tree.debug();
|
|
22
|
+
tree.expect.hasChildren("x12", [
|
|
23
|
+
"loading",
|
|
24
|
+
"loading",
|
|
25
|
+
"loading",
|
|
26
|
+
"loading",
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("has loaded items after expanding and loading", async () => {
|
|
31
|
+
tree.do.selectItem("x12");
|
|
32
|
+
await tree.resolveAsyncVisibleItems();
|
|
33
|
+
tree.expect.hasChildren("x12", ["x121", "x122", "x123", "x124"]);
|
|
34
|
+
tree.expect.hasChildren("x12", ["x121", "x122", "x123", "x124"]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("calls handlers", () => {
|
|
39
|
+
it("updates setLoadingItems", async () => {
|
|
40
|
+
const setLoadingItems = tree.mockedHandler("setLoadingItems");
|
|
41
|
+
tree.do.selectItem("x12");
|
|
42
|
+
expect(setLoadingItems).toHaveBeenCalledWith(["x12"]);
|
|
43
|
+
await tree.resolveAsyncVisibleItems();
|
|
44
|
+
expect(setLoadingItems).toHaveBeenCalledWith([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("calls onLoadedItem", async () => {
|
|
48
|
+
const onLoadedItem = tree.mockedHandler("onLoadedItem");
|
|
49
|
+
tree.do.selectItem("x12");
|
|
50
|
+
await tree.resolveAsyncVisibleItems();
|
|
51
|
+
expect(onLoadedItem).toHaveBeenCalledWith("x121", "x121");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("calls onLoadedChildren", async () => {
|
|
55
|
+
const onLoadedChildren = tree.mockedHandler("onLoadedChildren");
|
|
56
|
+
tree.do.selectItem("x12");
|
|
57
|
+
await tree.resolveAsyncVisibleItems();
|
|
58
|
+
expect(onLoadedChildren).toHaveBeenCalledWith("x12", [
|
|
59
|
+
"x121",
|
|
60
|
+
"x122",
|
|
61
|
+
"x123",
|
|
62
|
+
"x124",
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("data invalidation", () => {
|
|
68
|
+
const getItem = vi.fn(async (id) => id);
|
|
69
|
+
const getChildren = vi.fn(async (id) => [
|
|
70
|
+
`${id}1`,
|
|
71
|
+
`${id}2`,
|
|
72
|
+
`${id}3`,
|
|
73
|
+
`${id}4`,
|
|
74
|
+
]);
|
|
75
|
+
const suiteTree = tree.with({ asyncDataLoader: { getItem, getChildren } });
|
|
76
|
+
suiteTree.resetBeforeEach();
|
|
77
|
+
|
|
78
|
+
it("invalidates item data on tree instance", async () => {
|
|
79
|
+
getItem.mockClear();
|
|
80
|
+
getItem.mockResolvedValueOnce("new");
|
|
81
|
+
suiteTree.instance.invalidateItemData("x1");
|
|
82
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
83
|
+
expect(getItem).toHaveBeenCalledWith("x1");
|
|
84
|
+
expect(suiteTree.instance.getItemInstance("x1").getItemData()).toBe(
|
|
85
|
+
"new",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("invalidates item data on item instance", async () => {
|
|
90
|
+
getItem.mockClear();
|
|
91
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
92
|
+
getItem.mockResolvedValueOnce("new");
|
|
93
|
+
suiteTree.instance.getItemInstance("x1").invalidateItemData();
|
|
94
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
95
|
+
expect(getItem).toHaveBeenCalledWith("x1");
|
|
96
|
+
expect(suiteTree.instance.getItemInstance("x1").getItemData()).toBe(
|
|
97
|
+
"new",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("invalidates children ids on tree instance", async () => {
|
|
102
|
+
getChildren.mockClear();
|
|
103
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
104
|
+
getChildren.mockResolvedValueOnce(["new1", "new2"]);
|
|
105
|
+
suiteTree.instance.invalidateChildrenIds("x1");
|
|
106
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
107
|
+
expect(getChildren).toHaveBeenCalledWith("x1");
|
|
108
|
+
suiteTree.expect.hasChildren("x1", ["new1", "new2"]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("invalidates children ids on item instance", async () => {
|
|
112
|
+
getChildren.mockClear();
|
|
113
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
114
|
+
getChildren.mockResolvedValueOnce(["new1", "new2"]);
|
|
115
|
+
suiteTree.instance.getItemInstance("x1").invalidateChildrenIds();
|
|
116
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
117
|
+
expect(getChildren).toHaveBeenCalledWith("x1");
|
|
118
|
+
suiteTree.expect.hasChildren("x1", ["new1", "new2"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("doesnt call item data getter twice", async () => {
|
|
122
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
123
|
+
getItem.mockClear();
|
|
124
|
+
suiteTree.instance.invalidateItemData("x1");
|
|
125
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
126
|
+
expect(suiteTree.instance.getItemInstance("x1").getItemData()).toBe("x1");
|
|
127
|
+
expect(suiteTree.instance.getItemInstance("x1").getItemData()).toBe("x1");
|
|
128
|
+
expect(getItem).toHaveBeenCalledTimes(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("doesnt call children getter twice", async () => {
|
|
132
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
133
|
+
getChildren.mockClear();
|
|
134
|
+
suiteTree.instance.invalidateChildrenIds("x1");
|
|
135
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
136
|
+
suiteTree.expect.hasChildren("x1", ["x11", "x12", "x13", "x14"]);
|
|
137
|
+
suiteTree.expect.hasChildren("x1", ["x11", "x12", "x13", "x14"]);
|
|
138
|
+
expect(getChildren).toHaveBeenCalledTimes(1);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe.todo("getChildrenWithData");
|
|
143
|
+
});
|
|
@@ -25,12 +25,10 @@ export const asyncDataLoaderFeature: FeatureImplementation<
|
|
|
25
25
|
loadingItems: "setLoadingItems",
|
|
26
26
|
},
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const config = instance.getConfig();
|
|
33
|
-
const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
|
|
28
|
+
treeInstance: {
|
|
29
|
+
retrieveItemData: ({ tree }, itemId) => {
|
|
30
|
+
const config = tree.getConfig();
|
|
31
|
+
const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
|
|
34
32
|
dataRef.current.itemData ??= {};
|
|
35
33
|
dataRef.current.childrenIds ??= {};
|
|
36
34
|
|
|
@@ -38,15 +36,15 @@ export const asyncDataLoaderFeature: FeatureImplementation<
|
|
|
38
36
|
return dataRef.current.itemData[itemId];
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
if (!
|
|
42
|
-
|
|
39
|
+
if (!tree.getState().loadingItems.includes(itemId)) {
|
|
40
|
+
tree.applySubStateUpdate("loadingItems", (loadingItems) => [
|
|
43
41
|
...loadingItems,
|
|
44
42
|
itemId,
|
|
45
43
|
]);
|
|
46
44
|
config.asyncDataLoader?.getItem(itemId).then((item) => {
|
|
47
45
|
dataRef.current.itemData[itemId] = item;
|
|
48
46
|
config.onLoadedItem?.(itemId, item);
|
|
49
|
-
|
|
47
|
+
tree.applySubStateUpdate("loadingItems", (loadingItems) =>
|
|
50
48
|
loadingItems.filter((id) => id !== itemId),
|
|
51
49
|
);
|
|
52
50
|
});
|
|
@@ -55,20 +53,20 @@ export const asyncDataLoaderFeature: FeatureImplementation<
|
|
|
55
53
|
return config.createLoadingItemData?.() ?? null;
|
|
56
54
|
},
|
|
57
55
|
|
|
58
|
-
retrieveChildrenIds: (itemId) => {
|
|
59
|
-
const config =
|
|
60
|
-
const dataRef =
|
|
56
|
+
retrieveChildrenIds: ({ tree }, itemId) => {
|
|
57
|
+
const config = tree.getConfig();
|
|
58
|
+
const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
|
|
61
59
|
dataRef.current.itemData ??= {};
|
|
62
60
|
dataRef.current.childrenIds ??= {};
|
|
63
61
|
if (dataRef.current.childrenIds[itemId]) {
|
|
64
62
|
return dataRef.current.childrenIds[itemId];
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
if (
|
|
65
|
+
if (tree.getState().loadingItems.includes(itemId)) {
|
|
68
66
|
return [];
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
tree.applySubStateUpdate("loadingItems", (loadingItems) => [
|
|
72
70
|
...loadingItems,
|
|
73
71
|
itemId,
|
|
74
72
|
]);
|
|
@@ -82,45 +80,49 @@ export const asyncDataLoaderFeature: FeatureImplementation<
|
|
|
82
80
|
const childrenIds = children.map(({ id }) => id);
|
|
83
81
|
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
84
82
|
config.onLoadedChildren?.(itemId, childrenIds);
|
|
85
|
-
|
|
83
|
+
tree.applySubStateUpdate("loadingItems", (loadingItems) =>
|
|
86
84
|
loadingItems.filter((id) => id !== itemId),
|
|
87
85
|
);
|
|
88
|
-
|
|
86
|
+
tree.rebuildTree();
|
|
89
87
|
});
|
|
90
88
|
} else {
|
|
91
89
|
config.asyncDataLoader?.getChildren(itemId).then((childrenIds) => {
|
|
92
90
|
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
93
91
|
config.onLoadedChildren?.(itemId, childrenIds);
|
|
94
|
-
|
|
92
|
+
tree.applySubStateUpdate("loadingItems", (loadingItems) =>
|
|
95
93
|
loadingItems.filter((id) => id !== itemId),
|
|
96
94
|
);
|
|
97
|
-
|
|
95
|
+
tree.rebuildTree();
|
|
98
96
|
});
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
return [];
|
|
102
100
|
},
|
|
103
101
|
|
|
104
|
-
invalidateItemData: (itemId) => {
|
|
105
|
-
const dataRef =
|
|
102
|
+
invalidateItemData: ({ tree }, itemId) => {
|
|
103
|
+
const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
|
|
106
104
|
delete dataRef.current.itemData?.[itemId];
|
|
107
|
-
|
|
105
|
+
tree.retrieveItemData(itemId);
|
|
108
106
|
},
|
|
109
107
|
|
|
110
|
-
invalidateChildrenIds: (itemId) => {
|
|
111
|
-
const dataRef =
|
|
108
|
+
invalidateChildrenIds: ({ tree }, itemId) => {
|
|
109
|
+
const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
|
|
112
110
|
delete dataRef.current.childrenIds?.[itemId];
|
|
113
|
-
|
|
111
|
+
tree.retrieveChildrenIds(itemId);
|
|
114
112
|
},
|
|
115
|
-
}
|
|
113
|
+
},
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
isLoading: () =>
|
|
115
|
+
itemInstance: {
|
|
116
|
+
isLoading: ({ tree, item }) =>
|
|
120
117
|
tree.getState().loadingItems.includes(item.getItemMeta().itemId),
|
|
121
|
-
invalidateItemData: () =>
|
|
118
|
+
invalidateItemData: ({ tree, item }) =>
|
|
122
119
|
tree.invalidateItemData(item.getItemMeta().itemId),
|
|
123
|
-
invalidateChildrenIds: () =>
|
|
120
|
+
invalidateChildrenIds: ({ tree, item }) =>
|
|
124
121
|
tree.invalidateChildrenIds(item.getItemMeta().itemId),
|
|
125
|
-
|
|
122
|
+
updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
|
|
123
|
+
const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
|
|
124
|
+
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
125
|
+
tree.rebuildTree();
|
|
126
|
+
},
|
|
127
|
+
},
|
|
126
128
|
};
|
|
@@ -31,11 +31,13 @@ export type AsyncDataLoaderFeatureDef<T> = {
|
|
|
31
31
|
/** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible */
|
|
32
32
|
invalidateItemData: (itemId: string) => void;
|
|
33
33
|
invalidateChildrenIds: (itemId: string) => void;
|
|
34
|
+
// TODO deprecate tree instance methods, move to item instance
|
|
34
35
|
};
|
|
35
36
|
itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
|
|
36
37
|
invalidateItemData: () => void;
|
|
37
38
|
invalidateChildrenIds: () => void;
|
|
38
|
-
|
|
39
|
+
updateCachedChildrenIds: (childrenIds: string[]) => void;
|
|
40
|
+
isLoading: () => boolean;
|
|
39
41
|
};
|
|
40
42
|
hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
|
|
41
43
|
};
|