@headless-tree/core 0.0.0-20250820225709 → 0.0.0-20250828223340

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 CHANGED
@@ -1,10 +1,13 @@
1
1
  # @headless-tree/core
2
2
 
3
- ## 0.0.0-20250820225709
3
+ ## 0.0.0-20250828223340
4
4
 
5
5
  ### Patch Changes
6
6
 
7
7
  - 215ab4b: add a new symbol that can be used in hotkey configurations "metaorcontrol" that will trigger if either any windows control key or mac meta key is pressed (#141)
8
+ - cf845d7: Added new state variable `loadingCheckPropagationItems` to indicate if, in async trees with checkboxes and state propagation enabled, data loading operations are currently loading due to a checkbox propagation taking place
9
+ - 597faad: Checkbox propagation is now supported for trees with async data loaders!
10
+ - b0ee382: triggering a data refetch will now always set the loadingItemData/loadingItemChildrens state variable to the associated items if they where not apart of the cache before
8
11
 
9
12
  ## 1.4.0
10
13
 
package/dist/index.d.mts CHANGED
@@ -312,7 +312,8 @@ type AsyncDataLoaderFeatureDef<T> = {
312
312
  * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
313
313
  * the tree will continue to display the old data until the new data has loaded. */
314
314
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
315
- updateCachedData: (data: T) => void;
315
+ /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
316
+ updateCachedData: (data: T | undefined) => void;
316
317
  updateCachedChildrenIds: (childrenIds: string[]) => void;
317
318
  isLoading: () => boolean;
318
319
  };
@@ -448,9 +449,11 @@ declare enum CheckedState {
448
449
  type CheckboxesFeatureDef<T> = {
449
450
  state: {
450
451
  checkedItems: string[];
452
+ loadingCheckPropagationItems: string[];
451
453
  };
452
454
  config: {
453
455
  setCheckedItems?: SetStateFn<string[]>;
456
+ setLoadingCheckPropagationItems?: SetStateFn<string[]>;
454
457
  canCheckFolders?: boolean;
455
458
  propagateCheckedState?: boolean;
456
459
  };
@@ -458,11 +461,18 @@ type CheckboxesFeatureDef<T> = {
458
461
  setCheckedItems: (checkedItems: string[]) => void;
459
462
  };
460
463
  itemInstance: {
461
- setChecked: () => void;
462
- setUnchecked: () => void;
463
- toggleCheckedState: () => void;
464
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
465
+ * this will return immediately. */
466
+ setChecked: () => Promise<void>;
467
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
468
+ * this will return immediately. */
469
+ setUnchecked: () => Promise<void>;
470
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
471
+ * this will return immediately. */
472
+ toggleCheckedState: () => Promise<void>;
464
473
  getCheckedState: () => CheckedState;
465
474
  getCheckboxProps: () => Record<string, any>;
475
+ isLoadingCheckPropagation: () => boolean;
466
476
  };
467
477
  hotkeys: never;
468
478
  };
package/dist/index.d.ts CHANGED
@@ -312,7 +312,8 @@ type AsyncDataLoaderFeatureDef<T> = {
312
312
  * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
313
313
  * the tree will continue to display the old data until the new data has loaded. */
314
314
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
315
- updateCachedData: (data: T) => void;
315
+ /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
316
+ updateCachedData: (data: T | undefined) => void;
316
317
  updateCachedChildrenIds: (childrenIds: string[]) => void;
317
318
  isLoading: () => boolean;
318
319
  };
@@ -448,9 +449,11 @@ declare enum CheckedState {
448
449
  type CheckboxesFeatureDef<T> = {
449
450
  state: {
450
451
  checkedItems: string[];
452
+ loadingCheckPropagationItems: string[];
451
453
  };
452
454
  config: {
453
455
  setCheckedItems?: SetStateFn<string[]>;
456
+ setLoadingCheckPropagationItems?: SetStateFn<string[]>;
454
457
  canCheckFolders?: boolean;
455
458
  propagateCheckedState?: boolean;
456
459
  };
@@ -458,11 +461,18 @@ type CheckboxesFeatureDef<T> = {
458
461
  setCheckedItems: (checkedItems: string[]) => void;
459
462
  };
460
463
  itemInstance: {
461
- setChecked: () => void;
462
- setUnchecked: () => void;
463
- toggleCheckedState: () => void;
464
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
465
+ * this will return immediately. */
466
+ setChecked: () => Promise<void>;
467
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
468
+ * this will return immediately. */
469
+ setUnchecked: () => Promise<void>;
470
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
471
+ * this will return immediately. */
472
+ toggleCheckedState: () => Promise<void>;
464
473
  getCheckedState: () => CheckedState;
465
474
  getCheckboxProps: () => Record<string, any>;
475
+ isLoadingCheckPropagation: () => boolean;
466
476
  };
467
477
  hotkeys: never;
468
478
  };
package/dist/index.js CHANGED
@@ -817,33 +817,60 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
817
817
  if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
818
818
  return [itemId];
819
819
  }
820
- const descendants = tree.retrieveChildrenIds(itemId).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
820
+ const descendants = tree.retrieveChildrenIds(itemId, true).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
821
821
  return includeFolders ? [itemId, ...descendants] : descendants;
822
822
  };
823
+ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
824
+ yield tree.loadItemData(itemId);
825
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
826
+ return [itemId];
827
+ }
828
+ const childrenIds = yield tree.loadChildrenIds(itemId);
829
+ const descendants = (yield Promise.all(
830
+ childrenIds.map(
831
+ (child) => getAllDescendants(tree, child, includeFolders)
832
+ )
833
+ )).flat();
834
+ return includeFolders ? [itemId, ...descendants] : descendants;
835
+ });
836
+ var withLoadingState = (tree, itemId, callback) => __async(null, null, function* () {
837
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
838
+ ...items,
839
+ itemId
840
+ ]);
841
+ try {
842
+ yield callback();
843
+ } finally {
844
+ tree.applySubStateUpdate(
845
+ "loadingCheckPropagationItems",
846
+ (items) => items.filter((id) => id !== itemId)
847
+ );
848
+ }
849
+ });
823
850
  var checkboxesFeature = {
824
851
  key: "checkboxes",
825
852
  overwrites: ["selection"],
826
853
  getInitialState: (initialState) => __spreadValues({
827
- checkedItems: []
854
+ checkedItems: [],
855
+ loadingCheckPropagationItems: []
828
856
  }, initialState),
829
857
  getDefaultConfig: (defaultConfig, tree) => {
830
- var _a, _b, _c;
831
- const hasAsyncLoader = (_a = defaultConfig.features) == null ? void 0 : _a.some(
832
- (f) => f.key === "async-data-loader"
833
- );
834
- if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
835
- throwError(`propagateCheckedState not supported with async trees`);
836
- }
837
- const propagateCheckedState = (_b = defaultConfig.propagateCheckedState) != null ? _b : !hasAsyncLoader;
838
- const canCheckFolders = (_c = defaultConfig.canCheckFolders) != null ? _c : !propagateCheckedState;
858
+ var _a, _b;
859
+ const propagateCheckedState = (_a = defaultConfig.propagateCheckedState) != null ? _a : true;
860
+ const canCheckFolders = (_b = defaultConfig.canCheckFolders) != null ? _b : !propagateCheckedState;
839
861
  return __spreadValues({
840
862
  setCheckedItems: makeStateUpdater("checkedItems", tree),
863
+ setLoadingCheckPropagationItems: makeStateUpdater(
864
+ "loadingCheckPropagationItems",
865
+ tree
866
+ ),
841
867
  propagateCheckedState,
842
868
  canCheckFolders
843
869
  }, defaultConfig);
844
870
  },
845
871
  stateHandlerNames: {
846
- checkedItems: "setCheckedItems"
872
+ checkedItems: "setCheckedItems",
873
+ loadingCheckPropagationItems: "setLoadingCheckPropagationItems"
847
874
  },
848
875
  treeInstance: {
849
876
  setCheckedItems: ({ tree }, checkedItems) => {
@@ -863,13 +890,13 @@ var checkboxesFeature = {
863
890
  }
864
891
  };
865
892
  },
866
- toggleCheckedState: ({ item }) => {
893
+ toggleCheckedState: (_0) => __async(null, [_0], function* ({ item }) {
867
894
  if (item.getCheckedState() === "checked" /* Checked */) {
868
- item.setUnchecked();
895
+ yield item.setUnchecked();
869
896
  } else {
870
- item.setChecked();
897
+ yield item.setChecked();
871
898
  }
872
- },
899
+ }),
873
900
  getCheckedState: ({ item, tree }) => {
874
901
  const { checkedItems } = tree.getState();
875
902
  const { propagateCheckedState } = tree.getConfig();
@@ -879,6 +906,7 @@ var checkboxesFeature = {
879
906
  }
880
907
  if (item.isFolder() && propagateCheckedState) {
881
908
  const descendants = getAllLoadedDescendants(tree, itemId);
909
+ if (descendants.length === 0) return "unchecked" /* Unchecked */;
882
910
  if (descendants.every((d) => checkedItems.includes(d))) {
883
911
  return "checked" /* Checked */;
884
912
  }
@@ -888,36 +916,48 @@ var checkboxesFeature = {
888
916
  }
889
917
  return "unchecked" /* Unchecked */;
890
918
  },
891
- setChecked: ({ item, tree, itemId }) => {
892
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
893
- if (item.isFolder() && propagateCheckedState) {
894
- tree.applySubStateUpdate("checkedItems", (items) => [
895
- ...items,
896
- ...getAllLoadedDescendants(tree, itemId, canCheckFolders)
897
- ]);
898
- } else if (!item.isFolder() || canCheckFolders) {
899
- tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
900
- }
901
- },
902
- setUnchecked: ({ item, tree, itemId }) => {
903
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
904
- if (item.isFolder() && propagateCheckedState) {
905
- const descendants = getAllLoadedDescendants(
906
- tree,
907
- itemId,
908
- canCheckFolders
909
- );
910
- tree.applySubStateUpdate(
911
- "checkedItems",
912
- (items) => items.filter((id) => !descendants.includes(id) && id !== itemId)
913
- );
914
- } else {
915
- tree.applySubStateUpdate(
916
- "checkedItems",
917
- (items) => items.filter((id) => id !== itemId)
918
- );
919
- }
920
- }
919
+ setChecked: (_0) => __async(null, [_0], function* ({ item, tree, itemId }) {
920
+ yield withLoadingState(tree, itemId, () => __async(null, null, function* () {
921
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
922
+ if (item.isFolder() && propagateCheckedState) {
923
+ const descendants = yield getAllDescendants(
924
+ tree,
925
+ itemId,
926
+ canCheckFolders
927
+ );
928
+ tree.applySubStateUpdate("checkedItems", (items) => [
929
+ ...items,
930
+ ...descendants
931
+ ]);
932
+ } else if (!item.isFolder() || canCheckFolders) {
933
+ tree.applySubStateUpdate("checkedItems", (items) => [
934
+ ...items,
935
+ itemId
936
+ ]);
937
+ }
938
+ }));
939
+ }),
940
+ setUnchecked: (_0) => __async(null, [_0], function* ({ item, tree, itemId }) {
941
+ yield withLoadingState(tree, itemId, () => __async(null, null, function* () {
942
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
943
+ if (item.isFolder() && propagateCheckedState) {
944
+ const descendants = yield getAllDescendants(
945
+ tree,
946
+ itemId,
947
+ canCheckFolders
948
+ );
949
+ tree.applySubStateUpdate(
950
+ "checkedItems",
951
+ (items) => items.filter((id) => !descendants.includes(id) && id !== itemId)
952
+ );
953
+ } else {
954
+ tree.applySubStateUpdate(
955
+ "checkedItems",
956
+ (items) => items.filter((id) => id !== itemId)
957
+ );
958
+ }
959
+ }));
960
+ })
921
961
  }
922
962
  };
923
963
 
@@ -1035,6 +1075,12 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
1035
1075
  var _a;
1036
1076
  const config = tree.getConfig();
1037
1077
  const dataRef = getDataRef(tree);
1078
+ if (!dataRef.current.itemData[itemId]) {
1079
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1080
+ ...loadingItemData,
1081
+ itemId
1082
+ ]);
1083
+ }
1038
1084
  const item = yield config.dataLoader.getItem(itemId);
1039
1085
  dataRef.current.itemData[itemId] = item;
1040
1086
  (_a = config.onLoadedItem) == null ? void 0 : _a.call(config, itemId, item);
@@ -1049,6 +1095,12 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1049
1095
  const config = tree.getConfig();
1050
1096
  const dataRef = getDataRef(tree);
1051
1097
  let childrenIds;
1098
+ if (!dataRef.current.childrenIds[itemId]) {
1099
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
1100
+ ...loadingItemChildrens,
1101
+ itemId
1102
+ ]);
1103
+ }
1052
1104
  if ("getChildrenWithData" in config.dataLoader) {
1053
1105
  const children = yield config.dataLoader.getChildrenWithData(itemId);
1054
1106
  childrenIds = children.map((c) => c.id);
@@ -1109,10 +1161,6 @@ var asyncDataLoaderFeature = {
1109
1161
  return dataRef.current.itemData[itemId];
1110
1162
  }
1111
1163
  if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
1112
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1113
- ...loadingItemData,
1114
- itemId
1115
- ]);
1116
1164
  loadItemData(tree, itemId);
1117
1165
  }
1118
1166
  return (_b = (_a = config.createLoadingItemData) == null ? void 0 : _a.call(config)) != null ? _b : null;
@@ -1125,10 +1173,6 @@ var asyncDataLoaderFeature = {
1125
1173
  if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
1126
1174
  return [];
1127
1175
  }
1128
- tree.applySubStateUpdate(
1129
- "loadingItemChildrens",
1130
- (loadingItemChildrens) => [...loadingItemChildrens, itemId]
1131
- );
1132
1176
  loadChildrenIds(tree, itemId);
1133
1177
  return [];
1134
1178
  }
@@ -1139,10 +1183,6 @@ var asyncDataLoaderFeature = {
1139
1183
  var _a;
1140
1184
  if (!optimistic) {
1141
1185
  (_a = getDataRef(tree).current.itemData) == null ? true : delete _a[itemId];
1142
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1143
- ...loadingItemData,
1144
- itemId
1145
- ]);
1146
1186
  }
1147
1187
  yield loadItemData(tree, itemId);
1148
1188
  }),
@@ -1150,10 +1190,6 @@ var asyncDataLoaderFeature = {
1150
1190
  var _a;
1151
1191
  if (!optimistic) {
1152
1192
  (_a = getDataRef(tree).current.childrenIds) == null ? true : delete _a[itemId];
1153
- tree.applySubStateUpdate(
1154
- "loadingItemChildrens",
1155
- (loadingItemChildrens) => [...loadingItemChildrens, itemId]
1156
- );
1157
1193
  }
1158
1194
  yield loadChildrenIds(tree, itemId);
1159
1195
  }),
package/dist/index.mjs CHANGED
@@ -773,33 +773,60 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
773
773
  if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
774
774
  return [itemId];
775
775
  }
776
- const descendants = tree.retrieveChildrenIds(itemId).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
776
+ const descendants = tree.retrieveChildrenIds(itemId, true).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
777
777
  return includeFolders ? [itemId, ...descendants] : descendants;
778
778
  };
779
+ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
780
+ yield tree.loadItemData(itemId);
781
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
782
+ return [itemId];
783
+ }
784
+ const childrenIds = yield tree.loadChildrenIds(itemId);
785
+ const descendants = (yield Promise.all(
786
+ childrenIds.map(
787
+ (child) => getAllDescendants(tree, child, includeFolders)
788
+ )
789
+ )).flat();
790
+ return includeFolders ? [itemId, ...descendants] : descendants;
791
+ });
792
+ var withLoadingState = (tree, itemId, callback) => __async(null, null, function* () {
793
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
794
+ ...items,
795
+ itemId
796
+ ]);
797
+ try {
798
+ yield callback();
799
+ } finally {
800
+ tree.applySubStateUpdate(
801
+ "loadingCheckPropagationItems",
802
+ (items) => items.filter((id) => id !== itemId)
803
+ );
804
+ }
805
+ });
779
806
  var checkboxesFeature = {
780
807
  key: "checkboxes",
781
808
  overwrites: ["selection"],
782
809
  getInitialState: (initialState) => __spreadValues({
783
- checkedItems: []
810
+ checkedItems: [],
811
+ loadingCheckPropagationItems: []
784
812
  }, initialState),
785
813
  getDefaultConfig: (defaultConfig, tree) => {
786
- var _a, _b, _c;
787
- const hasAsyncLoader = (_a = defaultConfig.features) == null ? void 0 : _a.some(
788
- (f) => f.key === "async-data-loader"
789
- );
790
- if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
791
- throwError(`propagateCheckedState not supported with async trees`);
792
- }
793
- const propagateCheckedState = (_b = defaultConfig.propagateCheckedState) != null ? _b : !hasAsyncLoader;
794
- const canCheckFolders = (_c = defaultConfig.canCheckFolders) != null ? _c : !propagateCheckedState;
814
+ var _a, _b;
815
+ const propagateCheckedState = (_a = defaultConfig.propagateCheckedState) != null ? _a : true;
816
+ const canCheckFolders = (_b = defaultConfig.canCheckFolders) != null ? _b : !propagateCheckedState;
795
817
  return __spreadValues({
796
818
  setCheckedItems: makeStateUpdater("checkedItems", tree),
819
+ setLoadingCheckPropagationItems: makeStateUpdater(
820
+ "loadingCheckPropagationItems",
821
+ tree
822
+ ),
797
823
  propagateCheckedState,
798
824
  canCheckFolders
799
825
  }, defaultConfig);
800
826
  },
801
827
  stateHandlerNames: {
802
- checkedItems: "setCheckedItems"
828
+ checkedItems: "setCheckedItems",
829
+ loadingCheckPropagationItems: "setLoadingCheckPropagationItems"
803
830
  },
804
831
  treeInstance: {
805
832
  setCheckedItems: ({ tree }, checkedItems) => {
@@ -819,13 +846,13 @@ var checkboxesFeature = {
819
846
  }
820
847
  };
821
848
  },
822
- toggleCheckedState: ({ item }) => {
849
+ toggleCheckedState: (_0) => __async(null, [_0], function* ({ item }) {
823
850
  if (item.getCheckedState() === "checked" /* Checked */) {
824
- item.setUnchecked();
851
+ yield item.setUnchecked();
825
852
  } else {
826
- item.setChecked();
853
+ yield item.setChecked();
827
854
  }
828
- },
855
+ }),
829
856
  getCheckedState: ({ item, tree }) => {
830
857
  const { checkedItems } = tree.getState();
831
858
  const { propagateCheckedState } = tree.getConfig();
@@ -835,6 +862,7 @@ var checkboxesFeature = {
835
862
  }
836
863
  if (item.isFolder() && propagateCheckedState) {
837
864
  const descendants = getAllLoadedDescendants(tree, itemId);
865
+ if (descendants.length === 0) return "unchecked" /* Unchecked */;
838
866
  if (descendants.every((d) => checkedItems.includes(d))) {
839
867
  return "checked" /* Checked */;
840
868
  }
@@ -844,36 +872,48 @@ var checkboxesFeature = {
844
872
  }
845
873
  return "unchecked" /* Unchecked */;
846
874
  },
847
- setChecked: ({ item, tree, itemId }) => {
848
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
849
- if (item.isFolder() && propagateCheckedState) {
850
- tree.applySubStateUpdate("checkedItems", (items) => [
851
- ...items,
852
- ...getAllLoadedDescendants(tree, itemId, canCheckFolders)
853
- ]);
854
- } else if (!item.isFolder() || canCheckFolders) {
855
- tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
856
- }
857
- },
858
- setUnchecked: ({ item, tree, itemId }) => {
859
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
860
- if (item.isFolder() && propagateCheckedState) {
861
- const descendants = getAllLoadedDescendants(
862
- tree,
863
- itemId,
864
- canCheckFolders
865
- );
866
- tree.applySubStateUpdate(
867
- "checkedItems",
868
- (items) => items.filter((id) => !descendants.includes(id) && id !== itemId)
869
- );
870
- } else {
871
- tree.applySubStateUpdate(
872
- "checkedItems",
873
- (items) => items.filter((id) => id !== itemId)
874
- );
875
- }
876
- }
875
+ setChecked: (_0) => __async(null, [_0], function* ({ item, tree, itemId }) {
876
+ yield withLoadingState(tree, itemId, () => __async(null, null, function* () {
877
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
878
+ if (item.isFolder() && propagateCheckedState) {
879
+ const descendants = yield getAllDescendants(
880
+ tree,
881
+ itemId,
882
+ canCheckFolders
883
+ );
884
+ tree.applySubStateUpdate("checkedItems", (items) => [
885
+ ...items,
886
+ ...descendants
887
+ ]);
888
+ } else if (!item.isFolder() || canCheckFolders) {
889
+ tree.applySubStateUpdate("checkedItems", (items) => [
890
+ ...items,
891
+ itemId
892
+ ]);
893
+ }
894
+ }));
895
+ }),
896
+ setUnchecked: (_0) => __async(null, [_0], function* ({ item, tree, itemId }) {
897
+ yield withLoadingState(tree, itemId, () => __async(null, null, function* () {
898
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
899
+ if (item.isFolder() && propagateCheckedState) {
900
+ const descendants = yield getAllDescendants(
901
+ tree,
902
+ itemId,
903
+ canCheckFolders
904
+ );
905
+ tree.applySubStateUpdate(
906
+ "checkedItems",
907
+ (items) => items.filter((id) => !descendants.includes(id) && id !== itemId)
908
+ );
909
+ } else {
910
+ tree.applySubStateUpdate(
911
+ "checkedItems",
912
+ (items) => items.filter((id) => id !== itemId)
913
+ );
914
+ }
915
+ }));
916
+ })
877
917
  }
878
918
  };
879
919
 
@@ -991,6 +1031,12 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
991
1031
  var _a;
992
1032
  const config = tree.getConfig();
993
1033
  const dataRef = getDataRef(tree);
1034
+ if (!dataRef.current.itemData[itemId]) {
1035
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1036
+ ...loadingItemData,
1037
+ itemId
1038
+ ]);
1039
+ }
994
1040
  const item = yield config.dataLoader.getItem(itemId);
995
1041
  dataRef.current.itemData[itemId] = item;
996
1042
  (_a = config.onLoadedItem) == null ? void 0 : _a.call(config, itemId, item);
@@ -1005,6 +1051,12 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1005
1051
  const config = tree.getConfig();
1006
1052
  const dataRef = getDataRef(tree);
1007
1053
  let childrenIds;
1054
+ if (!dataRef.current.childrenIds[itemId]) {
1055
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
1056
+ ...loadingItemChildrens,
1057
+ itemId
1058
+ ]);
1059
+ }
1008
1060
  if ("getChildrenWithData" in config.dataLoader) {
1009
1061
  const children = yield config.dataLoader.getChildrenWithData(itemId);
1010
1062
  childrenIds = children.map((c) => c.id);
@@ -1065,10 +1117,6 @@ var asyncDataLoaderFeature = {
1065
1117
  return dataRef.current.itemData[itemId];
1066
1118
  }
1067
1119
  if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
1068
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1069
- ...loadingItemData,
1070
- itemId
1071
- ]);
1072
1120
  loadItemData(tree, itemId);
1073
1121
  }
1074
1122
  return (_b = (_a = config.createLoadingItemData) == null ? void 0 : _a.call(config)) != null ? _b : null;
@@ -1081,10 +1129,6 @@ var asyncDataLoaderFeature = {
1081
1129
  if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
1082
1130
  return [];
1083
1131
  }
1084
- tree.applySubStateUpdate(
1085
- "loadingItemChildrens",
1086
- (loadingItemChildrens) => [...loadingItemChildrens, itemId]
1087
- );
1088
1132
  loadChildrenIds(tree, itemId);
1089
1133
  return [];
1090
1134
  }
@@ -1095,10 +1139,6 @@ var asyncDataLoaderFeature = {
1095
1139
  var _a;
1096
1140
  if (!optimistic) {
1097
1141
  (_a = getDataRef(tree).current.itemData) == null ? true : delete _a[itemId];
1098
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1099
- ...loadingItemData,
1100
- itemId
1101
- ]);
1102
1142
  }
1103
1143
  yield loadItemData(tree, itemId);
1104
1144
  }),
@@ -1106,10 +1146,6 @@ var asyncDataLoaderFeature = {
1106
1146
  var _a;
1107
1147
  if (!optimistic) {
1108
1148
  (_a = getDataRef(tree).current.childrenIds) == null ? true : delete _a[itemId];
1109
- tree.applySubStateUpdate(
1110
- "loadingItemChildrens",
1111
- (loadingItemChildrens) => [...loadingItemChildrens, itemId]
1112
- );
1113
1149
  }
1114
1150
  yield loadChildrenIds(tree, itemId);
1115
1151
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "0.0.0-20250820225709",
3
+ "version": "0.0.0-20250828223340",
4
4
  "main": "dist/index.d.ts",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.mts",
@@ -18,6 +18,9 @@
18
18
  "./package.json": "./package.json"
19
19
  },
20
20
  "sideEffects": false,
21
+ "publishConfig": {
22
+ "provenance": true
23
+ },
21
24
  "scripts": {
22
25
  "build": "tsup ./src/index.ts --format esm,cjs --dts",
23
26
  "start": "tsup ./src/index.ts --format esm,cjs --dts --watch",
@@ -13,6 +13,13 @@ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
13
13
  const config = tree.getConfig();
14
14
  const dataRef = getDataRef(tree);
15
15
 
16
+ if (!dataRef.current.itemData[itemId]) {
17
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
18
+ ...loadingItemData,
19
+ itemId,
20
+ ]);
21
+ }
22
+
16
23
  const item = await config.dataLoader.getItem(itemId);
17
24
  dataRef.current.itemData[itemId] = item;
18
25
  config.onLoadedItem?.(itemId, item);
@@ -28,6 +35,13 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
28
35
  const dataRef = getDataRef(tree);
29
36
  let childrenIds: string[];
30
37
 
38
+ if (!dataRef.current.childrenIds[itemId]) {
39
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
40
+ ...loadingItemChildrens,
41
+ itemId,
42
+ ]);
43
+ }
44
+
31
45
  if ("getChildrenWithData" in config.dataLoader) {
32
46
  const children = await config.dataLoader.getChildrenWithData(itemId);
33
47
  childrenIds = children.map((c) => c.id);
@@ -104,11 +118,6 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
104
118
  }
105
119
 
106
120
  if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
107
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
108
- ...loadingItemData,
109
- itemId,
110
- ]);
111
-
112
121
  loadItemData(tree, itemId);
113
122
  }
114
123
 
@@ -125,11 +134,6 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
125
134
  return [];
126
135
  }
127
136
 
128
- tree.applySubStateUpdate(
129
- "loadingItemChildrens",
130
- (loadingItemChildrens) => [...loadingItemChildrens, itemId],
131
- );
132
-
133
137
  loadChildrenIds(tree, itemId);
134
138
 
135
139
  return [];
@@ -143,20 +147,12 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
143
147
  invalidateItemData: async ({ tree, itemId }, optimistic) => {
144
148
  if (!optimistic) {
145
149
  delete getDataRef(tree).current.itemData?.[itemId];
146
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
147
- ...loadingItemData,
148
- itemId,
149
- ]);
150
150
  }
151
151
  await loadItemData(tree, itemId);
152
152
  },
153
153
  invalidateChildrenIds: async ({ tree, itemId }, optimistic) => {
154
154
  if (!optimistic) {
155
155
  delete getDataRef(tree).current.childrenIds?.[itemId];
156
- tree.applySubStateUpdate(
157
- "loadingItemChildrens",
158
- (loadingItemChildrens) => [...loadingItemChildrens, itemId],
159
- );
160
156
  }
161
157
  await loadChildrenIds(tree, itemId);
162
158
  },
@@ -47,7 +47,8 @@ export type AsyncDataLoaderFeatureDef<T> = {
47
47
  * the tree will continue to display the old data until the new data has loaded. */
48
48
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
49
49
 
50
- updateCachedData: (data: T) => void;
50
+ /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
51
+ updateCachedData: (data: T | undefined) => void;
51
52
  updateCachedChildrenIds: (childrenIds: string[]) => void;
52
53
  isLoading: () => boolean;
53
54
  };
@@ -3,147 +3,136 @@ import { TestTree } from "../../test-utils/test-tree";
3
3
  import { checkboxesFeature } from "./feature";
4
4
  import { CheckedState } from "./types";
5
5
 
6
- const factory = TestTree.default({})
7
- .withFeatures(checkboxesFeature)
8
- .suits.sync().tree;
6
+ const factory = TestTree.default({
7
+ propagateCheckedState: true,
8
+ canCheckFolders: false,
9
+ }).withFeatures(checkboxesFeature);
9
10
 
10
11
  describe("core-feature/checkboxes", () => {
11
- it("should initialize with no checked items", async () => {
12
- const tree = await factory.createTestCaseTree();
13
- expect(tree.instance.getState().checkedItems).toEqual([]);
14
- });
15
-
16
- it("should check items", async () => {
17
- const tree = await factory.createTestCaseTree();
18
- tree.item("x111").setChecked();
19
- tree.item("x112").setChecked();
20
- expect(tree.instance.getState().checkedItems).toEqual(["x111", "x112"]);
21
- });
22
-
23
- it("should uncheck an item", async () => {
24
- const tree = await factory
25
- .with({ state: { checkedItems: ["x111"] } })
26
- .createTestCaseTree();
27
- tree.item("x111").setUnchecked();
28
- expect(tree.instance.getState().checkedItems).not.toContain("x111");
29
- });
30
-
31
- it("should toggle checked state", async () => {
32
- const tree = await factory.createTestCaseTree();
33
- const item = tree.item("x111");
12
+ factory.forSuits((tree) => {
13
+ it("should initialize with no checked items", async () => {
14
+ expect(tree.instance.getState().checkedItems).toEqual([]);
15
+ });
34
16
 
35
- item.toggleCheckedState();
36
- expect(tree.instance.getState().checkedItems).toContain("x111");
17
+ it("should check items", async () => {
18
+ await tree.item("x111").setChecked();
19
+ await tree.item("x112").setChecked();
20
+ expect(tree.instance.getState().checkedItems).toEqual(["x111", "x112"]);
21
+ });
37
22
 
38
- item.toggleCheckedState();
39
- expect(tree.instance.getState().checkedItems).not.toContain("x111");
40
- });
23
+ it("should uncheck an item", async () => {
24
+ await tree.item("x111").setChecked();
25
+ await tree.item("x111").setUnchecked();
26
+ expect(tree.instance.getState().checkedItems).not.toContain("x111");
27
+ });
41
28
 
42
- describe("props", () => {
43
29
  it("should toggle checked state", async () => {
44
- const tree = await factory.createTestCaseTree();
45
30
  const item = tree.item("x111");
46
-
47
- item.getCheckboxProps().onChange();
31
+ await item.toggleCheckedState();
48
32
  expect(tree.instance.getState().checkedItems).toContain("x111");
49
-
50
- item.getCheckboxProps().onChange();
33
+ await item.toggleCheckedState();
51
34
  expect(tree.instance.getState().checkedItems).not.toContain("x111");
52
35
  });
53
36
 
54
- it("should return checked state in props", async () => {
55
- const tree = await factory.createTestCaseTree();
56
- tree.item("x111").setChecked();
57
- expect(tree.item("x111").getCheckboxProps().checked).toBe(true);
58
- expect(tree.item("x112").getCheckboxProps().checked).toBe(false);
37
+ describe("props", () => {
38
+ it("should toggle checked state", async () => {
39
+ const item = tree.item("x111");
40
+ item.getCheckboxProps().onChange();
41
+ expect(tree.instance.getState().checkedItems).toContain("x111");
42
+ item.getCheckboxProps().onChange();
43
+ expect(tree.instance.getState().checkedItems).not.toContain("x111");
44
+ });
45
+
46
+ it("should return checked state in props", async () => {
47
+ tree.item("x111").setChecked();
48
+ expect(tree.item("x111").getCheckboxProps().checked).toBe(true);
49
+ expect(tree.item("x112").getCheckboxProps().checked).toBe(false);
50
+ });
51
+
52
+ it("should create indeterminate state", async () => {
53
+ await tree.item("x111").setChecked();
54
+ const refObject = { indeterminate: undefined };
55
+ tree.item("x11").getCheckboxProps().ref(refObject);
56
+ expect(refObject.indeterminate).toBe(true);
57
+ });
58
+
59
+ it("should not create indeterminate state", async () => {
60
+ const refObject = { indeterminate: undefined };
61
+ tree.item("x11").getCheckboxProps().ref(refObject);
62
+ expect(refObject.indeterminate).toBe(false);
63
+ });
59
64
  });
60
65
 
61
- it("should create indeterminate state", async () => {
62
- const tree = await factory.createTestCaseTree();
63
- tree.item("x111").setChecked();
64
- const refObject = { indeterminate: undefined };
65
- tree.item("x11").getCheckboxProps().ref(refObject);
66
- expect(refObject.indeterminate).toBe(true);
66
+ it("should handle folder checking", async () => {
67
+ const testTree = await tree
68
+ .with({ canCheckFolders: true, propagateCheckedState: false })
69
+ .createTestCaseTree();
70
+ testTree.item("x11").setChecked();
71
+ expect(testTree.instance.getState().checkedItems).toContain("x11");
67
72
  });
68
73
 
69
- it("should not create indeterminate state", async () => {
70
- const tree = await factory.createTestCaseTree();
71
- const refObject = { indeterminate: undefined };
72
- tree.item("x11").getCheckboxProps().ref(refObject);
73
- expect(refObject.indeterminate).toBe(false);
74
+ it("should not check folders if disabled", async () => {
75
+ const testTree = await tree
76
+ .with({ canCheckFolders: false, propagateCheckedState: false })
77
+ .createTestCaseTree();
78
+ testTree.item("x11").setChecked();
79
+ expect(testTree.instance.getState().checkedItems.length).toBe(0);
74
80
  });
75
- });
76
-
77
- it("should handle folder checking", async () => {
78
- const tree = await factory
79
- .with({ canCheckFolders: true, propagateCheckedState: false })
80
- .createTestCaseTree();
81
-
82
- tree.item("x11").setChecked();
83
- expect(tree.instance.getState().checkedItems).toContain("x11");
84
- });
85
81
 
86
- it("should not check folders if disabled", async () => {
87
- const tree = await factory
88
- .with({ canCheckFolders: false, propagateCheckedState: false })
89
- .createTestCaseTree();
90
-
91
- tree.item("x11").setChecked();
92
- expect(tree.instance.getState().checkedItems.length).toBe(0);
93
- });
94
-
95
- it("should propagate checked state", async () => {
96
- const tree = await factory
97
- .with({ propagateCheckedState: true })
98
- .createTestCaseTree();
99
-
100
- tree.item("x11").setChecked();
101
- expect(tree.instance.getState().checkedItems).toEqual(
102
- expect.arrayContaining(["x111", "x112", "x113", "x114"]),
103
- );
104
- });
105
-
106
- it("should turn folder indeterminate", async () => {
107
- const tree = await factory
108
- .with({ propagateCheckedState: true })
109
- .createTestCaseTree();
110
-
111
- tree.item("x111").setChecked();
112
- expect(tree.item("x11").getCheckedState()).toBe(CheckedState.Indeterminate);
113
- });
114
-
115
- it("should turn folder checked if all children are checked", async () => {
116
- const tree = await factory
117
- .with({
118
- isItemFolder: (item) => item.getItemData().length < 4,
119
- propagateCheckedState: true,
120
- canCheckFolders: false,
121
- })
122
- .createTestCaseTree();
123
-
124
- tree.item("x11").setChecked();
125
- tree.item("x12").setChecked();
126
- tree.item("x13").setChecked();
127
- expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
128
- tree.do.selectItem("x14");
129
- tree.item("x141").setChecked();
130
- tree.item("x142").setChecked();
131
- tree.item("x143").setChecked();
132
- expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
133
- tree.item("x144").setChecked();
134
- expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Checked);
135
- });
136
-
137
- it("should return correct checked state for items", async () => {
138
- const tree = await factory.createTestCaseTree();
139
- const item = tree.instance.getItemInstance("x111");
82
+ it("should propagate checked state", async () => {
83
+ const testTree = await tree
84
+ .with({ propagateCheckedState: true })
85
+ .createTestCaseTree();
86
+ await testTree.item("x11").setChecked();
87
+ expect(testTree.instance.getState().checkedItems).toEqual(
88
+ expect.arrayContaining(["x111", "x112", "x113", "x114"]),
89
+ );
90
+ });
140
91
 
141
- expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
92
+ it("should turn folder indeterminate", async () => {
93
+ const testTree = await tree
94
+ .with({ propagateCheckedState: true })
95
+ .createTestCaseTree();
96
+ testTree.item("x111").setChecked();
97
+ expect(testTree.item("x11").getCheckedState()).toBe(
98
+ CheckedState.Indeterminate,
99
+ );
100
+ });
142
101
 
143
- item.setChecked();
144
- expect(item.getCheckedState()).toBe(CheckedState.Checked);
102
+ it("should turn folder checked if all children are checked", async () => {
103
+ const testTree = await tree
104
+ .with({
105
+ isItemFolder: (item: any) => item.getItemData().length < 4,
106
+ propagateCheckedState: true,
107
+ canCheckFolders: false,
108
+ })
109
+ .createTestCaseTree();
110
+ testTree.do.selectItem("x14"); // all leafs must be loaded initially, checkpropagation check only respects visibly loaded items
111
+ // TODO ^ might be a restriction we want to avoid
112
+ await testTree.resolveAsyncVisibleItems();
113
+ await testTree.runWhileResolvingItems(testTree.item("x11").setChecked);
114
+ await testTree.runWhileResolvingItems(testTree.item("x12").setChecked);
115
+ await testTree.runWhileResolvingItems(testTree.item("x13").setChecked);
116
+ expect(testTree.item("x1").getCheckedState()).toBe(
117
+ CheckedState.Indeterminate,
118
+ );
119
+ await testTree.runWhileResolvingItems(testTree.item("x141").setChecked);
120
+ await testTree.runWhileResolvingItems(testTree.item("x142").setChecked);
121
+ await testTree.runWhileResolvingItems(testTree.item("x143").setChecked);
122
+ expect(testTree.item("x1").getCheckedState()).toBe(
123
+ CheckedState.Indeterminate,
124
+ );
125
+ await testTree.runWhileResolvingItems(testTree.item("x144").setChecked);
126
+ expect(testTree.item("x1").getCheckedState()).toBe(CheckedState.Checked);
127
+ });
145
128
 
146
- item.setUnchecked();
147
- expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
129
+ it("should return correct checked state for items", async () => {
130
+ const item = tree.instance.getItemInstance("x111");
131
+ expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
132
+ item.setChecked();
133
+ expect(item.getCheckedState()).toBe(CheckedState.Checked);
134
+ item.setUnchecked();
135
+ expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
136
+ });
148
137
  });
149
138
  });
@@ -1,7 +1,6 @@
1
- import { FeatureImplementation, TreeInstance } from "../../types/core";
1
+ import { type FeatureImplementation, TreeInstance } from "../../types/core";
2
2
  import { makeStateUpdater } from "../../utils";
3
3
  import { CheckedState } from "./types";
4
- import { throwError } from "../../utilities/errors";
5
4
 
6
5
  const getAllLoadedDescendants = <T>(
7
6
  tree: TreeInstance<T>,
@@ -12,12 +11,50 @@ const getAllLoadedDescendants = <T>(
12
11
  return [itemId];
13
12
  }
14
13
  const descendants = tree
15
- .retrieveChildrenIds(itemId)
14
+ .retrieveChildrenIds(itemId, true)
16
15
  .map((child) => getAllLoadedDescendants(tree, child, includeFolders))
17
16
  .flat();
18
17
  return includeFolders ? [itemId, ...descendants] : descendants;
19
18
  };
20
19
 
20
+ const getAllDescendants = async <T>(
21
+ tree: TreeInstance<T>,
22
+ itemId: string,
23
+ includeFolders = false,
24
+ ): Promise<string[]> => {
25
+ await tree.loadItemData(itemId);
26
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
27
+ return [itemId];
28
+ }
29
+ const childrenIds = await tree.loadChildrenIds(itemId);
30
+ const descendants = (
31
+ await Promise.all(
32
+ childrenIds.map((child) =>
33
+ getAllDescendants(tree, child, includeFolders),
34
+ ),
35
+ )
36
+ ).flat();
37
+ return includeFolders ? [itemId, ...descendants] : descendants;
38
+ };
39
+
40
+ const withLoadingState = async <T>(
41
+ tree: TreeInstance<T>,
42
+ itemId: string,
43
+ callback: () => Promise<void>,
44
+ ) => {
45
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
46
+ ...items,
47
+ itemId,
48
+ ]);
49
+ try {
50
+ await callback();
51
+ } finally {
52
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) =>
53
+ items.filter((id) => id !== itemId),
54
+ );
55
+ }
56
+ };
57
+
21
58
  export const checkboxesFeature: FeatureImplementation = {
22
59
  key: "checkboxes",
23
60
 
@@ -25,22 +62,20 @@ export const checkboxesFeature: FeatureImplementation = {
25
62
 
26
63
  getInitialState: (initialState) => ({
27
64
  checkedItems: [],
65
+ loadingCheckPropagationItems: [],
28
66
  ...initialState,
29
67
  }),
30
68
 
31
69
  getDefaultConfig: (defaultConfig, tree) => {
32
- const hasAsyncLoader = defaultConfig.features?.some(
33
- (f) => f.key === "async-data-loader",
34
- );
35
- if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
36
- throwError(`propagateCheckedState not supported with async trees`);
37
- }
38
- const propagateCheckedState =
39
- defaultConfig.propagateCheckedState ?? !hasAsyncLoader;
70
+ const propagateCheckedState = defaultConfig.propagateCheckedState ?? true;
40
71
  const canCheckFolders =
41
72
  defaultConfig.canCheckFolders ?? !propagateCheckedState;
42
73
  return {
43
74
  setCheckedItems: makeStateUpdater("checkedItems", tree),
75
+ setLoadingCheckPropagationItems: makeStateUpdater(
76
+ "loadingCheckPropagationItems",
77
+ tree,
78
+ ),
44
79
  propagateCheckedState,
45
80
  canCheckFolders,
46
81
  ...defaultConfig,
@@ -49,6 +84,7 @@ export const checkboxesFeature: FeatureImplementation = {
49
84
 
50
85
  stateHandlerNames: {
51
86
  checkedItems: "setCheckedItems",
87
+ loadingCheckPropagationItems: "setLoadingCheckPropagationItems",
52
88
  },
53
89
 
54
90
  treeInstance: {
@@ -71,11 +107,11 @@ export const checkboxesFeature: FeatureImplementation = {
71
107
  };
72
108
  },
73
109
 
74
- toggleCheckedState: ({ item }) => {
110
+ toggleCheckedState: async ({ item }) => {
75
111
  if (item.getCheckedState() === CheckedState.Checked) {
76
- item.setUnchecked();
112
+ await item.setUnchecked();
77
113
  } else {
78
- item.setChecked();
114
+ await item.setChecked();
79
115
  }
80
116
  },
81
117
 
@@ -90,6 +126,7 @@ export const checkboxesFeature: FeatureImplementation = {
90
126
 
91
127
  if (item.isFolder() && propagateCheckedState) {
92
128
  const descendants = getAllLoadedDescendants(tree, itemId);
129
+ if (descendants.length === 0) return CheckedState.Unchecked;
93
130
  if (descendants.every((d) => checkedItems.includes(d))) {
94
131
  return CheckedState.Checked;
95
132
  }
@@ -101,34 +138,46 @@ export const checkboxesFeature: FeatureImplementation = {
101
138
  return CheckedState.Unchecked;
102
139
  },
103
140
 
104
- setChecked: ({ item, tree, itemId }) => {
105
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
106
- if (item.isFolder() && propagateCheckedState) {
107
- tree.applySubStateUpdate("checkedItems", (items) => [
108
- ...items,
109
- ...getAllLoadedDescendants(tree, itemId, canCheckFolders),
110
- ]);
111
- } else if (!item.isFolder() || canCheckFolders) {
112
- tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
113
- }
141
+ setChecked: async ({ item, tree, itemId }) => {
142
+ await withLoadingState(tree, itemId, async () => {
143
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
144
+ if (item.isFolder() && propagateCheckedState) {
145
+ const descendants = await getAllDescendants(
146
+ tree,
147
+ itemId,
148
+ canCheckFolders,
149
+ );
150
+ tree.applySubStateUpdate("checkedItems", (items) => [
151
+ ...items,
152
+ ...descendants,
153
+ ]);
154
+ } else if (!item.isFolder() || canCheckFolders) {
155
+ tree.applySubStateUpdate("checkedItems", (items) => [
156
+ ...items,
157
+ itemId,
158
+ ]);
159
+ }
160
+ });
114
161
  },
115
162
 
116
- setUnchecked: ({ item, tree, itemId }) => {
117
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
118
- if (item.isFolder() && propagateCheckedState) {
119
- const descendants = getAllLoadedDescendants(
120
- tree,
121
- itemId,
122
- canCheckFolders,
123
- );
124
- tree.applySubStateUpdate("checkedItems", (items) =>
125
- items.filter((id) => !descendants.includes(id) && id !== itemId),
126
- );
127
- } else {
128
- tree.applySubStateUpdate("checkedItems", (items) =>
129
- items.filter((id) => id !== itemId),
130
- );
131
- }
163
+ setUnchecked: async ({ item, tree, itemId }) => {
164
+ await withLoadingState(tree, itemId, async () => {
165
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
166
+ if (item.isFolder() && propagateCheckedState) {
167
+ const descendants = await getAllDescendants(
168
+ tree,
169
+ itemId,
170
+ canCheckFolders,
171
+ );
172
+ tree.applySubStateUpdate("checkedItems", (items) =>
173
+ items.filter((id) => !descendants.includes(id) && id !== itemId),
174
+ );
175
+ } else {
176
+ tree.applySubStateUpdate("checkedItems", (items) =>
177
+ items.filter((id) => id !== itemId),
178
+ );
179
+ }
180
+ });
132
181
  },
133
182
  },
134
183
  };
@@ -9,9 +9,11 @@ export enum CheckedState {
9
9
  export type CheckboxesFeatureDef<T> = {
10
10
  state: {
11
11
  checkedItems: string[];
12
+ loadingCheckPropagationItems: string[];
12
13
  };
13
14
  config: {
14
15
  setCheckedItems?: SetStateFn<string[]>;
16
+ setLoadingCheckPropagationItems?: SetStateFn<string[]>;
15
17
  canCheckFolders?: boolean;
16
18
  propagateCheckedState?: boolean;
17
19
  };
@@ -19,11 +21,22 @@ export type CheckboxesFeatureDef<T> = {
19
21
  setCheckedItems: (checkedItems: string[]) => void;
20
22
  };
21
23
  itemInstance: {
22
- setChecked: () => void;
23
- setUnchecked: () => void;
24
- toggleCheckedState: () => void;
24
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
25
+ * this will return immediately. */
26
+ setChecked: () => Promise<void>;
27
+
28
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
29
+ * this will return immediately. */
30
+ setUnchecked: () => Promise<void>;
31
+
32
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
33
+ * this will return immediately. */
34
+ toggleCheckedState: () => Promise<void>;
35
+
25
36
  getCheckedState: () => CheckedState;
26
37
  getCheckboxProps: () => Record<string, any>;
38
+
39
+ isLoadingCheckPropagation: () => boolean;
27
40
  };
28
41
  hotkeys: never;
29
42
  };
@@ -101,6 +101,17 @@ export class TestTree<T = string> {
101
101
  await TestTree.resolveAsyncLoaders();
102
102
  }
103
103
 
104
+ async runWhileResolvingItems(cb: () => Promise<void>) {
105
+ const interval = setInterval(() => {
106
+ TestTree.resolveAsyncLoaders();
107
+ }, 5);
108
+ try {
109
+ await cb();
110
+ } finally {
111
+ clearInterval(interval);
112
+ }
113
+ }
114
+
104
115
  static default(config: Partial<TreeConfig<string>>) {
105
116
  return new TestTree({
106
117
  rootItemId: "x",