|
| 1 | +# Wizarding in OpenSCD |
| 2 | + |
| 3 | +## OpenSCD |
| 4 | + |
| 5 | +The current wizarding functionality in OpenSCD consists of these key components: |
| 6 | + |
| 7 | +1. **`<oscd-wizards>`** (`packages/openscd/src/addons/Wizards.ts`) |
| 8 | + - Central wizard queue manager |
| 9 | + - Listens for `wizard` events on the host element |
| 10 | + - Manages FIFO workflow of wizard factories |
| 11 | + |
| 12 | +2. **`<wizard-dialog>`** (`packages/openscd/src/wizard-dialog.ts`) |
| 13 | + - Renders wizard pages and handles user interactions |
| 14 | + - Dispatches editor actions when wizard completes |
| 15 | + |
| 16 | +3. **Host Integration** (`packages/openscd/src/open-scd.ts`) |
| 17 | + ```typescript |
| 18 | + render(): TemplateResult { |
| 19 | + return html`<oscd-waiter> |
| 20 | + <oscd-settings .host=${this}> |
| 21 | + <oscd-wizards .host=${this}> |
| 22 | + <oscd-history .host=${this}> |
| 23 | + <!-- ... rest of the app --> |
| 24 | + ``` |
| 25 | + |
| 26 | +**How it works:** |
| 27 | + |
| 28 | +1. **Wizard creation**: plugins create wizard definitions |
| 29 | + ```typescript |
| 30 | + // Example from ied-container.ts |
| 31 | + private openEditWizard(): void { |
| 32 | + const wizard = wizards['IED'].edit(this.element); |
| 33 | + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); |
| 34 | + } |
| 35 | + ``` |
| 36 | + |
| 37 | +2. **Event dispatching**: wizards are triggered by dispatching `wizard` events |
| 38 | + ```typescript |
| 39 | + // From foundation.ts |
| 40 | + export function newWizardEvent( |
| 41 | + wizardOrFactory?: Wizard | WizardFactory, |
| 42 | + eventInitDict?: CustomEventInit<Partial<WizardDetail>> |
| 43 | + ): WizardEvent |
| 44 | + |
| 45 | + // Usage examples: |
| 46 | + this.dispatchEvent(newWizardEvent(wizardFactory)); // Open wizard |
| 47 | + this.dispatchEvent(newWizardEvent()); // Close wizard |
| 48 | + this.dispatchEvent(newWizardEvent(subWizard, { detail: { subwizard: true } })); // Subwizard |
| 49 | + ``` |
| 50 | + |
| 51 | +3. **Central processing**: The `<oscd-wizards>` component catches all wizard events |
| 52 | + ```typescript |
| 53 | + // From Wizards.ts |
| 54 | + private onWizard(we: WizardEvent) { |
| 55 | + const wizard = we.detail.wizard; |
| 56 | + if (wizard === null) this.workflow.shift(); |
| 57 | + else if (we.detail.subwizard) this.workflow.unshift(wizard); |
| 58 | + else this.workflow.push(wizard); |
| 59 | + } |
| 60 | + ``` |
| 61 | + |
| 62 | +4. **Dialog rendering**: The central `<wizard-dialog>` renders the current wizard |
| 63 | + |
| 64 | +## scl-wizarding plugin |
| 65 | + |
| 66 | +The [scl-wizarding plugin](https://github.com/OpenEnergyTools/scl-wizarding) is an alternative wizarding system designed specifically for SCL operations with simplified events. |
| 67 | + |
| 68 | +**Key differences from OpenSCD:** |
| 69 | + |
| 70 | +- **Simplified Events**: Uses element-focused events instead of wizard factories |
| 71 | + ```typescript |
| 72 | + // scl-wizarding events: |
| 73 | + this.dispatchEvent(newEditWizardEvent(element)); // Edit element |
| 74 | + this.dispatchEvent(newCreateWizardEvent(parent, tagName)); // Create element |
| 75 | + |
| 76 | + // vs OpenSCD events: |
| 77 | + this.dispatchEvent(newWizardEvent(wizardFactory)); |
| 78 | + ``` |
| 79 | + |
| 80 | +- **Pre-built wizard registry**: SCL element types with ready-made wizards |
| 81 | + ```typescript |
| 82 | + // Comprehensive wizard coverage for SCL elements: |
| 83 | + // Substation, VoltageLevel, Bay, ConductingEquipment, PowerTransformer, |
| 84 | + // IED, LDevice, LNode, ConnectedAP, LNodeType, DOType, DAType, etc. |
| 85 | + ``` |
| 86 | + |
| 87 | + |
| 88 | +**Usage:** |
| 89 | +```typescript |
| 90 | +// Edit existing SCL element |
| 91 | +private editElement(): void { |
| 92 | + this.dispatchEvent(newEditWizardEvent(this.element)); |
| 93 | +} |
| 94 | +
|
| 95 | +// Create new SCL element |
| 96 | +private createElement(): void { |
| 97 | + this.dispatchEvent(newCreateWizardEvent(parentElement, 'ConductingEquipment')); |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +## Moving away from centralised wizarding |
| 102 | + |
| 103 | +Both the OpenSCD centralised wizarding and the scl-wizarding plugin represent centralised approaches that should be migrated away from in favour of plugin-based solutions. |
| 104 | + |
| 105 | +### Migration strategy |
| 106 | + |
| 107 | +**Step 1: Identify current usage** |
| 108 | +```typescript |
| 109 | +// Look for these patterns in your plugins: |
| 110 | +
|
| 111 | +// OpenSCD centralised: |
| 112 | +this.dispatchEvent(newWizardEvent(wizard)); |
| 113 | +
|
| 114 | +// scl-wizarding: |
| 115 | +this.dispatchEvent(newEditWizardEvent(element)); |
| 116 | +this.dispatchEvent(newCreateWizardEvent(parent, tagName)); |
| 117 | +``` |
| 118 | + |
| 119 | +**Step 2: Replace with direct dialog management** |
| 120 | + |
| 121 | +Instead of dispatching events to a central wizard system, manage dialog state directly in your plugin component: |
| 122 | + |
| 123 | +```typescript |
| 124 | +// BEFORE: Centralized approach - events go to external wizard system |
| 125 | +export class MyPlugin extends LitElement { |
| 126 | + private openEditWizard(): void { |
| 127 | + // Event gets handled by <oscd-wizards> or scl-wizarding |
| 128 | + this.dispatchEvent(newWizardEvent(wizard)); // OpenSCD |
| 129 | + // OR |
| 130 | + this.dispatchEvent(newEditWizardEvent(this.element)); // scl-wizarding |
| 131 | + } |
| 132 | + |
| 133 | + render() { |
| 134 | + return html` |
| 135 | + <mwc-icon-button icon="edit" @click=${this.openEditWizard}></mwc-icon-button> |
| 136 | + <!-- No dialog here - it's handled by external system --> |
| 137 | + `; |
| 138 | + } |
| 139 | +} |
| 140 | +
|
| 141 | +// AFTER: Direct management - plugin controls its own dialogs |
| 142 | +export class MyPlugin extends LitElement { |
| 143 | + @state() private showEditDialog = false; |
| 144 | + @state() private currentElement: Element | null = null; |
| 145 | +
|
| 146 | + private openEditor(element: Element): void { |
| 147 | + // No events dispatched - just update local state |
| 148 | + this.currentElement = element; |
| 149 | + this.showEditDialog = true; |
| 150 | + } |
| 151 | + |
| 152 | + render() { |
| 153 | + return html` |
| 154 | + <mwc-icon-button icon="edit" @click=${() => this.openEditor(element)}></mwc-icon-button> |
| 155 | + |
| 156 | + <!-- Plugin renders its own dialog directly --> |
| 157 | + ${this.showEditDialog ? html` |
| 158 | + <my-edit-dialog |
| 159 | + .element=${this.currentElement} |
| 160 | + @close=${() => this.showEditDialog = false} |
| 161 | + ></my-edit-dialog> |
| 162 | + ` : nothing} |
| 163 | + `; |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +**Key difference:** Instead of sending events to external systems, your plugin directly controls when dialogs open/close using its own state. |
| 169 | + |
| 170 | +**Step 3: Implement plugin-specific dialogs** |
| 171 | + |
| 172 | +Here are minimal examples showing the modern approach with `newEditEvent`: |
| 173 | + |
| 174 | +**Edit dialog example:** |
| 175 | +```typescript |
| 176 | +// simple-edit-dialog.ts |
| 177 | +import { LitElement, html } from 'lit'; |
| 178 | +import { property, customElement } from 'lit/decorators.js'; |
| 179 | +import { newEditEvent } from '@openscd/open-scd-core'; |
| 180 | +
|
| 181 | +@customElement('simple-edit-dialog') |
| 182 | +export class SimpleEditDialog extends LitElement { |
| 183 | + @property({ attribute: false }) element!: Element; |
| 184 | + @property() open = false; |
| 185 | +
|
| 186 | + private onSave() { |
| 187 | + const name = this.shadowRoot?.querySelector('#name')?.value; |
| 188 | + if (!name) return; |
| 189 | +
|
| 190 | + const update = { |
| 191 | + element: this.element, |
| 192 | + attributes: { name } |
| 193 | + }; |
| 194 | + this.dispatchEvent(newEditEvent(update)); |
| 195 | + this.close(); |
| 196 | + } |
| 197 | +
|
| 198 | + private close() { |
| 199 | + this.dispatchEvent(new CustomEvent('close')); |
| 200 | + } |
| 201 | +
|
| 202 | + render() { |
| 203 | + return html` |
| 204 | + <mwc-dialog .open=${this.open} @closed=${this.close}> |
| 205 | + <input |
| 206 | + id="name" |
| 207 | + placeholder="Name" |
| 208 | + .value=${this.element.getAttribute('name') ?? ''} |
| 209 | + /> |
| 210 | + <mwc-button slot="secondaryAction" @click=${this.close}>Cancel</mwc-button> |
| 211 | + <mwc-button slot="primaryAction" @click=${this.onSave}>Save</mwc-button> |
| 212 | + </mwc-dialog> |
| 213 | + `; |
| 214 | + } |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +**Create dialog example:** |
| 219 | +```typescript |
| 220 | +// simple-create-dialog.ts |
| 221 | +import { LitElement, html } from 'lit'; |
| 222 | +import { property, customElement } from 'lit/decorators.js'; |
| 223 | +import { newEditEvent, Insert } from '@openscd/open-scd-core'; |
| 224 | +
|
| 225 | +@customElement('simple-create-dialog') |
| 226 | +export class SimpleCreateDialog extends LitElement { |
| 227 | + @property({ attribute: false }) parent!: Element; |
| 228 | + @property() tagName!: string; |
| 229 | + @property() open = false; |
| 230 | +
|
| 231 | + private onCreate() { |
| 232 | + const name = this.shadowRoot?.querySelector('#name')?.value; |
| 233 | + if (!name) return; |
| 234 | +
|
| 235 | + const doc = this.parent.ownerDocument!; |
| 236 | + const newElement = doc.createElement(this.tagName); |
| 237 | + newElement.setAttribute('name', name); |
| 238 | + |
| 239 | + const insert: Insert = { |
| 240 | + parent: this.parent, |
| 241 | + node: newElement, |
| 242 | + reference: null |
| 243 | + }; |
| 244 | + this.dispatchEvent(newEditEvent(insert)); |
| 245 | + this.close(); |
| 246 | + } |
| 247 | +
|
| 248 | + private close() { |
| 249 | + this.dispatchEvent(new CustomEvent('close')); |
| 250 | + } |
| 251 | +
|
| 252 | + render() { |
| 253 | + return html` |
| 254 | + <mwc-dialog .open=${this.open} @closed=${this.close}> |
| 255 | + <input id="name" placeholder="Name" /> |
| 256 | + <mwc-button slot="secondaryAction" @click=${this.close}>Cancel</mwc-button> |
| 257 | + <mwc-button slot="primaryAction" @click=${this.onCreate}>Create</mwc-button> |
| 258 | + </mwc-dialog> |
| 259 | + `; |
| 260 | + } |
| 261 | +} |
| 262 | +``` |
| 263 | + |
| 264 | +**Usage in plugin:** |
| 265 | +```typescript |
| 266 | +// In your plugin component |
| 267 | +@state() private showEditDialog = false; |
| 268 | +@state() private showCreateDialog = false; |
| 269 | +@state() private editElement?: Element; |
| 270 | +
|
| 271 | +private openEditor(element: Element) { |
| 272 | + this.editElement = element; |
| 273 | + this.showEditDialog = true; |
| 274 | +} |
| 275 | +
|
| 276 | +private openCreator() { |
| 277 | + this.showCreateDialog = true; |
| 278 | +} |
| 279 | +
|
| 280 | +render() { |
| 281 | + return html` |
| 282 | + <mwc-icon-button icon="edit" @click=${() => this.openEditor(element)}></mwc-icon-button> |
| 283 | + <mwc-icon-button icon="add" @click=${() => this.openCreator()}></mwc-icon-button> |
| 284 | + |
| 285 | + <simple-edit-dialog |
| 286 | + .element=${this.editElement} |
| 287 | + .open=${this.showEditDialog} |
| 288 | + @close=${() => this.showEditDialog = false} |
| 289 | + ></simple-edit-dialog> |
| 290 | + |
| 291 | + <simple-create-dialog |
| 292 | + .parent=${this.parentElement} |
| 293 | + tagName="Function" |
| 294 | + .open=${this.showCreateDialog} |
| 295 | + @close=${() => this.showCreateDialog = false} |
| 296 | + ></simple-create-dialog> |
| 297 | + `; |
| 298 | +} |
| 299 | +``` |
0 commit comments