+ [checked]="node.selected" [disabled]="state() === 'online'" />
|
diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts
index 1b5711fa..aed4b13e 100644
--- a/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts
+++ b/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts
@@ -6,14 +6,26 @@
*
*****************************************************************************/
-import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { NgClass, NgStyle } from '@angular/common';
-import { BehaviorSubject, Subscription, Observable } from 'rxjs';
+import { Router } from '@angular/router';
+import { BehaviorSubject, Subscription } from 'rxjs';
import { WebSocketSubject } from 'rxjs/webSocket';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
-import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import isEqual from 'lodash-es/isEqual';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ EventEmitter,
+ Input,
+ OnDestroy,
+ OnInit,
+ Output,
+ computed,
+ effect,
+ input,
+} from '@angular/core';
+
import {
aas,
LiveNode,
@@ -64,13 +76,12 @@ interface PropertyValue {
standalone: true,
imports: [NgClass, NgStyle, TranslateModule],
providers: [AASTreeSearch, AASTreeStore],
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
+export class AASTreeComponent implements OnInit, OnDestroy {
private readonly liveNodes: LiveNode[] = [];
private readonly map = new Map ();
private readonly subscription = new Subscription();
- private searchSubscription?: Subscription;
- private _selected: aas.Referable[] = [];
private shiftKey = false;
private altKey = false;
@@ -90,26 +101,62 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
private readonly webSocketFactory: WebSocketFactoryService,
private readonly clipboard: ClipboardService,
) {
+ effect(() => {
+ const searchText = this.search();
+ if (searchText) {
+ this.searching.start(searchText);
+ }
+ });
+
+ effect(() => {
+ this.store.updateRows(this.document());
+ });
+
+ effect(() => {
+ if (this.state() === 'online') {
+ this.goOnline();
+ } else {
+ this.goOffline();
+ }
+ });
+
+ effect(() => {
+ this.selectedChange.emit(this.store.selectedElements());
+ });
+
+ effect(() => {
+ const matchIndex = this.store.matchIndex();
+ if (matchIndex >= 0) {
+ this.store.expandRow(matchIndex);
+ }
+ });
+
+ effect(() => {
+ const row = this.store.matchRow();
+ if (!row) return;
+ setTimeout(() => {
+ const element = this.dom.getElementById(row.id);
+ element?.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ });
+ });
+
this.window.addEventListener('keyup', this.keyup);
this.window.addEventListener('keydown', this.keydown);
}
- @Input()
- public document: AASDocument | null = null;
+ public readonly document = input(null);
- @Input()
- public state: OnlineState | null = 'offline';
+ public readonly state = input('offline');
- @Input()
- public search: Observable | null = null;
+ public readonly search = input('');
@Input()
public get selected(): aas.Referable[] {
- return this._selected;
+ return this.store.selectedElements();
}
public set selected(values: aas.Referable[]) {
- if (!isEqual(values, this._selected)) {
+ if (!isEqual(values, this.store.selectedElements())) {
this.store.setSelectedElements(values);
}
}
@@ -117,111 +164,48 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
@Output()
public selectedChange = new EventEmitter();
- public get onlineReady(): boolean {
- return this.document?.onlineReady ?? false;
- }
+ public readonly onlineReady = computed(() => this.document()?.onlineReady ?? false);
- public get readonly(): boolean {
- return this.document?.readonly ?? true;
- }
+ public readonly readonly = computed(() => this.document()?.readonly ?? true);
- public get modified(): boolean {
- return this.document?.modified ?? false;
- }
+ public readonly modified = computed(() => this.document()?.modified ?? false);
- public get someSelected(): boolean {
- const rows = this.store.rows;
+ public readonly someSelected = computed(() => {
+ const rows = this.store.rows();
return rows.length > 0 && rows.some(row => row.selected) && !rows.every(row => row.selected);
- }
+ });
- public get everySelected(): boolean {
- const rows = this.store.rows;
+ public readonly everySelected = computed(() => {
+ const rows = this.store.rows();
return rows.length > 0 && rows.every(row => row.selected);
- }
+ });
- public get nodes(): AASTreeRow[] {
- return this.store.nodes;
- }
+ public readonly nodes = this.store.nodes;
- public readonly selectMatchIndex = this.store.selectMatchIndex;
+ public readonly matchIndex = this.store.matchIndex;
- public readonly selectMatchRow = this.store.selectMatchRow;
+ public readonly matchRow = this.store.matchRow;
- public get rows(): AASTreeRow[] {
- return this.store.rows;
- }
+ public readonly rows = this.store.rows;
public ngOnInit(): void {
- this.subscription.add(
- this.store.selectSelectedElements.pipe().subscribe(elements => {
- this._selected = elements;
- this.selectedChange.emit(elements);
- }),
- );
-
- this.subscription.add(
- this.store.selectMatchIndex.pipe().subscribe(index => {
- if (index >= 0) {
- this.store.expandRow(index);
- }
- }),
- );
-
- this.subscription.add(
- this.store.selectMatchRow.pipe().subscribe(row => {
- if (row) {
- setTimeout(() => {
- const element = this.dom.getElementById(row.id);
- element?.scrollIntoView({ block: 'center', behavior: 'smooth' });
- });
- }
- }),
- );
-
this.subscription.add(
this.translate.onLangChange.subscribe(() => {
- this.store.updateRows(this.document);
+ this.store.updateRows(this.document());
}),
);
}
- public ngOnChanges(changes: SimpleChanges): void {
- if (changes['document']) {
- this.store.updateRows(this.document);
- }
-
- if (changes['search']) {
- if (this.searchSubscription) {
- this.searchSubscription.unsubscribe();
- this.searchSubscription = undefined;
- }
-
- if (this.search) {
- this.searchSubscription = this.search.subscribe(value => this.searching.start(value));
- }
- }
-
- const stateChange = changes['state'];
- if (stateChange) {
- if (stateChange.previousValue !== stateChange.currentValue) {
- if (this.state === 'online') {
- this.goOnline();
- } else {
- this.goOffline();
- }
- }
- }
- }
-
public get message(): string {
- if (this.document) {
- if (this.document.content) {
+ const document = this.document();
+ if (document) {
+ if (document.content) {
return '';
}
return stringFormat(
this.translate.instant('INFO_AAS_OFFLINE'),
- new Date(this.document.timestamp).toLocaleString(this.translate.currentLang),
+ new Date(document.timestamp).toLocaleString(this.translate.currentLang),
);
}
@@ -230,9 +214,7 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
public ngOnDestroy(): void {
this.subscription.unsubscribe();
- this.searchSubscription?.unsubscribe();
this.webSocketSubject?.unsubscribe();
- this.searching?.destroy();
this.window.removeEventListener('keyup', this.keyup);
this.window.removeEventListener('keydown', this.keydown);
}
@@ -252,7 +234,7 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
public getValue(node: AASTreeRow): string | boolean | undefined {
- if (this.state === 'online' && node.element.modelType === 'Property') {
+ if (this.state() === 'online' && node.element.modelType === 'Property') {
const property = node.element as aas.Property;
let value: string | boolean;
const item = property.nodeId && this.map.get(property.nodeId);
@@ -293,7 +275,7 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
public async openFile(file: aas.File | undefined): Promise {
- if (!file || !file.value || this.state === 'online') return;
+ if (!file || !file.value || this.state() === 'online') return;
const { name, url } = this.resolveFile(file);
if (name && url) {
@@ -311,7 +293,8 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
public async openBlob(blob: aas.Blob | undefined): Promise {
- if (!blob?.value || !this.document || !blob.parent || this.state === 'online') return;
+ const document = this.document();
+ if (!blob?.value || !document || !blob.parent || this.state() === 'online') return;
const extension = mimeTypeToExtension(blob.contentType);
if (extension) {
@@ -323,8 +306,8 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
await this.showVideoAsync(name, `data:${blob.contentType};base64,${blob.value}`);
}
} else {
- const endpoint = encodeBase64Url(this.document.endpoint);
- const id = encodeBase64Url(this.document.id);
+ const endpoint = encodeBase64Url(document.endpoint);
+ const id = encodeBase64Url(document.id);
const smId = encodeBase64Url(blob.parent.keys[0].value);
const path = getIdShortPath(blob);
const url = `/api/v1/containers/${endpoint}/documents/${id}/submodels/${smId}/blobs/${path}/value`;
@@ -342,7 +325,7 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
public async openOperation(operation: aas.Operation | undefined): Promise {
- if (!operation || this.state === 'online') return;
+ if (!operation || this.state() === 'online') return;
try {
if (operation) {
@@ -359,7 +342,7 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
public openReference(reference: aas.Reference | string | undefined): void {
- if (!reference || this.state === 'online') return;
+ if (!reference || this.state() === 'online') return;
if (typeof reference === 'string') {
this.openDocumentByAssetId(reference);
@@ -377,18 +360,19 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
public openSubmodel(submodel: aas.Submodel | undefined): void {
- if (!submodel || this.state === 'online') return;
+ if (!submodel || this.state() === 'online') return;
const semanticId = resolveSemanticId(submodel);
if (semanticId) {
+ const document = this.document();
const template = supportedSubmodelTemplates.get(semanticId);
- if (template && this.document) {
+ if (template && document) {
const descriptor: SubmodelViewDescriptor = {
template,
submodels: [
{
- id: this.document.id,
- endpoint: this.document.endpoint,
+ id: document.id,
+ endpoint: document.endpoint,
idShort: submodel.idShort,
},
],
@@ -460,7 +444,7 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
private goOnline(): void {
try {
- this.prepareOnline(this.store.rows.filter(row => row.selected));
+ this.prepareOnline(this.store.rows().filter(row => row.selected));
this.play();
} catch (error) {
this.stop();
@@ -472,14 +456,15 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
private play(): void {
- if (this.document) {
+ const document = this.document();
+ if (document) {
this.webSocketSubject = this.webSocketFactory.create();
this.webSocketSubject.subscribe({
next: this.onMessage,
error: this.onError,
});
- this.webSocketSubject.next(this.createMessage(this.document));
+ this.webSocketSubject.next(this.createMessage(document));
}
}
@@ -491,21 +476,19 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
private prepareOnline(rows: AASTreeRow[]): void {
- if (this.document) {
- this.liveNodes.splice(0, this.liveNodes.length);
- this.map.clear();
- for (const row of rows) {
- if (row.selected) {
- const property = row.element as aas.Property;
- if (property.nodeId) {
- this.liveNodes.push({
- nodeId: property.nodeId,
- valueType: property.valueType ?? 'undefined',
- });
-
- const subject = new BehaviorSubject(this.getPropertyValue(property));
- this.map.set(property.nodeId, { property: property, value: subject });
- }
+ this.liveNodes.splice(0, this.liveNodes.length);
+ this.map.clear();
+ for (const row of rows) {
+ if (row.selected) {
+ const property = row.element as aas.Property;
+ if (property.nodeId) {
+ this.liveNodes.push({
+ nodeId: property.nodeId,
+ valueType: property.valueType ?? 'undefined',
+ });
+
+ const subject = new BehaviorSubject(this.getPropertyValue(property));
+ this.map.set(property.nodeId, { property: property, value: subject });
}
}
}
@@ -532,11 +515,12 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
}
private selectModelReference(reference: aas.Reference): void {
- if (!this.document?.content) {
+ const content = this.document()?.content;
+ if (!content) {
return;
}
- const referable = selectReferable(this.document.content, reference);
+ const referable = selectReferable(content, reference);
if (referable) {
this.searching.find(referable);
} else if (reference.keys[0].type === 'AssetAdministrationShell') {
@@ -587,14 +571,15 @@ export class AASTreeComponent implements OnInit, OnChanges, OnDestroy {
private resolveFile(file: aas.File): { url?: string; name?: string } {
const value: { url?: string; name?: string } = {};
- if (this.document?.content && file.value) {
- const submodel = selectSubmodel(this.document.content, file);
+ const document = this.document();
+ if (document?.content && file.value) {
+ const submodel = selectSubmodel(document.content, file);
if (submodel) {
const smId = encodeBase64Url(submodel.id);
const path = getIdShortPath(file);
value.name = basename(file.value);
- const name = encodeBase64Url(this.document.endpoint);
- const id = encodeBase64Url(this.document.id);
+ const name = encodeBase64Url(document.endpoint);
+ const id = encodeBase64Url(document.id);
value.url = `/api/v1/containers/${name}/documents/${id}/submodels/${smId}/submodel-elements/${path}/value`;
}
}
diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree.store.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree.store.ts
index 7af6f9c9..5bb68080 100644
--- a/projects/aas-lib/src/lib/aas-tree/aas-tree.store.ts
+++ b/projects/aas-lib/src/lib/aas-tree/aas-tree.store.ts
@@ -6,10 +6,9 @@
*
*****************************************************************************/
-import { Injectable } from '@angular/core';
+import { Injectable, computed, signal } from '@angular/core';
import { AASTree, AASTreeRow } from './aas-tree-row';
import { AASDocument, aas } from 'common';
-import { BehaviorSubject, Subject, map } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { NotifyService } from '../notify/notify.service';
@@ -27,121 +26,128 @@ export interface SearchTerm {
query?: SearchQuery;
}
+interface AASTreeState {
+ matchIndex: number;
+ rows: AASTreeRow[];
+ nodes: AASTreeRow[];
+}
+
@Injectable()
export class AASTreeStore {
- private readonly terms$ = new BehaviorSubject([]);
- private readonly index$ = new BehaviorSubject(-1);
- private readonly selectedElements = new Subject();
- private _rows: AASTreeRow[] = [];
- private _nodes: AASTreeRow[] = [];
+ private readonly _state = signal({ matchIndex: -1, rows: [], nodes: [] });
public constructor(
private readonly notify: NotifyService,
private readonly translate: TranslateService,
) {}
- public get rows(): AASTreeRow[] {
- return this._rows;
- }
+ public readonly rows = computed(() => this._state().rows);
- public get terms(): SearchTerm[] {
- return this.terms$.getValue();
- }
+ public readonly matchIndex = computed(() => this._state().matchIndex);
- public get index(): number {
- return this.index$.getValue();
- }
+ public readonly nodes = computed(() => this._state().nodes);
- public get nodes(): AASTreeRow[] {
- return this._nodes;
- }
+ public readonly selectedRows = computed(() => this._state().rows.filter(row => row.selected));
- public readonly selectTerms = this.terms$.asObservable();
+ public readonly selectedElements = computed(() =>
+ this._state()
+ .rows.filter(row => row.selected)
+ .map(item => item.element),
+ );
- public get selectSelectedRows(): AASTreeRow[] {
- return this._rows.filter(row => row.selected);
- }
-
- public readonly selectSelectedElements = this.selectedElements.asObservable();
-
- public readonly selectMatchIndex = this.index$.asObservable();
-
- public readonly selectMatchRow = this.index$
- .asObservable()
- .pipe(map(index => (index >= 0 ? this._rows[index] : undefined)));
+ public readonly matchRow = computed(() => {
+ const state = this._state();
+ return state.matchIndex >= 0 ? state.rows[state.matchIndex] : undefined;
+ });
public toggleSelected(row: AASTreeRow, altKey: boolean, shiftKey: boolean): void {
- const tree = new AASTree(this._rows);
+ const tree = new AASTree(this._state().rows);
tree.toggleSelected(row, altKey, shiftKey);
- this._rows = tree.nodes;
- this._nodes = tree.expanded;
- this.selectedElements.next(this._rows.filter(row => row.selected).map(item => item.element));
+ this._state.update(state => ({
+ ...state,
+ rows: tree.nodes,
+ nodes: tree.expanded,
+ }));
}
public toggleSelections(): void {
- const tree = new AASTree(this._rows);
+ const tree = new AASTree(this._state().rows);
tree.toggleSelections();
- this._rows = tree.nodes;
- this._nodes = tree.expanded;
- this.selectedElements.next(this._rows.filter(row => row.selected).map(item => item.element));
+ this._state.update(state => ({
+ ...state,
+ rows: tree.nodes,
+ nodes: tree.expanded,
+ }));
}
public collapse(): void {
- const tree = new AASTree(this._rows);
+ const tree = new AASTree(this._state().rows);
tree.collapse();
- this._rows = tree.nodes;
- this._nodes = tree.expanded;
+ this._state.update(state => ({
+ ...state,
+ rows: tree.nodes,
+ nodes: tree.expanded,
+ }));
}
public collapseRow(row: AASTreeRow): void {
- const tree = new AASTree(this._rows);
+ const tree = new AASTree(this._state().rows);
tree.collapse(row);
- this._rows = tree.nodes;
- this._nodes = tree.expanded;
+ this._state.update(state => ({
+ ...state,
+ rows: tree.nodes,
+ nodes: tree.expanded,
+ }));
}
public expandRow(arg: AASTreeRow | number): void {
- const tree = new AASTree(this._rows);
+ const tree = new AASTree(this._state().rows);
tree.expand(arg);
- this._rows = tree.nodes;
- this._nodes = tree.expanded;
+ this._state.update(state => ({
+ ...state,
+ rows: tree.nodes,
+ nodes: tree.expanded,
+ }));
}
public updateRows(document: AASDocument | null): void {
try {
if (document) {
const tree = AASTree.from(document, this.translate.currentLang);
- this._rows = tree.nodes;
- this._nodes = tree.expanded;
+ this._state.set({
+ matchIndex: -1,
+ rows: tree.nodes,
+ nodes: tree.expanded,
+ });
} else {
- this._rows = [];
- this._nodes = [];
+ this._state.set({
+ matchIndex: -1,
+ rows: [],
+ nodes: [],
+ });
}
-
- this.index$.next(-1);
- this.selectedElements.next([]);
} catch (error) {
this.notify.error(error);
}
}
public setSelectedElements(elements: aas.Referable[]): void {
- const tree = new AASTree(this._rows);
+ const tree = new AASTree(this._state().rows);
tree.selectedElements = elements;
- this._rows = tree.nodes;
- this._nodes = tree.expanded;
- this.selectedElements.next(this._rows.filter(row => row.selected).map(item => item.element));
- }
-
- public setSearchText(terms: SearchTerm[]): void {
- this.terms$.next(terms);
+ this._state.update(state => ({
+ ...state,
+ rows: tree.nodes,
+ nodes: tree.expanded,
+ }));
}
- public setMatchIndex(index: number): void {
- const tree = new AASTree(this._rows);
- tree.highlight(index);
- this._rows = tree.nodes;
- this._nodes = tree.expanded;
- this.index$.next(index);
+ public setMatchIndex(matchIndex: number): void {
+ const tree = new AASTree(this._state().rows);
+ tree.highlight(matchIndex);
+ this._state.set({
+ matchIndex: matchIndex,
+ rows: tree.nodes,
+ nodes: tree.expanded,
+ });
}
}
diff --git a/projects/aas-lib/src/test/aas-tree/aas-tree-search.spec.ts b/projects/aas-lib/src/test/aas-tree/aas-tree-search.spec.ts
index 9486227e..2115de67 100644
--- a/projects/aas-lib/src/test/aas-tree/aas-tree-search.spec.ts
+++ b/projects/aas-lib/src/test/aas-tree/aas-tree-search.spec.ts
@@ -42,10 +42,6 @@ describe('AASTreeSearch', function () {
store.updateRows(sampleDocument);
});
- afterEach(function () {
- search.destroy();
- });
-
it('should create', () => {
expect(search).toBeTruthy();
});
diff --git a/projects/aas-lib/src/test/aas-tree/aas-tree.component.spec.ts b/projects/aas-lib/src/test/aas-tree/aas-tree.component.spec.ts
index c828d730..adaaad27 100644
--- a/projects/aas-lib/src/test/aas-tree/aas-tree.component.spec.ts
+++ b/projects/aas-lib/src/test/aas-tree/aas-tree.component.spec.ts
@@ -7,10 +7,9 @@
*****************************************************************************/
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { SimpleChange } from '@angular/core';
import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { AASDocument, WebSocketData } from 'common';
-import { BehaviorSubject, Subject, first, skipWhile, takeWhile } from 'rxjs';
+import { Subject } from 'rxjs';
import { AASTreeComponent } from '../../lib/aas-tree/aas-tree.component';
import { sampleDocument } from '../assets/sample-document';
import { NotifyService } from '../../lib/notify/notify.service';
@@ -66,12 +65,8 @@ describe('AASTreeComponent', () => {
fixture = TestBed.createComponent(AASTreeComponent);
component = fixture.componentInstance;
+ fixture.componentRef.setInput('document', document);
fixture.detectChanges();
-
- component.document = document;
- component.ngOnChanges({
- document: new SimpleChange(null, document, true),
- });
});
afterEach(() => {
@@ -83,31 +78,31 @@ describe('AASTreeComponent', () => {
});
it('gets the current document', () => {
- expect(component.document).toEqual(document);
+ expect(component.document()).toEqual(document);
});
it('indicates if document is online-ready', () => {
- expect(component.onlineReady).toEqual(document.onlineReady ? document.onlineReady : false);
+ expect(component.onlineReady()).toEqual(document.onlineReady ? document.onlineReady : false);
});
it('indicates if document is read-only', () => {
- expect(component.readonly).toEqual(document.readonly);
+ expect(component.readonly()).toEqual(document.readonly);
});
it('indicates if the document is modified', () => {
- expect(component.modified).toEqual(document.modified ? document.modified : false);
+ expect(component.modified()).toEqual(document.modified ? document.modified : false);
});
it('shows the current offline state', () => {
- expect(component.state).toEqual('offline');
+ expect(component.state()).toEqual('offline');
});
it('indicates if no node is selected', () => {
- expect(component.someSelected).toBeFalse();
+ expect(component.someSelected()).toBeFalse();
});
it('shows the first level ExampleMotor', () => {
- const nodes = component.nodes;
+ const nodes = component.nodes();
expect(nodes).toBeTruthy();
expect(nodes.length).toEqual(5);
expect(nodes[0].element.idShort).toEqual('ExampleMotor');
@@ -121,130 +116,80 @@ describe('AASTreeComponent', () => {
describe('toggleSelection', () => {
it('toggle selection of all rows', () => {
component.toggleSelections();
- expect(component.rows.every(value => value.selected)).toBeTrue();
+ expect(component.rows().every(value => value.selected)).toBeTrue();
});
});
describe('collapse', () => {
it('collapse root element', () => {
- component.collapse(component.nodes[0]);
- expect(component.nodes.length).toEqual(1);
- expect(component.nodes[0].element.idShort).toEqual('ExampleMotor');
- expect(component.nodes[0].expanded).toBeFalse();
+ component.collapse(component.nodes()[0]);
+ expect(component.nodes().length).toEqual(1);
+ expect(component.nodes()[0].element.idShort).toEqual('ExampleMotor');
+ expect(component.nodes()[0].expanded).toBeFalse();
});
it('collapse to initial view', () => {
component.collapse();
- expect(component.nodes.length).toEqual(5);
- expect(component.nodes[0].element.idShort).toEqual('ExampleMotor');
- expect(component.nodes[0].expanded).toBeTrue();
- expect(component.nodes[1].element.idShort).toEqual('Identification');
- expect(component.nodes[2].element.idShort).toEqual('TechnicalData');
- expect(component.nodes[3].element.idShort).toEqual('OperationalData');
- expect(component.nodes[4].element.idShort).toEqual('Documentation');
+ expect(component.nodes().length).toEqual(5);
+ expect(component.nodes()[0].element.idShort).toEqual('ExampleMotor');
+ expect(component.nodes()[0].expanded).toBeTrue();
+ expect(component.nodes()[1].element.idShort).toEqual('Identification');
+ expect(component.nodes()[2].element.idShort).toEqual('TechnicalData');
+ expect(component.nodes()[3].element.idShort).toEqual('OperationalData');
+ expect(component.nodes()[4].element.idShort).toEqual('Documentation');
});
});
describe('expand', () => {
it('expand submodel "Identification"', () => {
- component.expand(component.nodes[1]);
- expect(component.nodes.length).toEqual(9);
- expect(component.nodes[1].element.idShort).toEqual('Identification');
- expect(component.nodes[0].expanded).toBeTrue();
+ component.expand(component.nodes()[1]);
+ expect(component.nodes().length).toEqual(9);
+ expect(component.nodes()[1].element.idShort).toEqual('Identification');
+ expect(component.nodes()[0].expanded).toBeTrue();
});
});
describe('search text "max"', () => {
- let search: BehaviorSubject;
-
- beforeEach(() => {
- search = new BehaviorSubject('');
- component.search = search.asObservable();
- component.ngOnChanges({
- search: new SimpleChange(null, search, true),
- });
- });
-
- it('the search text must be at least three characters long', (done: DoneFn) => {
- const subscription = component.selectMatchRow.pipe().subscribe(row => {
- if (row) {
- expect(row.name).toEqual('MaxRotationSpeed');
- subscription.unsubscribe();
- done();
- }
- });
-
- search.next('z');
- search.next('zy');
- search.next('max');
+ it('the search text must be at least three characters long', () => {
+ fixture.componentRef.setInput('search', 'z');
+ fixture.componentRef.setInput('search', 'zy');
+ fixture.componentRef.setInput('search', 'max');
+ expect(component.matchRow()?.name).toEqual('MaxRotationSpeed');
});
- it('finds the first occurrence of "max" at row 7', (done: DoneFn) => {
- component.selectMatchIndex.pipe(skipWhile(index => index < 0)).subscribe(index => {
- expect(index).toEqual(7);
- done();
- });
-
- search.next('max');
+ it('finds the first occurrence of "max" at row 7', () => {
+ fixture.componentRef.setInput('search', 'max');
+ expect(component.matchIndex()).toEqual(7);
});
- it('finds the next occurrence of "max" at row 8', (done: DoneFn) => {
- search.next('max');
+ it('finds the next occurrence of "max" at row 8', () => {
+ fixture.componentRef.setInput('search', 'max');
component.findNext();
- component.selectMatchIndex.pipe(first()).subscribe(value => {
- expect(value).toEqual(8);
- done();
- });
+ expect(component.matchIndex()).toEqual(8);
});
- it('finds the previous occurrence of "max" at row 25', (done: DoneFn) => {
- search.next('max');
+ it('finds the previous occurrence of "max" at row 25', () => {
+ fixture.componentRef.setInput('search', 'max');
component.findPrevious();
- component.selectMatchIndex.pipe(first()).subscribe(value => {
- expect(value).toEqual(8);
- done();
- });
+ expect(component.matchIndex()).toEqual(8);
});
});
describe('search pattern', () => {
- let search: BehaviorSubject;
-
- beforeEach(() => {
- search = new BehaviorSubject('');
- component.search = search.asObservable();
- component.ngOnChanges({
- search: {
- currentValue: search,
- previousValue: null,
- firstChange: true,
- isFirstChange: () => true,
- },
- });
- });
-
- it('finds the first occurrence of "#prop:max" at row 7', (done: DoneFn) => {
- search.next('#prop:max');
- component.selectMatchIndex.pipe(first()).subscribe(value => {
- expect(value).toEqual(7);
- done();
- });
+ it('finds the first occurrence of "#prop:max" at row 7', () => {
+ fixture.componentRef.setInput('search', '#prop:max');
+ expect(component.matchIndex()).toEqual(7);
});
- it('finds the first occurrence of "#prop:MaxTorque" at row 8', (done: DoneFn) => {
- search.next('#prop:MaxTorque');
- component.selectMatchIndex.pipe(first()).subscribe(value => {
- expect(value).toEqual(8);
- done();
- });
+ it('finds the first occurrence of "#prop:MaxTorque" at row 8', () => {
+ fixture.componentRef.setInput('search', '#prop:MaxTorque');
+ expect(component.matchIndex()).toEqual(8);
});
- it('finds the first occurrence of "#prop:serialnumber=P12345678I40" at row 5', (done: DoneFn) => {
- search.next('#prop:serialnumber=P12345678I40');
- component.selectMatchIndex.pipe(first()).subscribe(value => {
- expect(value).toEqual(5);
- done();
- });
+ it('finds the first occurrence of "#prop:serialnumber=P12345678I40" at row 5', () => {
+ fixture.componentRef.setInput('search', '#prop:serialnumber=P12345678I40');
+ fixture.detectChanges();
+ expect(component.matchIndex()).toEqual(5);
});
});
});
diff --git a/projects/aas-portal/src/app/aas/aas-store.service.ts b/projects/aas-portal/src/app/aas/aas-store.service.ts
index 0a14d0a8..058ad62a 100644
--- a/projects/aas-portal/src/app/aas/aas-store.service.ts
+++ b/projects/aas-portal/src/app/aas/aas-store.service.ts
@@ -6,67 +6,47 @@
*
*****************************************************************************/
-import { Injectable } from '@angular/core';
-import { NotifyService, OnlineState } from 'aas-lib';
+import { Injectable, signal } from '@angular/core';
+import { OnlineState } from 'aas-lib';
import { AASDocument } from 'common';
-import { BehaviorSubject, Observable } from 'rxjs';
import { AASApiService } from './aas-api.service';
@Injectable({
providedIn: 'root',
})
export class AASStoreService {
- private _document: AASDocument | null = null;
- private _state: OnlineState = 'offline';
- private readonly search$ = new BehaviorSubject('');
+ private readonly _document = signal(null);
- public constructor(
- private readonly api: AASApiService,
- private readonly notify: NotifyService,
- ) {}
+ public constructor(private readonly api: AASApiService) {}
- public get document(): AASDocument | null {
- return this._document;
- }
+ public readonly document = this._document.asReadonly();
- public get state(): OnlineState {
- return this._state;
- }
+ public readonly state = signal('offline');
- public get search(): Observable {
- return this.search$.asObservable();
- }
+ public readonly search = signal('');
public getDocumentContent(document: AASDocument): void {
this.api.getContent(document.id, document.endpoint).subscribe({
- next: content => (this._document = { ...document, content }),
- error: () => (this._document = document),
+ next: content => this._document.set({ ...document, content }),
+ error: () => this._document.set(document),
});
}
public getDocument(id: string, endpoint: string): void {
this.api.getDocument(id, endpoint).subscribe({
- next: document => (this._document = document),
+ next: document => this._document.set(document),
});
}
public setDocument(document: AASDocument | null): void {
- this._document = document;
+ this._document.set(document);
}
public applyDocument(document: AASDocument): void {
- this._document = { ...document, modified: true };
+ this._document.set({ ...document, modified: true });
}
public resetModified(document: AASDocument): void {
- this._document = { ...document, modified: false };
- }
-
- public setSearch(value: string): void {
- this.search$.next(value);
- }
-
- public setState(value: 'offline' | 'online'): void {
- this._state = value;
+ this._document.set({ ...document, modified: false });
}
}
diff --git a/projects/aas-portal/src/app/aas/aas.component.html b/projects/aas-portal/src/app/aas/aas.component.html
index 3cd6258b..f9367daf 100644
--- a/projects/aas-portal/src/app/aas/aas.component.html
+++ b/projects/aas-portal/src/app/aas/aas.component.html
@@ -7,11 +7,11 @@
!---------------------------------------------------------------------------->
- @if (document) {
+ @if (document()) {
-
+
@@ -22,35 +22,36 @@
LABEL_ASSET_ID
- {{address}}
- {{idShort}}
- {{id}}
- {{version}}
- {{assetId}}
+ {{address()}}
+ {{idShort()}}
+ {{id()}}
+ {{version()}}
+ {{assetId()}}
}
-
+
-
-
-
+
+ [disabled]="canSynchronize() === false">
@@ -73,7 +74,7 @@
- |