From b967902748e93d57519a71b362b3c9771e7aaaeb Mon Sep 17 00:00:00 2001 From: Pascal Wilbrink Date: Mon, 3 Jun 2024 11:01:58 +0200 Subject: [PATCH] Chore: Added oscd-edit-completed Event (#1533) * Chore: Added oscd-edit-completed Event * Added import to initiator * Added import to initiator * Updated event to EditCompletedEvent * Changed DocChangedEvent * Fixed tests * Always emit the editCompletedEvent from the Editor * Fixed tests * Removed @open-wc from core eslint * Removed unused files * Fixed codacy issues * Fixed codacy issue * Fixed review comment * Added import --- packages/core/foundation.ts | 7 + packages/core/foundation/deprecated/editor.ts | 14 +- .../core/foundation/edit-completed-event.ts | 31 + packages/core/foundation/edit-event.ts | 21 +- packages/core/mixins/Editing.ts | 2 +- packages/core/package.json | 1 - packages/openscd/src/addons/Editor.ts | 16 +- packages/openscd/src/addons/History.ts | 943 +++++++++--------- packages/openscd/src/addons/Layout.ts | 239 +++-- packages/openscd/src/open-scd.ts | 52 +- packages/openscd/test/mock-editor-logger.ts | 37 +- packages/openscd/test/unit/Historing.test.ts | 31 +- .../GooseSubscriberLaterBinding.test.ts | 3 + .../GooseSubscriberMessageBinding.test.ts | 4 + .../SMVSubscriberMessageBinding.test.ts | 5 +- 15 files changed, 745 insertions(+), 661 deletions(-) create mode 100644 packages/core/foundation/edit-completed-event.ts diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index d15341aaf..466136ed8 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -28,3 +28,10 @@ export { cyrb64 } from './foundation/cyrb64.js'; export { Editing } from './mixins/Editing.js'; export type { Plugin, PluginSet } from './foundation/plugin.js'; + +export { newEditCompletedEvent } from './foundation/edit-completed-event.js'; + +export type { + EditCompletedEvent, + EditCompletedDetail, +} from './foundation/edit-completed-event.js'; diff --git a/packages/core/foundation/deprecated/editor.ts b/packages/core/foundation/deprecated/editor.ts index 0aa9a5a0c..5aa7350ba 100644 --- a/packages/core/foundation/deprecated/editor.ts +++ b/packages/core/foundation/deprecated/editor.ts @@ -1,3 +1,5 @@ +import { Initiator } from '../edit-event.js'; + /** Inserts `new.element` to `new.parent` before `new.reference`. */ export interface Create { new: { parent: Node; element: Node; reference?: Node | null }; @@ -148,6 +150,7 @@ export function invert(action: EditorAction): EditorAction { /** Represents some intended modification of a `Document` being edited. */ export interface EditorActionDetail { action: T; + initiator?: Initiator; } export type EditorActionEvent = CustomEvent< EditorActionDetail @@ -155,20 +158,19 @@ export type EditorActionEvent = CustomEvent< export function newActionEvent( action: T, + initiator: Initiator = 'user', eventInitDict?: CustomEventInit>> ): EditorActionEvent { return new CustomEvent>('editor-action', { bubbles: true, composed: true, ...eventInitDict, - detail: { action, ...eventInitDict?.detail }, + detail: { action, initiator, ...eventInitDict?.detail }, }); } - declare global { - interface ElementEventMap { - ['editor-action']: EditorActionEvent; - } + interface ElementEventMap { + ['editor-action']: EditorActionEvent; } - \ No newline at end of file +} diff --git a/packages/core/foundation/edit-completed-event.ts b/packages/core/foundation/edit-completed-event.ts new file mode 100644 index 000000000..b2128ded9 --- /dev/null +++ b/packages/core/foundation/edit-completed-event.ts @@ -0,0 +1,31 @@ +import { Edit, Initiator } from './edit-event.js'; + +import { EditorAction } from './deprecated/editor.js'; + +export type EditCompletedDetail = { + edit: Edit | EditorAction; + initiator: Initiator; +}; + +/** Represents the intent to open `doc` with filename `docName`. */ +export type EditCompletedEvent = CustomEvent; + +export function newEditCompletedEvent( + edit: Edit | EditorAction, + initiator: Initiator = 'user' +): EditCompletedEvent { + return new CustomEvent('oscd-edit-completed', { + bubbles: true, + composed: true, + detail: { + edit: edit, + initiator: initiator, + }, + }); +} + +declare global { + interface ElementEventMap { + ['oscd-edit-completed']: EditCompletedEvent; + } +} diff --git a/packages/core/foundation/edit-event.ts b/packages/core/foundation/edit-event.ts index e8685f06e..1cd664993 100644 --- a/packages/core/foundation/edit-event.ts +++ b/packages/core/foundation/edit-event.ts @@ -1,3 +1,5 @@ +export type Initiator = 'user' | 'system' | 'undo' | 'redo' | string; + /** Intent to `parent.insertBefore(node, reference)` */ export type Insert = { parent: Node; @@ -48,13 +50,24 @@ export function isRemove(edit: Edit): edit is Remove { ); } -export type EditEvent = CustomEvent; +export interface EditEventDetail { + edit: E; + initiator: Initiator; +} + +export type EditEvent = CustomEvent; -export function newEditEvent(edit: E): EditEvent { - return new CustomEvent('oscd-edit', { +export function newEditEvent( + edit: E, + initiator: Initiator = 'user' +): EditEvent { + return new CustomEvent('oscd-edit', { composed: true, bubbles: true, - detail: edit, + detail: { + edit: edit, + initiator: initiator, + }, }); } diff --git a/packages/core/mixins/Editing.ts b/packages/core/mixins/Editing.ts index a14850aef..c42845491 100644 --- a/packages/core/mixins/Editing.ts +++ b/packages/core/mixins/Editing.ts @@ -181,7 +181,7 @@ export function Editing( } handleEditEvent(event: EditEvent) { - const edit = event.detail; + const edit = event.detail.edit; this.history.splice(this.editCount); this.history.push({ undo: handleEdit(edit), redo: edit }); this.editCount += 1; diff --git a/packages/core/package.json b/packages/core/package.json index 2beef030c..7d358242c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -78,7 +78,6 @@ ] }, "extends": [ - "@open-wc", "prettier" ], "plugins": [ diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 3fc5e1171..c1ab8901a 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -1,4 +1,4 @@ -import { OpenEvent } from '@openscd/core'; +import { OpenEvent, newEditCompletedEvent } from '@openscd/core'; import { property, LitElement, @@ -16,16 +16,13 @@ import { EditorActionEvent, SimpleAction, Replace, - Update + Update, } from '@openscd/core/foundation/deprecated/editor.js'; import { newLogEvent } from '@openscd/core/foundation/deprecated/history.js'; -import { newValidateEvent } from '@openscd/core/foundation/deprecated/validation.js' -import {OpenDocEvent} from '@openscd/core/foundation/deprecated/open-event.js'; -import { - getReference, - SCLTag, -} from '../foundation.js'; +import { newValidateEvent } from '@openscd/core/foundation/deprecated/validation.js'; +import { OpenDocEvent } from '@openscd/core/foundation/deprecated/open-event.js'; +import { getReference, SCLTag } from '../foundation.js'; import { isCreate, isDelete, @@ -439,6 +436,9 @@ export class OscdEditor extends LitElement { await this.updateComplete; this.dispatchEvent(newValidateEvent()); + this.dispatchEvent( + newEditCompletedEvent(event.detail.action, event.detail.initiator) + ); } /** diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index 95b07880c..fce5ea338 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -1,4 +1,13 @@ -import { html, state, property, query, TemplateResult, customElement, LitElement } from 'lit-element'; +import { + html, + state, + property, + query, + TemplateResult, + customElement, + LitElement, +} from 'lit-element'; + import { get } from 'lit-translate'; import '@material/mwc-button'; @@ -13,7 +22,8 @@ import { Dialog } from '@material/mwc-dialog'; import { Snackbar } from '@material/mwc-snackbar'; import '../filtered-list.js'; -import { + +import { CommitDetail, CommitEntry, InfoDetail, @@ -24,8 +34,14 @@ import { LogEntryType, LogEvent, } from '@openscd/core/foundation/deprecated/history.js'; -import { newActionEvent, invert } from '@openscd/core/foundation/deprecated/editor.js'; + +import { + newActionEvent, + invert, +} from '@openscd/core/foundation/deprecated/editor.js'; + import { getFilterIcon, iconColors } from '../icons/icons.js'; + import { Plugin } from '../open-scd.js'; const icons = { @@ -93,33 +109,6 @@ export function newEmptyIssuesEvent( }); } -export interface UndoRedoChangedDetail { - canUndo: boolean; - canRedo: boolean; - editCount: number; -} - -export type UndoRedoChangedEvent = CustomEvent; - -export function newUndoRedoChangedEvent( - canUndo: boolean, - canRedo: boolean, - editCount: number, - eventInitDict?: CustomEventInit> -): UndoRedoChangedEvent { - return new CustomEvent('undo-redo-changed', { - bubbles: true, - composed: true, - ...eventInitDict, - detail: { - canUndo, - canRedo, - editCount, - ...eventInitDict?.detail, - }, - }); -} - export function newUndoEvent(): CustomEvent { return new CustomEvent('undo', { bubbles: true, composed: true }); } @@ -128,511 +117,493 @@ export function newRedoEvent(): CustomEvent { return new CustomEvent('redo', { bubbles: true, composed: true }); } - @customElement('oscd-history') export class OscdHistory extends LitElement { - /** All [[`LogEntry`]]s received so far through [[`LogEvent`]]s. */ - @property({ type: Array }) - log: InfoEntry[] = []; - - /** All [[`CommitEntry`]]s received so far through [[`LogEvent`]]s */ - @property({ type: Array }) - history: CommitEntry[] = []; - - /** Index of the last [[`EditorAction`]] applied. */ - @property({ type: Number }) - editCount = -1; - - @property() - diagnoses = new Map(); - - @property({ + /** All [[`LogEntry`]]s received so far through [[`LogEvent`]]s. */ + @property({ type: Array }) + log: InfoEntry[] = []; + + /** All [[`CommitEntry`]]s received so far through [[`LogEvent`]]s */ + @property({ type: Array }) + history: CommitEntry[] = []; + + /** Index of the last [[`EditorAction`]] applied. */ + @property({ type: Number }) + editCount = -1; + + @property() + diagnoses = new Map(); + + @property({ type: Object, - }) - host!: HTMLElement; - - @state() - latestIssue!: IssueDetail; - - @query('#log') logUI!: Dialog; - @query('#history') historyUI!: Dialog; - @query('#diagnostic') diagnosticUI!: Dialog; - @query('#error') errorUI!: Snackbar; - @query('#warning') warningUI!: Snackbar; - @query('#info') infoUI!: Snackbar; - @query('#issue') issueUI!: Snackbar; - - get canUndo(): boolean { - return this.editCount >= 0; - } - get canRedo(): boolean { - return this.nextAction >= 0; - } + }) + host!: HTMLElement; + + @state() + latestIssue!: IssueDetail; + + @query('#log') logUI!: Dialog; + @query('#history') historyUI!: Dialog; + @query('#diagnostic') diagnosticUI!: Dialog; + @query('#error') errorUI!: Snackbar; + @query('#warning') warningUI!: Snackbar; + @query('#info') infoUI!: Snackbar; + @query('#issue') issueUI!: Snackbar; + + get canUndo(): boolean { + return this.editCount >= 0; + } + get canRedo(): boolean { + return this.nextAction >= 0; + } - get previousAction(): number { - if (!this.canUndo) return -1; - return this.history - .slice(0, this.editCount) - .map(entry => (entry.kind == 'action' ? true : false)) - .lastIndexOf(true); - } - get nextAction(): number { - let index = this.history - .slice(this.editCount + 1) - .findIndex(entry => entry.kind == 'action'); - if (index >= 0) index += this.editCount + 1; - return index; - } + get previousAction(): number { + if (!this.canUndo) return -1; + return this.history + .slice(0, this.editCount) + .map(entry => (entry.kind == 'action' ? true : false)) + .lastIndexOf(true); + } + get nextAction(): number { + let index = this.history + .slice(this.editCount + 1) + .findIndex(entry => entry.kind == 'action'); + if (index >= 0) index += this.editCount + 1; + return index; + } - private onIssue(de: IssueEvent): void { - const issues = this.diagnoses.get(de.detail.validatorId); + private onIssue(de: IssueEvent): void { + const issues = this.diagnoses.get(de.detail.validatorId); - if (!issues) this.diagnoses.set(de.detail.validatorId, [de.detail]); - else issues?.push(de.detail); + if (!issues) this.diagnoses.set(de.detail.validatorId, [de.detail]); + else issues?.push(de.detail); - this.latestIssue = de.detail; - this.issueUI.close(); - this.issueUI.show(); - } + this.latestIssue = de.detail; + this.issueUI.close(); + this.issueUI.show(); + } - undo(): boolean { - if (!this.canUndo) return false; - this.dispatchEvent( - newActionEvent( - invert((this.history[this.editCount]).action) - ) - ); - this.editCount = this.previousAction; - this.dispatchEvent(newUndoRedoChangedEvent(this.canUndo, this.canRedo, this.editCount)); - return true; - } - redo(): boolean { - if (!this.canRedo) return false; - this.dispatchEvent( - newActionEvent((this.history[this.nextAction]).action) - ); - this.editCount = this.nextAction; - this.dispatchEvent(newUndoRedoChangedEvent(this.canUndo, this.canRedo, this.editCount)); - return true; - } + undo(): boolean { + if (!this.canUndo) return false; + const invertedAction = invert( + (this.history[this.editCount]).action + ); + this.dispatchEvent(newActionEvent(invertedAction, 'undo')); + this.editCount = this.previousAction; + return true; + } + redo(): boolean { + if (!this.canRedo) return false; + const nextAction = (this.history[this.nextAction]).action; + this.dispatchEvent(newActionEvent(nextAction, 'redo')); + this.editCount = this.nextAction; + return true; + } - private onHistory(detail: CommitDetail) { - const entry: CommitEntry = { - time: new Date(), - ...detail, - }; - - if (entry.kind === 'action') { - if (entry.action.derived) return; - entry.action.derived = true; - if (this.nextAction !== -1) this.history.splice(this.nextAction); - this.editCount = this.history.length; - this.dispatchEvent(newUndoRedoChangedEvent(this.canUndo, this.canRedo, this.editCount)); - } - - this.history.push(entry); - this.requestUpdate('history', []); + private onHistory(detail: CommitDetail) { + const entry: CommitEntry = { + time: new Date(), + ...detail, + }; + + if (entry.kind === 'action') { + if (entry.action.derived) return; + entry.action.derived = true; + if (this.nextAction !== -1) this.history.splice(this.nextAction); + this.editCount = this.history.length; } - private onReset() { - this.log = []; - this.history = []; - this.editCount = -1; - this.dispatchEvent(newUndoRedoChangedEvent(this.canUndo, this.canRedo, this.editCount)); - } + this.history.push(entry); + this.requestUpdate('history', []); + } - private onInfo(detail: InfoDetail) { - const entry: InfoEntry = { - time: new Date(), - ...detail, - }; - - this.log.push(entry); - if (!this.logUI.open) { - const ui = { - error: this.errorUI, - warning: this.warningUI, - info: this.infoUI, - }[detail.kind]; - - ui.close(); - ui.show(); - } - if (detail.kind == 'error') { - this.errorUI.close(); // hack to reset timeout - this.errorUI.show(); - } - this.requestUpdate('log', []); - } + private onReset() { + this.log = []; + this.history = []; + this.editCount = -1; + } - private onLog(le: LogEvent): void { - switch (le.detail.kind) { - case 'reset': - this.onReset(); - break; - case 'action': - this.onHistory(le.detail); - break; - default: - this.onInfo(le.detail); - break; - } - } + private onInfo(detail: InfoDetail) { + const entry: InfoEntry = { + time: new Date(), + ...detail, + }; - private historyUIHandler(e: HistoryUIEvent): void { + this.log.push(entry); + if (!this.logUI.open) { const ui = { - log: this.logUI, - history: this.historyUI, - diagnostic: this.diagnosticUI, - }[e.detail.kind]; + error: this.errorUI, + warning: this.warningUI, + info: this.infoUI, + }[detail.kind]; - if (e.detail.show) ui.show(); - else ui.close(); + ui.close(); + ui.show(); } - - private emptyIssuesHandler(e: EmptyIssuesEvent): void { - const issues = this.diagnoses.get(e.detail.pluginSrc); - if (this.diagnoses.get(e.detail.pluginSrc)) - this.diagnoses.get(e.detail.pluginSrc)!.length = 0; + if (detail.kind == 'error') { + this.errorUI.close(); // hack to reset timeout + this.errorUI.show(); } + this.requestUpdate('log', []); + } - private handleKeyPress(e: KeyboardEvent): void { - let handled = false; - const ctrlAnd = (key: string) => - e.key === key && e.ctrlKey && (handled = true); - - if (ctrlAnd('y')) this.redo(); - if (ctrlAnd('z')) this.undo(); - if (ctrlAnd('l')) this.logUI.open ? this.logUI.close() : this.logUI.show(); - if (ctrlAnd('d')) - this.diagnosticUI.open - ? this.diagnosticUI.close() - : this.diagnosticUI.show(); + private onLog(le: LogEvent): void { + switch (le.detail.kind) { + case 'reset': + this.onReset(); + break; + case 'action': + this.onHistory(le.detail); + break; + default: + this.onInfo(le.detail); + break; } + } - constructor() { - super(); - this.undo = this.undo.bind(this); - this.redo = this.redo.bind(this); - this.onLog = this.onLog.bind(this); - this.onIssue = this.onIssue.bind(this); - this.historyUIHandler = this.historyUIHandler.bind(this); - this.emptyIssuesHandler = this.emptyIssuesHandler.bind(this); - this.handleKeyPress = this.handleKeyPress.bind(this); - document.onkeydown = this.handleKeyPress; - } + private historyUIHandler(e: HistoryUIEvent): void { + const ui = { + log: this.logUI, + history: this.historyUI, + diagnostic: this.diagnosticUI, + }[e.detail.kind]; - connectedCallback(): void { - super.connectedCallback(); + if (e.detail.show) ui.show(); + else ui.close(); + } - this.host.addEventListener('log', this.onLog); - this.host.addEventListener('issue', this.onIssue); - this.host.addEventListener('history-dialog-ui', this.historyUIHandler); - this.host.addEventListener('empty-issues', this.emptyIssuesHandler); - this.host.addEventListener('undo', this.undo); - this.host.addEventListener('redo', this.redo); - this.diagnoses.clear(); - } + private emptyIssuesHandler(e: EmptyIssuesEvent): void { + if (this.diagnoses.get(e.detail.pluginSrc)) + this.diagnoses.get(e.detail.pluginSrc)!.length = 0; + } + + private handleKeyPress(e: KeyboardEvent): void { + const ctrlAnd = (key: string) => e.key === key && e.ctrlKey; + + if (ctrlAnd('y')) this.redo(); + if (ctrlAnd('z')) this.undo(); + if (ctrlAnd('l')) this.logUI.open ? this.logUI.close() : this.logUI.show(); + if (ctrlAnd('d')) + this.diagnosticUI.open + ? this.diagnosticUI.close() + : this.diagnosticUI.show(); + } + + constructor() { + super(); + this.undo = this.undo.bind(this); + this.redo = this.redo.bind(this); + this.onLog = this.onLog.bind(this); + this.onIssue = this.onIssue.bind(this); + this.historyUIHandler = this.historyUIHandler.bind(this); + this.emptyIssuesHandler = this.emptyIssuesHandler.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); + document.onkeydown = this.handleKeyPress; + } + + connectedCallback(): void { + super.connectedCallback(); - renderLogEntry( - entry: InfoEntry, - index: number, - log: LogEntry[] - ): TemplateResult { - return html` - + + + + ${entry.time?.toLocaleString()} + ${entry.title} - - - ${entry.time?.toLocaleString()} - ${entry.title} - ${entry.message} - ${icons[entry.kind]} - `; - } + ${entry.message} + ${icons[entry.kind]} + `; + } - renderHistoryEntry( - entry: CommitEntry, - index: number, - history: LogEntry[] - ): TemplateResult { - return html` - + + + + ${entry.time?.toLocaleString()} + ${entry.title} - - - ${entry.time?.toLocaleString()} - ${entry.title} - ${entry.message} - history - `; - } + ${entry.message} + history + `; + } - private renderLog(): TemplateResult[] | TemplateResult { - if (this.log.length > 0) - return this.log.slice().reverse().map(this.renderLogEntry, this); - else - return html` - ${get('log.placeholder')} - info - `; - } + private renderLog(): TemplateResult[] | TemplateResult { + if (this.log.length > 0) + return this.log.slice().reverse().map(this.renderLogEntry, this); + else + return html` + ${get('log.placeholder')} + info + `; + } - private renderHistory(): TemplateResult[] | TemplateResult { - if (this.history.length > 0) - return this.history - .slice() - .reverse() - .map(this.renderHistoryEntry, this); - else - return html` - ${get('history.placeholder')} - info - `; - } + private renderHistory(): TemplateResult[] | TemplateResult { + if (this.history.length > 0) + return this.history.slice().reverse().map(this.renderHistoryEntry, this); + else + return html` + ${get('history.placeholder')} + info + `; + } - private renderIssueEntry(issue: IssueDetail): TemplateResult { - return html` - ${issue.title} - ${issue.message} - `; - } + private renderIssueEntry(issue: IssueDetail): TemplateResult { + return html` + ${issue.title} + ${issue.message} + `; + } - renderValidatorsIssues(issues: IssueDetail[]): TemplateResult[] { - if (issues.length === 0) return [html``]; - return [ - html`${getPluginName(issues[0].validatorId)}`, - html`
  • `, - ...issues.map(issue => this.renderIssueEntry(issue)), - ]; - } + renderValidatorsIssues(issues: IssueDetail[]): TemplateResult[] { + if (issues.length === 0) return [html``]; + return [ + html`${getPluginName(issues[0].validatorId)}`, + html`
  • `, + ...issues.map(issue => this.renderIssueEntry(issue)), + ]; + } - private renderIssues(): TemplateResult[] | TemplateResult { - const issueItems: TemplateResult[] = []; - - this.diagnoses.forEach(issues => { - this.renderValidatorsIssues(issues).forEach(issueItem => - issueItems.push(issueItem) - ); - }); - - return issueItems.length - ? issueItems - : html` - ${get('diag.placeholder')} - info - `; - } + private renderIssues(): TemplateResult[] | TemplateResult { + const issueItems: TemplateResult[] = []; - private renderFilterButtons() { - return (Object.keys(icons)).map( - kind => html`${getFilterIcon(kind, false)} - ${getFilterIcon(kind, true)}` + this.diagnoses.forEach(issues => { + this.renderValidatorsIssues(issues).forEach(issueItem => + issueItems.push(issueItem) ); - } + }); - private renderLogDialog(): TemplateResult { - return html` - ${this.renderFilterButtons()} - ${this.renderLog()} - ${get('close')} - `; - } + return issueItems.length + ? issueItems + : html` + ${get('diag.placeholder')} + info + `; + } + + private renderFilterButtons() { + return (Object.keys(icons)).map( + kind => html`${getFilterIcon(kind, false)} + ${getFilterIcon(kind, true)}` + ); + } - private renderHistoryUI(): TemplateResult { - return html` + ${this.renderFilterButtons()} + ${this.renderLog()} + ${get('close')} - ${this.renderHistory()} - - + `; + } + + private renderHistoryUI(): TemplateResult { + return html` + ${this.renderHistory()} + + + ${get('close')} + `; + } + + render(): TemplateResult { + return html` + + ${this.renderLogDialog()} ${this.renderHistoryUI()} + + ${this.renderIssues()} ${get('close')} - `; - } + - render(): TemplateResult { - return html` - - ${this.renderLogDialog()} ${this.renderHistoryUI()} - - ${this.renderIssues()} - ${get('close')} - - - - - - + + + + this.logUI.show()} + >${get('log.snackbar.show')} - this.logUI.show()} - >${get('log.snackbar.show')} - - - + + + this.logUI.show()} + >${get('log.snackbar.show')} - this.logUI.show()} - >${get('log.snackbar.show')} - - - + + + this.diagnosticUI.show()} + >${get('log.snackbar.show')} - this.diagnosticUI.show()} - >${get('log.snackbar.show')} - - `; - } + + `; + } } declare global { interface ElementEventMap { 'history-dialog-ui': CustomEvent; 'empty-issues': CustomEvent; - 'undo-redo-changed': CustomEvent; } } - diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index d07b3f171..4080a4fb0 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -1,18 +1,36 @@ import { - customElement, - html, - LitElement, - property, - state, - TemplateResult, - query, - css + customElement, + html, + LitElement, + property, + state, + TemplateResult, + query, + css, } from 'lit-element'; import { get } from 'lit-translate'; import { newPendingStateEvent } from '@openscd/core/foundation/deprecated/waiter.js'; import { newSettingsUIEvent } from '@openscd/core/foundation/deprecated/settings.js'; -import { MenuItem, Plugin, Validator, PluginKind, MenuPosition, MenuPlugin, menuPosition, pluginIcons, newResetPluginsEvent, newAddExternalPluginEvent, newSetPluginsEvent } from '../open-scd.js'; -import { HistoryUIKind, newEmptyIssuesEvent, newHistoryUIEvent, newRedoEvent, newUndoEvent, UndoRedoChangedEvent } from './History.js'; +import { + MenuItem, + Plugin, + Validator, + PluginKind, + MenuPosition, + MenuPlugin, + menuPosition, + pluginIcons, + newResetPluginsEvent, + newAddExternalPluginEvent, + newSetPluginsEvent, +} from '../open-scd.js'; +import { + HistoryUIKind, + newEmptyIssuesEvent, + newHistoryUIEvent, + newRedoEvent, + newUndoEvent, +} from './History.js'; import type { Drawer } from '@material/mwc-drawer'; import type { ActionDetail } from '@material/mwc-list'; import { List } from '@material/mwc-list'; @@ -29,15 +47,15 @@ import '@material/mwc-dialog'; import '@material/mwc-switch'; import '@material/mwc-select'; import '@material/mwc-textfield'; +import { EditCompletedEvent } from '@openscd/core'; @customElement('oscd-layout') export class OscdLayout extends LitElement { - - /** The `XMLDocument` to be edited */ + /** The `XMLDocument` to be edited */ @property({ attribute: false }) doc: XMLDocument | null = null; /** The name of the current [[`doc`]] */ - @property({ type: String }) + @property({ type: String }) docName = ''; /** Index of the last [[`EditorAction`]] applied. */ @property({ type: Number }) @@ -46,36 +64,41 @@ export class OscdLayout extends LitElement { @property({ type: Number }) activeTab = 0; - /** The plugins to render the layout. */ - @property({ type: Array }) - plugins: Plugin[] = []; + /** The plugins to render the layout. */ + @property({ type: Array }) + plugins: Plugin[] = []; - /** The open-scd host element */ - @property({ type: Object }) - host!: HTMLElement; + /** The open-scd host element */ + @property({ type: Object }) + host!: HTMLElement; - @state() + @state() validated: Promise = Promise.resolve(); - @state() - shouldValidate = false; - @state() - canRedo = false; + shouldValidate = false; @state() - canUndo = false; + redoCount = 0; + + get canUndo(): boolean { + return this.editCount >= 0; + } + + get canRedo(): boolean { + return this.redoCount > 0; + } - @query('#menu') - menuUI!: Drawer; - @query('#pluginManager') + @query('#menu') + menuUI!: Drawer; + @query('#pluginManager') pluginUI!: Dialog; @query('#pluginList') pluginList!: List; @query('#pluginAdd') pluginDownloadUI!: Dialog; - // Computed properties + // Computed properties get validators(): Plugin[] { return this.plugins.filter( @@ -97,7 +120,7 @@ export class OscdLayout extends LitElement { return this.menuEntries.filter(plugin => plugin.position === 'bottom'); } - get menu(): (MenuItem | 'divider')[] { + get menu(): (MenuItem | 'divider')[] { const topMenu: (MenuItem | 'divider')[] = []; const middleMenu: (MenuItem | 'divider')[] = []; const bottomMenu: (MenuItem | 'divider')[] = []; @@ -201,7 +224,7 @@ export class OscdLayout extends LitElement { name: 'undo', actionItem: true, action: (): void => { - this.dispatchEvent(newUndoEvent()) + this.dispatchEvent(newUndoEvent()); }, disabled: (): boolean => !this.canUndo, kind: 'static', @@ -211,7 +234,7 @@ export class OscdLayout extends LitElement { name: 'redo', actionItem: true, action: (): void => { - this.dispatchEvent(newRedoEvent()) + this.dispatchEvent(newRedoEvent()); }, disabled: (): boolean => !this.canRedo, kind: 'static', @@ -222,7 +245,7 @@ export class OscdLayout extends LitElement { name: 'menu.viewLog', actionItem: true, action: (): void => { - this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.log)) + this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.log)); }, kind: 'static', }, @@ -231,7 +254,7 @@ export class OscdLayout extends LitElement { name: 'menu.viewHistory', actionItem: true, action: (): void => { - this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.history)) + this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.history)); }, kind: 'static', }, @@ -240,7 +263,7 @@ export class OscdLayout extends LitElement { name: 'menu.viewDiag', actionItem: true, action: (): void => { - this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.diagnostic)) + this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.diagnostic)); }, kind: 'static', }, @@ -270,10 +293,8 @@ export class OscdLayout extends LitElement { ); } - - - // Keyboard Shortcuts - private handleKeyPress(e: KeyboardEvent): void { + // Keyboard Shortcuts + private handleKeyPress(e: KeyboardEvent): void { let handled = false; const ctrlAnd = (key: string) => e.key === key && e.ctrlKey && (handled = true); @@ -296,7 +317,7 @@ export class OscdLayout extends LitElement { if (handled) e.preventDefault(); } - private handleAddPlugin() { + private handleAddPlugin() { const pluginSrcInput = ( this.pluginDownloadUI.querySelector('#pluginSrcInput') ); @@ -324,26 +345,28 @@ export class OscdLayout extends LitElement { ) return; - this.dispatchEvent(newAddExternalPluginEvent({ - src: pluginSrcInput.value, - name: pluginNameInput.value, - kind: (pluginKindList.selected).value, - requireDoc: requireDoc.checked, - position: positionList.value, - installed: true, - })); + this.dispatchEvent( + newAddExternalPluginEvent({ + src: pluginSrcInput.value, + name: pluginNameInput.value, + kind: (pluginKindList.selected).value, + requireDoc: requireDoc.checked, + position: positionList.value, + installed: true, + }) + ); this.requestUpdate(); this.pluginUI.requestUpdate(); this.pluginDownloadUI.close(); } - connectedCallback(): void { - super.connectedCallback(); - this.host.addEventListener('close-drawer', async () => { - this.menuUI.open = false; - }); - this.host.addEventListener('validate', async () => { + connectedCallback(): void { + super.connectedCallback(); + this.host.addEventListener('close-drawer', async () => { + this.menuUI.open = false; + }); + this.host.addEventListener('validate', async () => { this.shouldValidate = true; await this.validated; @@ -361,14 +384,25 @@ export class OscdLayout extends LitElement { ).then(); this.dispatchEvent(newPendingStateEvent(this.validated)); }); - this.handleKeyPress = this.handleKeyPress.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); document.onkeydown = this.handleKeyPress; - this.host.addEventListener('undo-redo-changed', (e: UndoRedoChangedEvent) => { - this.canRedo = e.detail.canRedo; - this.canUndo = e.detail.canUndo; - }); - } - + + this.host.addEventListener( + 'oscd-edit-completed', + (evt: EditCompletedEvent) => { + const initiator = evt.detail.initiator; + + if (initiator === 'undo') { + this.redoCount += 1; + } else if (initiator === 'redo') { + this.redoCount -= 1; + } + + this.requestUpdate(); + } + ); + } + private renderMenuItem(me: MenuItem | 'divider'): TemplateResult { if (me === 'divider') return html`
  • `; @@ -408,26 +442,21 @@ export class OscdLayout extends LitElement { /** Renders top bar which features icon buttons for undo, redo, log, scl history and diagnostics*/ protected renderHeader(): TemplateResult { return html` - (this.menuUI.open = true)} - > -
    ${this.docName}
    - ${this.menu.map(this.renderActionItem)} -
    `; + (this.menuUI.open = true)} + > +
    ${this.docName}
    + ${this.menu.map(this.renderActionItem)} + `; } - + /** Renders a drawer toolbar featuring the scl filename, enabled menu plugins, settings, help, scl history and plug-ins management */ protected renderAside(): TemplateResult { return html` - + ${get('menu.title')} ${this.docName ? html`${this.docName}` @@ -455,22 +484,23 @@ export class OscdLayout extends LitElement { protected renderContent(): TemplateResult { return html` ${this.doc - ? html` - (this.activeTab = e.detail.index)} - > - ${this.editors.map(this.renderEditorTab)} - - ${this.editors[this.activeTab]?.content? this.editors[this.activeTab].content: ``}` - : ``} + ? html` + (this.activeTab = e.detail.index)} + > + ${this.editors.map(this.renderEditorTab)} + + ${this.editors[this.activeTab]?.content + ? this.editors[this.activeTab].content + : ``}` + : ``} `; } /** Renders the landing buttons (open project and new project)*/ protected renderLanding(): TemplateResult { - return html` - ${!this.doc? - html`
    + return html` ${!this.doc + ? html`
    ${(this.menu.filter(mi => mi !== 'divider')).map( (mi: MenuItem, index) => mi.kind === 'top' && !mi.disabled?.() @@ -488,8 +518,8 @@ export class OscdLayout extends LitElement { ` : html`` )} -
    ` :`` - }`; +
    ` + : ``}`; } /** Renders the "Add Custom Plug-in" UI*/ @@ -697,23 +727,20 @@ export class OscdLayout extends LitElement { private renderPlugging(): TemplateResult { return html` ${this.renderPluginUI()} ${this.renderDownloadUI()} `; } - - render(): TemplateResult { - return html` + + render(): TemplateResult { + return html` - ${this.renderHeader()} - ${this.renderAside()} - ${this.renderContent()} - ${this.renderLanding()} - ${this.renderPlugging()} - `; - } - - static styles = css` - mwc-drawer { - position: absolute; - top: 0; - } + ${this.renderHeader()} ${this.renderAside()} ${this.renderContent()} + ${this.renderLanding()} ${this.renderPlugging()} + `; + } + + static styles = css` + mwc-drawer { + position: absolute; + top: 0; + } mwc-top-app-bar-fixed { --mdc-theme-text-disabled-on-light: rgba(255, 255, 255, 0.38); diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index b3feffced..f6e867320 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -40,8 +40,11 @@ import { ActionDetail } from '@material/mwc-list'; import { officialPlugins } from './plugins.js'; import { initializeNsdoc, Nsdoc } from './foundation/nsdoc.js'; -import { UndoRedoChangedEvent } from './addons/History.js'; -import type { PluginSet, Plugin as CorePlugin } from '@openscd/core'; +import type { + PluginSet, + Plugin as CorePlugin, + EditCompletedEvent, +} from '@openscd/core'; // HOSTING INTERFACES @@ -74,8 +77,9 @@ export interface AddExternalPluginDetail { export type AddExternalPluginEvent = CustomEvent; - -export function newAddExternalPluginEvent(plugin: Omit): AddExternalPluginEvent { +export function newAddExternalPluginEvent( + plugin: Omit +): AddExternalPluginEvent { return new CustomEvent('add-external-plugin', { bubbles: true, composed: true, @@ -198,7 +202,6 @@ function withoutContent

    ( return { ...plugin, content: undefined }; } - export const pluginIcons: Record = { editor: 'tab', menu: 'play_circle', @@ -278,28 +281,37 @@ export class OpenSCD extends LitElement { connectedCallback(): void { super.connectedCallback(); this.addEventListener('reset-plugins', this.resetPlugins); - this.addEventListener('add-external-plugin', (e: AddExternalPluginEvent) => { - this.addExternalPlugin(e.detail.plugin); - }); + this.addEventListener( + 'add-external-plugin', + (e: AddExternalPluginEvent) => { + this.addExternalPlugin(e.detail.plugin); + } + ); this.addEventListener('set-plugins', (e: SetPluginsEvent) => { this.setPlugins(e.detail.indices); }); this.updatePlugins(); this.requestUpdate(); + + this.addEventListener('oscd-edit-completed', (evt: EditCompletedEvent) => { + const initiator = evt.detail.initiator; + + if (initiator === 'undo') { + this.editCount -= 1; + } else { + this.editCount += 1; + } + + this.requestUpdate(); + }); } render(): TemplateResult { return html` - + ) { - localStorage.setItem('plugins', JSON.stringify(plugins.map(withoutContent))); + localStorage.setItem( + 'plugins', + JSON.stringify(plugins.map(withoutContent)) + ); this.requestUpdate(); } private resetPlugins(): void { @@ -389,7 +404,6 @@ export class OpenSCD extends LitElement { ); } - protected get locale(): string { return navigator.language || 'en-US'; } @@ -439,7 +453,9 @@ export class OpenSCD extends LitElement { this.storePlugins(newPlugins); } - private async addExternalPlugin(plugin: Omit): Promise { + private async addExternalPlugin( + plugin: Omit + ): Promise { if (this.storedPlugins.some(p => p.src === plugin.src)) return; const newPlugins: Omit[] = this.storedPlugins; diff --git a/packages/openscd/test/mock-editor-logger.ts b/packages/openscd/test/mock-editor-logger.ts index d867824d3..2894e6206 100644 --- a/packages/openscd/test/mock-editor-logger.ts +++ b/packages/openscd/test/mock-editor-logger.ts @@ -1,9 +1,17 @@ -import { LitElement, customElement, property, state, html, query, TemplateResult } from 'lit-element'; +import { + LitElement, + customElement, + property, + state, + html, + query, + TemplateResult, +} from 'lit-element'; import '../src/addons/Editor.js'; import '../src/addons/History.js'; import { OscdEditor } from '../src/addons/Editor.js'; -import { UndoRedoChangedEvent, OscdHistory } from '../src/addons/History.js'; +import { OscdHistory } from '../src/addons/History.js'; @customElement('mock-editor-logger') export class MockEditorLogger extends LitElement { @@ -23,20 +31,15 @@ export class MockEditorLogger extends LitElement { editor!: OscdEditor; render(): TemplateResult { - return html` - - - - - `; + return html` + + + `; } } diff --git a/packages/openscd/test/unit/Historing.test.ts b/packages/openscd/test/unit/Historing.test.ts index 8e5523443..f85ac3346 100644 --- a/packages/openscd/test/unit/Historing.test.ts +++ b/packages/openscd/test/unit/Historing.test.ts @@ -1,8 +1,8 @@ import { expect, fixture, html } from '@open-wc/testing'; -import '../mock-editor-logger.js'; +import '../mock-open-scd.js'; import { MockAction } from './mock-actions.js'; -import { MockEditorLogger } from '../mock-editor-logger.js'; +import { MockOpenSCD } from '../mock-open-scd.js'; import { CommitEntry, @@ -12,11 +12,11 @@ import { import { OscdHistory } from '../../src/addons/History.js'; describe('HistoringElement', () => { - let mock: MockEditorLogger; + let mock: MockOpenSCD; let element: OscdHistory; beforeEach(async () => { - mock = await fixture(html``); - element = mock.history; + mock = await fixture(html``); + element = mock.historyAddon; }); it('starts out with an empty log', () => @@ -112,8 +112,8 @@ describe('HistoringElement', () => { ); element.requestUpdate(); await element.updateComplete; - element.requestUpdate(); - await element.updateComplete; + mock.requestUpdate(); + await mock.updateComplete; }); it('can undo', () => expect(element).property('canUndo').to.be.true); @@ -184,7 +184,7 @@ describe('HistoringElement', () => { it('can redo', () => expect(element).property('canRedo').to.be.true); - it('removes the undone action when a new action is logged', () => { + it('removes the undone action when a new action is logged', async () => { element.dispatchEvent( newLogEvent({ kind: 'action', @@ -192,6 +192,7 @@ describe('HistoringElement', () => { action: MockAction.mov, }) ); + await element.updateComplete; expect(element).property('log').to.have.lengthOf(1); expect(element).property('history').to.have.lengthOf(2); expect(element).to.have.property('editCount', 1); @@ -199,10 +200,16 @@ describe('HistoringElement', () => { }); describe('with the second action undone', () => { - beforeEach(() => element.undo()); - - it('cannot undo any funther', () => - expect(element.undo()).to.be.false); + beforeEach(async () => { + element.undo(); + await element.updateComplete; + await mock.updateComplete; + }); + + it('cannot undo any funther', () => { + console.log('error'); + expect(element.undo()).to.be.false; + }); }); describe('with the action redone', () => { diff --git a/packages/plugins/test/integration/editors/GooseSubscriberLaterBinding.test.ts b/packages/plugins/test/integration/editors/GooseSubscriberLaterBinding.test.ts index b02e95282..3d5595871 100644 --- a/packages/plugins/test/integration/editors/GooseSubscriberLaterBinding.test.ts +++ b/packages/plugins/test/integration/editors/GooseSubscriberLaterBinding.test.ts @@ -90,6 +90,7 @@ describe('GOOSE Subscribe Later Binding Plugin', () => { ); await element.requestUpdate(); await extRefListElement.requestUpdate(); + await parent.historyAddon.requestUpdate(); expect( extRefListElement['getSubscribedExtRefElements']().length @@ -106,6 +107,7 @@ describe('GOOSE Subscribe Later Binding Plugin', () => { )).click(); await element.requestUpdate(); await parent.updateComplete; + await parent.historyAddon.requestUpdate(); expect( extRefListElement['getSubscribedExtRefElements']().length @@ -186,6 +188,7 @@ describe('GOOSE Subscribe Later Binding Plugin', () => { )).click(); await element.requestUpdate(); await parent.updateComplete; + await parent.historyAddon.requestUpdate(); expect( extRefListElement['getSubscribedExtRefElements']().length diff --git a/packages/plugins/test/integration/editors/GooseSubscriberMessageBinding.test.ts b/packages/plugins/test/integration/editors/GooseSubscriberMessageBinding.test.ts index 228e9ed14..0cab4b61b 100644 --- a/packages/plugins/test/integration/editors/GooseSubscriberMessageBinding.test.ts +++ b/packages/plugins/test/integration/editors/GooseSubscriberMessageBinding.test.ts @@ -191,6 +191,8 @@ describe('GOOSE subscriber plugin', async () => { describe('after clicking on the IEDs list element', () => { beforeEach(async () => { (getItemFromSubscriberList('IED1')).click(); + await parent.historyAddon.requestUpdate(); + await element.requestUpdate(); }); @@ -220,6 +222,8 @@ describe('GOOSE subscriber plugin', async () => { describe('after clicking on the IEDs list element', () => { beforeEach(async () => { (getItemFromSubscriberList('IED4')).click(); + await parent.historyAddon.requestUpdate(); + await element.requestUpdate(); }); diff --git a/packages/plugins/test/integration/editors/SMVSubscriberMessageBinding.test.ts b/packages/plugins/test/integration/editors/SMVSubscriberMessageBinding.test.ts index d5a9cb00b..0540c9bfd 100644 --- a/packages/plugins/test/integration/editors/SMVSubscriberMessageBinding.test.ts +++ b/packages/plugins/test/integration/editors/SMVSubscriberMessageBinding.test.ts @@ -1,5 +1,5 @@ import { expect, fixture, html } from '@open-wc/testing'; -import {initializeNsdoc} from '@openscd/open-scd/src/foundation/nsdoc.js'; +import { initializeNsdoc } from '@openscd/open-scd/src/foundation/nsdoc.js'; import SMVSubscriberMessageBindingPlugin from '../../../src/editors/SMVSubscriberMessageBinding.js'; import { ListItem } from '@material/mwc-list/mwc-list-item.js'; @@ -35,7 +35,7 @@ describe('Sampled Values Plugin', async () => { .then(str => new DOMParser().parseFromString(str, 'application/xml')); parent = await fixture( - html`` + html`` ); await parent.requestUpdate(); await parent.updateComplete; @@ -303,6 +303,7 @@ describe('Sampled Values Plugin', async () => { (getItemFromSubscriberList('MSVCB01')).click(); await element.requestUpdate(); + await parent.historyAddon.requestUpdate(); }); it('it looks like the latest snapshot', async () =>