Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('SiDashboardToolbarComponent', () => {
await fixture.whenStable();
fixture.detectChanges();

expect(component.editable(), 'Cancel shall not change editable state').toBe(true);
expect(component.editable()).toBe(false);
});

it('#onSave() shall cancel editable mode and emit save', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,6 @@ export class SiDashboardToolbarComponent {
*/
readonly save = output<void>();

/**
* Emits on cancel button click.
*/
// eslint-disable-next-line @angular-eslint/no-output-native
readonly cancel = output<void>();

protected labelEdit = t(() => $localize`:@@DASHBOARD.EDIT:Edit`);
protected labelCancel = t(() => $localize`:@@DASHBOARD.CANCEL:Cancel`);
protected labelSave = t(() => $localize`:@@DASHBOARD.SAVE:Save`);
Expand Down Expand Up @@ -140,7 +134,7 @@ export class SiDashboardToolbarComponent {

protected onCancel(): void {
if (this.editable()) {
this.cancel.emit();
this.editable.set(false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
[class.d-none]="!isDashboardVisible()"
[primaryEditActions]="(primaryEditActions$ | async) ?? []"
[secondaryEditActions]="(secondaryEditActions$ | async) ?? []"
[editable]="editable()"
[disabled]="(grid.isLoading | async) ?? false"
[hideEditButton]="hideEditButton()"
[showEditButtonLabel]="showEditButtonLabel()"
[grid]="grid"
(editableChange)="grid.edit()"
[(editable)]="editable"
(save)="grid.save()"
(cancel)="grid.cancel()"
>
<ng-content select="[filters-slot]" filters-slot />
</si-dashboard-toolbar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,25 +194,6 @@ describe('SiFlexibleDashboardComponent', () => {
expect(restoreSpy).toHaveBeenCalled();
});

it('should call #grid.edit() on changing editable input to true', () => {
const spy = vi.spyOn(grid, 'edit');
expect(component.editable()).toBe(false);
fixture.componentRef.setInput('editable', true);
fixture.detectChanges();
expect(spy).toHaveBeenCalled();
});

it('should call #grid.cancel() on changing editable input to false', () => {
expect(component.editable()).toBe(false);
grid.editable.set(true);
expect(component.editable()).toBe(true);
const spy = vi.spyOn(grid, 'cancel');

fixture.componentRef.setInput('editable', false);
fixture.detectChanges();
expect(spy).toHaveBeenCalled();
});

it('should emit editableChange events on changing grid editable state', () => {
grid.editable.set(true);
expect(component.editable()).toBe(true);
Expand Down Expand Up @@ -240,12 +221,11 @@ describe('SiFlexibleDashboardComponent', () => {
});

it('should cancel edit state on dashboardId changes', () => {
const spy = vi.spyOn(component.grid(), 'cancel');
fixture.componentRef.setInput('editable', true);

fixture.componentRef.setInput('dashboardId', '1');
fixture.detectChanges();
expect(spy).toHaveBeenCalledTimes(1);
expect(component.editable()).toBe(false);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,21 +238,14 @@ export class SiFlexibleDashboardComponent implements OnInit, OnChanges, OnDestro
dashboard.restore();
}
if (this.editable()) {
this.grid().cancel();
this.editable.set(false);
this.viewState.set('dashboard');
this.catalogHost().clear();
}
this.dashboardId$.next(changes.dashboardId.currentValue);
this.setupMenuItems();
}

if (changes.editable) {
if (changes.editable.currentValue) {
this.grid().edit();
} else {
this.grid().cancel();
}
}
if (changes.hideAddWidgetInstanceButton) {
this.hideAddWidgetInstanceButton$.next(changes.hideAddWidgetInstanceButton.currentValue);
}
Expand All @@ -276,7 +269,7 @@ export class SiFlexibleDashboardComponent implements OnInit, OnChanges, OnDestro
dashboard.restore();
}
if (!this.editable()) {
this.grid().edit();
this.editable.set(true);
}
this.viewState.set('catalog');
const componentType = this.widgetCatalogComponent() ?? SiWidgetCatalogComponent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,26 +100,68 @@ describe('SiGridComponent', () => {
});
});

it('should call edit() on setting editable to true', () => {
const spy = vi.spyOn(component, 'edit');
it('should resetEditState on setting editable to true', () => {
component.transientWidgetInstances = [{ id: '1', widgetId: 'w1' }];
component.markedForRemoval = [{ id: '2', widgetId: 'w2' }];
expect(component.editable()).toBe(false);

fixture.componentRef.setInput('editable', true);
fixture.detectChanges();
expect(spy).toHaveBeenCalled();
expect(component.editable()).toBe(true);
expect(component.transientWidgetInstances).toEqual([]);
expect(component.markedForRemoval).toEqual([]);
});

it('should call cancel() on setting editable to false', () => {
const spy = vi.spyOn(component, 'cancel');
fixture.componentRef.setInput('editable', true);
it('should resetEditState on calling edit()', () => {
component.transientWidgetInstances = [{ id: '1', widgetId: 'w1' }];
component.markedForRemoval = [{ id: '2', widgetId: 'w2' }];
expect(component.editable()).toBe(false);

component.edit();
fixture.detectChanges();
expect(component.editable()).toBe(true);
expect(spy).not.toHaveBeenCalled();
expect(component.transientWidgetInstances).toEqual([]);
expect(component.markedForRemoval).toEqual([]);
});

it('should restoreSavedState on setting editable to false', () => {
vi.useFakeTimers();
fixture.componentRef.setInput('editable', true);

const savedWidgets = [...component.persistedWidgetInstances];
component.addWidgetInstance({ widgetId: 'id' });
expect(component.visibleWidgetInstances$.value.length).toBe(savedWidgets.length + 1);

// Simulate grid modification so restoreSavedState actually restores
fixture.debugElement
.query(By.css('si-gridstack-wrapper'))
.triggerEventHandler('gridEvent', { event: { type: 'added' } });
vi.advanceTimersByTime(0);

fixture.componentRef.setInput('editable', false);
fixture.detectChanges();
expect(spy).toHaveBeenCalled();
expect(component.editable()).toBe(false);
expect(component.visibleWidgetInstances$.value).toEqual(savedWidgets);
vi.useRealTimers();
});

it('should restoreSavedState on calling cancel()', () => {
vi.useFakeTimers();
component.edit();

const savedWidgets = [...component.persistedWidgetInstances];
component.addWidgetInstance({ widgetId: 'id' });
expect(component.visibleWidgetInstances$.value.length).toBe(savedWidgets.length + 1);

// Simulate grid modification so restoreSavedState actually restores
fixture.debugElement
.query(By.css('si-gridstack-wrapper'))
.triggerEventHandler('gridEvent', { event: { type: 'added' } });
vi.advanceTimersByTime(0);

component.cancel();
fixture.detectChanges();
expect(component.visibleWidgetInstances$.value).toEqual(savedWidgets);
vi.useRealTimers();
});

it('#addWidget() shall add a new WidgetConfig to the visible widgets of the grid and assign unique ids', () => {
Expand Down
48 changes: 19 additions & 29 deletions projects/dashboards-ng/src/components/grid/si-grid.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,6 @@ export class SiGridComponent implements OnInit, OnChanges, OnDestroy {
* @defaultValue false
*/
readonly editable = model(false);
/**
* This is the internal owner of the current editable state and is used to track if
* editable or not. Not editable can be changed by either calling the `edit()` api
* method or by setting the `editable` input. When setting the input, the `ngOnChanges(...)`
* hook is used to call the `edit()` method. Similar, to get from editable to not editable,
* `cancel()` or `save()` is used and can be triggered from `ngOnChanges(...)`.
*/
private editableInternal = false;

/**
* An optional, but recommended dashboard id that is used in persistence and passed
Expand Down Expand Up @@ -193,9 +185,9 @@ export class SiGridComponent implements OnInit, OnChanges, OnDestroy {

if (changes.editable) {
if (changes.editable.currentValue) {
this.edit();
this.resetEditState();
} else {
this.cancel();
this.restoreSavedState();
}
}

Expand All @@ -218,13 +210,9 @@ export class SiGridComponent implements OnInit, OnChanges, OnDestroy {
* Set dashboard grid in editable mode to modify widget instances.
*/
edit(): void {
if (!this.editableInternal) {
this.transientWidgetInstances = [];
this.markedForRemoval = [];
this.setModified(false);
this.editableInternal = true;
if (!this.editable()) {
this.editable.set(true);
this.gridService.editable$.next(this.editableInternal);
this.resetEditState();
}
}

Expand All @@ -233,7 +221,7 @@ export class SiGridComponent implements OnInit, OnChanges, OnDestroy {
* changes the editable and isModified modes.
*/
save(): void {
if (!this.editableInternal) {
if (!this.editable()) {
return;
}

Expand All @@ -250,9 +238,7 @@ export class SiGridComponent implements OnInit, OnChanges, OnDestroy {
.subscribe({
next: (value: WidgetConfig[]) => {
this.setModified(false);
this.editableInternal = false;
this.editable.set(false);
this.gridService.editable$.next(this.editableInternal);
this.isLoading.next(false);
},
error: (err: any) => {
Comment thread
chintankavathia marked this conversation as resolved.
Expand All @@ -266,19 +252,10 @@ export class SiGridComponent implements OnInit, OnChanges, OnDestroy {
* Cancel current changes and restore last saved state.
*/
cancel(): void {
if (!this.editableInternal) {
return;
}

if (this.modified) {
this.visibleWidgetInstances$.next([...this.persistedWidgetInstances]);
this.setModified(false);
}
this.editableInternal = false;
if (this.editable()) {
this.editable.set(false);
this.restoreSavedState();
}
this.gridService.editable$.next(this.editableInternal);
}

/**
Expand Down Expand Up @@ -440,6 +417,19 @@ export class SiGridComponent implements OnInit, OnChanges, OnDestroy {
return widgets || [];
}

private resetEditState(): void {
this.transientWidgetInstances = [];
this.markedForRemoval = [];
this.setModified(false);
}

private restoreSavedState(): void {
if (this.modified) {
this.visibleWidgetInstances$.next([...this.persistedWidgetInstances]);
this.setModified(false);
}
}

private setModified(modified: boolean): void {
if (this.modified !== modified) {
this.modified = modified;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export class SiGridstackWrapperComponent implements OnInit, OnChanges {
const componentRef = this.gridstackContainer()!.createComponent(SiWidgetHostComponent, {
bindings: [
inputBinding('widgetConfig', configSignal),
inputBinding('editable', this.editable),
outputBinding<string>('remove', widgetId => {
this.widgetInstanceRemove.emit(widgetId);
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="grid-stack-item-content p-4">
@if (editable$ | async) {
@if (editable()) {
<div class="resize-handle">
<svg
width="24"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { NgTemplateOutlet } from '@angular/common';
import {
Component,
ComponentRef,
Expand Down Expand Up @@ -37,7 +37,7 @@ import { setupWidgetInstance } from '../../widget-loader';

@Component({
selector: 'si-widget-host',
imports: [SiDashboardCardComponent, AsyncPipe, NgTemplateOutlet],
imports: [SiDashboardCardComponent, NgTemplateOutlet],
templateUrl: './si-widget-host.component.html',
styleUrl: './si-widget-host.component.scss',
host: {
Expand All @@ -53,6 +53,13 @@ export class SiWidgetHostComponent implements OnInit, OnChanges {

readonly widgetConfig = input.required<WidgetConfig>();

/**
* Sets the widget host into editable mode.
*
* @defaultValue false
*/
readonly editable = input(false);

readonly remove = output<string>();
readonly edit = output<WidgetConfig>();

Expand Down Expand Up @@ -86,8 +93,6 @@ export class SiWidgetHostComponent implements OnInit, OnChanges {
secondaryActions: (MenuItemLegacy | MenuItem)[] = [];
/** @defaultValue 'expanded' */
actionBarViewType: ViewType = 'expanded';
editable$ = this.gridService.editable$;

/** @defaultValue [] */
editablePrimaryActions: (MenuItemLegacy | ContentActionBarMainItem)[] = [];
/** @defaultValue [] */
Expand Down Expand Up @@ -148,13 +153,14 @@ export class SiWidgetHostComponent implements OnInit, OnChanges {
}
}
}

if (changes.editable && !changes.editable.firstChange) {
this.setupEditable(this.editable());
}
}

ngOnInit(): void {
this.attachWidgetInstance();
this.editable$
.pipe<boolean>(takeUntilDestroyed(this.destroyRef))
.subscribe(editable => this.setupEditable(editable));
}

private attachWidgetInstance(): void {
Expand All @@ -175,17 +181,15 @@ export class SiWidgetHostComponent implements OnInit, OnChanges {
// to the DOM.
this.widgetInstance.configChange
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(event =>
setTimeout(() => this.setupEditable(this.editable$.value, event))
);
.subscribe(event => setTimeout(() => this.setupEditable(this.editable(), event)));
}
if (isSignal(this.widgetInstance.config)) {
this.widgetRef.setInput('config', this.widgetConfig());
} else {
this.widgetInstance.config = this.widgetConfig();
}
this.widgetInstanceFooter = this.widgetInstance.footer;
this.setupEditable(this.gridService.editable$.value);
this.setupEditable(this.editable());
},
error: error => console.error('Error: ', error)
Comment thread
chintankavathia marked this conversation as resolved.
});
Expand Down
Loading
Loading