Skip to content

Commit 83c9253

Browse files
committed
Improve the look and feel of characteristics in forms and details page
1 parent 67b69d4 commit 83c9253

12 files changed

Lines changed: 300 additions & 39 deletions

src/app/pages/product-details/product-details.component.html

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -456,32 +456,56 @@ <h2 class="text-4xl font-extrabold text-primary-100 text-center pb-8 pt-12 dark:
456456
<div class="flex flex-wrap -mx-4 justify-center">
457457
@for (char of prodChars; track char.id; let idx = $index) {
458458
<div class="w-full md:w-1/2 lg:w-1/3 px-4 mb-8">
459-
<div class="border border-gray-200 rounded-lg shadow bg-white dark:bg-secondary-200 dark:border-gray-800 shadow-md p-8 h-full">
459+
<div class="relative border border-gray-200 rounded-lg shadow bg-white dark:bg-secondary-200 dark:border-gray-800 shadow-md p-8 h-full">
460460
<h3 [ngClass]="{
461461
'break-all': hasLongWord(char.name, 20),
462462
'break-words': !hasLongWord(char.name, 20)
463-
}" class="text-2xl font-bold mb-4 dark:text-white break-words">{{char.name}}</h3>
464-
@for (val of char?.productSpecCharacteristicValue; track val.value) {
465-
@if (val?.isDefault == true) {
466-
<div class="flex items-center pl-4">
467-
<input disabled checked id="disabled-checked-checkbox" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 dark:bg-gray-600 dark:border-gray-800 rounded-full focus:ring-blue-500 focus:ring-2">
468-
@if(val.value !== undefined && val.value !== null){
469-
<label [title]="getCharacteristicValueLabel(val)" for="disabled-checked-checkbox" class="ms-2 text-sm font-medium text-gray-700 dark:text-gray-200 dark:bg-secondary-200 dark:border-gray-800 text-wrap break-all">{{ getCharacteristicValuePreview(val) }}</label>
470-
} @else {
471-
<label [title]="getCharacteristicRangeLabel(val)" for="disabled-checked-checkbox" class="ms-2 text-sm font-medium text-gray-700 dark:text-gray-200 dark:bg-secondary-200 dark:border-gray-800 text-wrap break-all">{{ getCharacteristicRangePreview(val) }}</label>
472-
}
463+
}" class="text-2xl font-bold mb-4 dark:text-white break-words pr-28">{{char.name}}</h3>
464+
@if (char.description) {
465+
<markdown [ngClass]="{
466+
'break-all': hasLongWord(char.description, 20),
467+
'break-words': !hasLongWord(char.description, 20)
468+
}" class="text-sm text-gray-700 dark:text-gray-200 text-wrap break-words mb-4" [data]="char.description"></markdown>
469+
}
470+
@if (isOptionalCharacteristic(char)) {
471+
<span class="absolute top-4 right-4 inline-flex items-center rounded-md bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900/60 dark:text-blue-200">Optional</span>
472+
}
473+
<div class="mt-3">
474+
@if (isBooleanCharacteristic(char)) {
475+
<div class="pl-4 flex items-center gap-2">
476+
<span class="text-sm font-medium text-gray-600 dark:text-gray-300">Default</span>
477+
<span
478+
class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium"
479+
[ngClass]="getBooleanDefaultValue(char)
480+
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
481+
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200'">
482+
{{ getBooleanDefaultValue(char) ? 'Enabled' : 'Disabled' }}
483+
</span>
473484
</div>
474485
} @else {
475-
<div class="flex items-center pl-4">
476-
<input disabled id="disabled-checkbox" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-600 dark:border-gray-800 border-gray-300 rounded-full focus:ring-blue-500 focus:ring-2">
477-
@if(val.value !== undefined && val.value !== null){
478-
<label [title]="getCharacteristicValueLabel(val)" for="disabled-checked-checkbox" class="ms-2 text-sm font-medium text-gray-700 dark:text-gray-200 text-wrap break-all">{{ getCharacteristicValuePreview(val) }}</label>
486+
@for (val of char?.productSpecCharacteristicValue; track val.value) {
487+
@if (val?.isDefault == true) {
488+
<div class="flex items-center pl-4">
489+
<input disabled checked id="disabled-checked-checkbox" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 dark:bg-gray-600 dark:border-gray-800 rounded-full focus:ring-blue-500 focus:ring-2">
490+
@if(val.value !== undefined && val.value !== null){
491+
<label [title]="getCharacteristicValueLabel(val)" for="disabled-checked-checkbox" class="ms-2 text-sm font-medium text-gray-700 dark:text-gray-200 dark:bg-secondary-200 dark:border-gray-800 text-wrap break-all">{{ getCharacteristicValuePreview(val) }}</label>
492+
} @else {
493+
<label [title]="getCharacteristicRangeLabel(val)" for="disabled-checked-checkbox" class="ms-2 text-sm font-medium text-gray-700 dark:text-gray-200 dark:bg-secondary-200 dark:border-gray-800 text-wrap break-all">{{ getCharacteristicRangePreview(val) }}</label>
494+
}
495+
</div>
479496
} @else {
480-
<label [title]="getCharacteristicRangeLabel(val)" for="disabled-checked-checkbox" class="ms-2 text-sm font-medium text-gray-700 dark:text-gray-200 text-wrap break-all">{{ getCharacteristicRangePreview(val) }}</label>
497+
<div class="flex items-center pl-4">
498+
<input disabled id="disabled-checkbox" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-600 dark:border-gray-800 border-gray-300 rounded-full focus:ring-blue-500 focus:ring-2">
499+
@if(val.value !== undefined && val.value !== null){
500+
<label [title]="getCharacteristicValueLabel(val)" for="disabled-checked-checkbox" class="ms-2 text-sm font-medium text-gray-700 dark:text-gray-200 text-wrap break-all">{{ getCharacteristicValuePreview(val) }}</label>
501+
} @else {
502+
<label [title]="getCharacteristicRangeLabel(val)" for="disabled-checked-checkbox" class="ms-2 text-sm font-medium text-gray-700 dark:text-gray-200 text-wrap break-all">{{ getCharacteristicRangePreview(val) }}</label>
503+
}
504+
</div>
481505
}
482-
</div>
506+
}
483507
}
484-
}
508+
</div>
485509
</div>
486510
</div>
487511
} @empty {

src/app/pages/product-details/product-details.component.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,37 @@ describe('ProductDetailsComponent', () => {
514514

515515
expect(preview).toBe('1 - 3 (GB)');
516516
});
517+
518+
it('isOptionalCharacteristic should detect companion - enabled characteristic', () => {
519+
component.prodSpec = {
520+
productSpecCharacteristic: [
521+
{ name: 'Storage' },
522+
{ name: 'Storage - enabled' },
523+
{ name: 'RAM' },
524+
],
525+
} as any;
526+
527+
expect(component.isOptionalCharacteristic({ name: 'Storage' })).toBeTrue();
528+
expect(component.isOptionalCharacteristic({ name: 'RAM' })).toBeFalse();
529+
});
530+
531+
it('boolean helpers should classify boolean characteristics and return default state', () => {
532+
const boolChar = {
533+
productSpecCharacteristicValue: [
534+
{ value: true, isDefault: false },
535+
{ value: false, isDefault: true },
536+
],
537+
};
538+
const nonBoolChar = {
539+
productSpecCharacteristicValue: [
540+
{ value: 'small', isDefault: true },
541+
{ value: 'large', isDefault: false },
542+
],
543+
};
544+
545+
expect(component.isBooleanCharacteristic(boolChar)).toBeTrue();
546+
expect(component.getBooleanDefaultValue(boolChar)).toBeFalse();
547+
expect(component.isBooleanCharacteristic(nonBoolChar)).toBeFalse();
548+
expect(component.getBooleanDefaultValue({ productSpecCharacteristicValue: [] })).toBeFalse();
549+
});
517550
});

src/app/pages/product-details/product-details.component.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,37 @@ async deleteProduct(product: Product | undefined){
10451045
return this.truncateCharacteristicLabel(this.getCharacteristicRangeLabel(valueSpec));
10461046
}
10471047

1048+
isOptionalCharacteristic(characteristic: any): boolean {
1049+
const baseName = (characteristic?.name ?? '').toString().trim();
1050+
if (!baseName || !this.prodSpec?.productSpecCharacteristic) {
1051+
return false;
1052+
}
1053+
1054+
const expectedOptionalName = `${baseName} - enabled`.toLowerCase();
1055+
return this.prodSpec.productSpecCharacteristic.some((char: any) =>
1056+
(char?.name ?? '').toString().trim().toLowerCase() === expectedOptionalName
1057+
);
1058+
}
1059+
1060+
isBooleanCharacteristic(characteristic: any): boolean {
1061+
const values = characteristic?.productSpecCharacteristicValue;
1062+
if (!Array.isArray(values) || values.length === 0) {
1063+
return false;
1064+
}
1065+
1066+
return values.every((valueSpec: any) => typeof valueSpec?.value === 'boolean');
1067+
}
1068+
1069+
getBooleanDefaultValue(characteristic: any): boolean {
1070+
const values = characteristic?.productSpecCharacteristicValue;
1071+
if (!Array.isArray(values) || values.length === 0) {
1072+
return false;
1073+
}
1074+
1075+
const defaultValueSpec = values.find((valueSpec: any) => valueSpec?.isDefault === true) ?? values[0];
1076+
return defaultValueSpec?.value === true;
1077+
}
1078+
10481079
private formatCharacteristicScalar(value: any): string {
10491080
if (value === undefined || value === null) {
10501081
return '';

src/app/shared/forms/offer/price-plans/configuration-profile-drawer/configuration-profile-drawer.component.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,82 @@ describe('ConfigurationProfileDrawerComponent', () => {
2525
it('should create', () => {
2626
expect(component).toBeTruthy();
2727
});
28+
29+
it('should exclude optional toggle characteristics ending with - enabled', () => {
30+
const testFixture = TestBed.createComponent(ConfigurationProfileDrawerComponent);
31+
const testComponent = testFixture.componentInstance;
32+
33+
testComponent.profileData = [
34+
{
35+
id: 'char-1',
36+
name: 'Storage',
37+
productSpecCharacteristicValue: [{ value: 'small', isDefault: true }]
38+
},
39+
{
40+
id: 'char-2',
41+
name: 'Storage - enabled',
42+
productSpecCharacteristicValue: [{ value: true, isDefault: true }]
43+
}
44+
];
45+
46+
testComponent.ngOnInit();
47+
48+
const names = testComponent.characteristics.value.map((char: any) => char.name);
49+
expect(names).toEqual(['Storage']);
50+
});
51+
52+
it('should keep filtering compliance and data-space-specific characteristics', () => {
53+
const testFixture = TestBed.createComponent(ConfigurationProfileDrawerComponent);
54+
const testComponent = testFixture.componentInstance;
55+
56+
testComponent.profileData = [
57+
{
58+
id: 'char-1',
59+
name: 'Compliance:SelfAtt',
60+
productSpecCharacteristicValue: [{ value: true, isDefault: true }]
61+
},
62+
{
63+
id: 'char-2',
64+
name: 'Policy JSON',
65+
valueType: 'authorizationPolicy',
66+
productSpecCharacteristicValue: [{ value: '{"a":1}', isDefault: true }]
67+
},
68+
{
69+
id: 'char-3',
70+
name: 'Credential JSON',
71+
valueType: 'credentialsConfiguration',
72+
productSpecCharacteristicValue: [{ value: '{"b":2}', isDefault: true }]
73+
},
74+
{
75+
id: 'char-4',
76+
name: 'Region',
77+
productSpecCharacteristicValue: [{ value: 'EU', isDefault: true }]
78+
}
79+
];
80+
81+
testComponent.ngOnInit();
82+
83+
const names = testComponent.characteristics.value.map((char: any) => char.name);
84+
expect(names).toEqual(['Region']);
85+
});
86+
87+
it('should preserve boolean false as default selected value', () => {
88+
const testFixture = TestBed.createComponent(ConfigurationProfileDrawerComponent);
89+
const testComponent = testFixture.componentInstance;
90+
91+
testComponent.profileData = [
92+
{
93+
id: 'char-1',
94+
name: 'Platinum',
95+
productSpecCharacteristicValue: [
96+
{ value: true, isDefault: false },
97+
{ value: false, isDefault: true }
98+
]
99+
}
100+
];
101+
102+
testComponent.ngOnInit();
103+
104+
expect(testComponent.characteristics.at(0).value.selectedValue).toBeFalse();
105+
});
28106
});

src/app/shared/forms/offer/price-plans/configuration-profile-drawer/configuration-profile-drawer.component.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ export class ConfigurationProfileDrawerComponent implements OnInit {
3535
let profileChars = [];
3636
for(let i=0;i<this.profileData.length;i++){
3737
//if (!certifications.some(certification => certification.name === this.profileData[i].name) && this.profileData[i].name != 'Compliance:SelfAtt') {
38+
const charName = (this.profileData[i]?.name ?? '').toString().trim();
39+
const isOptionalToggle = charName.toLowerCase().endsWith('- enabled');
3840

39-
if (!certifications.some(certification => certification.name === this.profileData[i].name)
40-
&& !this.profileData[i].name.startsWith('Compliance:')
41+
if (!certifications.some(certification => certification.name === charName)
42+
&& !charName.startsWith('Compliance:')
43+
&& !isOptionalToggle
4144
&& this.profileData[i].valueType != 'credentialsConfiguration'
4245
&& this.profileData[i].valueType != 'authorizationPolicy') {
4346
profileChars.push(this.profileData[i]);
@@ -57,12 +60,13 @@ export class ConfigurationProfileDrawerComponent implements OnInit {
5760

5861

5962
private createCharacteristicForm(char: any): FormGroup {
63+
const defaultOption = char.productSpecCharacteristicValue.find((v: any) => v.isDefault);
64+
const defaultValue = defaultOption?.value ?? defaultOption?.valueFrom ?? '';
65+
6066
return this.fb.group({
6167
id: new FormControl(char.id),
6268
name: new FormControl(char.name),
63-
selectedValue: new FormControl(
64-
char.productSpecCharacteristicValue.find((v: any) => v.isDefault)?.value || ''
65-
),
69+
selectedValue: new FormControl(defaultValue),
6670
options: new FormControl(char.productSpecCharacteristicValue || [])
6771
});
6872
}
@@ -72,7 +76,16 @@ export class ConfigurationProfileDrawerComponent implements OnInit {
7276
}
7377

7478
changeProfileValue(index: number, event: any) {
75-
this.characteristics.at(index).patchValue({ selectedValue: event.target.value });
79+
const rawValue = event.target.value;
80+
let parsedValue: any = rawValue;
81+
82+
if (rawValue === 'true') {
83+
parsedValue = true;
84+
} else if (rawValue === 'false') {
85+
parsedValue = false;
86+
}
87+
88+
this.characteristics.at(index).patchValue({ selectedValue: parsedValue });
7689
console.log(this.characteristics.at(index))
7790
}
7891

src/app/shared/forms/offer/price-plans/price-plan-drawer/price-plan-drawer.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ <h3 class="font-bold text-lg dark:text-white">
7474
{{ 'FORMS.PRICE_PLANS._config_profile' | translate }}
7575
</h3>
7676
@if(!checkPriceCompChars()){
77-
@if ((formGroup.get('prodSpecCharValueUse')?.value?.length > 0) || (formGroup.get('productProfile')?.value?.selectedValues.length > 0)){
77+
@if (getProcessedProfileData().length > 0){
7878
<div class="overflow-x-auto">
7979
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
8080
<thead class="text-xs text-gray-700 uppercase bg-gray-100 dark:bg-gray-700 dark:text-gray-400">
@@ -84,11 +84,11 @@ <h3 class="font-bold text-lg dark:text-white">
8484
</tr>
8585
</thead>
8686
<tbody>
87-
<tr *ngFor="let char of getProcessedProfileData()" class="bg-white dark:bg-gray-800 border-b">
87+
<tr *ngFor="let char of paginatedProfileData" class="bg-white dark:bg-gray-800 border-b">
8888
<td class="px-4 py-1">{{ char.name }}</td>
8989
<td class="px-4 py-1">
90-
@if (char.selectedValue) {
91-
@if(char.selectedValue.value){
90+
@if (char.selectedValue !== null && char.selectedValue !== undefined) {
91+
@if(char.selectedValue?.value !== undefined){
9292
{{ char.selectedValue.value }}
9393
{{ char.selectedValue.unitOfMeasure || '' }}
9494
} @else {

src/app/shared/forms/offer/price-plans/price-plan-drawer/price-plan-drawer.component.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
22
import { NO_ERRORS_SCHEMA } from '@angular/core';
3+
import { FormBuilder } from '@angular/forms';
34
import { TranslateModule } from '@ngx-translate/core';
45
import { RouterTestingModule } from '@angular/router/testing';
56
import { HttpClientTestingModule } from '@angular/common/http/testing';
@@ -24,4 +25,50 @@ describe('PricePlanDrawerComponent', () => {
2425
it('should create', () => {
2526
expect(component).toBeTruthy();
2627
});
28+
29+
it('updateIsDefault should match boolean values coming from select controls', () => {
30+
const profileData = [
31+
{
32+
id: 'char-1',
33+
name: 'platinum',
34+
productSpecCharacteristicValue: [
35+
{ value: true, isDefault: true },
36+
{ value: false, isDefault: false }
37+
]
38+
}
39+
];
40+
const selectedValues = [
41+
{ id: 'char-1', selectedValue: 'false' }
42+
];
43+
44+
const updated = component.updateIsDefault(profileData, selectedValues);
45+
46+
expect(updated[0].productSpecCharacteristicValue[0].isDefault).toBeFalse();
47+
expect(updated[0].productSpecCharacteristicValue[1].isDefault).toBeTrue();
48+
});
49+
50+
it('should ignore - enabled characteristics in processed and paginated profile data', () => {
51+
const fb = TestBed.inject(FormBuilder);
52+
component.formGroup = fb.group({
53+
prodSpecCharValueUse: [[
54+
{ id: '1', name: 'Char 1', productSpecCharacteristicValue: [{ value: 'A', isDefault: true }] },
55+
{ id: '2', name: 'Char 1 - enabled', productSpecCharacteristicValue: [{ value: true, isDefault: true }] },
56+
{ id: '3', name: 'Char 2', productSpecCharacteristicValue: [{ value: 'B', isDefault: true }] },
57+
{ id: '4', name: 'Char 3', productSpecCharacteristicValue: [{ value: 'C', isDefault: true }] },
58+
{ id: '5', name: 'Char 4', productSpecCharacteristicValue: [{ value: 'D', isDefault: true }] },
59+
{ id: '6', name: 'Char 5', productSpecCharacteristicValue: [{ value: 'E', isDefault: true }] }
60+
]],
61+
productProfile: [null],
62+
paymentOnline: [true],
63+
priceComponents: [[]]
64+
});
65+
component.pageSize = 5;
66+
67+
const processed = component.getProcessedProfileData();
68+
69+
expect(processed.length).toBe(5);
70+
expect(processed.some((char: any) => char.name === 'Char 1 - enabled')).toBeFalse();
71+
expect(component.totalPages).toBe(1);
72+
expect(component.paginatedProfileData.length).toBe(5);
73+
});
2774
});

0 commit comments

Comments
 (0)