mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
feat: enhance return process with product category selection and update dropdown functionality
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
DropdownAppearance,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionAppearance,
|
||||
DropdownOptionComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
import { type Meta, type StoryObj, argsToTemplate, moduleMetadata } from '@storybook/angular';
|
||||
@@ -10,7 +9,6 @@ type DropdownInputProps = {
|
||||
value: string;
|
||||
label: string;
|
||||
appearance: DropdownAppearance;
|
||||
optionAppearance: DropdownOptionAppearance;
|
||||
};
|
||||
|
||||
const meta: Meta<DropdownInputProps> = {
|
||||
@@ -27,10 +25,6 @@ const meta: Meta<DropdownInputProps> = {
|
||||
control: 'select',
|
||||
options: Object.values(DropdownAppearance),
|
||||
},
|
||||
optionAppearance: {
|
||||
control: 'select',
|
||||
options: Object.values(DropdownOptionAppearance),
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
|
||||
@@ -13,7 +13,7 @@ export class IDBStorageProvider implements StorageProvider {
|
||||
}
|
||||
|
||||
return new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open('isa-cache', 1);
|
||||
const request = indexedDB.open('isa-storage', 1);
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event);
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div uiItemRowProdcutCheckbox class="flex justify-center items-center">
|
||||
<ui-checkbox appearance="bullet">
|
||||
<input type="checkbox" [checked]="selected()" (click)="selected.set(!selected())" />
|
||||
</ui-checkbox>
|
||||
@if (selectable()) {
|
||||
<ui-checkbox appearance="bullet">
|
||||
<input type="checkbox" [checked]="selected()" (click)="selected.set(!selected())" />
|
||||
</ui-checkbox>
|
||||
}
|
||||
</div>
|
||||
</ui-item-row>
|
||||
|
||||
@@ -29,4 +29,8 @@ export class ReturnDetailsOrderGroupItemComponent {
|
||||
item = input.required<ReceiptItem>();
|
||||
|
||||
selected = model(false);
|
||||
|
||||
selectable = computed(() => {
|
||||
return this.item()?.actions?.some((a) => a.key === 'canReturn' && Boolean(a.value));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<ui-toolbar size="small" class="justify-self-stretch">
|
||||
<div class="isa-text-body-2-bold text-isa-neutral-900">{{ items().length }} Artikel</div>
|
||||
<div class="flex-grow"></div>
|
||||
<button
|
||||
type="button"
|
||||
uiTextButton
|
||||
color="strong"
|
||||
size="small"
|
||||
(click)="selectAll.emit(!allSelected())"
|
||||
>
|
||||
@if (allSelected()) {
|
||||
Alles abwählen
|
||||
} @else {
|
||||
Alles auswählen
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (selectableItems().length) {
|
||||
<button type="button" uiTextButton color="strong" size="small" (click)="selectOrUnselectAll()">
|
||||
@if (allSelected()) {
|
||||
Alles abwählen
|
||||
} @else {
|
||||
Alles auswählen
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</ui-toolbar>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, input, model, output } from '@angular/core';
|
||||
import { ReceiptItem } from '@feature/return/services';
|
||||
import { ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core';
|
||||
import { ReceiptItem } from '@isa/oms/data-access';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { ToolbarComponent } from '@isa/ui/toolbar';
|
||||
|
||||
@@ -14,7 +14,28 @@ import { ToolbarComponent } from '@isa/ui/toolbar';
|
||||
export class ReturnDetailsOrderGroupComponent {
|
||||
items = input.required<ReceiptItem[]>();
|
||||
|
||||
allSelected = input<boolean>(false);
|
||||
selectedItems = model<ReceiptItem[]>([]);
|
||||
|
||||
selectAll = output<boolean>();
|
||||
selectableItems = computed(() => {
|
||||
return this.items().filter((item) =>
|
||||
item.actions?.some((a) => a.key === 'canReturn' && Boolean(a.value)),
|
||||
);
|
||||
});
|
||||
|
||||
selectOrUnselectAll() {
|
||||
const selectedItems = this.selectedItems();
|
||||
const selectableItems = this.selectableItems();
|
||||
if (selectedItems.length === selectableItems.length) {
|
||||
this.selectedItems.set([]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedItems.set(this.selectableItems());
|
||||
}
|
||||
|
||||
allSelected = computed(() => {
|
||||
const selectedItems = this.selectedItems();
|
||||
const selectableItems = this.selectableItems();
|
||||
return selectedItems.length === selectableItems.length;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,16 +43,15 @@
|
||||
}
|
||||
<div></div>
|
||||
<lib-return-details-order-group
|
||||
[items]="receipt.items"
|
||||
[allSelected]="receipt.items.length === selectedItems().length"
|
||||
(selectAll)="selectAll($event)"
|
||||
[items]="receiptItems()"
|
||||
[(selectedItems)]="selectedItems"
|
||||
></lib-return-details-order-group>
|
||||
@for (item of receipt.items; track item.id; let last = $last) {
|
||||
<lib-return-details-order-group-item
|
||||
class="border-b border-solid border-isa-neutral-300 last:border-none"
|
||||
[item]="item.data"
|
||||
(selectedChange)="selectedChange(item.id)"
|
||||
[selected]="selectedItems().includes(item.id)"
|
||||
(selectedChange)="selectItemById(item.id, $event)"
|
||||
[selected]="selectedItemIds().includes(item.id)"
|
||||
></lib-return-details-order-group-item>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,11 @@ export class DetailsPageComponent {
|
||||
|
||||
params = toSignal(this._activatedRoute.params);
|
||||
|
||||
// DUMMY Implementierung
|
||||
selectedItems = signal<number[]>([]);
|
||||
selectedItems = signal<ReceiptItem[]>([]);
|
||||
|
||||
selectedItemIds = computed(() => {
|
||||
return this.selectedItems().map((item) => item.id);
|
||||
});
|
||||
|
||||
showMore = signal(false);
|
||||
|
||||
@@ -73,6 +76,15 @@ export class DetailsPageComponent {
|
||||
return this.#returnDetailsStore.entityMap()[itemId];
|
||||
});
|
||||
|
||||
receiptItems = computed<ReceiptItem[]>(() => {
|
||||
const receiptResult = this.receiptResult();
|
||||
if (!receiptResult) {
|
||||
return [];
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return receiptResult.data?.items?.map((i) => i.data!) || [];
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const itemId = this.itemId();
|
||||
@@ -82,27 +94,16 @@ export class DetailsPageComponent {
|
||||
});
|
||||
}
|
||||
|
||||
// DUMMY Implementierung
|
||||
selectedChange(itemId: number) {
|
||||
const selectedItems = this.selectedItems();
|
||||
const index = selectedItems.indexOf(itemId);
|
||||
if (index === -1) {
|
||||
this.selectedItems.set([...selectedItems, itemId]);
|
||||
} else {
|
||||
this.selectedItems.set(selectedItems.filter((id) => id !== itemId));
|
||||
selectItemById(id: number, selected: boolean) {
|
||||
const items = this.receiptItems();
|
||||
const item = items.find((i) => i.id === id);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// DUMMY Implementierung
|
||||
selectAll(selected: boolean) {
|
||||
if (selected) {
|
||||
const receiptResult = this.receiptResult();
|
||||
if (receiptResult) {
|
||||
const allIds = receiptResult.data?.items?.map((item) => item.id) || [];
|
||||
this.selectedItems.set(allIds);
|
||||
}
|
||||
this.selectedItems.update((items) => [...items, item]);
|
||||
} else {
|
||||
this.selectedItems.set([]);
|
||||
this.selectedItems.update((items) => items.filter((i) => i.id !== id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,13 +119,11 @@ export class DetailsPageComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = selectedItems.reduce((acc, id) => {
|
||||
const item = data.items.find((item) => item.id === id);
|
||||
if (item?.data) {
|
||||
acc.push(item.data);
|
||||
}
|
||||
return acc;
|
||||
}, [] as ReceiptItem[]);
|
||||
const items = this.selectedItems();
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#returnProcessStore.startProcess({
|
||||
processId,
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface ReturnProcess {
|
||||
receiptId: number;
|
||||
receiptItem: ReceiptItem;
|
||||
answers: Record<string, unknown>;
|
||||
productCategory?: string;
|
||||
}
|
||||
|
||||
@@ -184,11 +184,101 @@ export function validateNonbookQuestions(answers: Record<string, unknown>): Elig
|
||||
return { state: EligibleForReturnState.NotEligible, reason: 'Invalid answers' };
|
||||
}
|
||||
|
||||
export const andereElektronischeGeraete: ReturnProcessQuestion[] = [
|
||||
{
|
||||
key: ReturnProcessQuestionKey.ItemCondition,
|
||||
description: 'Wie ist der Artkelzustand?',
|
||||
type: ReturnProcessQuestionType.Select,
|
||||
options: [
|
||||
{
|
||||
label: 'Originalverpackt',
|
||||
value: 'ovp',
|
||||
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
|
||||
},
|
||||
{
|
||||
label: 'Geöffnet / Beschädigt',
|
||||
value: 'damaged',
|
||||
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.ReturnReason,
|
||||
description: 'Warum möchtest du den Artikel zurückgeben?',
|
||||
type: ReturnProcessQuestionType.Select,
|
||||
options: [
|
||||
{ label: 'Gefällt nicht/Wiederruf', value: 'dislike' },
|
||||
{
|
||||
label: 'Fehllieferung',
|
||||
value: 'wrong_item',
|
||||
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.ItemDefective,
|
||||
description: 'Ist der Artikel defekt?',
|
||||
type: ReturnProcessQuestionType.Select,
|
||||
options: [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: 'yes',
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: 'no',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.DeliveredItem,
|
||||
description: 'Welcher Artikel wurde geliefert?',
|
||||
type: ReturnProcessQuestionType.Product,
|
||||
},
|
||||
];
|
||||
|
||||
export function validateAndereElektronischeGeraeteQuestions(
|
||||
answers: Record<string, unknown>,
|
||||
): EligibleForReturn {
|
||||
// Check if the item is in original condition
|
||||
if (answers[ReturnProcessQuestionKey.ItemCondition] === 'ovp') {
|
||||
// Check if the return reason is dislike
|
||||
if (answers[ReturnProcessQuestionKey.ReturnReason] === 'dislike') {
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
}
|
||||
// Check if the return reason is wrong item
|
||||
else if (answers[ReturnProcessQuestionKey.ReturnReason] === 'wrong_item') {
|
||||
// Check if the delivered item is selected
|
||||
if (answers[ReturnProcessQuestionKey.DeliveredItem]) {
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
}
|
||||
// Return pending if the delivered item is not selected
|
||||
else {
|
||||
return { state: EligibleForReturnState.Pending, reason: 'Missing delivered item' };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if the item is damaged
|
||||
else if (answers[ReturnProcessQuestionKey.ItemCondition] === 'damaged') {
|
||||
// Check if the item is defective
|
||||
if (answers[ReturnProcessQuestionKey.ItemDefective] === 'yes') {
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
}
|
||||
// Check if the item is not defective
|
||||
else if (answers[ReturnProcessQuestionKey.ItemDefective] === 'no') {
|
||||
return { state: EligibleForReturnState.NotEligible, reason: 'Item not defective' };
|
||||
}
|
||||
}
|
||||
|
||||
return { state: EligibleForReturnState.NotEligible, reason: 'Invalid answers' };
|
||||
}
|
||||
|
||||
export const CategoryQuestions: Record<string, ReturnProcessQuestion[]> = {
|
||||
'Buch/Kalender': bookCalendarQuestions,
|
||||
'Ton-/Datenträger': tonDatentraegerQuestions,
|
||||
'Nonbook': nonbookQuestions,
|
||||
'default': bookCalendarQuestions,
|
||||
'Spielwaren/Puzzle': nonbookQuestions,
|
||||
'Sonstiges und Nonbook': nonbookQuestions,
|
||||
'Andere Elektronische Geräte': andereElektronischeGeraete,
|
||||
};
|
||||
|
||||
export const CategoryQuestionValidators: Record<
|
||||
@@ -197,6 +287,7 @@ export const CategoryQuestionValidators: Record<
|
||||
> = {
|
||||
'Buch/Kalender': validateBookCalendarQuestions,
|
||||
'Ton-/Datenträger': validateTonDatentraegerQuestions,
|
||||
'Nonbook': validateNonbookQuestions,
|
||||
'default': validateBookCalendarQuestions,
|
||||
'Spielwaren/Puzzle': validateNonbookQuestions,
|
||||
'Sonstiges und Nonbook': validateNonbookQuestions,
|
||||
'Andere Elektronische Geräte': validateAndereElektronischeGeraeteQuestions,
|
||||
};
|
||||
|
||||
@@ -7,34 +7,43 @@ import {
|
||||
ReturnProcessQuestionType,
|
||||
} from './models';
|
||||
import { CategoryQuestions, CategoryQuestionValidators } from './return-process-questions.token';
|
||||
import { KeyValue } from '@angular/common';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ReturnProcessService {
|
||||
returnProcessQuestions(process: ReturnProcess): ReturnProcessQuestion[] {
|
||||
const category = process.receiptItem.features?.['category'];
|
||||
const defaultQuestions = CategoryQuestions['default'];
|
||||
availableCategories(): KeyValue<string, string>[] {
|
||||
return Object.keys(CategoryQuestions).map((key) => {
|
||||
return { key, value: key };
|
||||
});
|
||||
}
|
||||
|
||||
returnProcessQuestions(process: ReturnProcess): ReturnProcessQuestion[] | undefined {
|
||||
const category = process.productCategory || process.receiptItem.features?.['category'];
|
||||
|
||||
if (category) {
|
||||
return CategoryQuestions[category] || defaultQuestions;
|
||||
return CategoryQuestions[category];
|
||||
}
|
||||
return defaultQuestions;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
returnProcessQuestionValidator(
|
||||
process: ReturnProcess,
|
||||
): (answers: Record<string, unknown>) => EligibleForReturn {
|
||||
const category = process.receiptItem.features?.['category'];
|
||||
const defaultValidator = CategoryQuestionValidators['defaultValidator'];
|
||||
): ((answers: Record<string, unknown>) => EligibleForReturn) | undefined {
|
||||
const category = process.productCategory || process.receiptItem.features?.['category'];
|
||||
|
||||
if (category) {
|
||||
return CategoryQuestionValidators[category] || defaultValidator;
|
||||
return CategoryQuestionValidators[category];
|
||||
}
|
||||
return defaultValidator;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
activeReturnProcessQuestions(process: ReturnProcess): ReturnProcessQuestion[] {
|
||||
activeReturnProcessQuestions(process: ReturnProcess): ReturnProcessQuestion[] | undefined {
|
||||
const questions = this.returnProcessQuestions(process);
|
||||
|
||||
if (!questions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let questionKey: string | undefined = questions[0].key;
|
||||
const result: ReturnProcessQuestion[] = [];
|
||||
const visited = new Set<string>();
|
||||
@@ -78,6 +87,10 @@ export class ReturnProcessService {
|
||||
): { answered: number; total: number } | undefined {
|
||||
const questions = this.returnProcessQuestions(returnProcess);
|
||||
|
||||
if (!questions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const visited = new Set<string>();
|
||||
function computeLongestPath(questions: ReturnProcessQuestion[], startKey: string) {
|
||||
if (visited.has(startKey)) return 0;
|
||||
@@ -106,6 +119,10 @@ export class ReturnProcessService {
|
||||
|
||||
const activeQuestions = this.activeReturnProcessQuestions(returnProcess);
|
||||
|
||||
if (!activeQuestions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const answered = activeQuestions.reduce((acc, q) => {
|
||||
if (q.key in returnProcess.answers) {
|
||||
return acc + 1;
|
||||
@@ -120,12 +137,21 @@ export class ReturnProcessService {
|
||||
eligibleForReturn(returnProcess: ReturnProcess): EligibleForReturn {
|
||||
const questions = this.activeReturnProcessQuestions(returnProcess);
|
||||
|
||||
if (!questions) {
|
||||
return { state: EligibleForReturnState.Pending };
|
||||
}
|
||||
|
||||
const everyQuestionAnswered = questions.every((q) => q.key in returnProcess.answers);
|
||||
if (!everyQuestionAnswered) {
|
||||
return { state: EligibleForReturnState.Pending, reason: 'Not all questions answered' };
|
||||
}
|
||||
|
||||
const validator = this.returnProcessQuestionValidator(returnProcess);
|
||||
|
||||
if (!validator) {
|
||||
return { state: EligibleForReturnState.Pending, reason: 'No validator found' };
|
||||
}
|
||||
|
||||
return validator(returnProcess.answers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,14 @@ export const ReturnProcessStore = signalStore(
|
||||
store.storeState();
|
||||
}
|
||||
},
|
||||
|
||||
setProductCategory: (id: number, category: string) => {
|
||||
const entity = store.entityMap()[id];
|
||||
if (entity) {
|
||||
patchState(store, updateEntity({ id: entity.id, changes: { productCategory: category } }));
|
||||
store.storeState();
|
||||
}
|
||||
},
|
||||
})),
|
||||
withMethods((store) => ({
|
||||
startProcess: (params: StartProcess) => {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
@if (showProductCategoryDropdown()) {
|
||||
<div
|
||||
class="flex flex-row gap-2 w-full justify-between items-center border-b border-isa-neutral-300 pb-6 last:border-b-0"
|
||||
>
|
||||
<div class="isa-text-body-1-regular">Um welches Produkt handelt es sich?</div>
|
||||
<ui-dropdown
|
||||
label="Produktart auswählen"
|
||||
[value]="productCategoryDropdown()"
|
||||
(valueChange)="setProductCategory($event)"
|
||||
>
|
||||
@for (kv of availableCategories(); track kv.key) {
|
||||
<ui-dropdown-option [value]="kv.key">{{ kv.value }}</ui-dropdown-option>
|
||||
}
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (question of questions(); track question.key; let last = $last) {
|
||||
<div class="item-content" [class.with-border-bottom]="!last">
|
||||
@switch (question.type) {
|
||||
|
||||
@@ -3,13 +3,20 @@ import { ChangeDetectionStrategy, Component, computed, inject, input } from '@an
|
||||
import { ReturnProcess, ReturnProcessService, ReturnProcessStore } from '@isa/oms/data-access';
|
||||
import { ReturnProcessSelectQuestionComponent } from './return-process-select-question/return-process-select-question.component';
|
||||
import { ReturnProcessProductQuestionComponent } from './return-process-product-question/return-process-product-question.component';
|
||||
import { DropdownButtonComponent, DropdownOptionComponent } from '@isa/ui/input-controls';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-return-process-questions',
|
||||
templateUrl: './return-process-questions.component.html',
|
||||
styleUrls: ['./return-process-questions.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [JsonPipe, ReturnProcessSelectQuestionComponent, ReturnProcessProductQuestionComponent],
|
||||
imports: [
|
||||
JsonPipe,
|
||||
ReturnProcessSelectQuestionComponent,
|
||||
ReturnProcessProductQuestionComponent,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
],
|
||||
})
|
||||
export class ReturnProcessQuestionsComponent {
|
||||
#returnProcessStore = inject(ReturnProcessStore);
|
||||
@@ -29,4 +36,38 @@ export class ReturnProcessQuestionsComponent {
|
||||
}
|
||||
return this.#returnProcessSerivce.activeReturnProcessQuestions(returnProcess);
|
||||
});
|
||||
|
||||
availableCategories = computed(() => {
|
||||
return this.#returnProcessSerivce.availableCategories();
|
||||
});
|
||||
|
||||
setProductCategory(category: string) {
|
||||
this.#returnProcessStore.setProductCategory(this.returnProcessId(), category);
|
||||
}
|
||||
|
||||
productCategoryDropdown = computed(() => {
|
||||
const returnProcess = this.returnProcess();
|
||||
if (!returnProcess) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return returnProcess.productCategory || returnProcess.receiptItem.features?.['category'];
|
||||
});
|
||||
|
||||
showProductCategoryDropdown = computed(() => {
|
||||
const returnProcess = this.returnProcess();
|
||||
if (!returnProcess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedCategory = returnProcess.productCategory;
|
||||
|
||||
if (selectedCategory) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const questions = this.questions();
|
||||
|
||||
return !questions;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
[cdkConnectedOverlayOrigin]="cdkOverlayOrigin"
|
||||
[cdkConnectedOverlayOpen]="isOpen()"
|
||||
[cdkConnectedOverlayOffsetY]="12"
|
||||
[cdkConnectedOverlayMinWidth]="overlayMinWidth"
|
||||
(detach)="isOpen.set(false)"
|
||||
>
|
||||
<ul [class]="['ui-dorpdown__options', optionAppearanceClass()]" role="listbox">
|
||||
<ul [class]="['ui-dorpdown__options']" role="listbox">
|
||||
<ng-content></ng-content>
|
||||
</ul>
|
||||
</ng-template>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
border-radius: 1.25rem;
|
||||
background: var(--Neutral-White, #fff);
|
||||
box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
|
||||
.ui-dropdown-option {
|
||||
display: flex;
|
||||
@@ -48,6 +49,7 @@
|
||||
word-wrap: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
@apply isa-text-body-2-bold;
|
||||
|
||||
@@ -61,16 +63,4 @@
|
||||
@apply text-isa-accent-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-dropdown-option__options-text {
|
||||
.ui-dropdown-option {
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-dropdown-option__options-number {
|
||||
.ui-dropdown-option {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,10 @@ import {
|
||||
import { ControlValueAccessor } from '@angular/forms';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionChevronUp, isaActionChevronDown } from '@isa/icons';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { ActiveDescendantKeyManager, Highlightable } from '@angular/cdk/a11y';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||
import { isEqual } from 'lodash';
|
||||
import { DropdownAppearance, DropdownOptionAppearance } from './dropdown.types';
|
||||
import { DropdownAppearance } from './dropdown.types';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-dropdown-option',
|
||||
@@ -94,15 +92,16 @@ export class DropdownOptionComponent<T> implements Highlightable {
|
||||
})
|
||||
export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterViewInit {
|
||||
readonly init = signal(false);
|
||||
private elementRef = inject(ElementRef);
|
||||
|
||||
get overlayMinWidth() {
|
||||
return this.elementRef.nativeElement.offsetWidth;
|
||||
}
|
||||
|
||||
appearance = input<DropdownAppearance>(DropdownAppearance.Button);
|
||||
|
||||
appearanceClass = computed(() => `ui-dropdown__${this.appearance()}`);
|
||||
|
||||
optionAppearance = input<DropdownOptionAppearance>(DropdownOptionAppearance.Text);
|
||||
|
||||
optionAppearanceClass = computed(() => `ui-dropdown-option__options-${this.optionAppearance()}`);
|
||||
|
||||
id = input<string>();
|
||||
|
||||
value = model<T>();
|
||||
@@ -117,13 +116,13 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
||||
|
||||
cdkOverlayOrigin = inject(CdkOverlayOrigin, { self: true });
|
||||
|
||||
private selectionModel = new SelectionModel<DropdownOptionComponent<T>>(false);
|
||||
selectedOption = computed(() => {
|
||||
const options = this.options();
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
selectionChanged = toSignal(this.selectionModel.changed);
|
||||
|
||||
selected = computed(() => {
|
||||
this.selectionChanged();
|
||||
return this.selectionModel.selected[0];
|
||||
return options.find((option) => option.value() === this.value());
|
||||
});
|
||||
|
||||
private keyManger?: ActiveDescendantKeyManager<DropdownOptionComponent<T>>;
|
||||
@@ -139,11 +138,13 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
||||
isOpenIcon = computed(() => (this.isOpen() ? 'isaActionChevronUp' : 'isaActionChevronDown'));
|
||||
|
||||
viewLabel = computed(() => {
|
||||
if (!this.selected()) {
|
||||
return this.label();
|
||||
const selectedOption = this.selectedOption();
|
||||
|
||||
if (!selectedOption) {
|
||||
return this.label() ?? this.value();
|
||||
}
|
||||
|
||||
return this.selected().getLabel() ?? this.value();
|
||||
return selectedOption.getLabel();
|
||||
});
|
||||
|
||||
constructor() {
|
||||
@@ -159,7 +160,7 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
||||
}
|
||||
|
||||
open() {
|
||||
const selected = this.selected();
|
||||
const selected = this.selectedOption();
|
||||
if (selected) {
|
||||
this.keyManger?.setActiveItem(selected);
|
||||
} else {
|
||||
@@ -197,8 +198,8 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
||||
}
|
||||
|
||||
select(option: DropdownOptionComponent<T>, options: { emit: boolean } = { emit: true }) {
|
||||
this.selectionModel.select(option);
|
||||
this.value.set(option.value());
|
||||
|
||||
if (options.emit) {
|
||||
this.onChange?.(option.value());
|
||||
}
|
||||
|
||||
@@ -3,11 +3,3 @@ export const DropdownAppearance = {
|
||||
} as const;
|
||||
|
||||
export type DropdownAppearance = (typeof DropdownAppearance)[keyof typeof DropdownAppearance];
|
||||
|
||||
export const DropdownOptionAppearance = {
|
||||
Text: 'text',
|
||||
Number: 'number',
|
||||
} as const;
|
||||
|
||||
export type DropdownOptionAppearance =
|
||||
(typeof DropdownOptionAppearance)[keyof typeof DropdownOptionAppearance];
|
||||
|
||||
Reference in New Issue
Block a user