mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Enhance return process with checklist questions and related components.
- ✨ **Feature**: Added checklist question type for return process - ✨ **Feature**: Implemented checklist question component - 🛠️ **Refactor**: Updated return process service to handle checklist answers - 📚 **Docs**: Added schemas for checklist answers and return details - 🧪 **Test**: Updated tests for new checklist functionality
This commit is contained in:
@@ -6,6 +6,7 @@ export const ReturnProcessQuestionKey = {
|
||||
DisplayCondition: 'display_condition',
|
||||
DevicePower: 'device_power',
|
||||
PackageComplete: 'package_complete',
|
||||
PackageIncomplete: 'package_incomplete',
|
||||
CaseCondition: 'case_condition',
|
||||
UsbPort: 'usb_port',
|
||||
} as const;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const ReturnProcessQuestionType = {
|
||||
Select: 'select',
|
||||
Product: 'product',
|
||||
Checklist: 'checklist',
|
||||
} as const;
|
||||
|
||||
export type ReturnProcessQuestionType =
|
||||
|
||||
@@ -7,20 +7,44 @@ export interface ReturnProcessQuestionBase {
|
||||
type: ReturnProcessQuestionType;
|
||||
}
|
||||
|
||||
export interface ReturnProcessQuestionOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ReturnProcessChecklistQuestion
|
||||
extends ReturnProcessQuestionBase {
|
||||
type: typeof ReturnProcessQuestionType.Checklist;
|
||||
options: ReturnProcessChecklistQuestionOptions[];
|
||||
other?: {
|
||||
label: string;
|
||||
value?: string;
|
||||
};
|
||||
nextQuestion?: ReturnProcessQuestionKey;
|
||||
}
|
||||
|
||||
export type ReturnProcessChecklistQuestionOptions =
|
||||
ReturnProcessQuestionOptions;
|
||||
|
||||
export interface ReturnProcessSelectQuestion extends ReturnProcessQuestionBase {
|
||||
type: typeof ReturnProcessQuestionType.Select;
|
||||
options: ReturnProcessQuestionSelectOptions[];
|
||||
}
|
||||
|
||||
export interface ReturnProcessQuestionSelectOptions {
|
||||
export interface ReturnProcessQuestionSelectOptions
|
||||
extends ReturnProcessQuestionOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
nextQuestion?: ReturnProcessQuestionKey;
|
||||
}
|
||||
|
||||
export interface ReturnProcessProductQuestion extends ReturnProcessQuestionBase {
|
||||
export interface ReturnProcessProductQuestion
|
||||
extends ReturnProcessQuestionBase {
|
||||
type: typeof ReturnProcessQuestionType.Product;
|
||||
nextQuestion?: ReturnProcessQuestionKey;
|
||||
}
|
||||
|
||||
export type ReturnProcessQuestion = ReturnProcessSelectQuestion | ReturnProcessProductQuestion;
|
||||
export type ReturnProcessQuestion =
|
||||
| ReturnProcessSelectQuestion
|
||||
| ReturnProcessProductQuestion
|
||||
| ReturnProcessChecklistQuestion;
|
||||
|
||||
@@ -33,3 +33,12 @@ export const enum ItemDefectiveValue {
|
||||
Yes = 'yes',
|
||||
No = 'no',
|
||||
}
|
||||
|
||||
export const enum PackageIncompleteValue {
|
||||
// Karton/Umverpackung
|
||||
Ovp = 'ovp',
|
||||
// Ladekabel
|
||||
CharchingCable = 'charching_cable',
|
||||
// Quickstart Guide
|
||||
QuickstartGuide = 'quickstart_guide',
|
||||
}
|
||||
|
||||
@@ -12,39 +12,6 @@ describe('Tolino Return Process', () => {
|
||||
const result = validateTolinoQuestions(answers);
|
||||
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
|
||||
});
|
||||
|
||||
it('should return Eligible when damaged and has significant defects', () => {
|
||||
const answers = {
|
||||
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
|
||||
[ReturnProcessQuestionKey.DevicePower]: 'yes',
|
||||
};
|
||||
const result = validateTolinoQuestions(answers);
|
||||
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
|
||||
});
|
||||
|
||||
it('should return NotEligible when damaged but no significant defects', () => {
|
||||
const answers = {
|
||||
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
|
||||
[ReturnProcessQuestionKey.DevicePower]: 'no',
|
||||
[ReturnProcessQuestionKey.PackageComplete]: 'no',
|
||||
[ReturnProcessQuestionKey.CaseCondition]: 'no',
|
||||
[ReturnProcessQuestionKey.UsbPort]: 'no',
|
||||
};
|
||||
const result = validateTolinoQuestions(answers);
|
||||
expect(result).toEqual({
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'No significant defects found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return NotEligible for invalid answers', () => {
|
||||
const answers = {};
|
||||
const result = validateTolinoQuestions(answers);
|
||||
expect(result).toEqual({
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Invalid answers',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Registry mappings', () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import {
|
||||
ItemConditionValue,
|
||||
ItemDefectiveValue,
|
||||
PackageIncompleteValue,
|
||||
ReturnReasonValue,
|
||||
} from './constants';
|
||||
|
||||
@@ -60,7 +61,7 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: ItemDefectiveValue.No,
|
||||
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
|
||||
nextQuestion: ReturnProcessQuestionKey.PackageIncomplete,
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
@@ -69,6 +70,29 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.PackageIncomplete,
|
||||
description: 'Was fehlt?',
|
||||
type: ReturnProcessQuestionType.Checklist,
|
||||
options: [
|
||||
{
|
||||
label: 'Karton/Umverpackung',
|
||||
value: PackageIncompleteValue.Ovp,
|
||||
},
|
||||
{
|
||||
label: 'Ladekabel',
|
||||
value: PackageIncompleteValue.CharchingCable,
|
||||
},
|
||||
{
|
||||
label: 'Quickstart Guide',
|
||||
value: PackageIncompleteValue.QuickstartGuide,
|
||||
},
|
||||
],
|
||||
other: {
|
||||
label: 'Sonstiges',
|
||||
},
|
||||
nextQuestion: ReturnProcessQuestionKey.CaseCondition,
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.CaseCondition,
|
||||
description: 'Hat das Gehäuse Mängel oder ist zerkratzt?',
|
||||
@@ -137,45 +161,11 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
|
||||
/**
|
||||
* Validates the answers for the Tolino return process and determines return eligibility.
|
||||
*
|
||||
* The validation logic follows these rules:
|
||||
* 1. If item is in original packaging, it's always eligible for return
|
||||
* 2. If item is damaged, it requires at least one defect to be eligible
|
||||
* 3. If no valid condition is provided, the item is not eligible
|
||||
*
|
||||
* @param {ReturnProcessAnswers} answers - A record of answers keyed by question keys
|
||||
* @returns {EligibleForReturn} Object containing eligibility state and reason if applicable
|
||||
*/
|
||||
export function validateTolinoQuestions(
|
||||
answers: ReturnProcessAnswers,
|
||||
): EligibleForReturn {
|
||||
if (
|
||||
answers[ReturnProcessQuestionKey.ItemCondition] ===
|
||||
ItemConditionValue.OriginalPackaging
|
||||
) {
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
} else if (
|
||||
answers[ReturnProcessQuestionKey.ItemCondition] ===
|
||||
ItemConditionValue.Damaged
|
||||
) {
|
||||
if (
|
||||
answers[ReturnProcessQuestionKey.DevicePower] ===
|
||||
ItemDefectiveValue.Yes ||
|
||||
answers[ReturnProcessQuestionKey.PackageComplete] ===
|
||||
ItemDefectiveValue.Yes ||
|
||||
answers[ReturnProcessQuestionKey.CaseCondition] ===
|
||||
ItemDefectiveValue.Yes ||
|
||||
answers[ReturnProcessQuestionKey.UsbPort] === ItemDefectiveValue.Yes
|
||||
) {
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
}
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'No significant defects found',
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Invalid answers',
|
||||
};
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from './models';
|
||||
import { CategoryQuestions, CategoryQuestionValidators } from './questions';
|
||||
import { KeyValue } from '@angular/common';
|
||||
import { ReturnProcessChecklistAnswerSchema } from './schemas';
|
||||
|
||||
/**
|
||||
* Service responsible for managing the return process workflow.
|
||||
@@ -57,7 +58,9 @@ export class ReturnProcessService {
|
||||
process.productCategory || process.receiptItem.features?.['category'];
|
||||
|
||||
if (category) {
|
||||
return CategoryQuestionValidators[category];
|
||||
return CategoryQuestionValidators[
|
||||
category as keyof typeof CategoryQuestionValidators
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -102,6 +105,16 @@ export class ReturnProcessService {
|
||||
questionKey = process.answers[question.key]
|
||||
? question.nextQuestion
|
||||
: undefined;
|
||||
} else if (question.type === ReturnProcessQuestionType.Checklist) {
|
||||
const answer = ReturnProcessChecklistAnswerSchema.parse(
|
||||
process.answers[question.key],
|
||||
);
|
||||
|
||||
if (answer.options?.length || answer.other) {
|
||||
questionKey = question.nextQuestion;
|
||||
} else {
|
||||
questionKey = undefined;
|
||||
}
|
||||
} else {
|
||||
console.error('Unknown question type', question);
|
||||
break;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './fetch-return-details';
|
||||
export * from './fetch-return-details.schema';
|
||||
export * from './query-token.schema';
|
||||
export * from './return-process-question-answer.schema';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ReturnProcessChecklistAnswerSchema = z.object({
|
||||
options: z.array(z.string()).default([]),
|
||||
other: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ReturnProcessChecklistAnswer = z.infer<
|
||||
typeof ReturnProcessChecklistAnswerSchema
|
||||
>;
|
||||
@@ -0,0 +1,24 @@
|
||||
@let q = question();
|
||||
@if (q) {
|
||||
<div>
|
||||
{{ q.description }}
|
||||
</div>
|
||||
<div>
|
||||
<ui-checklist [(ngModel)]="options">
|
||||
@for (option of q.options; track option.label) {
|
||||
<label uiCheckboxLabel>
|
||||
<ui-checkbox>
|
||||
<input type="checkbox" [uiChecklistValue]="option.value" />
|
||||
</ui-checkbox>
|
||||
{{ option.label }}
|
||||
</label>
|
||||
}
|
||||
</ui-checklist>
|
||||
</div>
|
||||
@if (q.other) {
|
||||
<div></div>
|
||||
<ui-textarea [label]="q.other.label">
|
||||
<textarea rows="4" [(ngModel)]="other"></textarea>
|
||||
</ui-textarea>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-cols-2 gap-6;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
linkedSignal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
ReturnProcessChecklistAnswer,
|
||||
ReturnProcessChecklistAnswerSchema,
|
||||
ReturnProcessChecklistQuestion,
|
||||
ReturnProcessStore,
|
||||
} from '@isa/oms/data-access';
|
||||
import {
|
||||
ChipOptionComponent,
|
||||
ChipsComponent,
|
||||
CheckboxComponent,
|
||||
ChecklistComponent,
|
||||
CheckboxLabelDirective,
|
||||
ChecklistValueDirective,
|
||||
TextareaComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
import { isEqual } from 'lodash';
|
||||
import { z } from 'zod';
|
||||
|
||||
@Component({
|
||||
selector: 'oms-feature-return-process-checklist-question',
|
||||
templateUrl: './return-process-checklist-question.component.html',
|
||||
styleUrls: ['./return-process-checklist-question.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
FormsModule,
|
||||
ChipsComponent,
|
||||
ChipOptionComponent,
|
||||
CheckboxComponent,
|
||||
ChecklistComponent,
|
||||
ChecklistValueDirective,
|
||||
CheckboxLabelDirective,
|
||||
TextareaComponent,
|
||||
],
|
||||
})
|
||||
export class ReturnProcessChecklistQuestionComponent {
|
||||
#returnProcessStore = inject(ReturnProcessStore);
|
||||
|
||||
processId = input.required<number>();
|
||||
|
||||
question = input.required<ReturnProcessChecklistQuestion>();
|
||||
|
||||
currentAnswer = computed(() => {
|
||||
const currentAnswersRaw =
|
||||
this.#returnProcessStore.entityMap()[this.processId()]?.answers[
|
||||
this.question().key
|
||||
];
|
||||
|
||||
if (!currentAnswersRaw) {
|
||||
return ReturnProcessChecklistAnswerSchema.parse({});
|
||||
}
|
||||
|
||||
return ReturnProcessChecklistAnswerSchema.parse(currentAnswersRaw);
|
||||
});
|
||||
|
||||
options = linkedSignal(() => this.currentAnswer().options, {
|
||||
equal: isEqual,
|
||||
});
|
||||
|
||||
other = linkedSignal(() => this.currentAnswer().other, { equal: isEqual });
|
||||
|
||||
valueChangesEffect = effect(() => {
|
||||
const options = this.options();
|
||||
const other = this.other();
|
||||
untracked(() => {
|
||||
this.updateAnswer({ options, other });
|
||||
});
|
||||
});
|
||||
|
||||
updateAnswer(answer: ReturnProcessChecklistAnswer) {
|
||||
this.#returnProcessStore.setAnswer(
|
||||
this.processId(),
|
||||
this.question().key,
|
||||
answer,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,18 +20,24 @@
|
||||
@for (question of questions(); track question.key; let last = $last) {
|
||||
<div class="item-content" [class.with-border-bottom]="!last">
|
||||
@switch (question.type) {
|
||||
@case ('select') {
|
||||
@case (ReturnProcessQuestionType.Select) {
|
||||
<oms-feature-return-process-select-question
|
||||
[processId]="returnProcessId()"
|
||||
[question]="question"
|
||||
></oms-feature-return-process-select-question>
|
||||
}
|
||||
@case ('product') {
|
||||
@case (ReturnProcessQuestionType.Product) {
|
||||
<oms-feature-return-process-product-question
|
||||
[processId]="returnProcessId()"
|
||||
[question]="question"
|
||||
></oms-feature-return-process-product-question>
|
||||
}
|
||||
@case (ReturnProcessQuestionType.Checklist) {
|
||||
<oms-feature-return-process-checklist-question
|
||||
[processId]="returnProcessId()"
|
||||
[question]="question"
|
||||
></oms-feature-return-process-checklist-question>
|
||||
}
|
||||
@default {
|
||||
{{ question | json }}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
import {
|
||||
ReturnProcess,
|
||||
ReturnProcessQuestion,
|
||||
ReturnProcessQuestionType,
|
||||
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 { ReturnProcessChecklistQuestionComponent } from './return-process-checklist-question/return-process-checklist-question.component';
|
||||
import {
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
JsonPipe,
|
||||
ReturnProcessSelectQuestionComponent,
|
||||
ReturnProcessProductQuestionComponent,
|
||||
ReturnProcessChecklistQuestionComponent,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
],
|
||||
@@ -37,6 +40,8 @@ export class ReturnProcessQuestionsComponent {
|
||||
// Renamed service to match naming conventions
|
||||
#returnProcessService = inject(ReturnProcessService);
|
||||
|
||||
ReturnProcessQuestionType = ReturnProcessQuestionType;
|
||||
|
||||
returnProcessId = input.required<number>();
|
||||
|
||||
returnProcess = computed<ReturnProcess | undefined>(() => {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export * from './lib/checkbox/checkbox-label.directive';
|
||||
export * from './lib/checkbox/checkbox.component';
|
||||
export * from './lib/checkbox/checklist-value.directive';
|
||||
export * from './lib/checkbox/checklist.component';
|
||||
export * from './lib/core/input-control.directive';
|
||||
export * from './lib/dropdown/dropdown.component';
|
||||
export * from './lib/dropdown/dropdown.types';
|
||||
export * from './lib/text-field/text-field.component';
|
||||
export * from './lib/text-field/textarea.component';
|
||||
export * from './lib/chips/chips.component';
|
||||
export * from './lib/chips/chip-option.component';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@use './lib/checkbox/checkbox';
|
||||
@use './lib/chips/chips';
|
||||
@use './lib/dropdown/dropdown';
|
||||
@use './lib/text-field/text-field.scss';
|
||||
@use "./lib/checkbox/checkbox";
|
||||
@use "./lib/checkbox/checklist";
|
||||
@use "./lib/chips/chips";
|
||||
@use "./lib/dropdown/dropdown";
|
||||
@use "./lib/text-field/text-field";
|
||||
@use "./lib/text-field/textarea";
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.ui-checkbox-label {
|
||||
@apply inline-flex items-center gap-4 text-isa-neutral-900 isa-text-body-2-regular;
|
||||
}
|
||||
|
||||
.ui-checkbox-label:has(:checked) {
|
||||
@apply isa-text-body-2-bold;
|
||||
}
|
||||
|
||||
.ui-checkbox.ui-checkbox__checkbox {
|
||||
@apply relative inline-flex p-3 items-center justify-center rounded-lg bg-isa-white size-6 border border-solid border-isa-neutral-900;
|
||||
font-size: 1.5rem;
|
||||
|
||||
3
libs/ui/input-controls/src/lib/checkbox/_checklist.scss
Normal file
3
libs/ui/input-controls/src/lib/checkbox/_checklist.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.ui-checklist {
|
||||
@apply inline-grid grid-flow-row gap-6;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Directive } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: 'label[uiCheckboxLabel]',
|
||||
host: {
|
||||
class: 'ui-checkbox-label',
|
||||
},
|
||||
})
|
||||
export class CheckboxLabelDirective {}
|
||||
@@ -13,7 +13,8 @@ export const CheckboxAppearance = {
|
||||
Checkbox: 'checkbox',
|
||||
} as const;
|
||||
|
||||
export type CheckboxAppearance = (typeof CheckboxAppearance)[keyof typeof CheckboxAppearance];
|
||||
export type CheckboxAppearance =
|
||||
(typeof CheckboxAppearance)[keyof typeof CheckboxAppearance];
|
||||
|
||||
@Component({
|
||||
selector: 'ui-checkbox',
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Directive, input, model } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[uiChecklistValue]',
|
||||
host: {
|
||||
'class': 'ui-checklist-value',
|
||||
'[checked]': 'checked()',
|
||||
'(change)': 'onChange($event)',
|
||||
},
|
||||
})
|
||||
export class ChecklistValueDirective {
|
||||
uiChecklistValue = input<unknown>(null);
|
||||
|
||||
checked = model<boolean>(false);
|
||||
|
||||
onChange(event: Event) {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
this.checked.set(event.target.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
contentChildren,
|
||||
model,
|
||||
effect,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { CdkObserveContent } from '@angular/cdk/observers';
|
||||
import { ChecklistValueDirective } from './checklist-value.directive';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-checklist',
|
||||
template: '<ng-content></ng-content>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CdkObserveContent],
|
||||
host: {
|
||||
'[class]': '["ui-checklist"]',
|
||||
},
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: ChecklistComponent,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class ChecklistComponent implements ControlValueAccessor {
|
||||
value = model<unknown[]>([]);
|
||||
|
||||
onChange?: (value: unknown[]) => void;
|
||||
onTouched?: () => void;
|
||||
|
||||
valueDirectives = contentChildren(ChecklistValueDirective, {
|
||||
descendants: true,
|
||||
});
|
||||
|
||||
valueDirectivesEffect = effect(() => {
|
||||
const valueDirectives = this.valueDirectives();
|
||||
|
||||
const currentValue = this.value() ?? [];
|
||||
const nextValue = structuredClone(currentValue);
|
||||
|
||||
for (const directive of valueDirectives) {
|
||||
const value = directive.uiChecklistValue();
|
||||
const checked = directive.checked();
|
||||
|
||||
if (checked) {
|
||||
if (!nextValue.includes(value)) {
|
||||
nextValue.push(value);
|
||||
}
|
||||
} else {
|
||||
const index = nextValue.indexOf(value);
|
||||
if (index !== -1) {
|
||||
nextValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEqual(currentValue, nextValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value.set(nextValue);
|
||||
this.onChange?.(nextValue);
|
||||
this.onTouched?.();
|
||||
});
|
||||
|
||||
writeValue(obj: unknown): void {
|
||||
if (Array.isArray(obj)) {
|
||||
this.value.set(obj);
|
||||
for (const directive of this.valueDirectives()) {
|
||||
const value = directive.uiChecklistValue();
|
||||
const checked = obj.includes(value);
|
||||
directive.checked.set(checked);
|
||||
}
|
||||
} else {
|
||||
this.value.set([]);
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: unknown[]) => void): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
}
|
||||
24
libs/ui/input-controls/src/lib/text-field/_textarea.scss
Normal file
24
libs/ui/input-controls/src/lib/text-field/_textarea.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
.ui-textarea {
|
||||
@apply px-2 py-1;
|
||||
@apply bg-white text-isa-neutral-900 isa-text-body-2-regular;
|
||||
@apply inline-flex flex-col gap-1;
|
||||
@apply border border-solid rounded-lg;
|
||||
|
||||
textarea {
|
||||
&:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
&:placeholder {
|
||||
@apply text-isa-neutral-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-textarea__empty {
|
||||
@apply border-isa-neutral-600;
|
||||
}
|
||||
|
||||
.ui-textarea__not-empty {
|
||||
@apply border-isa-neutral-900;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@if (label()) {
|
||||
<div class="ui-textarea__label">{{ label() }}</div>
|
||||
}
|
||||
<ng-content></ng-content>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component, computed, contentChild, input } from '@angular/core';
|
||||
import { NgControl } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-textarea',
|
||||
templateUrl: './textarea.component.html',
|
||||
host: { '[class]': '["ui-textarea", controlEmptyClass()]' },
|
||||
})
|
||||
export class TextareaComponent {
|
||||
label = input<string>();
|
||||
|
||||
ngControl = contentChild(NgControl);
|
||||
|
||||
controlEmptyClass = computed(() => {
|
||||
return this.ngControl()?.value
|
||||
? 'ui-textarea__not-empty'
|
||||
: 'ui-textarea__empty';
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user