Skip to content

Commit 6f30f0d

Browse files
authored
update GUI tree when data changes structurally (#890)
* update GUI tree when data changes structurally * apply formatting changes --------- Co-authored-by: Logende <[email protected]>
1 parent 1209202 commit 6f30f0d

5 files changed

Lines changed: 213 additions & 13 deletions

File tree

meta_configurator/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

meta_configurator/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"js-yaml": "^4.1.0",
4646
"json-cst": "^1.2.0",
4747
"json-pointer": "^0.6.2",
48-
"json-schema-faker": "^0.5.8",
48+
"json-schema-faker": "^0.5.9",
4949
"json-schema-merge-allof": "^0.8.1",
5050
"json-schema-ref-parser": "^9.0.9",
5151
"json-schema-resolver": "^3.0.0",

meta_configurator/src/components/panels/gui-editor/PropertiesPanel.vue

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ It also contains the logic for adding, removing and renaming properties.
66
TODO: This component is too big. Some of the logic should be moved to other files.
77
-->
88
<script setup lang="ts">
9-
import {onMounted, type Ref} from 'vue';
9+
import {onBeforeUnmount, onMounted, type Ref} from 'vue';
1010
import {ref, watch} from 'vue';
1111
import TreeTable from 'primevue/treetable';
1212
import Column from 'primevue/column';
@@ -36,8 +36,9 @@ import {
3636
} from '@/data/useDataLink';
3737
import {dataAt} from '@/utility/resolveDataAtPath';
3838
import type {SessionMode} from '@/store/sessionMode';
39-
import _ from 'lodash';
39+
import _, {debounce} from 'lodash';
4040
import {replacePropertyNameUtils} from '@/utility/renameUtils';
41+
import {isStructuralChangeInInstance} from '@/components/panels/gui-editor/isStructuralChangeInInstance.ts';
4142
4243
const props = defineProps<{
4344
currentSchema: JsonSchemaWrapper;
@@ -63,6 +64,15 @@ const data = getDataForMode(props.sessionMode);
6364
// to avoid scrolling on a click in the GUI itself, we remember the last path clicked by the user in the GUI and do not scroll when the user clicks on the same path again
6465
const lastClickedElement = ref<Path>([]);
6566
67+
const treeNodeResolver = new ConfigTreeNodeResolver();
68+
69+
const loading = ref(false);
70+
const loadingDebounced = refDebounced(loading, 100);
71+
72+
const treeTableFilters = ref<Record<string, string>>({});
73+
74+
const currentTree = ref<GuiEditorTreeNode>();
75+
6676
watch(
6777
session.currentSelectedElement,
6878
() => {
@@ -95,22 +105,33 @@ onMounted(() => {
95105
updateTree(true);
96106
});
97107
108+
onBeforeUnmount(() => {
109+
debouncedUpdateTreeForDataChange.cancel();
110+
});
111+
112+
// update tree when the data changes in a significant way (change not of values but of structure)
113+
// also uses debouncing
114+
watch(getDataForMode(props.sessionMode).data, (newValue, oldValue) => {
115+
debouncedUpdateTreeForDataChange(oldValue, newValue);
116+
});
117+
118+
const debouncedUpdateTreeForDataChange = debounce(
119+
(oldValue, newValue) => {
120+
if (isStructuralChangeInInstance(oldValue, newValue)) {
121+
updateTree();
122+
}
123+
},
124+
1000, // 1 second
125+
{leading: false, trailing: true} // trailing = after stopped typing
126+
);
127+
98128
// update tree when the current path changes
99129
watch(session.currentPath, () => {
100130
updateTree();
101131
focusOnFirstProperty();
102132
allowShowOverlay.value = true; // reset in case the user was editing a property name
103133
});
104134
105-
const treeNodeResolver = new ConfigTreeNodeResolver();
106-
107-
const loading = ref(false);
108-
const loadingDebounced = refDebounced(loading, 100);
109-
110-
const treeTableFilters = ref<Record<string, string>>({});
111-
112-
const currentTree = ref<GuiEditorTreeNode>();
113-
114135
/**
115136
* Compute the tree that should be displayed and expand all nodes that were expanded before.
116137
*/
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {beforeEach, describe, expect, it, vi} from 'vitest';
2+
import {isStructuralChangeInInstance} from '@/components/panels/gui-editor/isStructuralChangeInInstance.ts';
3+
4+
describe('isStructuralChangeInInstance', () => {
5+
beforeEach(() => {
6+
Object.assign(navigator, {
7+
clipboard: {
8+
writeText: vi.fn().mockResolvedValue(undefined),
9+
},
10+
});
11+
});
12+
13+
it('two identical arrays', async () => {
14+
const oldIns = [1, 2, 3, {a: 1}];
15+
const newIns = [1, 2, 3, {a: 1}];
16+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(false);
17+
});
18+
19+
it('two identical objects', async () => {
20+
const oldIns = {a: 1, b: 2, c: {d: 3}};
21+
const newIns = {a: 1, b: 2, c: {d: 3}};
22+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(false);
23+
});
24+
25+
it('two identical primitives of each type', async () => {
26+
expect(isStructuralChangeInInstance(1, 1)).toBe(false);
27+
expect(isStructuralChangeInInstance('string', 'string')).toBe(false);
28+
expect(isStructuralChangeInInstance(true, true)).toBe(false);
29+
expect(isStructuralChangeInInstance(null, null)).toBe(false);
30+
});
31+
32+
it('two different primitives of each type', async () => {
33+
expect(isStructuralChangeInInstance(1, 2)).toBe(false);
34+
expect(isStructuralChangeInInstance('string', 'different string')).toBe(false);
35+
expect(isStructuralChangeInInstance(true, false)).toBe(false);
36+
37+
expect(isStructuralChangeInInstance(null, undefined)).toBe(true);
38+
});
39+
40+
it('intermixed primitive types', async () => {
41+
expect(isStructuralChangeInInstance(1, 'string')).toBe(false);
42+
expect(isStructuralChangeInInstance(true, null)).toBe(false);
43+
44+
expect(isStructuralChangeInInstance(undefined, 1)).toBe(true);
45+
});
46+
47+
it('primitive to object', async () => {
48+
const oldIns = 1;
49+
const newIns = {a: 1};
50+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(true);
51+
});
52+
53+
it('primitive to array', async () => {
54+
const oldIns = 1;
55+
const newIns = [1];
56+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(true);
57+
});
58+
59+
it('object where only values changed but not keys', async () => {
60+
const oldIns = {a: 1, b: 2};
61+
const newIns = {a: 2, b: 3};
62+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(false);
63+
});
64+
65+
it('array where only values changed but not keys', async () => {
66+
const oldIns = [1, 2, 3];
67+
const newIns = [2, 3, 4];
68+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(false);
69+
});
70+
71+
it('object where keys have changed', async () => {
72+
const oldIns = {a: 1, b: 2};
73+
const newIns = {a: 1, c: 2};
74+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(true);
75+
});
76+
77+
it('object where new key was added', async () => {
78+
const oldIns = {a: 1};
79+
const newIns = {a: 1, b: 2};
80+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(true);
81+
});
82+
83+
it('object where a key was removed', async () => {
84+
const oldIns = {a: 1, b: 2};
85+
const newIns = {a: 1};
86+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(true);
87+
});
88+
89+
it('array where count has changed', async () => {
90+
const oldIns = [1, 2, 3];
91+
const newIns = [1, 2, 3, 4];
92+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(true);
93+
});
94+
95+
it('array where nested object has changed', async () => {
96+
// only value in object changed -> no structural change
97+
const oldIns = [1, 2, {a: 1}];
98+
const newIns = [1, 2, {a: 2}];
99+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(false);
100+
101+
// key in object changed -> structural change
102+
const oldIns2 = [1, 2, {a: 1}];
103+
const newIns2 = [1, 2, {b: 1}];
104+
expect(isStructuralChangeInInstance(oldIns2, newIns2)).toBe(true);
105+
});
106+
107+
it('nested object which changed', async () => {
108+
// only value in nested object changed -> no structural change
109+
const oldIns = {a: 1, b: {c: 1}};
110+
const newIns = {a: 1, b: {c: 2}};
111+
expect(isStructuralChangeInInstance(oldIns, newIns)).toBe(false);
112+
113+
// key in nested object changed -> structural change
114+
const oldIns2 = {a: 1, b: {c: 1}};
115+
const newIns2 = {a: 1, b: {d: 1}};
116+
expect(isStructuralChangeInInstance(oldIns2, newIns2)).toBe(true);
117+
});
118+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type {GuiEditorTreeNode} from '@/components/panels/gui-editor/configDataTreeNode.ts';
2+
3+
export function isStructuralChangeInInstance(oldObject: any, newObject: any): boolean {
4+
// algorithm to determine whether any key or structure has changed between oldObject and newObject
5+
// changes in values of existing keys are not considered structural changes
6+
// the element might also be an array, in which case we need to check for changes in the length of the array and the keys of the objects in the array
7+
// also this algorithm is recursive, so if there are nested objects or arrays, we need to check for structural changes in those as well
8+
9+
// if both are of primitive type, we can skip the check for structural changes
10+
if (isOfPrimitiveType(oldObject) && isOfPrimitiveType(newObject)) {
11+
return false;
12+
}
13+
14+
// otherwise, if the types of oldObject and newObject are different, we can consider it a structural change
15+
if (typeof oldObject !== typeof newObject) {
16+
return true;
17+
}
18+
19+
// logic for arrays
20+
if (Array.isArray(oldObject) && Array.isArray(newObject)) {
21+
if (oldObject.length !== newObject.length) {
22+
return true;
23+
}
24+
for (let i = 0; i < oldObject.length; i++) {
25+
if (isStructuralChangeInInstance(oldObject[i], newObject[i])) {
26+
return true;
27+
}
28+
}
29+
return false;
30+
}
31+
32+
// logic for objects
33+
if (typeof oldObject === 'object' && oldObject !== null && newObject !== null) {
34+
const oldKeys = Object.keys(oldObject);
35+
const newKeys = Object.keys(newObject);
36+
37+
if (oldKeys.length !== newKeys.length) {
38+
return true;
39+
}
40+
41+
for (const key of oldKeys) {
42+
if (!newKeys.includes(key)) {
43+
return true;
44+
}
45+
if (isStructuralChangeInInstance(oldObject[key], newObject[key])) {
46+
return true;
47+
}
48+
}
49+
}
50+
51+
return false;
52+
}
53+
54+
function isOfPrimitiveType(value: any): boolean {
55+
return (
56+
typeof value === 'string' ||
57+
typeof value === 'number' ||
58+
typeof value === 'boolean' ||
59+
value === null
60+
);
61+
}

0 commit comments

Comments
 (0)