@arrai-innovations/reactive-helpers 9.0.3 → 10.0.1

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,4 +1,4 @@
1
- import { useCancellableIntent } from "../../../use/index.js";
1
+ import { useCancellableIntent } from "../../../use/cancellableIntent.js";
2
2
  import { CancellableResolvable } from "../crudPromise.js";
3
3
  import flushPromises from "flush-promises";
4
4
  import { nextTick, reactive, ref } from "vue";
@@ -9,12 +9,13 @@ afterAll(() => {
9
9
  });
10
10
 
11
11
  const fields = ["id", "__str__", "name"];
12
- describe.skip("use/listInstance.spec.js", function () {
12
+ describe("use/listInstance.spec.js", function () {
13
13
  let useListInstance, ListError, useListInstances, globalList;
14
14
  beforeEach(async () => {
15
+ const listCrud = await import("../../../config/listCrud.js");
15
16
  const imported = await import("../../../use/listInstance");
16
17
  globalList = vi.fn();
17
- imported.setListInstanceCrud({
18
+ listCrud.setListCrud({
18
19
  list: globalList,
19
20
  args: { stream: "test_stream" },
20
21
  });
@@ -143,8 +144,8 @@ describe.skip("use/listInstance.spec.js", function () {
143
144
  expect({ ...listInstance.state.objects }).toEqual({});
144
145
  globalList.mockImplementation(() => new Promise(() => {}));
145
146
 
146
- listInstance.list();
147
- await expect(listInstance.list()).rejects.toThrow(ListError);
147
+ const firstPromise = listInstance.list();
148
+ expect(listInstance.list()).toBe(firstPromise);
148
149
 
149
150
  expect(globalList).toHaveBeenCalledWith({
150
151
  crudArgs: { stream: "test_stream" },
@@ -68,7 +68,7 @@ describe("use/useListSort", () => {
68
68
  expect(listSort.state.order).toEqual([]);
69
69
  expect(listSort.state.objectsInOrder).toEqual([]);
70
70
  expect(listSort.state.sortCriteria).toEqual({});
71
- expect(listSort.state.sortCriteriaWatches).toEqual({});
71
+ expect(listSort.state.sortCriteriaEffectScopes).toEqual({});
72
72
  expect(listSort.state.orderByDesc).toEqual([true, false]);
73
73
  });
74
74
  describe("addSortCriteria and removeSortCriteria", () => {
@@ -21,6 +21,7 @@ describe("use/listSubscription.spec.js", function () {
21
21
  crudSubscribeResolvable = [];
22
22
  crudListResolvable.push(new CancellableResolvable());
23
23
  crudSubscribeResolvable.push(new CancellableResolvable());
24
+ const listCrudModule = await import("../../../config/listCrud.js");
24
25
  const listInstanceModule = await import("../../../use/listInstance");
25
26
  crudList = vi
26
27
  .fn()
@@ -30,8 +31,9 @@ describe("use/listSubscription.spec.js", function () {
30
31
  crudListResolvable.push(newResolvable);
31
32
  return newResolvable.promise;
32
33
  });
33
- listInstanceModule.setListInstanceCrud({
34
+ listCrudModule.setListCrud({
34
35
  list: crudList,
36
+ subscribe: crudSubscribe,
35
37
  args: { stream: "test_stream" },
36
38
  });
37
39
  const listSubscriptionModule = await import("../../../use/listSubscription");
@@ -49,10 +51,6 @@ describe("use/listSubscription.spec.js", function () {
49
51
  passedSubscriptionEventCallback = subscriptionEventCallback;
50
52
  return newResolvable.promise;
51
53
  });
52
- listSubscriptionModule.setListSubscriptionCrud({
53
- subscribe: crudSubscribe,
54
- args: { stream: "test_stream" },
55
- });
56
54
 
57
55
  useListInstance = listInstanceModule.useListInstance;
58
56
  useListInstances = listInstanceModule.useListInstances;
@@ -15,6 +15,10 @@ describe("utils/flattenPaths", () => {
15
15
  it("with a single level and a nested object and array", () => {
16
16
  expect(flattenPaths({ a: 1, b: { c: [2] } })).toEqual(["a", "b.c[0]"]);
17
17
  });
18
+ it("should limit to a specific depth", () => {
19
+ expect(flattenPaths({ a: 1, b: { c: [2] } }, { limit: 1 })).toEqual(["a"]);
20
+ expect(flattenPaths({ a: 1, b: { c: 1, d: [2], e: { f: 3 } } }, { limit: 2 })).toEqual(["a", "b.c"]);
21
+ });
18
22
  });
19
23
  describe("should work on arrays as the base", () => {
20
24
  it("with a single level", () => {
@@ -29,6 +33,10 @@ describe("utils/flattenPaths", () => {
29
33
  it("with a single level and a nested object and array", () => {
30
34
  expect(flattenPaths([1, { c: [2] }])).toEqual(["[0]", "[1].c[0]"]);
31
35
  });
36
+ it("should limit to a specific depth", () => {
37
+ expect(flattenPaths([1, { c: [2] }], { limit: 1 })).toEqual(["[0]"]);
38
+ expect(flattenPaths([1, [1, [2, [3], 4], 5]], { limit: 2 })).toEqual(["[0]", "[1][0]", "[1][2]"]);
39
+ });
32
40
  });
33
41
  it("should work on a real world example", () => {
34
42
  const toBeFlattened = {
@@ -1,4 +1,4 @@
1
- import { keyDiff } from "../../../utils/keyDiff.js";
1
+ import { keyDiff, keyDiffDeep } from "../../../utils/keyDiff.js";
2
2
 
3
3
  describe("keyDiff", function () {
4
4
  describe("should return the difference between two arrays of object keys", function () {
@@ -64,4 +64,47 @@ describe("keyDiff", function () {
64
64
  expect(removedKeys).toEqual(new Set(["c"]));
65
65
  expect(sameKeys).toEqual(new Set(["a"]));
66
66
  });
67
+ describe("keyDiffDeep", function () {
68
+ it("should return the difference between two objects", function () {
69
+ const newObj = {
70
+ a: 1,
71
+ b: {
72
+ c: 2,
73
+ },
74
+ c: 3,
75
+ };
76
+ const oldObj = {
77
+ a: 1,
78
+ b: 2,
79
+ c: 3,
80
+ };
81
+ // if using to patch, be sure to remove keys before adding them
82
+ expect(keyDiffDeep(newObj, oldObj)).toEqual({
83
+ addedKeys: new Set(["b.c"]),
84
+ removedKeys: new Set(["b"]),
85
+ sameKeys: new Set(["a", "c"]),
86
+ });
87
+ });
88
+ it("should be able to limit the depth", function () {
89
+ const newObj = {
90
+ a: 1,
91
+ b: {
92
+ c: { d: 1, e: 2, f: 3 },
93
+ },
94
+ g: 3,
95
+ };
96
+ const oldObj = {
97
+ a: 1,
98
+ b: {
99
+ c: {},
100
+ },
101
+ g: 3,
102
+ };
103
+ expect(keyDiffDeep(newObj, oldObj, { limit: 1 })).toEqual({
104
+ addedKeys: new Set(),
105
+ removedKeys: new Set(),
106
+ sameKeys: new Set(["a", "g"]),
107
+ });
108
+ });
109
+ });
67
110
  });
package/use/list.js CHANGED
@@ -1,7 +1,9 @@
1
- import { useListCalculated } from "./listCalculated.js";
2
- import { useListInstance } from "./listInstance.js";
3
- import { useListRelated } from "./listRelated.js";
4
- import { useListSubscription } from "./listSubscription.js";
1
+ import { useListCalculated, listCalculatedFunctions } from "./listCalculated.js";
2
+ import { useListFilter, listFilterFunctions } from "./listFilter.js";
3
+ import { useListInstance, listInstanceFunctions } from "./listInstance.js";
4
+ import { useListRelated, listRelatedFunctions } from "./listRelated.js";
5
+ import { useListSort, listSortFunctions } from "./listSort.js";
6
+ import { useListSubscription, listSubscriptionFunctions } from "./listSubscription.js";
5
7
  import { usePagedListInstance } from "./paginatedListInstance.js";
6
8
  import { effectScope, reactive, shallowReactive, shallowReadonly, toRef } from "vue";
7
9
 
@@ -14,7 +16,7 @@ export const useLists = (listArgs) => {
14
16
  };
15
17
 
16
18
  // the big brother of useObject, managing a chain of useList* instances.
17
- export const useList = ({ props, functions, paged = false, keepOldPages = false }) => {
19
+ export const useList = ({ props, functions = {}, paged = false, keepOldPages = false, useTextSearch = false }) => {
18
20
  const managed = shallowReactive({
19
21
  listInstance: null,
20
22
  listSubscription: null,
@@ -42,6 +44,7 @@ export const useList = ({ props, functions, paged = false, keepOldPages = false
42
44
  listInstance: managed.listInstance,
43
45
  });
44
46
  managed.listSubscription.state.intendToList = toRef(props, "intendToList");
47
+ managed.listSubscription.state.intendToSubscribe = toRef(props, "intendToSubscribe");
45
48
 
46
49
  managed.listRelated = useListRelated({
47
50
  parentState: managed.listSubscription.state,
@@ -52,6 +55,23 @@ export const useList = ({ props, functions, paged = false, keepOldPages = false
52
55
  parentState: managed.listRelated.state,
53
56
  calculatedObjectsRules: toRef(props, "calculatedObjectsRules"),
54
57
  });
58
+
59
+ managed.listFilter = useListFilter({
60
+ parentState: managed.listCalculated.state,
61
+ useTextSearch,
62
+ textSearchRules: toRef(props, "textSearchRules"),
63
+ textSearchValue: toRef(props, "textSearchValue"),
64
+ allowedValues: toRef(props, "allowedValues"),
65
+ excludedValues: toRef(props, "excludedValues"),
66
+ allowedFilter: toRef(props, "allowedFilter"),
67
+ excludedFilter: toRef(props, "excludedFilter"),
68
+ });
69
+
70
+ managed.listSort = useListSort({
71
+ parentState: managed.listFilter.state,
72
+ orderByRules: toRef(props, "orderByRules"),
73
+ sortThrottleWait: functions.sortThrottleWait,
74
+ });
55
75
  });
56
76
 
57
77
  const clearError = (error) => {
@@ -59,18 +79,32 @@ export const useList = ({ props, functions, paged = false, keepOldPages = false
59
79
  managed.listInstance.clearError(error);
60
80
  };
61
81
 
62
- return reactive({
82
+ const returnObject = reactive({
63
83
  // we manage the keys on both of these, so hands off the root.
64
84
  managed: shallowReadonly(managed),
65
- state: managed.listCalculated.state,
66
- list: managed.listInstance.list,
67
- addListObject: managed.listInstance.addListObject,
68
- updateListObject: managed.listInstance.updateListObject,
69
- deleteListObject: managed.listInstance.deleteListObject,
70
- clearList: managed.listInstance.clearList,
71
- clearError,
72
- getFakeId: managed.listInstance.getFakeId,
73
- defaultPageCallback: managed.listInstance.defaultPageCallback,
85
+ state: managed.listSort.state,
74
86
  effectScope: es,
75
87
  });
88
+ const handledDuplicateFunctions = {
89
+ clearError,
90
+ };
91
+ for (const [source, fnNames] of [
92
+ [managed.listInstance, listInstanceFunctions],
93
+ [managed.listSubscription, listSubscriptionFunctions],
94
+ [managed.listRelated, listRelatedFunctions],
95
+ [managed.listCalculated, listCalculatedFunctions],
96
+ [managed.listFilter, listFilterFunctions],
97
+ [managed.listSort, listSortFunctions],
98
+ ]) {
99
+ for (const fnName of fnNames) {
100
+ if (handledDuplicateFunctions[fnName]) {
101
+ continue;
102
+ }
103
+ returnObject[fnName] = source[fnName];
104
+ }
105
+ }
106
+ for (const [fnName, fn] of Object.entries(handledDuplicateFunctions)) {
107
+ returnObject[fnName] = fn;
108
+ }
109
+ return returnObject;
76
110
  };
@@ -13,6 +13,8 @@ export const listCalculatedStateKeys = [
13
13
  "calculatedObjectsWatchRunning",
14
14
  ];
15
15
 
16
+ export const listCalculatedFunctions = [];
17
+
16
18
  export function useListCalculateds(instances, args) {
17
19
  for (const [key, value] of Object.entries(args)) {
18
20
  useListCalculated({
@@ -130,6 +132,7 @@ export function useListCalculated({ parentState, calculatedObjectsRules }) {
130
132
  ],
131
133
  });
132
134
 
135
+ state.calculatedRunning = toRef(watchesRunning.state, "running");
133
136
  state.running = computed(() => loadingCombine(watchesRunning.state.running, parentState.running));
134
137
 
135
138
  onScopeDispose(() => {
package/use/listFilter.js CHANGED
@@ -1,10 +1,30 @@
1
1
  import { keyDiff } from "../utils/keyDiff.js";
2
+ import { listCalculatedStateKeys } from "./listCalculated.js";
3
+ import { listInstanceStateKeys } from "./listInstance.js";
4
+ import { listRelatedStateKeys } from "./listRelated.js";
5
+ import { listSubscriptionStateKeys } from "./listSubscription.js";
2
6
  import { useSearch } from "./search.js";
3
7
  import get from "lodash-es/get.js";
4
8
  import identity from "lodash-es/identity.js";
5
9
  import isEmpty from "lodash-es/isEmpty.js";
6
10
  import { computed, effectScope, onScopeDispose, reactive, toRef, watch, watchEffect } from "vue";
7
11
 
12
+ export const listFilterStateKeys = [
13
+ "objectIndexes",
14
+ // override but not ours
15
+ // "objects",
16
+ "textSearchRules",
17
+ "textSearchValue",
18
+ "allowedValues",
19
+ "excludedValues",
20
+ "allowedFilter",
21
+ "excludedFilter",
22
+ "searched",
23
+ "searching",
24
+ ];
25
+
26
+ export const listFilterFunctions = [];
27
+
8
28
  export function useListFilters(listFilterArgs, parentInstances) {
9
29
  const filters = {};
10
30
  for (const [key, value] of Object.entries(listFilterArgs)) {
@@ -32,6 +52,8 @@ export function useListFilter({
32
52
  excludedValues,
33
53
  allowedFilter,
34
54
  excludedFilter,
55
+ searched: undefined,
56
+ searching: undefined,
35
57
  });
36
58
 
37
59
  const es = effectScope();
@@ -39,6 +61,21 @@ export function useListFilter({
39
61
  let textSearchIndex;
40
62
 
41
63
  es.run(() => {
64
+ for (const key of listInstanceStateKeys) {
65
+ if (key === "objects") {
66
+ continue;
67
+ }
68
+ state[key] = toRef(parentState, key);
69
+ }
70
+ for (const key of listSubscriptionStateKeys) {
71
+ state[key] = toRef(parentState, key);
72
+ }
73
+ for (const key of listRelatedStateKeys) {
74
+ state[key] = toRef(parentState, key);
75
+ }
76
+ for (const key of listCalculatedStateKeys) {
77
+ state[key] = toRef(parentState, key);
78
+ }
42
79
  if (useTextSearch) {
43
80
  textSearchIndex = useSearch();
44
81
  textSearchIndex.state.search = toRef(state, "textSearchValue");
@@ -46,9 +83,11 @@ export function useListFilter({
46
83
  state.searching = toRef(textSearchIndex.state, "searching");
47
84
  }
48
85
 
86
+ // todo: computed is not the solution here for deep reactions
49
87
  state.objectsInOrder = computed(() => parentState.order.map((id) => state.objects[id]).filter(identity));
50
88
  state.order = computed(() => state.objectsInOrder.map((object) => `${object.id}`));
51
89
 
90
+ // todo: this huge watchEffect is fairly gross, but also doesn't watch deep similarly to computed
52
91
  watchEffect(() => {
53
92
  const allowedValuesEmpty = !state.allowedValues || isEmpty(state.allowedValues);
54
93
  const excludedValuesEmpty = !state.excludedValues || isEmpty(state.excludedValues);
@@ -104,6 +143,7 @@ export function useListFilter({
104
143
  if (useTextSearch) {
105
144
  const stopIndexWatch = {};
106
145
 
146
+ // todo: this huge watchEffect is fairly gross, but also doesn't watch deep similarly to computed
107
147
  watchEffect(() => {
108
148
  const { removedKeys, addedKeys } = keyDiff(
109
149
  Object.keys(parentState.objects),
@@ -1,9 +1,8 @@
1
- import { addOrUpdateReactiveObject, assignReactiveObject } from "../utils/assignReactiveObject.js";
1
+ import { getListCrud } from "../config/listCrud.js";
2
+ import { assignReactiveObject } from "../utils/assignReactiveObject.js";
2
3
  import { getFakeId } from "../utils/getFakeId.js";
3
4
  import inspect from "browser-util-inspect";
4
- import cloneDeep from "lodash-es/cloneDeep.js";
5
- import isFunction from "lodash-es/isFunction.js";
6
- import { computed, effectScope, reactive, toRef, watchEffect } from "vue";
5
+ import { computed, effectScope, reactive, toRef } from "vue";
7
6
 
8
7
  export class ListError extends Error {
9
8
  constructor(message, code) {
@@ -13,11 +12,6 @@ export class ListError extends Error {
13
12
  }
14
13
  }
15
14
 
16
- const defaultCrud = {
17
- args: {},
18
- list: undefined,
19
- };
20
-
21
15
  export const listInstanceStateKeys = [
22
16
  "crud",
23
17
  "retrieveArgs",
@@ -34,11 +28,39 @@ export const listInstanceStateKeys = [
34
28
  "perPage",
35
29
  ];
36
30
 
37
- export function setListInstanceCrud({ list, args = {} } = {}) {
38
- defaultCrud.list = list;
39
- Object.assign(defaultCrud.args, args);
40
- }
31
+ export const listInstanceFunctions = [
32
+ "list",
33
+ "addListObject",
34
+ "updateListObject",
35
+ "deleteListObject",
36
+ "clearList",
37
+ "clearError",
38
+ "getFakeId",
39
+ "defaultPageCallback",
40
+ "pageCallback",
41
+ ];
41
42
 
43
+ /**
44
+ * The configuration options used to create a list instance.
45
+ * @typedef {object} ListInstanceOptions
46
+ * @property {object} props - props passed to the component
47
+ * @property {object} props.retrieveArgs - the arguments passed to the server
48
+ * @property {object} props.listArgs - the arguments passed to the server
49
+ * @property {object} props.crudArgs - implementation specific arguments
50
+ * @property {object} functions - optional. default implementation are used as set by `setListCrud`.
51
+ * @property {Function} functions.list - provide the implementation for the list function
52
+ * @property {Function} functions.subscribe - provide the implementation for the subscribe function
53
+ * @property {boolean} keepOldPages - if true, pages will not be cleared when defaultPageCallback is called. default is false.
54
+ */
55
+
56
+ /* eslint-disable jsdoc/check-types */
57
+ // types valid for jsdoc-to-markdown, which uses the strict jsdoc.app. Object shorthand syntax doesn't work.
58
+ /**
59
+ * A Vue composition function that creates multiple list instances, and returns them as an object.
60
+ * @param {Object.<string, ListInstanceOptions>} listInstanceArgs - each desired list instance options, keyed by an instance name.
61
+ * @returns {Object.<string, ListInstance>} - each list instance, keyed by the instance name.
62
+ */
63
+ /* eslint-enable jsdoc/check-types */
42
64
  export function useListInstances(listInstanceArgs) {
43
65
  const instances = {};
44
66
  for (const [key, value] of Object.entries(listInstanceArgs)) {
@@ -47,6 +69,48 @@ export function useListInstances(listInstanceArgs) {
47
69
  return instances;
48
70
  }
49
71
 
72
+ /**
73
+ * A reactive object that manages a list of objects, as returned by `useListInstance`.
74
+ * @typedef {object} ListInstanceState
75
+ * @property {object} crud - the crud functions
76
+ * @property {object} crud.args - the arguments passed to the crud functions
77
+ * @property {Function} crud.list - the list function
78
+ * @property {Function} crud.subscribe - the subscribe function
79
+ * @property {object} retrieveArgs - the arguments passed to the server
80
+ * @property {object} listArgs - the arguments passed to the server
81
+ * @property {Map} objects - the objects in the list
82
+ * @property {boolean} loading - true if the list is loading
83
+ * @property {boolean} errored - true if the list has errored
84
+ * @property {object} error - the error object
85
+ * @property {Array} objectsInOrder - the objects in the list in order
86
+ * @property {Array} order - the order of the objects in the list
87
+ * @property {number} totalRecords - the total number of records
88
+ * @property {number} totalPages - the total number of pages
89
+ * @property {number} perPage - the number of records per page
90
+ */
91
+
92
+ /**
93
+ * @typedef {object} ListInstance
94
+ * @property {Function} list - subscribe to updates from the implementation
95
+ * @property {Function} addListObject - add an object to the list
96
+ * @property {Function} updateListObject - update an object in the list
97
+ * @property {Function} deleteListObject - delete an object from the list
98
+ * @property {Function} clearList - clear the list
99
+ * @property {Function} clearError - clear the error
100
+ * @property {Function} getFakeId - get a fake id
101
+ * @property {Function} defaultPageCallback - the default page callback
102
+ * @property {Function} pageCallback - the page callback
103
+ * @property {ListInstanceState} state - the list instance state
104
+ * @property {object} effectScope - a Vue effect scope
105
+ */
106
+
107
+ /**
108
+ * `useListInstance` is a Vue composition function that manages a list of objects.
109
+ * It has the ability to retrieve the list from an implementation, or subscribe to updates from an implementation.
110
+ * It tracks the objects in the list, and their added order.
111
+ * @param {ListInstanceOptions} options - the options used to create the list instance
112
+ * @returns {ListInstance} - the list instance
113
+ */
50
114
  export function useListInstance({ props, functions = {}, keepOldPages = false }) {
51
115
  if (!props) {
52
116
  throw new ListError(`useListInstance requires props`);
@@ -98,20 +162,8 @@ export function useListInstance({ props, functions = {}, keepOldPages = false })
98
162
  error: null,
99
163
  });
100
164
  const es = effectScope();
101
- watchEffect(() => {
102
- // prevent linking of all instances to the same default .args object
103
- Object.assign(state.crud, cloneDeep(defaultCrud));
104
- if (props.crudArgs) {
105
- addOrUpdateReactiveObject(state.crud.args, props.crudArgs);
106
- }
107
- for (const [key, value] of Object.entries(functions)) {
108
- if (isFunction(value) && key in state.crud) {
109
- state.crud[key] = value;
110
- } else {
111
- throw ListError(`Invalid function "${key}" for useListInstance: invalid key or not a function.`);
112
- }
113
- }
114
- });
165
+
166
+ getListCrud(state.crud, { props, functions });
115
167
 
116
168
  const defaultPageCallback = (newObjects) => {
117
169
  // with keepOldPages, you are responsible for clearing the list as needed
@@ -1,10 +1,13 @@
1
- import { keyDiff, loadingCombine } from "../utils/index.js";
1
+ import { keyDiff } from "../utils/keyDiff.js";
2
+ import { loadingCombine } from "../utils/loadingCombine.js";
2
3
  import { listInstanceStateKeys } from "./listInstance.js";
3
4
  import { listSubscriptionStateKeys } from "./listSubscription.js";
4
5
  import { useWatchesRunning } from "./watchesRunning.js";
5
6
  import get from "lodash-es/get.js";
7
+ import identity from "lodash-es/identity.js";
6
8
  import isArray from "lodash-es/isArray.js";
7
9
  import isEmpty from "lodash-es/isEmpty.js";
10
+ import isEqual from "lodash-es/isEqual.js";
8
11
  import isUndefined from "lodash-es/isUndefined.js";
9
12
  import { computed, effectScope, onScopeDispose, reactive, toRef, unref, watch } from "vue";
10
13
 
@@ -15,6 +18,8 @@ export const listRelatedStateKeys = [
15
18
  "relatedObjectsParentStateObjectsWatchRunning",
16
19
  ];
17
20
 
21
+ export const listRelatedFunctions = [];
22
+
18
23
  export function useListRelateds(instances, args) {
19
24
  for (const [key, value] of Object.entries(args)) {
20
25
  useListRelated({
@@ -52,7 +57,7 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
52
57
  const relatedObjectsRulesIsEmpty = !state.relatedObjectsRules || isEmpty(state.relatedObjectsRules);
53
58
  for (const objectKey of Object.keys(state.relatedObjects)) {
54
59
  const relatedObjectsObject = state.relatedObjects[objectKey];
55
- const originalObject = parentState.objects[objectKey];
60
+ const originalObjectRef = toRef(parentState.objects, objectKey);
56
61
  let removedRuleKeys, addedRuleKeys;
57
62
  if (!relatedObjectsRulesIsEmpty) {
58
63
  ({ removedKeys: removedRuleKeys, addedKeys: addedRuleKeys } = keyDiff(
@@ -74,22 +79,48 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
74
79
  }
75
80
  relatedObjectsEffectScopes[objectKey].run(() => {
76
81
  for (const addedRuleKey of addedRuleKeys) {
77
- relatedObjectsObject[addedRuleKey] = computed(() => {
82
+ relatedObjectsObject[addedRuleKey] = undefined;
83
+ const relatedObjectsObjectWatchFn = () => {
78
84
  // deal with computed objects being passed.
79
85
  const ruleObjects = unref(state.relatedObjectsRules?.[addedRuleKey]?.objects);
80
86
  const rulePkKey = state.relatedObjectsRules?.[addedRuleKey]?.pkKey || addedRuleKey;
87
+ const ruleOrder = unref(state.relatedObjectsRules?.[addedRuleKey]?.order);
81
88
  if (!ruleObjects || !rulePkKey) {
82
- return undefined;
89
+ relatedObjectsObject[addedRuleKey] = undefined;
90
+ return;
83
91
  }
84
- const value = get(originalObject, rulePkKey);
92
+ let value = get(unref(originalObjectRef), rulePkKey);
85
93
  if (isUndefined(value)) {
86
- return undefined;
94
+ relatedObjectsObject[addedRuleKey] = undefined;
95
+ return;
87
96
  }
88
97
  if (isArray(value)) {
89
- return value.map((e) => ruleObjects[e]);
98
+ // the related list could be sorted differently than the original list.
99
+ if (ruleOrder?.length) {
100
+ value = value.filter(identity);
101
+ const indexById = Object.fromEntries(ruleOrder.map((e, i) => [e, i]));
102
+ value.sort((a, b) => {
103
+ const aIndex = indexById[a];
104
+ const bIndex = indexById[b];
105
+ return aIndex - bIndex;
106
+ });
107
+ }
108
+ value = value.map((e) => ruleObjects[e]).filter(identity);
109
+ } else {
110
+ value = ruleObjects[value];
111
+ }
112
+ if (!isEqual(value, relatedObjectsObject[addedRuleKey])) {
113
+ relatedObjectsObject[addedRuleKey] = value;
114
+ }
115
+ };
116
+ watch(
117
+ [toRef(state.relatedObjectsRules, addedRuleKey), originalObjectRef],
118
+ relatedObjectsObjectWatchFn,
119
+ {
120
+ deep: true,
121
+ immediate: true,
90
122
  }
91
- return ruleObjects[value];
92
- });
123
+ );
93
124
  }
94
125
  });
95
126
  }
@@ -130,6 +161,7 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
130
161
  ],
131
162
  });
132
163
 
164
+ state.relatedRunning = toRef(watchesRunning.state, "running");
133
165
  state.running = computed(() => loadingCombine(watchesRunning.state.running, parentState.running));
134
166
 
135
167
  onScopeDispose(() => {