diff --git a/public/js/plugins.js b/public/js/plugins.js index 0c943acd4b..23347f5f47 100644 --- a/public/js/plugins.js +++ b/public/js/plugins.js @@ -144,6 +144,15 @@ export const officialPlugins = [ requireDoc: true, position: 'top', }, + { + name: 'Save as version', + src: '/src/menu/CompasSaveAsVersion.js', + icon: 'save', + default: true, + kind: 'menu', + requireDoc: true, + position: 'top', + }, { name: 'Validate using OCL', src: '/src/validators/CompasValidateSchema.js', diff --git a/src/compas/CompasOpen.ts b/src/compas/CompasOpen.ts index 4473bf4b62..67450a8521 100644 --- a/src/compas/CompasOpen.ts +++ b/src/compas/CompasOpen.ts @@ -21,22 +21,27 @@ import '../WizardDivider.js'; import './CompasSclTypeList.js'; import './CompasSclList.js'; +import { nothing } from 'lit-html'; +import { buildDocName } from './foundation.js'; + /* Event that will be used when an SCL Document is retrieved. */ export interface DocRetrievedDetail { localFile: boolean; doc: Document; docName?: string; + docId?: string; } export type DocRetrievedEvent = CustomEvent; export function newDocRetrievedEvent( localFile: boolean, doc: Document, - docName?: string + docName?: string, + docId?: string ): DocRetrievedEvent { return new CustomEvent('doc-retrieved', { bubbles: true, composed: true, - detail: { localFile, doc, docName }, + detail: { localFile, doc, docName, docId }, }); } @@ -44,16 +49,19 @@ export function newDocRetrievedEvent( export default class CompasOpenElement extends LitElement { @property() selectedType: string | undefined; + @property() + allowLocalFile = true; @query('#scl-file') private sclFileUI!: HTMLInputElement; - private async getSclDocument(id?: string): Promise { - const sclDocument = await CompasSclDataService() - .getSclDocument(this.selectedType ?? '', id ?? '') + private async getSclDocument(docId?: string): Promise { + const doc = await CompasSclDataService() + .getSclDocument(this.selectedType ?? '', docId ?? '') .catch(reason => createLogEvent(this, reason)); - if (sclDocument instanceof Document) { - this.dispatchEvent(newDocRetrievedEvent(false, sclDocument)); + if (doc instanceof Document) { + const docName = buildDocName(doc.documentElement); + this.dispatchEvent(newDocRetrievedEvent(false, doc, docName, docId)); } } @@ -66,7 +74,6 @@ export default class CompasOpenElement extends LitElement { const doc = new DOMParser().parseFromString(text, 'application/xml'); this.dispatchEvent(newDocRetrievedEvent(true, doc, docName)); - this.sclFileUI.onchange = null; } private renderFileSelect(): TemplateResult { @@ -84,10 +91,8 @@ export default class CompasOpenElement extends LitElement { { - const input = ( - this.shadowRoot!.querySelector('#scl-file') - ); - input?.click(); + this.sclFileUI.value = ''; + this.sclFileUI.click(); }} > @@ -129,11 +134,13 @@ export default class CompasOpenElement extends LitElement { render(): TemplateResult { return html` - -
-

${translate('compas.open.localTitle')}

- ${this.renderFileSelect()} -
+ ${this.allowLocalFile + ? html` +
+

${translate('compas.open.localTitle')}

+ ${this.renderFileSelect()} +
` + : nothing}

${translate('compas.open.compasTitle')}

diff --git a/src/compas/CompasSave.ts b/src/compas/CompasSave.ts index d223d49775..8700040bd2 100644 --- a/src/compas/CompasSave.ts +++ b/src/compas/CompasSave.ts @@ -42,6 +42,7 @@ import './CompasComment.js'; import './CompasLabelsField.js'; import './CompasLoading.js'; import './CompasSclTypeSelect.js'; +import { nothing } from 'lit-html'; /* Event that will be used when an SCL Document is saved. */ export type DocSavedEvent = CustomEvent; @@ -56,6 +57,8 @@ export function newDocSavedEvent(): DocSavedEvent { export default class CompasSaveElement extends CompasExistsIn(LitElement) { @property() doc!: XMLDocument; + @property() + allowLocalFile = true; @query('mwc-textfield#name') private nameField!: TextFieldBase; @@ -223,11 +226,13 @@ export default class CompasSaveElement extends CompasExistsIn(LitElement) { render(): TemplateResult { return html` - -
-

${translate('compas.save.localTitle')}

- ${this.renderSaveFilePart()} -
+ ${this.allowLocalFile + ? html` +
+

${translate('compas.save.localTitle')}

+ ${this.renderSaveFilePart()} +
` + : nothing}

${translate('compas.save.compasTitle')}

diff --git a/src/compas/foundation.ts b/src/compas/foundation.ts index 7c62434107..f1d2f35718 100644 --- a/src/compas/foundation.ts +++ b/src/compas/foundation.ts @@ -1,6 +1,12 @@ import { get } from 'lit-translate'; import { newLogEvent, newOpenDocEvent } from '../foundation.js'; +import { + COMPAS_SCL_PRIVATE_TYPE, + getCompasSclFileType, + getCompasSclName, + getPrivate, +} from './private.js'; const FILE_EXTENSION_LENGTH = 3; @@ -26,43 +32,37 @@ export function stripExtensionFromName(docName: string): string { return name; } +export function buildDocName(sclElement: Element): string { + const headerElement = sclElement.querySelector(':scope > Header'); + const privateElement = getPrivate(sclElement, COMPAS_SCL_PRIVATE_TYPE); + + const version = headerElement?.getAttribute('version') ?? ''; + const name = getCompasSclName(privateElement)?.textContent ?? ''; + const type = getCompasSclFileType(privateElement)?.textContent ?? 'SCD'; + + let docName = name; + if (docName === '') { + docName = headerElement?.getAttribute('id') ?? ''; + } + docName += '-' + version + '.' + type?.toLowerCase(); + return docName; +} + export function updateDocumentInOpenSCD( element: Element, doc: Document, docName?: string ): void { - const id = - (doc.querySelectorAll(':root > Header') ?? []) - .item(0) - ?.getAttribute('id') ?? ''; - - if (!docName) { - const version = - (doc.querySelectorAll(':root > Header') ?? []) - .item(0) - ?.getAttribute('version') ?? ''; - const name = - ( - doc.querySelectorAll(':root > Private[type="compas_scl"] > SclName') ?? - [] - ).item(0)?.textContent ?? ''; - const type = - ( - doc.querySelectorAll( - ':root > Private[type="compas_scl"] > SclFileType' - ) ?? [] - ).item(0)?.textContent ?? ''; - - docName = name; - if (docName === '') { - docName = id; - } - docName += '-' + version + '.' + type?.toLowerCase(); - } + const headerElement = doc.querySelector(':root > Header'); + const id = headerElement?.getAttribute('id') ?? ''; element.dispatchEvent(newLogEvent({ kind: 'reset' })); element.dispatchEvent( - newOpenDocEvent(doc, docName, { detail: { docId: id } }) + newOpenDocEvent( + doc, + docName ? docName : buildDocName(doc.documentElement), + { detail: { docId: id } } + ) ); } diff --git a/src/compas/private.ts b/src/compas/private.ts index 702d1fe69d..5ea4ac4129 100644 --- a/src/compas/private.ts +++ b/src/compas/private.ts @@ -22,7 +22,7 @@ export function createPrivate(parent: Element, type: string): Element { export function getCompasSclName( privateElement: Element | null ): Element | null { - return privateElement?.querySelector(`SclName`) ?? null; + return privateElement?.querySelector(`:scope > SclName`) ?? null; } export function createCompasSclName(parent: Element, value: string): Element { @@ -37,6 +37,44 @@ export function createCompasSclName(parent: Element, value: string): Element { return newSclNameElement; } +export function copyCompasSclName( + fromParent: Element | null, + toParent: Element | null +): void { + if (fromParent && toParent) { + const fromSclNameElement = getCompasSclName(fromParent); + const toSclNameElement = getCompasSclName(toParent); + + if (toSclNameElement && fromSclNameElement) { + toSclNameElement.textContent = fromSclNameElement.textContent; + } else if (toSclNameElement) { + toSclNameElement.textContent = ''; + } + } +} + +export function getCompasSclFileType( + privateElement: Element | null +): Element | null { + return privateElement?.querySelector(`:scope > SclFileType`) ?? null; +} + +export function copyCompasSclFileType( + fromParent: Element | null, + toParent: Element | null +): void { + if (fromParent && toParent) { + const fromSclFileTypeElement = getCompasSclFileType(fromParent); + const toSclFileTypeElement = getCompasSclFileType(toParent); + + if (toSclFileTypeElement && fromSclFileTypeElement) { + toSclFileTypeElement.textContent = fromSclFileTypeElement.textContent; + } else if (toSclFileTypeElement) { + toSclFileTypeElement.textContent = ''; + } + } +} + export function getLabels(privateElement: Element): Element | null { return ( Array.from(privateElement.querySelectorAll(`:scope > Labels`)).find( @@ -70,6 +108,25 @@ export function createLabel(labelsElement: Element, value: string): Element { return labelElement; } +export function copyCompasLabels( + fromParent: Element | null, + toParent: Element | null +): void { + if (fromParent && toParent) { + const fromLabels = getLabels(fromParent); + const toLabels = getLabels(toParent); + + if (toLabels) { + toParent.removeChild(toLabels); + } + if (fromLabels) { + toParent.appendChild( + toParent.ownerDocument.adoptNode(fromLabels.cloneNode(true)) + ); + } + } +} + export function addPrefixAndNamespaceToDocument( element: Element, namespace: string, diff --git a/src/menu/CompasSave.ts b/src/menu/CompasSave.ts index bfa892787a..c9a59fa7d7 100644 --- a/src/menu/CompasSave.ts +++ b/src/menu/CompasSave.ts @@ -40,7 +40,7 @@ export default class CompasSaveMenuPlugin extends LitElement { render(): TemplateResult { return html` ${!this.doc || !this.docName ? html`` diff --git a/src/menu/CompasSaveAs.ts b/src/menu/CompasSaveAs.ts index e065c2f824..f5555e5b54 100644 --- a/src/menu/CompasSaveAs.ts +++ b/src/menu/CompasSaveAs.ts @@ -38,7 +38,7 @@ export default class CompasSaveAsMenuPlugin extends LitElement { render(): TemplateResult { return html` ${!this.doc || !this.docName ? html`` diff --git a/src/menu/CompasSaveAsVersion.ts b/src/menu/CompasSaveAsVersion.ts new file mode 100644 index 0000000000..7e656d5b16 --- /dev/null +++ b/src/menu/CompasSaveAsVersion.ts @@ -0,0 +1,147 @@ +import { + css, + html, + LitElement, + property, + query, + state, + TemplateResult, +} from 'lit-element'; +import { translate } from 'lit-translate'; + +import '@material/mwc-button'; +import '@material/mwc-dialog'; +import { Dialog } from '@material/mwc-dialog'; + +import { newPendingStateEvent } from '../foundation.js'; + +import CompasSaveElement from '../compas/CompasSave.js'; +import { DocRetrievedEvent } from '../compas/CompasOpen.js'; + +import '../compas/CompasOpen.js'; +import '../compas/CompasSave.js'; +import { + COMPAS_SCL_PRIVATE_TYPE, + copyCompasLabels, + copyCompasSclFileType, + copyCompasSclName, + getPrivate, +} from '../compas/private.js'; + +export default class CompasSaveAsVersionMenuPlugin extends LitElement { + @property() + doc!: XMLDocument; + @property() + docName!: string; + + @state() + private saveToDoc?: XMLDocument; + @state() + private saveToDocName?: string; + @state() + private saveToDocId?: string; + + @query('mwc-dialog#compas-save-as-version-dlg') + dialog!: Dialog; + + @query('compas-save') + compasSaveElement?: CompasSaveElement; + + @query('compas-open') + compasOpenElement?: CompasSaveElement; + + async run(): Promise { + this.saveToDoc = undefined; + this.saveToDocName = undefined; + this.saveToDocId = undefined; + + if (this.compasSaveElement) { + await this.compasSaveElement.requestUpdate(); + } + if (this.compasOpenElement) { + await this.compasOpenElement.requestUpdate(); + } + this.dialog.show(); + } + + /** + * To prevent problem with double SCL Filenames, we will retrieve the CoMPAS Private for SCL Element from the + * selected document, to which the current document will be added as new version, and copy the CoMPAS SCL Private + * Elements to the current document. + */ + private copyCompasPrivates(): void { + if (this.saveToDoc) { + const toPrivateElement = getPrivate( + this.doc.documentElement, + COMPAS_SCL_PRIVATE_TYPE + ); + const fromPrivateElement = getPrivate( + this.saveToDoc.documentElement, + COMPAS_SCL_PRIVATE_TYPE + ); + + copyCompasSclName(fromPrivateElement, toPrivateElement); + copyCompasSclFileType(fromPrivateElement, toPrivateElement); + copyCompasLabels(fromPrivateElement, toPrivateElement); + } + } + + render(): TemplateResult { + return html` + ${!this.doc || !this.docName + ? html` ` + : !this.saveToDoc || !this.saveToDocId + ? html` + { + this.saveToDoc = event.detail.doc; + this.saveToDocName = event.detail.docName; + this.saveToDocId = event.detail.docId; + this.copyCompasPrivates(); + }} + > + ` + : html` { + this.dialog.close(); + }} + > + { + if (this.compasSaveElement && this.compasSaveElement.valid()) { + this.dispatchEvent( + newPendingStateEvent(this.compasSaveElement.saveToCompas()) + ); + } + }} + >`} + + + `; + } + + static styles = css` + mwc-dialog { + --mdc-dialog-min-width: 23vw; + --mdc-dialog-max-width: 92vw; + } + `; +} diff --git a/src/translations/de.ts b/src/translations/de.ts index 8e969be808..e7098e798c 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -844,7 +844,9 @@ export const de: Translations = { otherTypeButton: '???', }, save: { - title: '???', + saveTitle: '???', + saveAsTitle: '???', + saveAsVersionTitle: '???', localTitle: '???', saveFileButton: '???', compasTitle: 'CoMPAS', diff --git a/src/translations/en.ts b/src/translations/en.ts index bb1afc59fc..4f784c502a 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -838,7 +838,9 @@ export const en = { otherTypeButton: 'Other type...', }, save: { - title: 'Save project', + saveTitle: 'Save project', + saveAsTitle: 'Save as new project', + saveAsVersionTitle: 'Save as new version to existing project', localTitle: 'Local', saveFileButton: 'Save to file...', compasTitle: 'CoMPAS', diff --git a/test/integration/__snapshots__/open-scd.test.snap.js b/test/integration/__snapshots__/open-scd.test.snap.js index 477f76034e..b70ac5a4b1 100644 --- a/test/integration/__snapshots__/open-scd.test.snap.js +++ b/test/integration/__snapshots__/open-scd.test.snap.js @@ -109,6 +109,24 @@ snapshots["open-scd looks like its snapshot"] = + + + save + + + Save as version + + + +
  • Save project as + + + save + + Save as version +
  • -
    +
    diff --git a/test/testfiles/compas/compas-scl-private-missing-private.scd b/test/testfiles/compas/compas-scl-private-missing-private.scd index 457459d9cf..86d99ebb0f 100644 --- a/test/testfiles/compas/compas-scl-private-missing-private.scd +++ b/test/testfiles/compas/compas-scl-private-missing-private.scd @@ -1,4 +1,4 @@ -
    +
    diff --git a/test/testfiles/compas/compas-scl-private-update-existing.scd b/test/testfiles/compas/compas-scl-private-update-existing.scd index 88ae923819..438998f144 100644 --- a/test/testfiles/compas/compas-scl-private-update-existing.scd +++ b/test/testfiles/compas/compas-scl-private-update-existing.scd @@ -7,5 +7,5 @@ Label1 -
    +
    diff --git a/test/testfiles/compas/save-compas-as-version.scd b/test/testfiles/compas/save-compas-as-version.scd new file mode 100644 index 0000000000..5654853f7b --- /dev/null +++ b/test/testfiles/compas/save-compas-as-version.scd @@ -0,0 +1,11 @@ + + + + AmsterdamCS + SCD + + Amsterdam + + +
    + diff --git a/test/testfiles/compas/save-compas.scd b/test/testfiles/compas/save-compas.scd index a9d0a266c1..d4b99fc9c6 100644 --- a/test/testfiles/compas/save-compas.scd +++ b/test/testfiles/compas/save-compas.scd @@ -1,228 +1,11 @@ - ied_utrecht_station235 + UtrechtCS CID + + Utrecht +
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - IEC 61850-7-3:2007B - - - - - - - - - - - - - - - - - - - - - - sbo-with-enhanced-security - - - 30000 - - - 600 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sbo-with-enhanced-security - - - 30000 - - - 600 - - - - - - - - - - IEC 61850-8-1:2003 - - - - - IEC 61850-8-1:2003 - - - - - IEC 61850-8-1:2003 - - - - - - - - - - - - - IEC 61850-8-1:2003 - - - - - - - - IEC 61850-8-1:2003 - - - - - - - - - Completed - Cancelled - New adjustments - AnotherValue - - - Va - Vb - Vc - Aa - Ab - Ac - Vab - Vbc - Vca - AnotherValue - - - not-supported - bay-control - station-control - remote-control - automatic-bay - automatic-station - automatic-remote - maintenance - process - - - on - blocked - test - test/blocked - off - - - status-only - direct-with-normal-security - sbo-with-normal-security - direct-with-enhanced-security - sbo-with-enhanced-security - - - Ok - Warning - Alarm - - - pulse - persistent - persistent-feedback - - diff --git a/test/unit/compas/CompasOpen.test.ts b/test/unit/compas/CompasOpen.test.ts index 08f81712de..7f7e5184b2 100644 --- a/test/unit/compas/CompasOpen.test.ts +++ b/test/unit/compas/CompasOpen.test.ts @@ -1,14 +1,14 @@ -import {expect, fixtureSync, html} from '@open-wc/testing'; +import { expect, fixtureSync, html } from '@open-wc/testing'; -import CompasOpenElement from "../../../src/compas/CompasOpen.js"; +import CompasOpenElement from '../../../src/compas/CompasOpen.js'; -import "../../../src/compas/CompasOpen.js"; -import {Button} from "@material/mwc-button"; +import '../../../src/compas/CompasOpen.js'; +import { Button } from '@material/mwc-button'; describe('compas-open', () => { let element: CompasOpenElement; - describe('when-type-needs-to-be-selected', () => { + describe('When type needs to be selected', () => { beforeEach(async () => { element = fixtureSync(html``); await element; @@ -19,7 +19,20 @@ describe('compas-open', () => { }); }); - describe('when-project-needs-to-be-selected', () => { + describe('When no local file can be selected', () => { + beforeEach(async () => { + element = fixtureSync( + html`` + ); + await element; + }); + + it('looks like the latest snapshot', async () => { + await expect(element).shadowDom.to.equalSnapshot(); + }); + }); + + describe('When project needs to be selected', () => { beforeEach(async () => { element = fixtureSync(html``); element.selectedType = 'SCD'; @@ -27,7 +40,9 @@ describe('compas-open', () => { }); it('when other type selected then selectedType set to undefined', async () => { - const button =