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
2 changes: 1 addition & 1 deletion docs/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ interface CardConfig {
```

For sections, `w` controls the column span while height is determined by the section content.
For cards, `w` and `h` control the initial rendered grid span. `x` and `y` persist the loose-card position reported by drag and drop when edit mode is saved. `minH`/`minW` and `maxH`/`maxW` set hard resize bounds enforced by the grid — the user cannot drag a card below the minimum or above the maximum size in edit mode.
For cards, `w` and `h` control the initial rendered grid span. When edit mode is saved, `x`, `y`, `w`, and `h` are all persisted in the `saved` event payload — resizing a card updates its dimensions and dragging updates its position. `minH`/`minW` and `maxH`/`maxW` set hard resize bounds enforced by the grid — the user cannot drag a card below the minimum or above the maximum size in edit mode.

`component` and `type` work together to determine how the card is rendered:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
/>
}
</div>
<gridstack #grid [options]="gridOptions()" (changeCB)="onOrderChange($event)">
<gridstack #grid [options]="gridOptions()" (removedCB)="onGridChange($event)" (addedCB)="onGridChange($event)" (changeCB)="onGridChange($event)">
@for (card of looseCards(); track card.id) {
<gridstack-item
[options]="card"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ describe('Dashboard', () => {
() => ({
gridstackItems: {
toArray: () => [
{ options: { id: 'card-1', x: 4, y: 2 } },
{ options: { id: 'card-2', x: 1, y: 3 } },
{ options: { id: 'card-1', x: 4, y: 2, w: 6, h: 20 } },
{ options: { id: 'card-2', x: 1, y: 3, w: 4, h: 10 } },
],
},
});
Expand All @@ -184,8 +184,8 @@ describe('Dashboard', () => {
expect(
(component as unknown as { cardsSnapshot: CardConfig[] }).cardsSnapshot,
).toEqual(cards);
expect(component.cardsPosition.get('card-1')).toEqual({ x: 4, y: 2 });
expect(component.cardsPosition.get('card-2')).toEqual({ x: 1, y: 3 });
expect(component.cardsPosition.get('card-1')).toEqual({ x: 4, y: 2, w: 6, h: 20 });
expect(component.cardsPosition.get('card-2')).toEqual({ x: 1, y: 3, w: 4, h: 10 });
});

it('emits the saved payload and persists the latest order on save', () => {
Expand All @@ -198,19 +198,37 @@ describe('Dashboard', () => {
component.cards.set(cards);
component.editMode.set(true);
component.saved.subscribe((value) => emitted.push(value));
component.onOrderChange({
nodes: [{ id: 'card-1', x: 7, y: 5 }],
component.onGridChange({
nodes: [{ id: 'card-1', x: 7, y: 5, w: 8, h: 30 }],
} as never);

component.saveEdit();

expect(emitted).toEqual([
{
sections,
cards: [{ id: 'card-1', component: 'mfp-a', x: 7, y: 5 }],
cards: [{ id: 'card-1', component: 'mfp-a', x: 7, y: 5, w: 8, h: 30 }],
},
]);
expect(component.cardsPosition.get('card-1')).toEqual({ x: 7, y: 5 });
expect(component.cardsPosition.get('card-1')).toEqual({ x: 7, y: 5, w: 8, h: 30 });
expect(component.editMode()).toBe(false);
});

it('emits updated w and h in the saved payload when a card is resized', () => {
const { component } = setup();
const cards: CardConfig[] = [{ id: 'card-1', component: 'mfp-a', w: 6, h: 20 }];
const emitted: { sections: SectionConfig[]; cards: CardConfig[] }[] = [];

component.cards.set(cards);
component.editMode.set(true);
component.saved.subscribe((value) => emitted.push(value));
component.onGridChange({
nodes: [{ id: 'card-1', x: 0, y: 0, w: 3, h: 10 }],
} as never);

component.saveEdit();

expect(emitted[0].cards[0]).toMatchObject({ id: 'card-1', w: 3, h: 10 });
expect(component.editMode()).toBe(false);
});

Expand Down Expand Up @@ -329,7 +347,7 @@ describe('Dashboard', () => {
component.cards.set(cards);
component.editMode.set(true);
component.saved.subscribe(() => false);
component.onOrderChange({
component.onGridChange({
nodes: [{ id: 'card-1', x: 1, y: 2 }],
} as never);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ export class Dashboard implements OnInit, OnDestroy {
return (sectionId: string) => all.filter((c) => c.sectionId === sectionId);
});

cardsPosition = new Map<string, { x?: number; y?: number }>();
cardsPosition = new Map<
string,
{ x?: number; y?: number; w?: number; h?: number }
>();
looseCards = linkedSignal(() => this.cards().filter((c) => !c.sectionId));

private newGridStackNodes: GridStackNode[] = [];
Expand Down Expand Up @@ -186,7 +189,13 @@ export class Dashboard implements OnInit, OnDestroy {
sections: this.sections(),
cards: this.cards().map((c) => {
const pos = this.cardsPosition.get(c.id);
return { ...c, x: pos?.x, y: pos?.y };
return {
...c,
x: pos?.x,
y: pos?.y,
w: pos?.w ?? c.w,
h: pos?.h ?? c.h,
};
}),
});
this.editMode.set(false);
Expand Down Expand Up @@ -230,14 +239,19 @@ export class Dashboard implements OnInit, OnDestroy {
this.closeCardPanel();
}

onOrderChange(event: nodesCB): void {
onGridChange(event: nodesCB): void {
this.newGridStackNodes = event.nodes;
}

private saveCardsPosition(items: GridStackNode[]): void {
items.forEach((node) => {
if (node.id) {
this.cardsPosition.set(node.id, { x: node.x, y: node.y });
this.cardsPosition.set(node.id, {
x: node.x,
y: node.y,
w: node.w,
h: node.h,
});
}
});
}
Expand Down
3 changes: 2 additions & 1 deletion projects/ngx/declarative-ui/stories/dashboard.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const meta: Meta<Dashboard> = {
cards: { control: 'object' },
availableCards: { control: 'object' },
actionButtonClick: { action: 'actionButtonClick' },
saved: { action: 'saved' },
},
args: {
config: SAMPLE_CONFIG,
Expand All @@ -184,7 +185,7 @@ const meta: Meta<Dashboard> = {
},
render: (args) => ({
props: args,
template: `<mfp-dashboard [config]="config" [sections]="sections" [cards]="cards" [availableCards]="availableCards" (actionButtonClick)="actionButtonClick($event)" />`,
template: `<mfp-dashboard [config]="config" [sections]="sections" [cards]="cards" [availableCards]="availableCards" (actionButtonClick)="actionButtonClick($event)" (saved)="saved($event)" />`,
}),
};

Expand Down