Skip to content

Commit

Permalink
NAS-128557 / 24.10 / Finishing WidgetGroupFormComponent (#10022)
Browse files Browse the repository at this point in the history
  • Loading branch information
RehanY147 committed May 14, 2024
1 parent 2552f15 commit c174c12
Show file tree
Hide file tree
Showing 118 changed files with 1,310 additions and 65 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"build:prod:aot": "yarn run build:prod --base-href /ui/ && node scripts/verify_build.js",
"test": "jest",
"test:watch": "jest --watch",
"test:pr": "yarn run check-env -s && echo 'Setting up temporary environment file...\\n' && yarn run ui remote -i 'headless.local' && jest --coverage --maxWorkers=2",
"test:ci": "jest --runInBand",
"test:pr": "yarn run check-env -s && echo 'Setting up temporary environment file...\\n' && yarn run ui remote -i 'headless.local' && jest --coverage --maxWorkers=2 src/app/pages/dashboard/components",
"test:changed": "node scripts/test_changed.js",
"lint": "ng lint && stylelint 'src/**/*.scss' && markuplint 'src/**/*.html'",
"lint:fix": "ng lint --fix && stylelint --fix 'src/**/*.scss' && markuplint --fix 'src/**/*.html'",
Expand Down
11 changes: 11 additions & 0 deletions src/app/modules/ix-forms/utils/get-form-errors.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FormGroup, ValidationErrors } from '@angular/forms';

export function getAllFormErrors(form: FormGroup, fields: string[]): Record<string, ValidationErrors> {
let errorsByName: Record<string, ValidationErrors> = {};
for (const field of fields) {
if (form.controls[field as keyof (typeof form.controls)].errors) {
errorsByName = { ...errorsByName, [field]: form.controls[field as keyof (typeof form.controls)].errors };
}
}
return errorsByName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('DashboardComponent', () => {
save: jest.fn(() => of(undefined)),
}),
mockProvider(IxChainedSlideInService, {
open: jest.fn(() => of(undefined)),
open: jest.fn(() => of({ error: false, response: groupA })),
}),
],
});
Expand Down Expand Up @@ -105,11 +105,7 @@ describe('DashboardComponent', () => {
await addButton.click();

expect(spectator.inject(IxChainedSlideInService).open)
.toHaveBeenCalledWith(WidgetGroupFormComponent, true, groupA);

const groups = spectator.queryAll(WidgetGroupComponent);
expect(groups).toHaveLength(5);
expect(groups[4].group).toEqual({ layout: WidgetGroupLayout.Full, slots: [] });
.toHaveBeenCalledWith(WidgetGroupFormComponent, true);
});

it('saves new configuration when Save is pressed', async () => {
Expand Down
20 changes: 10 additions & 10 deletions src/app/pages/dashboard/components/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { EmptyType } from 'app/enums/empty-type.enum';
import { EmptyConfig } from 'app/interfaces/empty-config.interface';
import { WidgetGroupFormComponent } from 'app/pages/dashboard/components/widget-group-form/widget-group-form.component';
import { DashboardStore } from 'app/pages/dashboard/services/dashboard.store';
import { WidgetGroup, WidgetGroupLayout } from 'app/pages/dashboard/types/widget-group.interface';
import { WidgetGroup } from 'app/pages/dashboard/types/widget-group.interface';
import { ErrorHandlerService } from 'app/services/error-handler.service';
import { IxChainedSlideInService } from 'app/services/ix-chained-slide-in.service';
import { ChainedComponentResponse, IxChainedSlideInService } from 'app/services/ix-chained-slide-in.service';

@UntilDestroy()
@Component({
Expand Down Expand Up @@ -73,21 +73,21 @@ export class DashboardComponent implements OnInit {
}

protected onAddGroup(): void {
const newGroup: WidgetGroup = {
layout: WidgetGroupLayout.Full,
slots: [],
};

this.renderedGroups.update((groups) => [...groups, newGroup]);
this.onEditGroup(newGroup);
this.slideIn
.open(WidgetGroupFormComponent, true)
.pipe(untilDestroyed(this))
.subscribe((response: ChainedComponentResponse) => {
if (response.response) {
this.renderedGroups.update((groups) => [...groups, response.response as WidgetGroup]);
}
});
}

protected onEditGroup(group: WidgetGroup): void {
this.slideIn
.open(WidgetGroupFormComponent, true, group)
.pipe(untilDestroyed(this))
.subscribe(() => {

});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
class="inert-container"
inert
>
<ng-container *ngComponentOutlet="slot.component; inputs: slot.inputs;" />
<div *ngIf="hasErrors(slotIndex); else component" class="error">
<ix-widget-error [message]="'Widget has errors' | translate"></ix-widget-error>
</div>
<ng-template #component>
<ng-container *ngComponentOutlet="slot.component; inputs: slot.inputs;" />
</ng-template>
</div>

<ng-template #emptySlot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,33 @@

&.empty {
align-items: center;
background: var(--bg2);
color: var(--fg2);
display: flex;
flex-direction: column;
font-size: 13px;
justify-content: center;
}



.inert-container {
height: 100%;
}
}

.error {
align-items: center;
background: var(--bg2);
color: var(--fg2);
display: flex;
font-size: 13px;
height: 100%;
justify-content: center;
}

.error-icon {
margin-right: 5px;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('WidgetEditorGroupComponent - additions', () => {
{ type: WidgetType.Hostname },
],
},
validationErrors: [{}, {}, {}, {}],
},
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ChangeDetectionStrategy, Component, input, output,
} from '@angular/core';
import { ValidationErrors } from '@angular/forms';
import { WidgetGroupComponent } from 'app/pages/dashboard/components/widget-group/widget-group.component';

/**
Expand All @@ -17,11 +18,16 @@ import { WidgetGroupComponent } from 'app/pages/dashboard/components/widget-grou
})
export class WidgetEditorGroupComponent extends WidgetGroupComponent {
readonly selectedSlot = input(0);
validationErrors = input.required<ValidationErrors[]>();

selectedSlotChange = output<number>();

onSlotSelected(event: Event, slotIndex: number): void {
event.preventDefault();
this.selectedSlotChange.emit(slotIndex);
}

hasErrors(index: number): boolean {
return !!Object.keys(this.validationErrors()[index]).length;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<ix-modal-header2
[title]="'Widget Editor' | translate"
></ix-modal-header2>

<form class="ix-form-container" [formGroup]="form" (submit)="onSubmit()">
<ix-fieldset>
<div class="form-row">
Expand All @@ -23,15 +22,33 @@

<div class="editor-container">
<ix-widget-editor-group
[group]="group"
[(selectedSlot)]="selectedSlot"
[group]="group()"
[selectedSlot]="selectedSlot().slotPosition"
[validationErrors]="validationErrors()"
(selectedSlotChange)="selectedSlotChanged($event)"
></ix-widget-editor-group>
</div>

Selected slot: {{ selectedSlot }}

<!-- TODO: Category dropdown, pull widget definitions from widgetRegistry and regroup by category -->
<!-- TODO: Widget type dropdown -->
<!-- TODO: Custom settings form if necessary for the selected widget type, like WidgetInterfaceIpSettingsComponent. -->
<!-- TODO: This component shouldn't care what are the details of settings that are being set for every widget. -->
<ix-widget-group-slot-form
*ngIf="selectedSlot()"
[slotConfig]="selectedSlot()"
(validityChange)="updateSlotValidation($event)"
(settingsChange)="updateSlotSettings($event)"
></ix-widget-group-slot-form>

<ix-form-actions>
<button
mat-button
type="submit"
color="primary"
[ixTest]="['add-widget-group-btn']"
[disabled]="settingsHasErrors() || form.invalid"
>{{ 'Save' | translate }}</button>
<button
mat-button
type="button"
[ixTest]="['cancel-add-widget-group-btn']"
(click)="chainedRef.close({ response: false, error: null })"
>{{ 'Cancel' | translate }}</button>
</ix-form-actions>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@
max-width: 50%;
}
}

.widget-options-container {
padding: 16px 0;
}

.form-actions {
display: flex;
flex-direction: row-reverse;
gap: 8px;
justify-content: flex-end;
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { Spectator } from '@ngneat/spectator';
import { mockProvider, createComponentFactory } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { IxIconGroupHarness } from 'app/modules/ix-forms/components/ix-icon-group/ix-icon-group.harness';
import { ChainedRef } from 'app/modules/ix-forms/components/ix-slide-in/chained-component-ref';
import { IxSlideInRef } from 'app/modules/ix-forms/components/ix-slide-in/ix-slide-in-ref';
import { IxFormsModule } from 'app/modules/ix-forms/ix-forms.module';
import { FormErrorHandlerService } from 'app/modules/ix-forms/services/form-error-handler.service';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { TestIdModule } from 'app/modules/test-id/test-id.module';
import { WidgetEditorGroupComponent } from 'app/pages/dashboard/components/widget-group-form/widget-editor-group/widget-editor-group.component';
import { WidgetGroupFormComponent } from 'app/pages/dashboard/components/widget-group-form/widget-group-form.component';
import { WidgetGroupSlotFormComponent } from 'app/pages/dashboard/components/widget-group-form/widget-group-slot-form/widget-group-slot-form.component';
import { SlotPosition } from 'app/pages/dashboard/types/slot-position.enum';
import { WidgetGroup, WidgetGroupLayout } from 'app/pages/dashboard/types/widget-group.interface';
import { SlotSize, WidgetType } from 'app/pages/dashboard/types/widget.interface';
import { IxSlideInService } from 'app/services/ix-slide-in.service';

describe('WidgetGroupFormComponent', () => {
Expand All @@ -29,14 +35,17 @@ describe('WidgetGroupFormComponent', () => {
const createComponent = createComponentFactory({
component: WidgetGroupFormComponent,
imports: [
TestIdModule,
IxFormsModule,
ReactiveFormsModule,
MatIconTestingModule,
],
declarations: [
MockComponent(WidgetEditorGroupComponent),
MockComponent(WidgetGroupSlotFormComponent),
],
providers: [
mockAuth(),
mockProvider(ChainedRef, chainedComponentRef),
mockProvider(IxSlideInService),
mockProvider(FormErrorHandlerService),
Expand All @@ -45,16 +54,107 @@ describe('WidgetGroupFormComponent', () => {
],
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
describe('check layout selector', () => {
beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('checks layout selector', async () => {
const layoutSelector = await loader.getHarness(IxIconGroupHarness.with({ label: 'Layouts' }));
const editor = spectator.query(WidgetEditorGroupComponent);
expect(await layoutSelector.getValue()).toEqual(WidgetGroupLayout.Full);
expect(editor.group).toEqual({ layout: WidgetGroupLayout.Full, slots: [{ type: null }] });
await layoutSelector.setValue(WidgetGroupLayout.Halves);
expect(await layoutSelector.getValue()).toEqual(WidgetGroupLayout.Halves);
expect(editor.group).toEqual({ layout: WidgetGroupLayout.Halves, slots: [{ type: null }, { type: null }] });
});
});

it('checks layout selector', async () => {
const layoutSelector = await loader.getHarness(IxIconGroupHarness.with({ label: 'Layouts' }));
expect(await layoutSelector.getValue()).toEqual(WidgetGroupLayout.Full);
describe('returns group object based on form values', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
{
provide: ChainedRef,
useValue: {
getData: () => ({
layout: WidgetGroupLayout.Halves,
slots: [
{ type: WidgetType.InterfaceIp, settings: { interface: '1' } },
{ type: WidgetType.InterfaceIp, settings: { interface: '2' } },
],
}) as WidgetGroup,
close: jest.fn(),
} as ChainedRef<WidgetGroup>,
},
],
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('returns group object in chainedRef response when form is submitted', async () => {
const submitBtn = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await submitBtn.click();
const chainedRef = spectator.inject(ChainedRef);
expect(chainedRef.close).toHaveBeenCalledWith({
error: false,
response: {
layout: WidgetGroupLayout.Halves,
slots: [
{ type: WidgetType.InterfaceIp, settings: { interface: '1' } },
{ type: WidgetType.InterfaceIp, settings: { interface: '2' } },
],
},
});
});

it('changes slot', () => {
const editor = spectator.query(WidgetEditorGroupComponent);
editor.selectedSlotChange.emit(1);

spectator.detectChanges();
const slotForm = spectator.query(WidgetGroupSlotFormComponent);
expect(slotForm.slotConfig).toEqual({
type: WidgetType.InterfaceIp,
settings: {
interface: '2',
},
slotPosition: SlotPosition.Second,
slotSize: SlotSize.Half,
});
});

it('disables button when slot has validation errors', async () => {
const slotForm = spectator.query(WidgetGroupSlotFormComponent);
slotForm.validityChange.emit([SlotPosition.First, { interface: { required: true } }]);
spectator.detectChanges();
const submitBtn = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
expect(await submitBtn.isDisabled()).toBe(true);
});

it('updates settings', async () => {
const slotForm = spectator.query(WidgetGroupSlotFormComponent);
slotForm.settingsChange.emit({
slotPosition: SlotPosition.First,
type: WidgetType.InterfaceIp,
settings: { interface: '5' },
slotSize: SlotSize.Half,
});
spectator.detectChanges();
const submitBtn = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await submitBtn.click();

await layoutSelector.setValue(WidgetGroupLayout.Halves);
expect(await layoutSelector.getValue()).toEqual(WidgetGroupLayout.Halves);
expect(spectator.inject(ChainedRef).close).toHaveBeenCalledWith({
error: false,
response: {
layout: WidgetGroupLayout.Halves,
slots: [
{ type: WidgetType.InterfaceIp, settings: { interface: '5' } },
{ type: WidgetType.InterfaceIp, settings: { interface: '2' } },
],
} as WidgetGroup,
});
});
});
});

0 comments on commit c174c12

Please sign in to comment.