@arrai-innovations/reactive-helpers 10.2.0 → 10.4.0

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.
@@ -1,7 +1,7 @@
1
1
  version: 2.1
2
2
  orbs:
3
3
  eslint: arrai/eslint@7.4.0
4
- prettier: arrai/prettier@5.1.1
4
+ prettier: arrai/prettier@5.4.0
5
5
  npm: arrai/npm@2.4.1
6
6
  github: arrai/github@1.1.0
7
7
  workflows:
@@ -75,14 +75,12 @@ workflows:
75
75
  - prettier/code_style:
76
76
  name: prettier
77
77
  context: arrai-global
78
- version: 2.6.2
79
78
  filters:
80
79
  branches:
81
80
  only:
82
81
  - main
83
82
  - prettier/code_style:
84
83
  name: prettier-no-badge
85
- version: 2.6.2
86
84
  create_badges: false
87
85
  filters:
88
86
  branches:
package/docs.md CHANGED
@@ -494,11 +494,11 @@ It tracks the objects in the list, and their added order.
494
494
  causing the list to be re-fetched as needed and listening for updates to the list.
495
495
 
496
496
  **Kind**: global function
497
- **Returns**: ListSubscription
497
+ **Returns**: [`ListSubscription`] - - the list subscription
498
498
 
499
- | Param | Type |
500
- | ------- | --------------------------- |
501
- | options | [`ListSubscriptionOptions`] |
499
+ | Param | Type | Description |
500
+ | ------- | --------------------------- | --------------------------------------------------- |
501
+ | options | [`ListSubscriptionOptions`] | the configuration options for the list subscription |
502
502
 
503
503
  ## useObjectInstances(instanceArgs)
504
504
 
@@ -832,6 +832,7 @@ A CSS object or a space-separated list of CSS classes.
832
832
  [`cssclasseswithrefs`]: #cssclasseswithrefs
833
833
  [`listinstance`]: #listinstance
834
834
  [`listinstanceoptions`]: #listinstanceoptions
835
+ [`listsubscription`]: #listsubscription
835
836
  [`listsubscriptionoptions`]: #listsubscriptionoptions
836
837
  [`objectinstanceinstance`]: #objectinstanceinstance
837
838
  [`listinstancestate`]: #listinstancestate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arrai-innovations/reactive-helpers",
3
- "version": "10.2.0",
3
+ "version": "10.4.0",
4
4
  "description": "VueJS 3 utility composition functions to help manipulate objects and lists.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -2,12 +2,16 @@ import { nextTick } from "vue";
2
2
  import { deepUnref } from "vue-deepunref";
3
3
 
4
4
  describe("use/listCalculated", () => {
5
- let useListInstance, useListCalculated;
5
+ let useListInstance, useListCalculated, useListRelated, AwaitNot;
6
6
  beforeEach(async () => {
7
7
  const listInstanceModule = await import("../../../use/listInstance");
8
8
  useListInstance = listInstanceModule.useListInstance;
9
9
  const listCalculatedModule = await import("../../../use/listCalculated");
10
10
  useListCalculated = listCalculatedModule.useListCalculated;
11
+ const listRelatedModule = await import("../../../use/listRelated");
12
+ useListRelated = listRelatedModule.useListRelated;
13
+ const watchesModule = await import("../../../utils/watches.js");
14
+ AwaitNot = watchesModule.AwaitNot;
11
15
  });
12
16
  it("should return a list of calculated items", async () => {
13
17
  const mainListInstance = useListInstance({ props: {} });
@@ -71,4 +75,60 @@ describe("use/listCalculated", () => {
71
75
  },
72
76
  });
73
77
  });
78
+ it("should allow calculated objects to return results based on related objects", async () => {
79
+ const mainListInstance = useListInstance({ props: {} });
80
+ const relatedListInstance = useListInstance({ props: {} });
81
+ mainListInstance.addListObject({
82
+ id: "1",
83
+ name: "main",
84
+ related_items: ["2", "3"],
85
+ related_id: "4",
86
+ calculated_items: ["2", "3"],
87
+ calculated_id: "4",
88
+ });
89
+ relatedListInstance.addListObject({
90
+ id: "2",
91
+ name: "related1",
92
+ });
93
+ relatedListInstance.addListObject({
94
+ id: "3",
95
+ name: "related2",
96
+ });
97
+ relatedListInstance.addListObject({
98
+ id: "4",
99
+ name: "related3",
100
+ });
101
+ const listRelated = useListRelated({
102
+ parentState: mainListInstance.state,
103
+ relatedObjectsRules: {
104
+ relatedItems: {
105
+ objects: relatedListInstance.state.objects,
106
+ pkKey: "related_items",
107
+ },
108
+ relatedItem: {
109
+ objects: relatedListInstance.state.objects,
110
+ pkKey: "related_id",
111
+ },
112
+ },
113
+ });
114
+ const listCalculated = useListCalculated({
115
+ parentState: listRelated.state,
116
+ calculatedObjectsRules: {
117
+ calculatedItems: (obj, relatedObj) => relatedObj.relatedItems?.map((x) => x.name + "-modified"),
118
+ calculatedItem: (obj, relatedObj) => relatedObj.relatedItem?.name + "-modified",
119
+ },
120
+ });
121
+ const anr = new AwaitNot({
122
+ obj: listCalculated.state,
123
+ prop: "running",
124
+ });
125
+ anr.start();
126
+ await anr.promise;
127
+ expect(deepUnref(listCalculated.state.calculatedObjects)).toEqual({
128
+ 1: {
129
+ calculatedItems: ["related1-modified", "related2-modified"],
130
+ calculatedItem: "related3-modified",
131
+ },
132
+ });
133
+ });
74
134
  });
@@ -1,14 +1,15 @@
1
- // import { useListInstance, useListRelated } from "../../../use/listRelated.js";
2
1
  import { nextTick } from "vue";
3
2
  import { deepUnref } from "vue-deepunref";
4
3
 
5
4
  describe("use/listRelated", () => {
6
- let useListInstance, useListRelated;
5
+ let useListInstance, useListRelated, AwaitNot;
7
6
  beforeEach(async () => {
8
7
  const listInstanceModule = await import("../../../use/listInstance");
9
8
  useListInstance = listInstanceModule.useListInstance;
10
9
  const listRelatedModule = await import("../../../use/listRelated");
11
10
  useListRelated = listRelatedModule.useListRelated;
11
+ const watchesModule = await import("../../../utils/watches.js");
12
+ AwaitNot = watchesModule.AwaitNot;
12
13
  });
13
14
  it("should return a list of related items", async () => {
14
15
  const mainListInstance = useListInstance({ props: {} });
@@ -78,4 +79,117 @@ describe("use/listRelated", () => {
78
79
  },
79
80
  });
80
81
  });
82
+ it('should allow related objects pkKey to be prefixed with "relatedItem." to reference previously related objects', async () => {
83
+ //
84
+ const mainListInstance = useListInstance({ props: {} });
85
+ const intermediateListInstance = useListInstance({ props: {} });
86
+ const relatedListInstance = useListInstance({ props: {} });
87
+ mainListInstance.addListObject({
88
+ id: "1",
89
+ name: "main",
90
+ intermediate_ids: ["2", "3"],
91
+ intermediate_id: "2",
92
+ });
93
+ intermediateListInstance.addListObject({
94
+ id: "2",
95
+ name: "intermediate1",
96
+ related_ids: ["4", "5"],
97
+ related_id: "4",
98
+ });
99
+ intermediateListInstance.addListObject({
100
+ id: "3",
101
+ name: "intermediate2",
102
+ related_ids: ["6", "7"],
103
+ related_id: "6",
104
+ });
105
+ relatedListInstance.addListObject({
106
+ id: "4",
107
+ name: "related1",
108
+ });
109
+ relatedListInstance.addListObject({
110
+ id: "5",
111
+ name: "related2",
112
+ });
113
+ relatedListInstance.addListObject({
114
+ id: "6",
115
+ name: "related3",
116
+ });
117
+ relatedListInstance.addListObject({
118
+ id: "7",
119
+ name: "related4",
120
+ });
121
+ const listRelated = useListRelated({
122
+ parentState: mainListInstance.state,
123
+ relatedObjectsRules: {
124
+ intermediateItems: {
125
+ objects: intermediateListInstance.state.objects,
126
+ pkKey: "intermediate_ids",
127
+ },
128
+ intermediateItem: {
129
+ objects: intermediateListInstance.state.objects,
130
+ pkKey: "intermediate_id",
131
+ },
132
+ relatedItems: {
133
+ objects: relatedListInstance.state.objects,
134
+ pkKey: "relatedItem.intermediateItems.related_ids",
135
+ },
136
+ relatedItem: {
137
+ objects: relatedListInstance.state.objects,
138
+ pkKey: "relatedItem.intermediateItem.related_id",
139
+ },
140
+ },
141
+ });
142
+ const anr = new AwaitNot({
143
+ obj: listRelated.state,
144
+ prop: "running",
145
+ });
146
+ anr.start();
147
+ await anr.promise;
148
+ expect(deepUnref(listRelated.state.relatedObjects)).toEqual({
149
+ 1: {
150
+ intermediateItems: [
151
+ {
152
+ id: "2",
153
+ name: "intermediate1",
154
+ related_ids: ["4", "5"],
155
+ related_id: "4",
156
+ },
157
+ {
158
+ id: "3",
159
+ name: "intermediate2",
160
+ related_ids: ["6", "7"],
161
+ related_id: "6",
162
+ },
163
+ ],
164
+ intermediateItem: {
165
+ id: "2",
166
+ name: "intermediate1",
167
+ related_ids: ["4", "5"],
168
+ related_id: "4",
169
+ },
170
+ relatedItems: [
171
+ {
172
+ id: "4",
173
+ name: "related1",
174
+ },
175
+ {
176
+ id: "5",
177
+ name: "related2",
178
+ },
179
+ {
180
+ id: "6",
181
+ name: "related3",
182
+ },
183
+ {
184
+ id: "7",
185
+ name: "related4",
186
+ },
187
+ ],
188
+ relatedItem: {
189
+ id: "4",
190
+ name: "related1",
191
+ },
192
+ },
193
+ });
194
+ });
81
195
  });
@@ -1,5 +1,5 @@
1
1
  import { doAwaitTimeout } from "../../../utils/index.js";
2
- import { nextTick } from "vue";
2
+ import { reactive } from "vue";
3
3
  import { deepUnref } from "vue-deepunref";
4
4
 
5
5
  describe("use/useListSort", () => {
@@ -8,9 +8,12 @@ describe("use/useListSort", () => {
8
8
  sortThrottleWait,
9
9
  useListInstance,
10
10
  useListInstances,
11
+ useListRelated,
12
+ useListCalculated,
11
13
  useListSort,
12
14
  useListSorts,
13
- setListSortDefaultOptions;
15
+ setListSortDefaultOptions,
16
+ AwaitNot;
14
17
  const contactsResolved = [
15
18
  {
16
19
  id: 15,
@@ -41,6 +44,8 @@ describe("use/useListSort", () => {
41
44
  const imported = await import("../../../use");
42
45
  useListInstance = imported.useListInstance;
43
46
  useListInstances = imported.useListInstances;
47
+ useListRelated = imported.useListRelated;
48
+ useListCalculated = imported.useListCalculated;
44
49
  orderByRules = [
45
50
  { key: "organization", desc: true, localeCompare: false },
46
51
  { key: "lexical_name", desc: false, localeCompare: true },
@@ -58,10 +63,22 @@ describe("use/useListSort", () => {
58
63
  setListSortDefaultOptions({
59
64
  sortThrottleWait: 0,
60
65
  });
66
+ const importedUtils = await import("../../../utils");
67
+ AwaitNot = importedUtils.AwaitNot;
61
68
  });
62
69
 
63
70
  afterEach(() => vi.resetAllMocks());
64
71
 
72
+ const waitForListSort = async (listSort) => {
73
+ const anr = new AwaitNot({
74
+ obj: listSort.state,
75
+ prop: "running",
76
+ timeout: 2000,
77
+ });
78
+ anr.start();
79
+ await anr.promise;
80
+ };
81
+
65
82
  it("generates initial values from inputs", () => {
66
83
  const listSort = useListSort({ parentState: listInstance.state, orderByRules, sortThrottleWait });
67
84
  expect(listSort.state.orderByRules).toEqual(orderByRules);
@@ -89,13 +106,14 @@ describe("use/useListSort", () => {
89
106
  listInstance.addListObject(contact);
90
107
  }
91
108
  const listSort = useListSort({ parentState: listInstance.state, orderByRules });
92
- await nextTick();
109
+ // sorts immediately
93
110
  expect(listSort.state.order).toEqual(testOrder1);
111
+ await waitForListSort(listSort);
94
112
  listInstance.addListObject(addObject);
95
- await nextTick();
113
+ await waitForListSort(listSort);
96
114
  expect(listSort.state.order).toEqual(testOrder2);
97
115
  listInstance.deleteListObject(12);
98
- await nextTick();
116
+ await waitForListSort(listSort);
99
117
  expect(listSort.state.order).toEqual(testOrder3);
100
118
  });
101
119
  });
@@ -115,17 +133,17 @@ describe("use/useListSort", () => {
115
133
  listSort.state.orderByRules.pop();
116
134
  listSort.state.orderByRules.push({ key: "lexical_name", desc: false, localeCompare: true });
117
135
  expect(listSort.state.order).toEqual(testOrder1);
118
- await nextTick();
136
+ await waitForListSort(listSort);
119
137
  expect(listSort.state.order).toEqual(testOrder2);
120
138
  listSort.state.orderByRules.pop();
121
139
  listSort.state.orderByRules.push({ key: "organization", desc: true, localeCompare: true });
122
- await nextTick();
140
+ await waitForListSort(listSort);
123
141
  expect(listSort.state.order).toEqual(testOrder3);
124
142
  listSort.state.orderByRules.pop();
125
- await nextTick();
143
+ await waitForListSort(listSort);
126
144
  expect(listSort.state.order).toEqual(testOrder4);
127
145
  listSort.state.orderByRules.push({ key: "organization", desc: false, localeCompare: false });
128
- await nextTick();
146
+ await waitForListSort(listSort);
129
147
  expect(listSort.state.order).toEqual(testOrder2);
130
148
  });
131
149
  });
@@ -194,6 +212,72 @@ describe("use/useListSort", () => {
194
212
  expect(deepUnref(listSorts.A.state)).toEqual(deepUnref(listSortA.state));
195
213
  expect(deepUnref(listSorts.B.state)).toEqual(deepUnref(listSortB.state));
196
214
  });
215
+ it("orderByRules can refer to relatedItem or calculatedItem", async () => {
216
+ const listInstance = useListInstance({
217
+ props: reactive({
218
+ crudArgs: { stream: "test_stream" },
219
+ listArgs: { user: 1 },
220
+ retrieveArgs: {
221
+ fields: ["id", "__str__", "name", "relatedItem", "calculatedItem"],
222
+ },
223
+ }),
224
+ });
225
+ const relatedListInstance = useListInstance({
226
+ props: reactive({
227
+ crudArgs: { stream: "test_related_stream" },
228
+ listArgs: { user: 1 },
229
+ retrieveArgs: {
230
+ fields: ["id", "__str__", "name"],
231
+ },
232
+ }),
233
+ });
234
+ const listRelated = useListRelated({
235
+ parentState: listInstance.state,
236
+ relatedObjectsRules: reactive({
237
+ relatedItemName: {
238
+ objects: relatedListInstance.state.objects,
239
+ pkKey: "relatedItem",
240
+ },
241
+ }),
242
+ });
243
+ const listCalculated = useListCalculated({
244
+ parentState: listRelated.state,
245
+ calculatedObjectsRules: reactive({
246
+ calculatedItemName: (obj, relatedObj) => {
247
+ return obj.sameValue + ":" + relatedObj.relatedItemName?.oppositeOrder;
248
+ },
249
+ }),
250
+ });
251
+ const orderByRules = reactive([
252
+ { key: "calculatedItem.calculatedItemName", desc: false, localeCompare: false },
253
+ ]);
254
+ const listSort = useListSort({
255
+ parentState: listCalculated.state,
256
+ orderByRules,
257
+ });
258
+ for (let i = 1; i <= 4; i++) {
259
+ listInstance.addListObject({
260
+ id: i,
261
+ name: `item${i}`,
262
+ relatedItem: i,
263
+ sameValue: "sameValue",
264
+ });
265
+ relatedListInstance.addListObject({
266
+ id: i,
267
+ name: `relatedItem${i}`,
268
+ oppositeOrder: 5 - i,
269
+ });
270
+ }
271
+ await waitForListSort(listSort);
272
+ expect(listSort.state.order).toEqual(["4", "3", "2", "1"]);
273
+ orderByRules[0].key = "relatedItemName.name";
274
+ await waitForListSort(listSort);
275
+ expect(listSort.state.order).toEqual(["1", "2", "3", "4"]);
276
+ orderByRules[0].key = "relatedItemName.sameValue";
277
+ orderByRules[1] = { key: "calculatedItem.calculatedItemName", desc: false, localeCompare: false };
278
+ await waitForListSort(listSort);
279
+ expect(listSort.state.order).toEqual(["4", "3", "2", "1"]);
280
+ });
197
281
  describe("useListSort/sortThrottleWait", () => {
198
282
  it("respects throttle time prior to triggering", async () => {
199
283
  setListSortDefaultOptions({
@@ -212,20 +296,31 @@ describe("use/useListSort", () => {
212
296
  }
213
297
 
214
298
  const testOrder1 = [];
215
- const testOrder2 = ["35", "12", "15", "9"];
216
- const testOrder3 = ["35", "15", "9"];
299
+ const testOrder2 = ["12", "15", "9"];
300
+ const testOrder3 = ["35", "12", "15", "9"];
301
+ const testOrder4 = ["35", "15", "9"];
217
302
 
218
303
  const listSort = useListSort({ parentState: listInstance.state, orderByRules, sortThrottleWait });
219
- await nextTick();
220
304
  expect(listSort.state.order).toEqual(testOrder1);
305
+
306
+ // wait for the original throttle to finish
307
+ await doAwaitTimeout(250);
308
+
309
+ expect(listSort.state.order).toEqual(testOrder2);
221
310
  listInstance.addListObject(addObject);
222
- await nextTick();
223
311
  expect(listSort.state.order).toEqual(testOrder2);
312
+ // trigger the leading edge of the throttle
313
+ await doAwaitTimeout(10);
314
+ expect(listSort.state.order).toEqual(testOrder3);
315
+ // trigger again before the 200ms throttle
224
316
  listInstance.deleteListObject(12);
225
- await nextTick();
226
- expect(listSort.state.order).toEqual(testOrder2);
227
- await doAwaitTimeout(200);
228
317
  expect(listSort.state.order).toEqual(testOrder3);
318
+ // this should trigger before the 200ms throttle
319
+ await doAwaitTimeout(100);
320
+ expect(listSort.state.order).toEqual(testOrder3);
321
+ // this should trigger after the 200ms throttle
322
+ await doAwaitTimeout(200);
323
+ expect(listSort.state.order).toEqual(testOrder4);
229
324
  });
230
325
  });
231
326
  });
@@ -58,6 +58,7 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
58
58
  for (const objectKey of Object.keys(state.relatedObjects)) {
59
59
  const relatedObjectsObject = state.relatedObjects[objectKey];
60
60
  const originalObjectRef = toRef(parentState.objects, objectKey);
61
+ const relatedObjectRef = toRef(state.relatedObjects, objectKey);
61
62
  let removedRuleKeys, addedRuleKeys;
62
63
  if (!relatedObjectsRulesIsEmpty) {
63
64
  ({ removedKeys: removedRuleKeys, addedKeys: addedRuleKeys } = keyDiff(
@@ -86,12 +87,30 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
86
87
  const rulePkKey = state.relatedObjectsRules?.[addedRuleKey]?.pkKey || addedRuleKey;
87
88
  const ruleOrder = unref(state.relatedObjectsRules?.[addedRuleKey]?.order);
88
89
  if (!ruleObjects || !rulePkKey) {
89
- relatedObjectsObject[addedRuleKey] = undefined;
90
+ if (!isUndefined(relatedObjectsObject[addedRuleKey])) {
91
+ relatedObjectsObject[addedRuleKey] = undefined;
92
+ }
90
93
  return;
91
94
  }
92
- let value = get(unref(originalObjectRef), rulePkKey);
95
+ let value;
96
+ if (rulePkKey.startsWith("relatedItem.")) {
97
+ value = get(unref(relatedObjectRef), rulePkKey.slice(12));
98
+ if (isUndefined(value)) {
99
+ // is the first level an array?
100
+ const firstLevelKey = rulePkKey.slice(12).split(".")[0];
101
+ const firstLevelItem = get(unref(relatedObjectRef), firstLevelKey);
102
+ if (isArray(firstLevelItem)) {
103
+ const restOfKey = rulePkKey.slice(12 + firstLevelKey.length + 1);
104
+ value = firstLevelItem.map((e) => get(e, restOfKey)).flat();
105
+ }
106
+ }
107
+ } else {
108
+ value = get(unref(originalObjectRef), rulePkKey);
109
+ }
93
110
  if (isUndefined(value)) {
94
- relatedObjectsObject[addedRuleKey] = undefined;
111
+ if (!isUndefined(relatedObjectsObject[addedRuleKey])) {
112
+ relatedObjectsObject[addedRuleKey] = undefined;
113
+ }
95
114
  return;
96
115
  }
97
116
  if (isArray(value)) {
@@ -114,7 +133,7 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
114
133
  }
115
134
  };
116
135
  watch(
117
- [toRef(state.relatedObjectsRules, addedRuleKey), originalObjectRef],
136
+ [toRef(state.relatedObjectsRules, addedRuleKey), originalObjectRef, relatedObjectRef],
118
137
  relatedObjectsObjectWatchFn,
119
138
  {
120
139
  deep: true,
package/use/listSort.js CHANGED
@@ -1,18 +1,20 @@
1
- import { assignReactiveObject, keyDiff } from "../utils/index.js";
1
+ import { assignReactiveObject, keyDiff, loadingCombine } from "../utils/index.js";
2
2
  import { listCalculatedStateKeys } from "./listCalculated.js";
3
3
  import { listFilterStateKeys } from "./listFilter.js";
4
4
  import { listInstanceStateKeys } from "./listInstance.js";
5
5
  import { listRelatedStateKeys } from "./listRelated.js";
6
6
  import { listSubscriptionStateKeys } from "./listSubscription.js";
7
+ import { useWatchesRunning } from "./watchesRunning.js";
7
8
  import cloneDeep from "lodash-es/cloneDeep.js";
8
9
  import get from "lodash-es/get.js";
9
10
  import identity from "lodash-es/identity.js";
10
11
  import isEmpty from "lodash-es/isEmpty.js";
12
+ import isEqual from "lodash-es/isEqual.js";
11
13
  import isNull from "lodash-es/isNull.js";
12
14
  import isUndefined from "lodash-es/isUndefined.js";
13
15
  import throttle from "lodash-es/throttle.js";
14
16
  import zip from "lodash-es/zip.js";
15
- import { effectScope, reactive, toRef, unref, watch } from "vue";
17
+ import { effectScope, reactive, toRef, unref, watch, computed } from "vue";
16
18
 
17
19
  export const listSortStateKeys = [
18
20
  "orderByRules",
@@ -21,6 +23,10 @@ export const listSortStateKeys = [
21
23
  "sortCriteria",
22
24
  "sortCriteriaEffectScopes",
23
25
  "orderByDesc",
26
+ "sortCriteriaWatchRunning",
27
+ "sortWatchRunning",
28
+ "outstandingEffects",
29
+ // "running",
24
30
  ];
25
31
 
26
32
  export const listSortFunctions = [];
@@ -56,6 +62,9 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
56
62
  sortCriteria: {},
57
63
  sortCriteriaEffectScopes: {},
58
64
  orderByDesc: [],
65
+ sortCriteriaWatchRunning: false,
66
+ sortWatchRunning: false,
67
+ outstandingEffects: false,
59
68
  });
60
69
  const es = effectScope();
61
70
 
@@ -68,7 +77,7 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
68
77
  delete state.sortCriteria[removedKey];
69
78
  }
70
79
 
71
- function addSortCriteria(object, key) {
80
+ function addSortCriteria(object, relatedObject, calculatedObject, key) {
72
81
  const oldScope = state.sortCriteriaEffectScopes[key];
73
82
  if (oldScope) {
74
83
  oldScope.stop();
@@ -79,20 +88,34 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
79
88
  state.sortCriteria[key] = [];
80
89
  }
81
90
  watch(
82
- [object, toRef(state, "orderByRules")],
91
+ [object, relatedObject, calculatedObject, toRef(state, "orderByRules")],
83
92
  () => {
84
93
  const obj = unref(object);
94
+ const relatedObj = unref(relatedObject);
95
+ const calculatedObj = unref(calculatedObject);
85
96
  const newSearchCriteria = [];
86
97
  for (const orderByObj of state.orderByRules.filter(identity)) {
87
98
  let newItem;
88
99
  if (orderByObj.keyFn) {
89
100
  newItem = orderByObj.keyFn(obj, state);
90
101
  } else {
91
- newItem = get(obj, orderByObj.key);
102
+ if (orderByObj.key.startsWith("relatedItem.")) {
103
+ newItem = get(relatedObj, orderByObj.key.slice(12));
104
+ } else if (orderByObj.key.startsWith("calculatedItem.")) {
105
+ newItem = get(calculatedObj, orderByObj.key.slice(15));
106
+ } else {
107
+ newItem = get(obj, orderByObj.key);
108
+ }
92
109
  }
93
110
  newSearchCriteria.push(newItem);
94
111
  }
112
+ if (isEqual(newSearchCriteria, state.sortCriteria[key])) {
113
+ return;
114
+ }
95
115
  assignReactiveObject(state.sortCriteria[key], newSearchCriteria);
116
+ if (!state.outstandingEffects) {
117
+ state.outstandingEffects = true;
118
+ }
96
119
  },
97
120
  {
98
121
  deep: true,
@@ -104,82 +127,98 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
104
127
  }
105
128
 
106
129
  function sortCriteriaWatch() {
107
- if (!state.orderByRules || !state.orderByRules.filter(identity).length) {
108
- if (!isEmpty(state.sortCriteria)) {
109
- for (const removedKey of Object.keys(state.sortCriteria)) {
110
- removeSortCriteria(removedKey);
130
+ try {
131
+ if (!state.orderByRules || !state.orderByRules.filter(identity).length) {
132
+ if (!isEmpty(state.sortCriteria)) {
133
+ for (const removedKey of Object.keys(state.sortCriteria)) {
134
+ removeSortCriteria(removedKey);
135
+ }
111
136
  }
137
+ assignReactiveObject(state.order, cloneDeep(parentState.order));
138
+ assignReactiveObject(state.objectsInOrder, cloneDeep(parentState.objectsInOrder));
139
+ return;
112
140
  }
113
- assignReactiveObject(state.order, cloneDeep(parentState.order));
114
- assignReactiveObject(state.objectsInOrder, cloneDeep(parentState.objectsInOrder));
115
- return;
116
- }
117
- const { removedKeys, addedKeys } = keyDiff(Object.keys(parentState.objects), Object.keys(state.sortCriteria));
118
- for (const removedKey of removedKeys) {
119
- removeSortCriteria(removedKey);
120
- }
121
-
122
- es.run(() => {
123
- for (const addedKey of addedKeys) {
124
- const object = toRef(() => parentState.objects[addedKey]);
125
- addSortCriteria(object, addedKey);
141
+ const { removedKeys, addedKeys } = keyDiff(
142
+ Object.keys(parentState.objects),
143
+ Object.keys(state.sortCriteria)
144
+ );
145
+ for (const removedKey of removedKeys) {
146
+ removeSortCriteria(removedKey);
126
147
  }
127
- });
128
- assignReactiveObject(
129
- state.orderByDesc,
130
- state.orderByRules.filter(identity).map((e) => e.desc || false)
131
- );
148
+
149
+ es.run(() => {
150
+ for (const addedKey of addedKeys) {
151
+ const object = toRef(() => parentState.objects[addedKey]);
152
+ const relatedObj = toRef(() => parentState.relatedObjects?.[addedKey]);
153
+ const calculatedObj = toRef(() => parentState.calculatedObjects?.[addedKey]);
154
+ addSortCriteria(object, relatedObj, calculatedObj, addedKey);
155
+ }
156
+ });
157
+ assignReactiveObject(
158
+ state.orderByDesc,
159
+ state.orderByRules.filter(identity).map((e) => e.desc || false)
160
+ );
161
+ } finally {
162
+ state.sortCriteriaWatchRunning = false;
163
+ }
132
164
  }
133
165
 
134
166
  function sortWatch() {
135
- if (!state.orderByRules || !state.orderByRules.length) {
136
- assignReactiveObject(state.order, cloneDeep(parentState.order));
137
- assignReactiveObject(state.objectsInOrder, cloneDeep(parentState.objectsInOrder));
138
- return;
139
- }
167
+ try {
168
+ if (!state.orderByRules || !state.orderByRules.length) {
169
+ assignReactiveObject(state.order, cloneDeep(parentState.order));
170
+ assignReactiveObject(state.objectsInOrder, cloneDeep(parentState.objectsInOrder));
171
+ return;
172
+ }
140
173
 
141
- let idList = Object.keys(parentState.objects);
142
- idList.sort((xKey, yKey) => {
143
- const xCriteria = state.sortCriteria[xKey];
144
- const yCriteria = state.sortCriteria[yKey];
145
- for (let [x, y, orderByObj] of zip(xCriteria, yCriteria, state.orderByRules)) {
146
- if (!orderByObj) {
147
- continue;
148
- }
149
- if (orderByObj.desc) {
150
- [x, y] = [y, x];
151
- }
152
- const isUndefinedX = isUndefined(x) || isNull(x);
153
- const isUndefinedY = isUndefined(y) || isNull(y);
154
- if (isUndefinedX && isUndefinedY) {
155
- continue;
156
- } else if (isUndefinedX) {
157
- return -1;
158
- } else if (isUndefinedY) {
159
- return 1;
160
- }
161
- if (orderByObj.localeCompare) {
162
- const strComp = collator.compare(x, y);
163
- if (strComp) {
164
- return strComp;
174
+ let idList = Object.keys(parentState.objects);
175
+ idList.sort((xKey, yKey) => {
176
+ const xCriteria = state.sortCriteria[xKey];
177
+ const yCriteria = state.sortCriteria[yKey];
178
+ for (let [x, y, orderByObj] of zip(xCriteria, yCriteria, state.orderByRules)) {
179
+ if (!orderByObj) {
180
+ continue;
165
181
  }
166
- } else {
167
- if (x < y) {
168
- return -1;
182
+ if (orderByObj.desc) {
183
+ [x, y] = [y, x];
169
184
  }
170
- if (x > y) {
185
+ const isUndefinedX = isUndefined(x) || isNull(x);
186
+ const isUndefinedY = isUndefined(y) || isNull(y);
187
+ if (isUndefinedX && isUndefinedY) {
188
+ continue;
189
+ } else if (isUndefinedX) {
190
+ return -1;
191
+ } else if (isUndefinedY) {
171
192
  return 1;
172
193
  }
194
+ if (orderByObj.localeCompare) {
195
+ const strComp = collator.compare(x, y);
196
+ if (strComp) {
197
+ return strComp;
198
+ }
199
+ } else {
200
+ if (x < y) {
201
+ return -1;
202
+ }
203
+ if (x > y) {
204
+ return 1;
205
+ }
206
+ }
173
207
  }
174
- }
175
- return 0;
176
- });
177
- assignReactiveObject(state.order, idList);
178
- assignReactiveObject(state.objectsInOrder, idList.map((e) => parentState.objects[e]).filter(identity));
208
+ return 0;
209
+ });
210
+ assignReactiveObject(state.order, idList);
211
+ assignReactiveObject(state.objectsInOrder, idList.map((e) => parentState.objects[e]).filter(identity));
212
+ } finally {
213
+ state.sortWatchRunning = false;
214
+ state.outstandingEffects = false;
215
+ }
179
216
  }
180
217
 
181
218
  const throttledSortWatch = throttle(sortWatch, sortThrottleWait);
182
219
 
220
+ let watchesRunning = null;
221
+
183
222
  es.run(() => {
184
223
  for (const key of listInstanceStateKeys) {
185
224
  if (["order", "objectsInOrder"].includes(key)) {
@@ -209,11 +248,23 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
209
248
  deep: true,
210
249
  immediate: true,
211
250
  });
251
+ watchesRunning = useWatchesRunning({
252
+ triggerRefs: [
253
+ computed(() => (state.orderByRules && !isEmpty(state.orderByRules) ? parentState.loading : false)),
254
+ ],
255
+ watchSentinelRefs: [toRef(state, "sortCriteriaWatchRunning"), toRef(state, "sortWatchRunning")],
256
+ });
257
+
258
+ state.sortRunning = computed(() => loadingCombine(watchesRunning.state.running, state.outstandingEffects));
259
+ state.running = computed(() =>
260
+ loadingCombine(watchesRunning.state.running, state.outstandingEffects, parentState.running)
261
+ );
212
262
  });
213
263
 
214
264
  return {
215
265
  state,
216
266
  parentState,
217
267
  effectScope: es,
268
+ watchesRunning,
218
269
  };
219
270
  }
@@ -77,8 +77,8 @@ export function useListSubscriptions(args, listInstances = {}) {
77
77
  /**
78
78
  * `useListSubscription` creates a reactive object that manages a list of objects, as returned by `useListInstance`,
79
79
  * causing the list to be re-fetched as needed and listening for updates to the list.
80
- * @param {ListSubscriptionOptions} options
81
- * @returns ListSubscription
80
+ * @param {ListSubscriptionOptions} options - the configuration options for the list subscription
81
+ * @returns {ListSubscription} - the list subscription
82
82
  */
83
83
  export function useListSubscription({ listInstance, props, functions, keepOldPages = false }) {
84
84
  if (!listInstance && !props) {
package/utils/watches.js CHANGED
@@ -100,7 +100,7 @@ export class AwaitNot {
100
100
  this.falseISW = new ImmediateStopWatch();
101
101
  // prebuild the exception for a more useful stack.
102
102
  if (this.timeout) {
103
- this.timeoutError = new AwaitTimeoutError("Timeout", "timeout");
103
+ this.timeoutError = new AwaitNotError("Timeout", "timeout");
104
104
  }
105
105
  }
106
106