Skip to content

Commit cb07c07

Browse files
Dennis LabordusJakobVogelsang
andauthored
feat(menu/compareied): compares two IED elements with one another (openscd#903)
* Added IED Compare functionality with Template Project. Signed-off-by: Dennis Labordus <[email protected]> * Added Unit tests for Dialog. Signed-off-by: Dennis Labordus <[email protected]> * Reverted Package Lock. Signed-off-by: Dennis Labordus <[email protected]> * Updated German translations. Signed-off-by: Dennis Labordus <[email protected]> * Fixed comparing elements. Signed-off-by: Dennis Labordus <[email protected]> * Review comments processed. Signed-off-by: Dennis Labordus <[email protected]> * Review comments processed, fixed compare. Signed-off-by: Dennis Labordus <[email protected]> * fix(translation): add missing german translation * refactor(menu/compareied): make linter happy Co-authored-by: Jakob Vogelsang <[email protected]>
1 parent cff0bb7 commit cb07c07

File tree

15 files changed

+2780
-5
lines changed

15 files changed

+2780
-5
lines changed

public/js/plugins.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ export const officialPlugins = [
6262
default: false,
6363
kind: 'editor',
6464
},
65+
{
66+
name: 'Cleanup',
67+
src: '/src/editors/Cleanup.js',
68+
icon: 'cleaning_services',
69+
default: false,
70+
kind: 'editor',
71+
},
6572
{
6673
name: 'Open project',
6774
src: '/src/menu/OpenProject.js',
@@ -119,7 +126,7 @@ export const officialPlugins = [
119126
default: false,
120127
kind: 'menu',
121128
requireDoc: true,
122-
position: 'middle'
129+
position: 'middle',
123130
},
124131
{
125132
name: 'Subscriber Update',
@@ -164,11 +171,13 @@ export const officialPlugins = [
164171
position: 'middle',
165172
},
166173
{
167-
name: 'Cleanup',
168-
src: '/src/editors/Cleanup.js',
169-
icon: 'cleaning_services',
174+
name: 'Compare IED',
175+
src: '/src/menu/CompareIED.js',
176+
icon: 'compare_arrows',
170177
default: false,
171-
kind: 'editor',
178+
kind: 'menu',
179+
requireDoc: true,
180+
position: 'middle',
172181
},
173182
{
174183
name: 'Help',

src/foundation/compare.ts

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import { html, TemplateResult } from 'lit-element';
2+
import { repeat } from 'lit-html/directives/repeat';
3+
import { get, translate } from 'lit-translate';
4+
5+
import '@material/mwc-list';
6+
import '@material/mwc-list/mwc-list-item';
7+
import '@material/mwc-icon';
8+
9+
import { identity } from '../foundation.js';
10+
import { nothing } from 'lit-html';
11+
12+
export type Diff<T> =
13+
| { oldValue: T; newValue: null }
14+
| { oldValue: null; newValue: T }
15+
| { oldValue: T; newValue: T };
16+
17+
/**
18+
* Returns the description of the Element that differs.
19+
*
20+
* @param element - The Element to retrieve the identifier from.
21+
*/
22+
function describe(element: Element): string {
23+
const id = identity(element);
24+
return typeof id === 'string' ? id : get('unidentifiable');
25+
}
26+
27+
/**
28+
* Check if there are any attribute values changed between the two elements.
29+
*
30+
* @param elementToBeCompared - The element to check for differences.
31+
* @param elementToCompareAgainst - The element used to check against.
32+
*/
33+
export function diffSclAttributes(
34+
elementToBeCompared: Element,
35+
elementToCompareAgainst: Element
36+
): [string, Diff<string>][] {
37+
const attrDiffs: [string, Diff<string>][] = [];
38+
39+
// First check if there is any text inside the element and there should be no child elements.
40+
const newText = elementToBeCompared.textContent?.trim() ?? '';
41+
const oldText = elementToCompareAgainst.textContent?.trim() ?? '';
42+
if (
43+
elementToBeCompared.childElementCount === 0 &&
44+
elementToCompareAgainst.childElementCount === 0 &&
45+
newText !== oldText
46+
) {
47+
attrDiffs.push(['value', { newValue: newText, oldValue: oldText }]);
48+
}
49+
50+
// Next check if there are any difference between attributes.
51+
const attributeNames = new Set(
52+
elementToCompareAgainst
53+
.getAttributeNames()
54+
.concat(elementToBeCompared.getAttributeNames())
55+
);
56+
for (const name of attributeNames) {
57+
if (
58+
elementToCompareAgainst.getAttribute(name) !==
59+
elementToBeCompared.getAttribute(name)
60+
) {
61+
attrDiffs.push([
62+
name,
63+
<Diff<string>>{
64+
newValue: elementToBeCompared.getAttribute(name),
65+
oldValue: elementToCompareAgainst.getAttribute(name),
66+
},
67+
]);
68+
}
69+
}
70+
return attrDiffs;
71+
}
72+
73+
/**
74+
* Function to retrieve the identity to compare 2 children on the same level.
75+
* This means we only need to last part of the Identity string to compare the children.
76+
*
77+
* @param element - The element to retrieve the identity from.
78+
*/
79+
export function identityForCompare(element: Element): string | number {
80+
let identityOfElement = identity(element);
81+
if (typeof identityOfElement === 'string') {
82+
identityOfElement = identityOfElement.split('>').pop() ?? '';
83+
}
84+
return identityOfElement;
85+
}
86+
87+
/**
88+
* Custom method for comparing to check if 2 elements are the same. Because they are on the same level
89+
* we don't need to compare the full identity, we just compare the part of the Element itself.
90+
*
91+
* <b>Remark</b>Private elements are already filtered out, so we don't need to bother them.
92+
*
93+
* @param newValue - The new element to compare with the old element.
94+
* @param oldValue - The old element to which the new element is compared.
95+
*/
96+
export function isSame(newValue: Element, oldValue: Element): boolean {
97+
return (
98+
newValue.tagName === oldValue.tagName &&
99+
identityForCompare(newValue) === identityForCompare(oldValue)
100+
);
101+
}
102+
103+
/**
104+
* List of all differences between children elements that both old and new element have.
105+
* The list contains children both elements have and children that were added or removed
106+
* from the new element.
107+
* <b>Remark</b>: Private elements are ignored.
108+
*
109+
* @param elementToBeCompared - The element to check for differences.
110+
* @param elementToCompareAgainst - The element used to check against.
111+
*/
112+
export function diffSclChilds(
113+
elementToBeCompared: Element,
114+
elementToCompareAgainst: Element
115+
): Diff<Element>[] {
116+
const childDiffs: Diff<Element>[] = [];
117+
const childrenToBeCompared = Array.from(elementToBeCompared.children);
118+
const childrenToCompareTo = Array.from(elementToCompareAgainst.children);
119+
120+
childrenToBeCompared.forEach(newElement => {
121+
if (!newElement.closest('Private')) {
122+
const twinIndex = childrenToCompareTo.findIndex(ourChild =>
123+
isSame(newElement, ourChild)
124+
);
125+
const oldElement = twinIndex > -1 ? childrenToCompareTo[twinIndex] : null;
126+
127+
if (oldElement) {
128+
childrenToCompareTo.splice(twinIndex, 1);
129+
childDiffs.push({ newValue: newElement, oldValue: oldElement });
130+
} else {
131+
childDiffs.push({ newValue: newElement, oldValue: null });
132+
}
133+
}
134+
});
135+
childrenToCompareTo.forEach(oldElement => {
136+
if (!oldElement.closest('Private')) {
137+
childDiffs.push({ newValue: null, oldValue: oldElement });
138+
}
139+
});
140+
return childDiffs;
141+
}
142+
143+
/**
144+
* Generate HTML (TemplateResult) containing all the differences between the two elements passed.
145+
* If null is returned there are no differences between the two elements.
146+
*
147+
* @param elementToBeCompared - The element to check for differences.
148+
* @param elementToCompareAgainst - The element used to check against.
149+
*/
150+
export function renderDiff(
151+
elementToBeCompared: Element,
152+
elementToCompareAgainst: Element
153+
): TemplateResult | null {
154+
// Determine the ID from the current tag. These can be numbers or strings.
155+
let idTitle: string | undefined = identity(elementToBeCompared).toString();
156+
if (idTitle === 'NaN') {
157+
idTitle = undefined;
158+
}
159+
160+
// First get all differences in attributes and text for the current 2 elements.
161+
const attrDiffs: [string, Diff<string>][] = diffSclAttributes(
162+
elementToBeCompared,
163+
elementToCompareAgainst
164+
);
165+
// Next check which elements are added, deleted or in both elements.
166+
const childDiffs: Diff<Element>[] = diffSclChilds(
167+
elementToBeCompared,
168+
elementToCompareAgainst
169+
);
170+
171+
const childAddedOrDeleted: Diff<Element>[] = [];
172+
const childToCompare: Diff<Element>[] = [];
173+
childDiffs.forEach(diff => {
174+
if (!diff.oldValue || !diff.newValue) {
175+
childAddedOrDeleted.push(diff);
176+
} else {
177+
childToCompare.push(diff);
178+
}
179+
});
180+
181+
// These children exist in both old and new element, let's check if there are any difference in the children.
182+
const childToCompareTemplates = childToCompare
183+
.map(diff => renderDiff(diff.newValue!, diff.oldValue!))
184+
.filter(result => result !== null);
185+
186+
// If there are difference generate the HTML otherwise just return null.
187+
if (
188+
childToCompareTemplates.length > 0 ||
189+
attrDiffs.length > 0 ||
190+
childAddedOrDeleted.length > 0
191+
) {
192+
return html` ${attrDiffs.length > 0 || childAddedOrDeleted.length > 0
193+
? html` <mwc-list multi>
194+
${attrDiffs.length > 0
195+
? html` <mwc-list-item noninteractive ?twoline=${!!idTitle}>
196+
<span class="resultTitle">
197+
${translate('compare.attributes', {
198+
elementName: elementToBeCompared.tagName,
199+
})}
200+
</span>
201+
${idTitle
202+
? html`<span slot="secondary">${idTitle}</span>`
203+
: nothing}
204+
</mwc-list-item>
205+
<li padded divider role="separator"></li>`
206+
: ''}
207+
${repeat(
208+
attrDiffs,
209+
e => e,
210+
([name, diff]) =>
211+
html` <mwc-list-item twoline left hasMeta>
212+
<span>${name}</span>
213+
<span slot="secondary">
214+
${diff.oldValue ?? ''}
215+
${diff.oldValue && diff.newValue ? html`&curarr;` : ' '}
216+
${diff.newValue ?? ''}
217+
</span>
218+
<mwc-icon slot="meta">
219+
${diff.oldValue ? (diff.newValue ? 'edit' : 'delete') : 'add'}
220+
</mwc-icon>
221+
</mwc-list-item>`
222+
)}
223+
${childAddedOrDeleted.length > 0
224+
? html` <mwc-list-item noninteractive ?twoline=${!!idTitle}>
225+
<span class="resultTitle">
226+
${translate('compare.children', {
227+
elementName: elementToBeCompared.tagName,
228+
})}
229+
</span>
230+
${idTitle
231+
? html`<span slot="secondary">${idTitle}</span>`
232+
: nothing}
233+
</mwc-list-item>
234+
<li padded divider role="separator"></li>`
235+
: ''}
236+
${repeat(
237+
childAddedOrDeleted,
238+
e => e,
239+
diff =>
240+
html` <mwc-list-item twoline left hasMeta>
241+
<span>${diff.oldValue?.tagName ?? diff.newValue?.tagName}</span>
242+
<span slot="secondary">
243+
${diff.oldValue
244+
? describe(diff.oldValue)
245+
: describe(diff.newValue)}
246+
</span>
247+
<mwc-icon slot="meta">
248+
${diff.oldValue ? 'delete' : 'add'}
249+
</mwc-icon>
250+
</mwc-list-item>`
251+
)}
252+
</mwc-list>`
253+
: ''}
254+
${childToCompareTemplates}`;
255+
}
256+
return null;
257+
}

0 commit comments

Comments
 (0)