Merged PR 1899: feat(empty-state): enhance empty state component with new appearance options...

feat(empty-state): enhance empty state component with new appearance options and integration in remission details

Related work items: #5232
This commit is contained in:
Lorenz Hilpert
2025-07-30 08:54:09 +00:00
committed by Nino Righi
parent 239ab52890
commit b39abe630d
13 changed files with 194 additions and 309 deletions

View File

@@ -1,10 +1,16 @@
import { argsToTemplate, type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
import { EmptyStateComponent } from '@isa/ui/empty-state';
import {
argsToTemplate,
type Meta,
type StoryObj,
moduleMetadata,
} from '@storybook/angular';
import { EmptyStateAppearance, EmptyStateComponent } from '@isa/ui/empty-state';
import { ButtonComponent } from '@isa/ui/buttons';
type EmptyStateComponentInputs = {
title: string;
description: string;
appearance: EmptyStateAppearance;
};
const meta: Meta<EmptyStateComponentInputs> = {
@@ -22,6 +28,10 @@ const meta: Meta<EmptyStateComponentInputs> = {
description: {
control: 'text',
},
appearance: {
control: 'select',
options: Object.values(EmptyStateAppearance),
},
},
render: (args) => ({
props: args,
@@ -40,5 +50,6 @@ export const Default: Story = {
args: {
title: 'Keine Suchergebnisse',
description: 'Suchen Sie nach einer Rechnungsnummer oder Kundennamen.',
appearance: EmptyStateAppearance.NoResults,
},
};

View File

@@ -130,7 +130,10 @@ describe('RemissionReturnReceiptService', () => {
of({ result: mockReturns, error: null }),
);
const result = await service.fetchRemissionReturnReceipts();
const result = await service.fetchRemissionReturnReceipts({
returncompleted: true,
start: new Date(),
});
expect(result).toEqual(mockReturns);
expect(mockRemissionStockService.fetchAssignedStock).toHaveBeenCalledWith(
@@ -139,7 +142,7 @@ describe('RemissionReturnReceiptService', () => {
expect(mockReturnService.ReturnQueryReturns).toHaveBeenCalledWith({
stockId: 123,
queryToken: {
input: { returncompleted: 'true' },
filter: { returncompleted: 'true' },
start: expect.any(String),
eagerLoading: 3,
},
@@ -151,7 +154,7 @@ describe('RemissionReturnReceiptService', () => {
of({ result: mockReturns, error: null }),
);
await service.fetchRemissionReturnReceipts();
await service.fetchRemissionReturnReceipts({ returncompleted: true });
const callArgs = mockReturnService.ReturnQueryReturns.mock.calls[0][0];
const startDate = new Date(callArgs.queryToken.start);
@@ -169,7 +172,10 @@ describe('RemissionReturnReceiptService', () => {
of({ result: mockReturns, error: null }),
);
await service.fetchRemissionReturnReceipts(abortController.signal);
await service.fetchRemissionReturnReceipts(
{ returncompleted: true },
abortController.signal,
);
expect(mockRemissionStockService.fetchAssignedStock).toHaveBeenCalledWith(
abortController.signal,
@@ -181,9 +187,9 @@ describe('RemissionReturnReceiptService', () => {
const errorResponse = { error: 'API Error', result: null };
mockReturnService.ReturnQueryReturns.mockReturnValue(of(errorResponse));
await expect(service.fetchRemissionReturnReceipts()).rejects.toThrow(
ResponseArgsError,
);
await expect(
service.fetchRemissionReturnReceipts({ returncompleted: true }),
).rejects.toThrow(ResponseArgsError);
});
it('should return empty array when result is null', async () => {
@@ -191,7 +197,9 @@ describe('RemissionReturnReceiptService', () => {
of({ result: null, error: null }),
);
const result = await service.fetchRemissionReturnReceipts();
const result = await service.fetchRemissionReturnReceipts({
returncompleted: true,
});
expect(result).toEqual([]);
});
@@ -199,7 +207,9 @@ describe('RemissionReturnReceiptService', () => {
it('should return empty array when result is undefined', async () => {
mockReturnService.ReturnQueryReturns.mockReturnValue(of({ error: null }));
const result = await service.fetchRemissionReturnReceipts();
const result = await service.fetchRemissionReturnReceipts({
returncompleted: true,
});
expect(result).toEqual([]);
});
@@ -209,158 +219,13 @@ describe('RemissionReturnReceiptService', () => {
new Error('Stock error'),
);
await expect(service.fetchRemissionReturnReceipts()).rejects.toThrow(
'Stock error',
);
await expect(
service.fetchRemissionReturnReceipts({ returncompleted: true }),
).rejects.toThrow('Stock error');
expect(mockReturnService.ReturnQueryReturns).not.toHaveBeenCalled();
});
});
// describe('fetchIncompletedRemissionReturnReceipts', () => {
// beforeEach(() => {
// mockRemissionStockService.fetchAssignedStock.mockResolvedValue(mockStock);
// });
// it('should fetch incompleted return receipts successfully', async () => {
// mockReturnService.ReturnQueryReturns.mockReturnValue(
// of({ result: mockReturns, error: null }),
// );
// const result = await service.fetchIncompletedRemissionReturnReceipts();
// expect(result).toEqual(mockReturns);
// expect(mockRemissionStockService.fetchAssignedStock).toHaveBeenCalledWith(
// undefined,
// );
// expect(mockReturnService.ReturnQueryReturns).toHaveBeenCalledWith({
// stockId: 123,
// queryToken: {
// input: { returncompleted: 'false' },
// start: expect.any(String),
// eagerLoading: 3,
// },
// });
// });
// it('should use correct date range (7 days ago)', async () => {
// mockReturnService.ReturnQueryReturns.mockReturnValue(
// of({ result: mockReturns, error: null }),
// );
// await service.fetchIncompletedRemissionReturnReceipts();
// const callArgs = mockReturnService.ReturnQueryReturns.mock.calls[0][0];
// const startDate = new Date(callArgs.queryToken.start);
// const expectedDate = subDays(new Date(), 7);
// // Check that dates are within 1 second of each other (to handle timing differences)
// expect(
// Math.abs(startDate.getTime() - expectedDate.getTime()),
// ).toBeLessThan(1000);
// });
// it('should handle abort signal', async () => {
// const abortController = new AbortController();
// mockReturnService.ReturnQueryReturns.mockReturnValue(
// of({ result: mockReturns, error: null }),
// );
// await service.fetchIncompletedRemissionReturnReceipts(
// abortController.signal,
// );
// expect(mockRemissionStockService.fetchAssignedStock).toHaveBeenCalledWith(
// abortController.signal,
// );
// expect(mockReturnService.ReturnQueryReturns).toHaveBeenCalled();
// });
// it('should throw ResponseArgsError when API returns error', async () => {
// const errorResponse = { error: 'API Error', result: null };
// mockReturnService.ReturnQueryReturns.mockReturnValue(of(errorResponse));
// await expect(
// service.fetchIncompletedRemissionReturnReceipts(),
// ).rejects.toThrow(ResponseArgsError);
// });
// it('should return empty array when result is null', async () => {
// mockReturnService.ReturnQueryReturns.mockReturnValue(
// of({ result: null, error: null }),
// );
// const result = await service.fetchIncompletedRemissionReturnReceipts();
// expect(result).toEqual([]);
// });
// it('should return empty array when result is undefined', async () => {
// mockReturnService.ReturnQueryReturns.mockReturnValue(of({ error: null }));
// const result = await service.fetchIncompletedRemissionReturnReceipts();
// expect(result).toEqual([]);
// });
// it('should handle stock service errors', async () => {
// mockRemissionStockService.fetchAssignedStock.mockRejectedValue(
// new Error('Stock error'),
// );
// await expect(
// service.fetchIncompletedRemissionReturnReceipts(),
// ).rejects.toThrow('Stock error');
// expect(mockReturnService.ReturnQueryReturns).not.toHaveBeenCalled();
// });
// it('should handle observable errors', async () => {
// mockReturnService.ReturnQueryReturns.mockReturnValue(
// throwError(() => new Error('Observable error')),
// );
// await expect(
// service.fetchIncompletedRemissionReturnReceipts(),
// ).rejects.toThrow('Observable error');
// });
// });
// describe('edge cases', () => {
// beforeEach(() => {
// mockRemissionStockService.fetchAssignedStock.mockResolvedValue(mockStock);
// });
// it('should handle empty returns array', async () => {
// mockReturnService.ReturnQueryReturns.mockReturnValue(
// of({ result: [], error: null }),
// );
// const completedResult =
// await service.fetchRemissionReturnReceipts();
// const incompletedResult =
// await service.fetchIncompletedRemissionReturnReceipts();
// expect(completedResult).toEqual([]);
// expect(incompletedResult).toEqual([]);
// });
// it('should handle stock with no id', async () => {
// mockRemissionStockService.fetchAssignedStock.mockResolvedValue({
// ...mockStock,
// id: undefined,
// });
// mockReturnService.ReturnQueryReturns.mockReturnValue(
// of({ result: mockReturns, error: null }),
// );
// await service.fetchRemissionReturnReceipts();
// expect(mockReturnService.ReturnQueryReturns).toHaveBeenCalledWith({
// stockId: undefined,
// queryToken: expect.any(Object),
// });
// });
// });
describe('fetchRemissionReturnReceipt', () => {
const mockReceipt: Receipt = {
id: 101,

View File

@@ -77,19 +77,22 @@ export class RemissionReturnReceiptService {
const { start, returncompleted } =
FetchRemissionReturnReceiptsSchema.parse(params);
// Default to 7 days ago if no start date is provided
const startDate = start ?? subDays(new Date(), 7);
const assignedStock =
await this.#remissionStockService.fetchAssignedStock(abortSignal);
this.#logger.info('Fetching completed returns from API', () => ({
stockId: assignedStock.id,
startDate: start?.toISOString(),
startDate: startDate.toISOString(),
}));
let req$ = this.#returnService.ReturnQueryReturns({
stockId: assignedStock.id,
queryToken: {
filter: { returncompleted: returncompleted ? 'true' : 'false' },
start: start?.toISOString(),
start: startDate.toISOString(),
eagerLoading: 3,
},
});

View File

@@ -0,0 +1,3 @@
export const EMPTY_WBS_TITLE = 'Keine Artikel';
export const EMPTY_WBS_DESCRIPTION =
'Fügen Sie dem Warenbegleitschein Artikel hinzu, um die Wanne abzuschließen';

View File

@@ -34,8 +34,24 @@
></ui-icon-button>
</div>
} @else if (items.length === 0) {
<div class="isa-text-body-2-regular">
Keine Artikel auf dem Warenbegleitschein.
<div class="flex items-center justify-center">
<ui-empty-state
[title]="emptyWbsTitle"
[description]="emptyWbsDescription"
appearance="noArticles"
>
<button
class="mt-[1.5rem]"
uiButton
type="button"
appearance="secondary"
size="large"
[disabled]="store.remissionStarted()"
(click)="continueRemission()"
>
Jetzt befüllen
</button>
</ui-empty-state>
</div>
} @else {
<div class="bg-isa-white rounded-2xl p-6 grid grid-flow-row gap-6">

View File

@@ -24,6 +24,8 @@ import {
RemissionStore,
} from '@isa/remission/data-access';
import { logger } from '@isa/core/logging';
import { EmptyStateComponent } from '@isa/ui/empty-state';
import { EMPTY_WBS_DESCRIPTION, EMPTY_WBS_TITLE } from './constants';
/**
* Component for displaying detailed information about a remission return receipt.
@@ -52,6 +54,7 @@ import { logger } from '@isa/core/logging';
RemissionReturnReceiptDetailsCardComponent,
RemissionReturnReceiptDetailsItemComponent,
StatefulButtonComponent,
EmptyStateComponent,
],
providers: [provideIcons({ isaActionChevronLeft, isaLoading })],
})
@@ -62,6 +65,9 @@ export class RemissionReturnReceiptDetailsComponent {
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
emptyWbsTitle = EMPTY_WBS_TITLE;
emptyWbsDescription = EMPTY_WBS_DESCRIPTION;
/** Instance of the RemissionStore for managing remission state */
store = inject(RemissionStore);
@@ -120,6 +126,13 @@ export class RemissionReturnReceiptDetailsComponent {
completingReturn = signal(false);
completeReturnError = signal<string | null>(null);
async continueRemission() {
this.store.startRemission({
returnId: this.returnId(),
receiptId: this.receiptId(),
});
}
async completeReturn() {
if (this.completingReturn()) {
return;

View File

@@ -31,7 +31,7 @@ vi.mock('@isa/shared/filter', async () => {
class MockReturnReceiptListItemComponent {}
@Component({
selector: 'isa-order-by-toolbar',
selector: 'remi-order-by-toolbar',
template: '<div>Mock Order By Toolbar</div>',
standalone: true,
})
@@ -43,7 +43,9 @@ describe('RemissionReturnReceiptListComponent', () => {
let mockRemissionReturnReceiptService: {
fetchRemissionReturnReceipts: ReturnType<typeof vi.fn>;
};
let mockFilterService: any;
let mockFilterService: {
orderBy: ReturnType<typeof signal>;
};
const mockCompletedReturn: Return = {
id: 1,
@@ -146,7 +148,8 @@ describe('RemissionReturnReceiptListComponent', () => {
// Assert - Private fields cannot be directly tested
// Instead, we verify the component initializes correctly with dependencies
expect(component).toBeDefined();
expect(component.remissionReturnsResource).toBeDefined();
expect(component.completedRemissionReturnsResource).toBeDefined();
expect(component.incompletedRemissionReturnsResource).toBeDefined();
expect(component.orderDateBy).toBeDefined();
});
@@ -161,9 +164,10 @@ describe('RemissionReturnReceiptListComponent', () => {
});
describe('Resource Loading', () => {
it('should initialize remissionReturnsResource', () => {
it('should initialize completed and incomplete resources', () => {
// Assert
expect(component.remissionReturnsResource).toBeDefined();
expect(component.completedRemissionReturnsResource).toBeDefined();
expect(component.incompletedRemissionReturnsResource).toBeDefined();
});
it('should fetch remission return receipts on component initialization', () => {
@@ -192,7 +196,8 @@ describe('RemissionReturnReceiptListComponent', () => {
const newComponent = newFixture.componentInstance;
// Assert
expect(newComponent.remissionReturnsResource.isLoading()).toBeDefined();
expect(newComponent.completedRemissionReturnsResource.isLoading()).toBeDefined();
expect(newComponent.incompletedRemissionReturnsResource.isLoading()).toBeDefined();
});
it('should handle error state when service fails', async () => {
@@ -211,16 +216,19 @@ describe('RemissionReturnReceiptListComponent', () => {
// Assert
const errorComponent = errorFixture.componentInstance;
expect(errorComponent.remissionReturnsResource.error).toBeDefined();
expect(errorComponent.completedRemissionReturnsResource.error).toBeDefined();
});
});
describe('returns computed signal', () => {
it('should combine returns with incompleted first', () => {
// Arrange
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue(
mockReturns
);
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
mockCompletedReturn
]);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([
mockIncompletedReturn
]);
// Act
const returns = component.returns();
@@ -236,9 +244,10 @@ describe('RemissionReturnReceiptListComponent', () => {
it('should filter out receipts without data', () => {
// Arrange
const returnsWithNullData = [mockCompletedReturn, mockReturnWithoutReceiptData];
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue(
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue(
returnsWithNullData
);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();
@@ -251,7 +260,8 @@ describe('RemissionReturnReceiptListComponent', () => {
it('should handle empty returns array', () => {
// Arrange
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue([]);
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([]);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();
@@ -263,9 +273,10 @@ describe('RemissionReturnReceiptListComponent', () => {
it('should handle null value from resource', () => {
// Arrange
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue(
null as any
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue(
undefined as Return[] | undefined
);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();
@@ -277,9 +288,10 @@ describe('RemissionReturnReceiptListComponent', () => {
it('should handle undefined value from resource', () => {
// Arrange
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue(
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue(
undefined as Return[] | undefined
);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();
@@ -318,9 +330,10 @@ describe('RemissionReturnReceiptListComponent', () => {
],
} as Return;
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue([
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
returnWithMultipleReceipts,
]);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();
@@ -409,10 +422,11 @@ describe('RemissionReturnReceiptListComponent', () => {
],
} as Return;
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue([
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
olderReturn,
newerReturn,
]);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();
@@ -469,10 +483,11 @@ describe('RemissionReturnReceiptListComponent', () => {
],
} as Return;
vi.spyOn(sortedComponent.remissionReturnsResource, 'value').mockReturnValue([
vi.spyOn(sortedComponent.completedRemissionReturnsResource, 'value').mockReturnValue([
newerReturn,
olderReturn,
]);
vi.spyOn(sortedComponent.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = sortedComponent.returns();
@@ -524,10 +539,11 @@ describe('RemissionReturnReceiptListComponent', () => {
],
} as Return;
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue([
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
returnWithDate,
returnWithoutDate,
]);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();
@@ -558,9 +574,10 @@ describe('RemissionReturnReceiptListComponent', () => {
receipts: [],
} as Return;
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue([
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
returnWithEmptyReceipts,
]);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();
@@ -571,15 +588,13 @@ describe('RemissionReturnReceiptListComponent', () => {
it('should handle mixed returns with and without receipt data', () => {
// Arrange
const mixedReturns = [
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
mockCompletedReturn,
mockReturnWithoutReceiptData,
mockIncompletedReturn,
];
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue(
mixedReturns
);
mockReturnWithoutReceiptData
]);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([
mockIncompletedReturn
]);
// Act
const returns = component.returns();
@@ -607,9 +622,10 @@ describe('RemissionReturnReceiptListComponent', () => {
})),
} as Return;
vi.spyOn(component.remissionReturnsResource, 'value').mockReturnValue([
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
returnWithManyReceipts,
]);
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
// Act
const returns = component.returns();

View File

@@ -7,11 +7,12 @@ describe('ReturnReceiptListItemComponent', () => {
let component: ReturnReceiptListItemComponent;
let fixture: ComponentFixture<ReturnReceiptListItemComponent>;
const createMockReturn = (overrides: Partial<Return> = {}): Return => ({
id: 1,
receipts: [],
...overrides,
} as Return);
const createMockReturn = (overrides: Partial<Return> = {}): Return =>
({
id: 1,
receipts: [],
...overrides,
}) as Return;
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -52,6 +53,7 @@ describe('ReturnReceiptListItemComponent', () => {
data: {
id: 1,
receiptNumber: 'REC-2024-001-ABC',
items: [],
},
},
],
@@ -70,6 +72,7 @@ describe('ReturnReceiptListItemComponent', () => {
data: {
id: 1,
receiptNumber: 'REC-2024-001-ABC',
items: [],
},
},
{
@@ -77,6 +80,7 @@ describe('ReturnReceiptListItemComponent', () => {
data: {
id: 2,
receiptNumber: 'REC-2024-002-DEF',
items: [],
},
},
{
@@ -84,6 +88,7 @@ describe('ReturnReceiptListItemComponent', () => {
data: {
id: 3,
receiptNumber: 'REC-2024-003-GHI',
items: [],
},
},
],
@@ -106,6 +111,7 @@ describe('ReturnReceiptListItemComponent', () => {
data: {
id: 2,
receiptNumber: 'REC-2024-002-DEF',
items: [],
},
},
],
@@ -124,6 +130,7 @@ describe('ReturnReceiptListItemComponent', () => {
data: {
id: 1,
receiptNumber: undefined as any,
items: [],
},
},
{
@@ -131,6 +138,7 @@ describe('ReturnReceiptListItemComponent', () => {
data: {
id: 2,
receiptNumber: 'REC-2024-002-DEF',
items: [],
},
},
],
@@ -149,6 +157,7 @@ describe('ReturnReceiptListItemComponent', () => {
data: {
id: 1,
receiptNumber: 'SHORT',
items: [],
},
},
],
@@ -176,6 +185,7 @@ describe('ReturnReceiptListItemComponent', () => {
id: 1,
data: {
id: 1,
receiptNumber: 'REC-2024-001-ABC',
items: new Array(5),
},
},
@@ -183,6 +193,7 @@ describe('ReturnReceiptListItemComponent', () => {
id: 2,
data: {
id: 2,
receiptNumber: 'REC-2024-002-DEF',
items: new Array(3),
},
},
@@ -190,6 +201,7 @@ describe('ReturnReceiptListItemComponent', () => {
id: 3,
data: {
id: 3,
receiptNumber: 'REC-2024-003-GHI',
items: new Array(7),
},
},
@@ -230,13 +242,15 @@ describe('ReturnReceiptListItemComponent', () => {
id: 1,
data: {
id: 1,
items: undefined,
receiptNumber: 'REC-2024-001-ABC',
items: undefined as any,
},
},
{
id: 2,
data: {
id: 2,
receiptNumber: 'REC-2024-002-DEF',
items: new Array(5),
},
},
@@ -255,6 +269,7 @@ describe('ReturnReceiptListItemComponent', () => {
id: 1,
data: {
id: 1,
receiptNumber: 'REC-2024-001-ABC',
items: [],
},
},
@@ -262,6 +277,7 @@ describe('ReturnReceiptListItemComponent', () => {
id: 2,
data: {
id: 2,
receiptNumber: 'REC-2024-002-DEF',
items: new Array(2),
},
},
@@ -394,6 +410,8 @@ describe('ReturnReceiptListItemComponent', () => {
id: 1,
data: {
id: 1,
receiptNumber: 'REC-2024-001-ABC',
items: [],
completed: undefined as any,
},
},
@@ -401,6 +419,8 @@ describe('ReturnReceiptListItemComponent', () => {
id: 2,
data: {
id: 2,
receiptNumber: 'REC-2024-002-DEF',
items: [],
completed: true,
},
},
@@ -562,7 +582,9 @@ describe('ReturnReceiptListItemComponent', () => {
id: 1,
data: {
id: 1,
receiptNumber: 'PREFIX-VERY-LONG-RECEIPT-NUMBER-THAT-EXCEEDS-NORMAL-LENGTH',
receiptNumber:
'PREFIX-VERY-LONG-RECEIPT-NUMBER-THAT-EXCEEDS-NORMAL-LENGTH',
items: [],
},
},
],
@@ -580,7 +602,7 @@ describe('ReturnReceiptListItemComponent', () => {
id: i + 1,
receiptNumber: `REC-2024-${String(i + 1).padStart(3, '0')}-ABC`,
items: new Array(2),
completed: i % 2 === 0,
completed: i % 2 === 0 ? '2024-01-15T10:30:00.000Z' : undefined,
},
}));
@@ -594,4 +616,4 @@ describe('ReturnReceiptListItemComponent', () => {
expect(component.receiptNumber()).toContain('24-100');
});
});
});
});

View File

@@ -1 +1,2 @@
export * from './lib/empty-state.component';
export * from './lib/types';

View File

@@ -0,0 +1,5 @@
export const NO_RESULTS =
'<svg class="ui-empty-state-icon" xmlns="http://www.w3.org/2000/svg" width="114" height="102" viewBox="0 0 114 102" fill="none" > <path d="M75.5 43.794V30.1846L61.9243 15H27.7699C23.2025 14.9998 19.5 18.5625 19.5 22.9574V82.0424C19.5 86.4373 23.2025 90 27.7699 90H67.2301C71.3894 90 74.4181 87.8467 75 84" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M33.5 49H59.5" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M33.5 59H53.5" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M33.5 41H63.5" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M33.5 32H50.5" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M61.5 16V30H74.5" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M53.8139 70H33.5V80H61.5" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M105.003 94.095C103.549 95.6424 101.19 95.6437 99.734 94.098L89.5 84.6013L94.7629 79L105 88.5C106.454 90.0444 106.455 92.5488 105.003 94.095Z" stroke="#6C757D" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M88 78L91.5 81" stroke="#6C757D" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M78.5 59L68.5 70" stroke="#6C757D" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M78.5 70L68.5 59" stroke="#6C757D" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> <path d="M73 84C83.7696 84 92.5 75.2695 92.5 64.5C92.5 53.7304 83.7696 45 73 45C62.2304 45 53.5 53.7304 53.5 64.5C53.5 75.2695 62.2304 84 73 84Z" stroke="#6C757D" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /> </svg>';
export const NO_ARTICLES =
'<svg xmlns="http://www.w3.org/2000/svg" width="113" height="102" viewBox="0 0 113 102" fill="none"> <path d="M74.5 84L75 30.1846L61.4243 15H27.2699C22.7025 14.9998 19 18.5625 19 22.9574V82.0424C19 86.4373 22.7025 90 27.2699 90H66.7301C70.8894 90 73.9181 87.8467 74.5 84Z" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> <path d="M61 16V30H74" stroke="#CED4DA" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> <path d="M54.25 61.5L39.75 47" stroke="#6C757D" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> <path d="M39.75 61.5L54.25 47" stroke="#6C757D" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></svg>';

View File

@@ -1,110 +1,4 @@
<div class="ui-empty-state-circle">
<svg
class="ui-empty-state-icon"
xmlns="http://www.w3.org/2000/svg"
width="114"
height="102"
viewBox="0 0 114 102"
fill="none"
>
<path
d="M75.5 43.794V30.1846L61.9243 15H27.7699C23.2025 14.9998 19.5 18.5625 19.5 22.9574V82.0424C19.5 86.4373 23.2025 90 27.7699 90H67.2301C71.3894 90 74.4181 87.8467 75 84"
stroke="#CED4DA"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M33.5 49H59.5"
stroke="#CED4DA"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M33.5 59H53.5"
stroke="#CED4DA"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M33.5 41H63.5"
stroke="#CED4DA"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M33.5 32H50.5"
stroke="#CED4DA"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M61.5 16V30H74.5"
stroke="#CED4DA"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M53.8139 70H33.5V80H61.5"
stroke="#CED4DA"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M105.003 94.095C103.549 95.6424 101.19 95.6437 99.734 94.098L89.5 84.6013L94.7629 79L105 88.5C106.454 90.0444 106.455 92.5488 105.003 94.095Z"
stroke="#6C757D"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M88 78L91.5 81"
stroke="#6C757D"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M78.5 59L68.5 70"
stroke="#6C757D"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M78.5 70L68.5 59"
stroke="#6C757D"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M73 84C83.7696 84 92.5 75.2695 92.5 64.5C92.5 53.7304 83.7696 45 73 45C62.2304 45 53.5 53.7304 53.5 64.5C53.5 75.2695 62.2304 84 73 84Z"
stroke="#6C757D"
stroke-width="3"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<div class="ui-empty-state-circle" [innerHTML]="sanitizedIcon()"></div>
<div></div>
<div class="ui-empty-state-title">{{ title() }}</div>
<div class="ui-empty-state-description">{{ description() }}</div>

View File

@@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, Component, input, ViewEncapsulation } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
ViewEncapsulation,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { NO_RESULTS, NO_ARTICLES } from './constants';
import { EmptyStateAppearance } from './types';
@Component({
selector: 'ui-empty-state',
@@ -11,6 +21,25 @@ import { ChangeDetectionStrategy, Component, input, ViewEncapsulation } from '@a
},
})
export class EmptyStateComponent {
#sanitizer = inject(DomSanitizer);
title = input.required<string>();
description = input.required<string>();
appearance = input<EmptyStateAppearance>(EmptyStateAppearance.NoResults);
icon = computed(() => {
const appearance = this.appearance();
switch (appearance) {
case EmptyStateAppearance.NoArticles:
return NO_ARTICLES;
case EmptyStateAppearance.NoResults:
default:
return NO_RESULTS;
}
});
sanitizedIcon = computed(() => {
return this.#sanitizer.bypassSecurityTrustHtml(this.icon());
});
}

View File

@@ -0,0 +1,7 @@
export const EmptyStateAppearance = {
NoResults: 'noResults',
NoArticles: 'noArticles',
} as const;
export type EmptyStateAppearance =
(typeof EmptyStateAppearance)[keyof typeof EmptyStateAppearance];