Skip to content
Merged
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
15 changes: 15 additions & 0 deletions packages/dashboard/src/vaadin-dashboard.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,26 @@ export type DashboardItemResizedEvent<TItem extends DashboardItem> = CustomEvent
items: Array<TItem | DashboardSectionItem<TItem>>;
}>;

/**
* Fired before an item is removed. Calling preventDefault() on the event will cancel the removal.
*/
export type DashboardItemBeforeRemoveEvent<TItem extends DashboardItem> = CustomEvent<{
item: TItem | DashboardSectionItem<TItem>;

items: Array<TItem | DashboardSectionItem<TItem>>;

section: DashboardSectionItem<TItem> | undefined;
}>;

/**
* Fired when an item was removed
*/
export type DashboardItemRemovedEvent<TItem extends DashboardItem> = CustomEvent<{
item: TItem | DashboardSectionItem<TItem>;

items: Array<TItem | DashboardSectionItem<TItem>>;

section: DashboardSectionItem<TItem> | undefined;
}>;

/**
Expand Down Expand Up @@ -123,6 +136,8 @@ export interface DashboardCustomEventMap<TItem extends DashboardItem> {

'dashboard-item-resized': DashboardItemResizedEvent<TItem>;

'dashboard-item-before-remove': DashboardItemBeforeRemoveEvent<TItem>;

'dashboard-item-removed': DashboardItemRemovedEvent<TItem>;

'dashboard-item-selected-changed': DashboardItemSelectedChangedEvent<TItem>;
Expand Down
26 changes: 23 additions & 3 deletions packages/dashboard/src/vaadin-dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import { WidgetResizeController } from './widget-resize-controller.js';
*
* @fires {CustomEvent} dashboard-item-moved - Fired when an item was moved
* @fires {CustomEvent} dashboard-item-resized - Fired when an item was resized
* @fires {CustomEvent} dashboard-item-before-remove - Fired before an item is removed. Calling preventDefault() on the event will cancel the removal.
* @fires {CustomEvent} dashboard-item-removed - Fired when an item was removed
* @fires {CustomEvent} dashboard-item-selected-changed - Fired when an item selected state changed
* @fires {CustomEvent} dashboard-item-move-mode-changed - Fired when an item move mode changed
Expand Down Expand Up @@ -427,12 +428,25 @@ class Dashboard extends DashboardLayoutMixin(
e.stopImmediatePropagation();
const item = getElementItem(e.target);
const items = getItemsArrayOfItem(item, this.items);
const section = this.items.find((i) => i.items && i.items.includes(item));

// Fire before-remove event
const beforeRemoveEvent = new CustomEvent('dashboard-item-before-remove', {
cancelable: true,
detail: { item, items: this.items, section },
});
this.dispatchEvent(beforeRemoveEvent);

// Check if removal was prevented
if (beforeRemoveEvent.defaultPrevented) {
return;
}

// Proceed with removal
items.splice(items.indexOf(item), 1);
this.items = [...this.items];
this.toggleAttribute('item-selected', false);
this.dispatchEvent(
new CustomEvent('dashboard-item-removed', { cancelable: true, detail: { item, items: this.items } }),
);
this.dispatchEvent(new CustomEvent('dashboard-item-removed', { detail: { item, items: this.items, section } }));
}

/** @private */
Expand Down Expand Up @@ -516,6 +530,12 @@ class Dashboard extends DashboardLayoutMixin(
* @event dashboard-item-resized
*/

/**
* Fired before an item is removed
*
* @event dashboard-item-before-remove
*/

/**
* Fired when an item was removed
*
Expand Down
89 changes: 89 additions & 0 deletions packages/dashboard/test/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,74 @@ describe('dashboard', () => {
expect(dashboard.items).to.eql([{ id: '0' }]);
});

it('should dispatch a dashboard-item-before-remove event before removal', () => {
const spy = sinon.spy();
dashboard.addEventListener('dashboard-item-before-remove', spy);
const widget = getElementFromCell(dashboard, 0, 1);
getRemoveButton(widget as DashboardWidget).click();
expect(spy).to.be.calledOnce;
expect(spy.firstCall.args[0].detail.item).to.eql({ id: '1' });
expect(spy.firstCall.args[0].detail.items).to.eql([{ id: '0' }]); // contains the state after removal
expect(spy.firstCall.args[0].detail.section).to.be.undefined;
});

it('should include section in dashboard-item-before-remove event for nested items', async () => {
const sectionItem: DashboardSectionItem<TestDashboardItem> = {
title: 'Section',
items: [{ id: '2' }, { id: '3' }],
};
dashboard.items = [{ id: '0' }, { id: '1' }, sectionItem];
await updateComplete(dashboard);

const spy = sinon.spy();
dashboard.addEventListener('dashboard-item-before-remove', spy);
const widget = getElementFromCell(dashboard, 1, 0);
const section = widget?.closest('vaadin-dashboard-section');
expect(section).to.be.ok;
getRemoveButton(widget as DashboardWidget).click();

expect(spy).to.be.calledOnce;
expect(spy.firstCall.args[0].detail.item).to.eql({ id: '2' });
expect(spy.firstCall.args[0].detail.section).to.equal(sectionItem);
});

it('should cancel removal when preventDefault is called on dashboard-item-before-remove', () => {
const originalItems = dashboard.items;
dashboard.addEventListener('dashboard-item-before-remove', (e) => {
e.preventDefault();
});
const widget = getElementFromCell(dashboard, 0, 1);
getRemoveButton(widget as DashboardWidget).click();
expect(dashboard.items).to.equal(originalItems);
expect(dashboard.items).to.eql([{ id: '0' }, { id: '1' }]);
});

it('should not fire dashboard-item-removed when dashboard-item-before-remove is prevented', () => {
const beforeRemoveSpy = sinon.spy();
const removedSpy = sinon.spy();
dashboard.addEventListener('dashboard-item-before-remove', (e) => {
beforeRemoveSpy();
e.preventDefault();
});
dashboard.addEventListener('dashboard-item-removed', removedSpy);
const widget = getElementFromCell(dashboard, 0, 1);
getRemoveButton(widget as DashboardWidget).click();
expect(beforeRemoveSpy).to.be.calledOnce;
expect(removedSpy).to.not.be.called;
});

it('should fire both events in correct order when not prevented', () => {
const beforeRemoveSpy = sinon.spy();
const removedSpy = sinon.spy();
dashboard.addEventListener('dashboard-item-before-remove', beforeRemoveSpy);
dashboard.addEventListener('dashboard-item-removed', removedSpy);
const widget = getElementFromCell(dashboard, 0, 1);
getRemoveButton(widget as DashboardWidget).click();
expect(beforeRemoveSpy).to.be.calledOnce;
expect(removedSpy).to.be.calledOnce;
expect(beforeRemoveSpy).to.have.been.calledBefore(removedSpy);
});

it('should dispatch an dashboard-item-removed event', () => {
const spy = sinon.spy();
dashboard.addEventListener('dashboard-item-removed', spy);
Expand All @@ -115,6 +183,27 @@ describe('dashboard', () => {
expect(spy).to.be.calledOnce;
expect(spy.firstCall.args[0].detail.item).to.eql({ id: '1' });
expect(spy.firstCall.args[0].detail.items).to.eql([{ id: '0' }]);
expect(spy.firstCall.args[0].detail.section).to.be.undefined;
});

it('should include section in dashboard-item-removed event for nested items', async () => {
const sectionItem: DashboardSectionItem<TestDashboardItem> = {
title: 'Section',
items: [{ id: '2' }, { id: '3' }],
};
dashboard.items = [{ id: '0' }, { id: '1' }, sectionItem];
await updateComplete(dashboard);

const spy = sinon.spy();
dashboard.addEventListener('dashboard-item-removed', spy);
const widget = getElementFromCell(dashboard, 1, 0);
const section = widget?.closest('vaadin-dashboard-section');
expect(section).to.be.ok;
getRemoveButton(widget as DashboardWidget).click();

expect(spy).to.be.calledOnce;
expect(spy.firstCall.args[0].detail.item).to.eql({ id: '2' });
expect(spy.firstCall.args[0].detail.section).to.equal(sectionItem);
});

it('should not dispatch an item-remove event', async () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/dashboard/test/typings/dashboard.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Dashboard,
DashboardI18n,
DashboardItem,
DashboardItemBeforeRemoveEvent,
DashboardItemMovedEvent,
DashboardItemMoveModeChangedEvent,
DashboardItemRemovedEvent,
Expand Down Expand Up @@ -90,11 +91,20 @@ narrowedDashboard.addEventListener('dashboard-item-resized', (event) => {
assertType<Array<TestDashboardItem | DashboardSectionItem<TestDashboardItem>>>(event.detail.items);
});

narrowedDashboard.addEventListener('dashboard-item-before-remove', (event) => {
assertType<DashboardItemBeforeRemoveEvent<TestDashboardItem>>(event);
assertType<TestDashboardItem>(event.detail.item as TestDashboardItem);
assertType<DashboardSectionItem<TestDashboardItem>>(event.detail.item as DashboardSectionItem<TestDashboardItem>);
assertType<Array<TestDashboardItem | DashboardSectionItem<TestDashboardItem>>>(event.detail.items);
assertType<DashboardSectionItem<TestDashboardItem> | undefined>(event.detail.section);
});

narrowedDashboard.addEventListener('dashboard-item-removed', (event) => {
assertType<DashboardItemRemovedEvent<TestDashboardItem>>(event);
assertType<TestDashboardItem>(event.detail.item as TestDashboardItem);
assertType<DashboardSectionItem<TestDashboardItem>>(event.detail.item as DashboardSectionItem<TestDashboardItem>);
assertType<Array<TestDashboardItem | DashboardSectionItem<TestDashboardItem>>>(event.detail.items);
assertType<DashboardSectionItem<TestDashboardItem> | undefined>(event.detail.section);
});

narrowedDashboard.addEventListener('dashboard-item-selected-changed', (event) => {
Expand Down