diff --git a/public/js/plugins.js b/public/js/plugins.js index af26d610f4..ec9a9aa6a2 100644 --- a/public/js/plugins.js +++ b/public/js/plugins.js @@ -213,9 +213,9 @@ export const officialPlugins = [ }, { name: 'Compare IED', - src: '/src/menu/CompareIED.js', + src: '/src/menu/CompasCompareIED.js', icon: 'compare_arrows', - default: false, + default: true, kind: 'menu', requireDoc: true, position: 'middle', diff --git a/src/compas-editors/CompasVersions.ts b/src/compas-editors/CompasVersions.ts index c163885aa8..82363c937e 100644 --- a/src/compas-editors/CompasVersions.ts +++ b/src/compas-editors/CompasVersions.ts @@ -15,13 +15,15 @@ import '@material/mwc-list'; import '@material/mwc-list/mwc-list-item'; import '@material/mwc-list/mwc-check-list-item'; +import { MultiSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; + import { newLogEvent, newOpenDocEvent, newWizardEvent, Wizard, } from '../foundation.js'; -import { MultiSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; +import { renderDiff } from '../foundation/compare.js'; import { CompasSclDataService, @@ -29,14 +31,49 @@ import { } from '../compas-services/CompasSclDataService.js'; import { createLogEvent } from '../compas-services/foundation.js'; import { + compareVersions, getTypeFromDocName, updateDocumentInOpenSCD, } from '../compas/foundation.js'; import { addVersionToCompasWizard } from '../compas/CompasUploadVersion.js'; -import { compareWizard } from '../compas/CompasCompareDialog.js'; import { getElementByName, styles } from './foundation.js'; import { wizards } from '../wizards/wizard-library.js'; +interface CompareOptions { + title: string; +} + +function compareWizard( + plugin: Element, + oldElement: Element, + newElement: Element, + options: CompareOptions +): Wizard { + function renderDialogContent(): TemplateResult { + return html` ${renderDiff(newElement, oldElement) ?? + html`${translate('compas.compare.noDiff')}`}`; + } + + function close() { + return function () { + plugin.dispatchEvent(newWizardEvent()); + return []; + }; + } + + return [ + { + title: options.title, + secondary: { + icon: '', + label: get('close'), + action: close(), + }, + content: [renderDialogContent()], + }, + ]; +} + /** An editor [[`plugin`]] for selecting the `Substation` section. */ export default class CompasVersionsPlugin extends LitElement { @property() @@ -244,15 +281,15 @@ export default class CompasVersionsPlugin extends LitElement { const selectedVersions = this.getSelectedVersions(); if (selectedVersions.length === 1) { const oldVersion = selectedVersions[0]; + const oldScl = await this.getVersion(oldVersion); const newScl = this.doc.documentElement; this.dispatchEvent( newWizardEvent( compareWizard(this, oldScl, newScl, { - title: get('compas.compare.title', { + title: get('compas.compare.titleCurrent', { oldVersion: oldVersion, - newVersion: 'current', }), }) ) @@ -274,8 +311,9 @@ export default class CompasVersionsPlugin extends LitElement { async compareVersions(): Promise { const selectedVersions = this.getSelectedVersions(); if (selectedVersions.length === 2) { - const oldVersion = selectedVersions[0]; - const newVersion = selectedVersions[1]; + const sortedVersions = selectedVersions.slice().sort(compareVersions); + const oldVersion = sortedVersions[0]; + const newVersion = sortedVersions[1]; const oldScl = await this.getVersion(oldVersion); const newScl = await this.getVersion(newVersion); diff --git a/src/compas/CompasCompareDialog.ts b/src/compas/CompasCompareDialog.ts deleted file mode 100644 index 5fdaf26642..0000000000 --- a/src/compas/CompasCompareDialog.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { html, TemplateResult } from 'lit-element'; -import { repeat } from 'lit-html/directives/repeat'; -import { get, translate } from 'lit-translate'; - -import '@material/mwc-list'; -import '@material/mwc-list/mwc-list-item'; -import '@material/mwc-icon'; - -import { identity, isSame, newWizardEvent, Wizard } from '../foundation.js'; - -interface CompareOptions { - title: string; -} - -export type Diff = - | { oldValue: T; newValue: null } - | { oldValue: null; newValue: T } - | { oldValue: T; newValue: T }; - -function describe(element: Element): string { - const id = identity(element); - return typeof id === 'string' ? id : get('unidentifiable'); -} - -export function diffSclAttributes( - oldElement: Element, - newElement: Element -): [string, Diff][] { - const attrDiffs: [string, Diff][] = []; - - // First check if there is any text inside the element and there should be no child elements. - const oldText = oldElement.textContent ?? ''; - const newText = newElement.textContent ?? ''; - if ( - oldElement.childElementCount === 0 && - newElement.childElementCount === 0 && - newText !== oldText - ) { - attrDiffs.push(['value', { oldValue: oldText, newValue: newText }]); - } - - // Next check if there are any difference between attributes. - const attributeNames = new Set( - newElement.getAttributeNames().concat(oldElement.getAttributeNames()) - ); - for (const name of attributeNames) { - if (newElement.getAttribute(name) !== oldElement.getAttribute(name)) { - attrDiffs.push([ - name, - >{ - newValue: newElement.getAttribute(name), - oldValue: oldElement.getAttribute(name), - }, - ]); - } - } - return attrDiffs; -} - -export function diffSclChilds( - oldElement: Element, - newElement: Element -): Diff[] { - const childDiffs: Diff[] = []; - const oldChildren = Array.from(oldElement.children); - const newChildren = Array.from(newElement.children); - - newChildren.forEach(newValue => { - if (!newValue.closest('Private')) { - const twinIndex = oldChildren.findIndex(ourChild => - isSame(newValue, ourChild) - ); - const oldValue = twinIndex > -1 ? oldChildren[twinIndex] : null; - - if (oldValue) { - oldChildren.splice(twinIndex, 1); - childDiffs.push({ newValue, oldValue }); - } else { - childDiffs.push({ newValue: newValue, oldValue: null }); - } - } - }); - oldChildren.forEach(oldValue => { - if (!oldValue.closest('Private')) { - childDiffs.push({ newValue: null, oldValue }); - } - }); - return childDiffs; -} - -export function renderDiff( - oldElement: Element, - newElement: Element -): TemplateResult { - // Determine the ID from the current tag. These can be numbers or strings. - let idTitle = identity(oldElement).toString(); - if (idTitle && idTitle !== '' && idTitle !== 'NaN') { - idTitle = '(' + idTitle + ')'; - } else { - idTitle = ''; - } - - // First get all differences in attributes and text for the current 2 elements. - const attrDiffs: [string, Diff][] = diffSclAttributes( - oldElement, - newElement - ); - // Next check which elements are added, deleted or in both elements. - const childDiffs: Diff[] = diffSclChilds(oldElement, newElement); - - const childAddedOrDeleted: Diff[] = []; - const childToCompare: Diff[] = []; - childDiffs.forEach(diff => { - if (!diff.oldValue || !diff.newValue) { - childAddedOrDeleted.push(diff); - } else { - childToCompare.push(diff); - } - }); - - return html` ${attrDiffs.length || childAddedOrDeleted.length - ? html` - ${attrDiffs.length - ? html` - ${translate('compas.compare.attributes')} ${oldElement.tagName} - ${idTitle} - -
  • ` - : ''} - ${repeat( - attrDiffs, - e => e, - ([name, diff]) => - html` - ${name} - - ${diff.oldValue ?? ''} - ${diff.oldValue && diff.newValue ? html`↶` : ' '} - ${diff.newValue ?? ''} - - - ${diff.oldValue ? (diff.newValue ? 'edit' : 'delete') : 'add'} - - ` - )} - ${childAddedOrDeleted.length - ? html`${translate('compas.compare.children')} ${oldElement.tagName} - ${idTitle} -
  • ` - : ''} - ${repeat( - childAddedOrDeleted, - e => e, - diff => - html` - ${diff.oldValue?.tagName ?? diff.newValue?.tagName} - - ${diff.oldValue - ? describe(diff.oldValue) - : describe(diff.newValue)} - - - ${diff.oldValue ? 'delete' : 'add'} - - ` - )} -
    ` - : ''} - ${repeat( - childToCompare, - e => e, - diff => { - return html`${renderDiff(diff.oldValue!, diff.newValue!)}`; - } - )}`; -} - -export function compareWizard( - plugin: Element, - oldElement: Element, - newElement: Element, - options: CompareOptions -): Wizard { - function close() { - return function () { - plugin.dispatchEvent(newWizardEvent()); - return []; - }; - } - - return [ - { - title: options.title, - secondary: { - icon: '', - label: get('close'), - action: close(), - }, - content: [html`${renderDiff(oldElement, newElement)}`], - }, - ]; -} diff --git a/src/compas/foundation.ts b/src/compas/foundation.ts index e82a3ef63d..7c62434107 100644 --- a/src/compas/foundation.ts +++ b/src/compas/foundation.ts @@ -65,3 +65,44 @@ export function updateDocumentInOpenSCD( newOpenDocEvent(doc, docName, { detail: { docId: id } }) ); } + +export function compareVersions( + leftVersion: string, + rightVersion: string +): number { + // Function to compare parts of the version. + function comparePart(leftPart: string, rightPart: string): number { + // First make convert them to number and check if the strings are numbers. + const leftNumber = parseInt(leftPart); + const rightNumber = parseInt(rightPart); + if (isNaN(leftNumber) || isNaN(rightNumber)) { + return 0; + } + // Now compare the two numbers. + return leftNumber < rightNumber ? -1 : leftNumber > rightNumber ? 1 : 0; + } + + // If the strings are the same, just return 0, because they are the same. + if (leftVersion.localeCompare(rightVersion) == 0) { + return 0; + } + + // Split the version into parts. + const leftParts = leftVersion.split('.'); + const rightParts = rightVersion.split('.'); + + // Version should exist out of 3 parts, major, minor, patch + if (leftParts.length != 3 && rightParts.length != 3) { + return 0; + } + + // Now first compare the major version, if they are the same repeat for minor version and patch version. + let result = comparePart(leftParts[0], rightParts[0]); + if (result === 0) { + result = comparePart(leftParts[1], rightParts[1]); + if (result === 0) { + result = comparePart(leftParts[2], rightParts[2]); + } + } + return result; +} diff --git a/src/menu/CompasCompareIED.ts b/src/menu/CompasCompareIED.ts new file mode 100644 index 0000000000..8c986e58e1 --- /dev/null +++ b/src/menu/CompasCompareIED.ts @@ -0,0 +1,24 @@ +import { html, TemplateResult } from 'lit-element'; + +import '../compas/CompasOpen.js'; + +import { DocRetrievedEvent } from '../compas/CompasOpen.js'; + +import CompareIEDPlugin from './CompareIED.js'; + +export default class CompasCompareIEDPlugin extends CompareIEDPlugin { + /** + * Overwriting the render function for opening the template project. + * Now it will also be possible to select the template project from the CoMPAS Data Service. + * + * @override + */ + protected renderSelectTemplateFile(): TemplateResult { + return html` { + this.templateDoc = evt.detail.doc; + }} + > + ${this.renderCloseButton()}`; + } +} diff --git a/src/translations/de.ts b/src/translations/de.ts index ae36a5ac54..5e72a4a4d5 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -854,6 +854,8 @@ export const de: Translations = { }, compare: { title: '???', + titleCurrent: '???', + noDiff: '???', attributes: 'Attribute', children: 'Kindelemente', }, diff --git a/src/translations/en.ts b/src/translations/en.ts index daece69084..862d48b380 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -850,7 +850,9 @@ export const en = { filenameHelper: 'Filename used by CoMPAS when saving to a filesystem', }, compare: { - title: 'Compare version {{oldVersion}} with version {{newVersion}}', + title: 'Compare version {{newVersion}} against version {{oldVersion}}', + titleCurrent: 'Compare current project against version {{oldVersion}}', + noDiff: 'No difference between versions', attributes: 'Attributes from', children: 'Child elements from', }, diff --git a/test/integration/__snapshots__/open-scd.test.snap.js b/test/integration/__snapshots__/open-scd.test.snap.js index b277167077..158770bbd7 100644 --- a/test/integration/__snapshots__/open-scd.test.snap.js +++ b/test/integration/__snapshots__/open-scd.test.snap.js @@ -305,6 +305,24 @@ snapshots["open-scd looks like its snapshot"] = + + + compare_arrows + + + Compare IED + + + + compare_arrows diff --git a/test/unit/compas/CompasCompareDialog.test.ts b/test/unit/compas/CompasCompareDialog.test.ts deleted file mode 100644 index c3197f62da..0000000000 --- a/test/unit/compas/CompasCompareDialog.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {expect, fixtureSync, html} from '@open-wc/testing'; - -import {diffSclAttributes, diffSclChilds, renderDiff} from "../../../src/compas/CompasCompareDialog.js"; - -describe('compas-compare-dialog', () => { - let oldSclElement: Element; - let newSclElement: Element; - - beforeEach(async () => { - oldSclElement = await fetch('/test/testfiles/compas/minigrid-3.0.0.cid') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')) - .then(document => document.documentElement); - newSclElement = await fetch('/test/testfiles/compas/minigrid-3.1.0.cid') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')) - .then(document => document.documentElement); - }); - - describe('diff SCL childs', () => { - it('get root child diffs', () => { - const diffChilds = diffSclChilds(oldSclElement, newSclElement); - expect(diffChilds).to.have.length(5); - }); - - it('get voltage level child diffs with removed bay', () => { - const oldVoltageLevel = oldSclElement.querySelector('VoltageLevel[name="S4 110kV"]') - const newVoltageLevel = newSclElement.querySelector('VoltageLevel[name="S4 110kV"]') - - const diffChilds = diffSclChilds(oldVoltageLevel!, newVoltageLevel!); - expect(diffChilds).to.have.length(9); - - const removedBay = diffChilds.filter(diff => diff.newValue == null) - expect(removedBay).to.have.length(1); - expect(removedBay[0].oldValue?.tagName).to.be.equal('Bay') - }); - }); - - describe('diff SCL attributes', () => { - it('get root attributes diffs', () => { - const diffAttributes = diffSclAttributes(oldSclElement, newSclElement); - expect(diffAttributes).to.have.length(0); - }); - - it('get header attributes diffs with different version', () => { - const oldHeader = oldSclElement.querySelector('Header') - const newHeader = newSclElement.querySelector('Header') - - const diffAttributes = diffSclAttributes(oldHeader!, newHeader!); - expect(diffAttributes).to.have.length(1); - expect(diffAttributes[0][0]).to.be.equal('version'); - expect(diffAttributes[0][1].oldValue).to.be.equal('3.0.0'); - expect(diffAttributes[0][1].newValue).to.be.equal('3.1.0'); - }); - }); - - describe('rendering full compare dialog', () => { - let element: Element; - - beforeEach(async () => { - element = fixtureSync(html`
    ${renderDiff(oldSclElement, newSclElement)}
    `); - await element; - }); - - it('looks like the latest snapshot', async () => { - await expect(element).to.equalSnapshot(); - }); - }); -}); diff --git a/test/unit/compas/foundation.test.ts b/test/unit/compas/foundation.test.ts index 6328c55a73..b0c5867c41 100644 --- a/test/unit/compas/foundation.test.ts +++ b/test/unit/compas/foundation.test.ts @@ -1,6 +1,7 @@ import { expect } from '@open-wc/testing'; import { + compareVersions, getTypeFromDocName, stripExtensionFromName, } from '../../../src/compas/foundation.js'; @@ -38,4 +39,41 @@ describe('compas-foundation', () => { expect(stripExtensionFromName(name)).to.be.equal(name); }); }); + + describe('compareVersions', () => { + it('when comparing non version strings, nothing is changed', () => { + expect(compareVersions('bbb', 'aaa')).to.be.equal(0); + expect(compareVersions('aaa', 'bbb')).to.be.equal(0); + expect(compareVersions('a.a.a', 'b.b.b')).to.be.equal(0); + }); + + it('when comparing same versions then 0 returned', () => { + expect(compareVersions('1.2.3', '1.2.3')).to.be.equal(0); + expect(compareVersions('10.2.3', '10.2.3')).to.be.equal(0); + }); + + it('when comparing two versions with different major digits then the major versions are leading', () => { + expect(compareVersions('1.3.0', '2.0.0')).to.be.equal(-1); + expect(compareVersions('1.3.0', '10.0.0')).to.be.equal(-1); + + expect(compareVersions('2.0.0', '1.3.0')).to.be.equal(1); + expect(compareVersions('10.0.0', '1.3.0')).to.be.equal(1); + }); + + it('when comparing two versions with different minor digits then the minor versions are leading', () => { + expect(compareVersions('1.3.0', '1.4.0')).to.be.equal(-1); + expect(compareVersions('1.3.0', '1.10.0')).to.be.equal(-1); + + expect(compareVersions('1.4.0', '1.3.0')).to.be.equal(1); + expect(compareVersions('1.10.0', '1.3.0')).to.be.equal(1); + }); + + it('when comparing two versions with different patch digits then the patch versions are leading', () => { + expect(compareVersions('1.1.3', '1.1.4')).to.be.equal(-1); + expect(compareVersions('1.1.3', '1.1.10')).to.be.equal(-1); + + expect(compareVersions('1.1.4', '1.1.3')).to.be.equal(1); + expect(compareVersions('1.1.10', '1.1.3')).to.be.equal(1); + }); + }); }); diff --git a/test/unit/menu/CompasCompareIED.test.ts b/test/unit/menu/CompasCompareIED.test.ts new file mode 100644 index 0000000000..eacf2bdaad --- /dev/null +++ b/test/unit/menu/CompasCompareIED.test.ts @@ -0,0 +1,43 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import CompasCompareIEDPlugin from '../../../src/menu/CompasCompareIED.js'; + +describe('Compas Compare IED Plugin', () => { + if (customElements.get('compas-compare-ied') === undefined) + customElements.define('compas-compare-ied', CompasCompareIEDPlugin); + + let plugin: CompasCompareIEDPlugin; + let doc: XMLDocument; + + beforeEach(async () => { + plugin = await fixture(html``); + doc = await fetch('/test/testfiles/menu/compare-ied-changed.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + }); + + describe('show template project selection dialog', () => { + beforeEach(async () => { + plugin.doc = doc; + plugin.run(); + await plugin.requestUpdate(); + }); + + it('after closing the dialog everything stays undefined', async () => { + expect(plugin.templateDoc).to.be.undefined; + expect(plugin.selectedProjectIed).to.be.undefined; + expect(plugin.selectedTemplateIed).to.be.undefined; + + plugin['onClosed'](); + await plugin.requestUpdate(); + + expect(plugin.templateDoc).to.be.undefined; + expect(plugin.selectedProjectIed).to.be.undefined; + expect(plugin.selectedTemplateIed).to.be.undefined; + }); + + it('looks like its latest snapshot', async () => { + await expect(plugin.dialog).to.equalSnapshot(); + }); + }); +}); diff --git a/test/unit/menu/__snapshots__/CompasCompareIED.test.snap.js b/test/unit/menu/__snapshots__/CompasCompareIED.test.snap.js new file mode 100644 index 0000000000..fa8af920c9 --- /dev/null +++ b/test/unit/menu/__snapshots__/CompasCompareIED.test.snap.js @@ -0,0 +1,18 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Compas Compare IED Plugin show template project selection dialog looks like its latest snapshot"] = +` + + + + + +`; +/* end snapshot Compas Compare IED Plugin show template project selection dialog looks like its latest snapshot */ +