Merged PR 1972: hotfix(remission-list-item, remission-list-empty-state): improve empty state...

hotfix(remission-list-item, remission-list-empty-state): improve empty state logic and cleanup selected items on destroy

Refactor empty state display conditions in remission-list-empty-state component
to correctly handle search term validation. Move hasValidSearchTerm check to
parent condition to prevent displaying empty states during active searches.

Add ngOnDestroy lifecycle hook to remission-list-item component to properly
clean up selected quantities from the store when items are removed from the list.
This prevents memory leaks and ensures the store state remains synchronized with
the displayed items.

Changes:
- Move hasValidSearchTerm check in displayEmptyState computed signal to improve
  empty state display logic
- Implement OnDestroy interface in RemissionListItemComponent
- Add removeItem call in ngOnDestroy to clean up store state
- Add corresponding unit tests for the cleanup behavior

Ref: #5387
This commit is contained in:
Nino Righi
2025-10-17 12:09:55 +00:00
committed by Lorenz Hilpert
parent a086111ab5
commit 4c56f394c5
4 changed files with 98 additions and 14 deletions

View File

@@ -49,7 +49,7 @@ export class RemissionListEmptyStateComponent {
* @remarks This logic ensures that the most relevant empty state is shown to the user based on their current context.
*/
displayEmptyState = computed<EmptyState>(() => {
if (!this.listFetching()) {
if (!this.listFetching() && !this.hasValidSearchTerm()) {
// Prio 1: Abteilungsremission - Es ist noch keine Abteilung ausgewählt
if (
this.isDepartment() &&
@@ -65,7 +65,6 @@ export class RemissionListEmptyStateComponent {
// Prio 2: Liste abgearbeitet und keine Artikel mehr vorhanden
if (
!this.hasValidSearchTerm() &&
this.hits() === 0 &&
this.isReloadSearch()
) {
@@ -77,7 +76,7 @@ export class RemissionListEmptyStateComponent {
}
// Prio 3: Keine Ergebnisse bei leerem Suchbegriff (nur Filter gesetzt)
if (!this.hasValidSearchTerm() && this.hits() === 0) {
if (this.hits() === 0) {
return {
title: 'Keine Suchergebnisse',
description:

View File

@@ -46,6 +46,7 @@ jest.mock('@isa/remission/data-access', () => ({
// Mock the RemissionStore
const mockRemissionStore = {
selectedQuantity: signal({}),
removeItem: jest.fn(),
};
describe('RemissionListItemComponent', () => {
@@ -112,6 +113,7 @@ describe('RemissionListItemComponent', () => {
// Reset mocks before each test
jest.clearAllMocks();
mockRemissionStore.selectedQuantity.set({});
mockRemissionStore.removeItem.mockClear();
// Reset the mocked functions to return default values
const {
@@ -712,4 +714,37 @@ describe('RemissionListItemComponent', () => {
});
});
});
describe('ngOnDestroy', () => {
it('should remove item from store when component is destroyed', () => {
// Arrange
const mockItem = createMockReturnItem({ id: 123 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
// Act
component.ngOnDestroy();
// Assert
expect(mockRemissionStore.removeItem).toHaveBeenCalledWith(123);
expect(mockRemissionStore.removeItem).toHaveBeenCalledTimes(1);
});
it('should not call removeItem when item has no id', () => {
// Arrange
const mockItem = createMockReturnItem({ id: undefined });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
// Act
component.ngOnDestroy();
// Assert
expect(mockRemissionStore.removeItem).not.toHaveBeenCalled();
});
});
});

View File

@@ -4,6 +4,7 @@ import {
computed,
inject,
input,
OnDestroy,
output,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
@@ -60,7 +61,7 @@ import { LabelComponent, Labeltype } from '@isa/ui/label';
LabelComponent,
],
})
export class RemissionListItemComponent {
export class RemissionListItemComponent implements OnDestroy {
/**
* Type of label to display for the item.
* Defaults to 'tag', can be changed to 'notice' or other types as needed.
@@ -219,4 +220,16 @@ export class RemissionListItemComponent {
const attempts = this.item()?.impediment?.attempts;
return `${comment}${attempts ? ` (${attempts})` : ''}`;
});
/**
* Cleans up the selected item from the store when the component is destroyed.
* Removes the item using its ID.
* @returns void
*/
ngOnDestroy(): void {
const itemId = this.item()?.id;
if (itemId) {
this.#store.removeItem(itemId);
}
}
}

57
package-lock.json generated
View File

@@ -1095,6 +1095,18 @@
"linux"
]
},
"node_modules/@angular/build/node_modules/@types/node": {
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz",
"integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.14.0"
}
},
"node_modules/@angular/build/node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1324,7 +1336,6 @@
"version": "20.1.2",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.1.2.tgz",
"integrity": "sha512-NMSDavN+CJYvSze6wq7DpbrUA/EqiAD7GQoeJtuOknzUpPlWQmFOoHzTMKW+S34XlNEw+YQT0trv3DKcrE+T/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "7.28.0",
@@ -11920,6 +11931,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/retry": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
@@ -14657,7 +14679,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
@@ -15132,7 +15153,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -16350,7 +16370,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.2"
@@ -19062,7 +19082,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -27562,6 +27582,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -27601,7 +27632,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
@@ -27656,7 +27686,6 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/regenerate": {
@@ -28638,7 +28667,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/sass": {
@@ -29179,7 +29208,6 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -31717,7 +31745,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -31777,6 +31805,15 @@
"node": "*"
}
},
"node_modules/undici-types": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",