Skip to content

Commit dbd35e6

Browse files
authored
Merge pull request #6 from com-pas/feat/highlight-function-psr
feat: highlight function
2 parents 5bc3377 + daa3b2f commit dbd35e6

6 files changed

Lines changed: 312 additions & 45 deletions

File tree

bay-template-editor.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,4 +918,55 @@ describe('Bay Template Editor Plugin', () => {
918918
expect(element.placingFunction).to.exist;
919919
});
920920
});
921+
922+
describe('Cancel button', () => {
923+
it('resets state when clicked', async () => {
924+
const doc = new DOMParser().parseFromString(
925+
docWithBay,
926+
'application/xml'
927+
);
928+
element.doc = doc;
929+
element.sldEditorInAction = true;
930+
await element.updateComplete;
931+
932+
const cancelBtn = element.shadowRoot?.querySelector(
933+
'oscd-icon-button[label="Cancel"]'
934+
) as HTMLElement;
935+
936+
cancelBtn.click();
937+
await element.updateComplete;
938+
939+
expect(element.sldEditorInAction).to.be.false;
940+
expect(element.functionsInAction).to.be.false;
941+
expect(element.inAction).to.be.false;
942+
});
943+
944+
it('toggles function layer visibility when adding function', async () => {
945+
const doc = new DOMParser().parseFromString(
946+
docWithBay,
947+
'application/xml'
948+
);
949+
element.doc = doc;
950+
await element.updateComplete;
951+
952+
const addFunctionButton = element.shadowRoot?.querySelector(
953+
'oscd-icon-button[label="Add Function"]'
954+
) as HTMLElement;
955+
956+
addFunctionButton.click();
957+
await element.updateComplete;
958+
959+
expect(element.addingFunction).to.be.true;
960+
expect(element.showFunctions).to.be.true;
961+
962+
const cancelBtn = element.shadowRoot?.querySelector(
963+
'oscd-icon-button[label="Cancel"]'
964+
) as HTMLElement;
965+
966+
cancelBtn.click();
967+
await element.updateComplete;
968+
expect(element.addingFunction).to.be.false;
969+
expect(element.showFunctions).to.be.false;
970+
});
971+
});
921972
});

bay-template-editor.ts

Lines changed: 118 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
getProcessPath,
3434
createPowerSystemRelationPrivate,
3535
getSldSvgs,
36+
eTr6100Ns,
3637
} from './util.js';
3738
import { FunctionsLayer } from './components/functions-layer/functions-layer.js';
3839
import { CreateFunctionDialog } from './components/create-function-dialog/create-function-dialog.js';
@@ -100,6 +101,12 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
100101
@state()
101102
highlight: { id: string; style: HighlightStyle }[] = [];
102103

104+
@state()
105+
functionHoverHighlight: { id: string; style: HighlightStyle }[] = [];
106+
107+
@state()
108+
private hoveredSubstation?: Element;
109+
103110
@state()
104111
selectedElement?: Element;
105112

@@ -139,6 +146,9 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
139146
private handleKeydown = (event: KeyboardEvent) => {
140147
if (event.key === 'Escape' && this.inAction) {
141148
event.preventDefault();
149+
if (this.addingFunction) {
150+
this.showFunctions = false;
151+
}
142152
this.reset();
143153
}
144154
};
@@ -149,6 +159,63 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
149159
this.functionsInAction = true;
150160
};
151161

162+
handleFunctionHover = (funcElement: Element | null) => {
163+
if (!funcElement) {
164+
this.functionHoverHighlight = [];
165+
this.hoveredSubstation = undefined;
166+
return;
167+
}
168+
169+
// If the Function has a PowerSystemRelation Private, resolve the referenced
170+
// element and highlight that instead of the DOM parent.
171+
const psrRelationEl = funcElement.getElementsByTagNameNS(
172+
eTr6100Ns,
173+
'PowerSystemRelation'
174+
)[0];
175+
const relation = psrRelationEl?.getAttribute('relation');
176+
const target = relation
177+
? this.getElementFromProcessPath(relation)
178+
: funcElement.parentElement;
179+
180+
if (!target) {
181+
this.functionHoverHighlight = [];
182+
this.hoveredSubstation = undefined;
183+
return;
184+
}
185+
186+
if (target.tagName === 'Substation') {
187+
this.functionHoverHighlight = [];
188+
this.hoveredSubstation = target;
189+
return;
190+
}
191+
192+
this.hoveredSubstation = undefined;
193+
this.functionHoverHighlight = [
194+
{
195+
id: identity(target).toString(),
196+
style: SELECTED_PSR_HIGHLIGHT_STYLE,
197+
},
198+
];
199+
};
200+
201+
private getElementFromProcessPath(path: string): Element | null {
202+
if (!this.doc) return null;
203+
const parts = path.split('/');
204+
if (!parts.length || !parts[0]) return null;
205+
206+
let current: Element | null = this.doc.querySelector(
207+
`:root > Substation[name="${parts[0]}"]`
208+
);
209+
for (let i = 1; i < parts.length && current; i += 1) {
210+
const name = parts[i];
211+
current =
212+
Array.from(current.children).find(
213+
child => child.getAttribute('name') === name
214+
) ?? null;
215+
}
216+
return current;
217+
}
218+
152219
get inAction(): boolean {
153220
return (
154221
this.sldEditorInAction || this.functionsInAction || this.addingFunction
@@ -517,33 +584,53 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
517584
}
518585

519586
private renderSubstationHighlight() {
520-
if (!this.addingFunction || !this.doc) return nothing;
587+
if (!this.doc) return nothing;
521588
const substations = Array.from(
522589
this.doc.querySelectorAll(':root > Substation')
523590
);
524591
if (!substations.length) return nothing;
525-
return substations.map((substation, i) => {
592+
593+
const result = [];
594+
595+
if (this.addingFunction) {
596+
result.push(
597+
...substations.map((substation, i) => {
598+
const b = this.sldBounds[i];
599+
const style = b
600+
? `top:${b.top}px;left:${b.left}px;width:${b.width}px;height:${b.height}px`
601+
: 'inset:0';
602+
return html`
603+
<div class="substation-highlight" style="${style}">
604+
<button
605+
class="substation-chip"
606+
title="Select Substation ${substation.getAttribute('name')}"
607+
@click=${() =>
608+
this.handleSldSelected(
609+
new CustomEvent('oscd-sld-selected', {
610+
detail: { element: substation },
611+
})
612+
)}
613+
>
614+
${substation.getAttribute('name')}
615+
</button>
616+
</div>
617+
`;
618+
})
619+
);
620+
}
621+
622+
if (this.hoveredSubstation) {
623+
const i = substations.indexOf(this.hoveredSubstation);
526624
const b = this.sldBounds[i];
527625
const style = b
528-
? `top:${b.top}px;left:${b.left}px;width:${b.width}px;height:${b.height}px`
626+
? `top:${b.top}px;left:${b.left}px;width:${b.width}px;height:${b.height}px;z-index:1;background:rgba(210,185,236,0.5);`
529627
: 'inset:0';
530-
return html`
531-
<div class="substation-highlight" style="${style}">
532-
<button
533-
class="substation-chip"
534-
title="Select Substation ${substation.getAttribute('name')}"
535-
@click=${() =>
536-
this.handleSldSelected(
537-
new CustomEvent('oscd-sld-selected', {
538-
detail: { element: substation },
539-
})
540-
)}
541-
>
542-
${substation.getAttribute('name')}
543-
</button>
544-
</div>
545-
`;
546-
});
628+
result.push(
629+
html`<div class="substation-highlight" style="${style}"></div>`
630+
);
631+
}
632+
633+
return result;
547634
}
548635

549636
private renderFunctionButtons() {
@@ -578,6 +665,7 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
578665
style: PSR_HIGHLIGHT_STYLE,
579666
}));
580667
this.addingFunction = true;
668+
this.showFunctions = true;
581669
}}
582670
>
583671
${functionAddIcon}
@@ -614,7 +702,12 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
614702
? html`<oscd-icon-button
615703
label="Cancel"
616704
title="Cancel"
617-
@click=${() => this.reset()}
705+
@click=${() => {
706+
if (this.addingFunction) {
707+
this.showFunctions = false;
708+
}
709+
this.reset();
710+
}}
618711
>
619712
<oscd-icon>close</oscd-icon>
620713
</oscd-icon-button>`
@@ -699,14 +792,13 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
699792
>${this.renderTransformerButtons()}${this.renderFunctionButtons()}
700793
</nav>
701794
<div class="editor-container">
702-
${this.renderSubstationHighlight()}
703795
<sld-editor
704796
.doc=${this.doc}
705797
.docVersion=${this.editCount}
706798
.gridSize=${this.gridSize}
707799
.showLabels=${this.showLabels}
708800
.disabled=${this.addingFunction || this.showFunctions}
709-
.highlight=${this.highlight}
801+
.highlight=${[...this.highlight, ...this.functionHoverHighlight]}
710802
.selectable=${this.addingFunction
711803
? this.highlight.map(h => h.id)
712804
: []}
@@ -715,6 +807,7 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
715807
}}
716808
@oscd-sld-selected=${this.handleSldSelected}
717809
></sld-editor>
810+
${this.renderSubstationHighlight()}
718811
${this.showFunctions
719812
? Array.from(this.doc.querySelectorAll(':root > Substation')).map(
720813
substation => html`<functions-layer
@@ -726,6 +819,7 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
726819
.placing=${this.placingFunction}
727820
.placingOffset=${this.placingFunctionOffset}
728821
.onStartPlaceFunction=${this.handleStartPlaceFunction}
822+
.onHoverFunction=${this.handleFunctionHover}
729823
></functions-layer>`
730824
)
731825
: nothing}
@@ -767,7 +861,7 @@ export default class BayTemplatePlugin extends ScopedElementsMixin(LitElement) {
767861
display: flex;
768862
gap: 4px;
769863
flex-wrap: wrap;
770-
z-index: 2;
864+
z-index: 3;
771865
}
772866
#bay-button {
773867
--md-filled-icon-button-container-color: #12579b;

components/functions-layer/functions-layer.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { html } from 'lit';
33
import sinon, { spy } from 'sinon';
44
import { fixture, expect } from '@open-wc/testing';
55
import { FunctionsLayer } from './functions-layer.js';
6+
import { SELECTED_PSR_HIGHLIGHT_STYLE } from '../../const.js';
67
import {
78
docWithBayAndFunctions,
89
docWithoutFunctions,
@@ -458,4 +459,81 @@ describe('FunctionsLayer', () => {
458459
expect(element.nsp).to.equal('customnsp');
459460
});
460461
});
462+
463+
describe('hover interaction', () => {
464+
beforeEach(async () => {
465+
const doc = new DOMParser().parseFromString(
466+
docWithBayAndFunctions,
467+
'application/xml'
468+
);
469+
element.doc = doc;
470+
await element.updateComplete;
471+
});
472+
473+
it('calls onHoverFunction with function element on mouseenter', async () => {
474+
const onHoverFunctionSpy = spy();
475+
element.onHoverFunction = onHoverFunctionSpy;
476+
477+
const functionGroup = element.shadowRoot?.querySelector(
478+
'g.function'
479+
) as SVGGElement;
480+
expect(functionGroup).to.exist;
481+
482+
functionGroup.dispatchEvent(
483+
new MouseEvent('mouseenter', { bubbles: false })
484+
);
485+
await element.updateComplete;
486+
487+
expect(onHoverFunctionSpy.calledOnce).to.be.true;
488+
const calledWith = onHoverFunctionSpy.firstCall.args[0] as Element;
489+
expect(calledWith).to.not.be.null;
490+
expect(calledWith.tagName).to.equal('Function');
491+
});
492+
493+
it('calls onHoverFunction with null on mouseleave', async () => {
494+
const onHoverFunctionSpy = spy();
495+
element.onHoverFunction = onHoverFunctionSpy;
496+
497+
const functionGroup = element.shadowRoot?.querySelector(
498+
'g.function'
499+
) as SVGGElement;
500+
expect(functionGroup).to.exist;
501+
502+
functionGroup.dispatchEvent(
503+
new MouseEvent('mouseenter', { bubbles: false })
504+
);
505+
await element.updateComplete;
506+
507+
functionGroup.dispatchEvent(
508+
new MouseEvent('mouseleave', { bubbles: false })
509+
);
510+
await element.updateComplete;
511+
512+
expect(onHoverFunctionSpy.calledTwice).to.be.true;
513+
expect(onHoverFunctionSpy.secondCall.args[0]).to.be.null;
514+
});
515+
516+
it('applies highlight styles on hover', async () => {
517+
const functionGroup = element.shadowRoot?.querySelector(
518+
'g.function'
519+
) as SVGGElement;
520+
expect(functionGroup).to.exist;
521+
522+
functionGroup.dispatchEvent(
523+
new MouseEvent('mouseenter', { bubbles: false })
524+
);
525+
await element.updateComplete;
526+
527+
const rect = functionGroup.querySelector('rect');
528+
expect(rect?.getAttribute('fill')).to.equal(
529+
SELECTED_PSR_HIGHLIGHT_STYLE.fill
530+
);
531+
expect(rect?.getAttribute('stroke')).to.equal(
532+
SELECTED_PSR_HIGHLIGHT_STYLE.stroke
533+
);
534+
expect(rect?.getAttribute('fill')).to.equal(
535+
SELECTED_PSR_HIGHLIGHT_STYLE.fill
536+
);
537+
});
538+
});
461539
});

0 commit comments

Comments
 (0)