import { NvmeofNamespacesFormComponent } from './nvmeof-namespaces-form/nvmeof-namespaces-form.component';
import { NvmeofInitiatorsListComponent } from './nvmeof-initiators-list/nvmeof-initiators-list.component';
import { NvmeofInitiatorsFormComponent } from './nvmeof-initiators-form/nvmeof-initiators-form.component';
+import { NvmeofGatewayGroupComponent } from './nvmeof-gateway-group/nvmeof-gateway-group.component';
+import { NvmeofSubsystemsStepOneComponent } from './nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component';
+import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node/nvmeof-gateway-node.component';
+import { NvmeofGroupFormComponent } from './nvmeof-group-form /nvmeof-group-form.component';
import {
ButtonModule,
import Reset from '@carbon/icons/es/reset/32';
import SubtractAlt from '@carbon/icons/es/subtract--alt/20';
import ProgressBarRound from '@carbon/icons/es/progress-bar--round/32';
-import { NvmeofGatewayGroupComponent } from './nvmeof-gateway-group/nvmeof-gateway-group.component';
-import { NvmeofGroupFormComponent } from './nvmeof-group-form /nvmeof-group-form.component';
-import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node/nvmeof-gateway-node.component';
@NgModule({
imports: [
NvmeofInitiatorsListComponent,
NvmeofInitiatorsFormComponent,
NvmeofGatewayNodeComponent,
- NvmeofGroupFormComponent
+ NvmeofGroupFormComponent,
+ NvmeofSubsystemsStepOneComponent
],
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
})
--- /dev/null
+
+<form [formGroup]="formGroup"
+ novalidate>
+ <div cdsGrid
+ [useCssGrid]="true"
+ [narrow]="true"
+ [fullWidth]="true">
+ <div cdsCol
+ [columnNumbers]="{sm: 4, md: 8}">
+ <div cdsRow
+ class="form-heading">
+ <h3 class="cds--type-heading-03"
+ i18n>Subsystem details</h3>
+ <p
+ class="cds--type-label-02"
+ i18n>Enter identifying and network details for this subsystem.</p>
+ </div>
+ <div cdsRow
+ class="form-item">
+ <cds-text-label
+ i18n
+ [invalid]="nqnRef.isInvalid"
+ [invalidText]="nqnInvalidTemplate"
+ helperText="A unique identifier for the subsystem."
+ i18n-helperText>
+ Subsystem NQN (NVMe Qualified Name)
+ <input cdsText
+ cdValidate
+ #nqnRef="cdValidate"
+ type="text"
+ formControlName="nqn"
+ [invalid]="nqnRef.isInvalid">
+ </cds-text-label>
+ </div>
+ <div cdsRow
+ class="form-item">
+ <cds-text-label
+ i18n
+ helperText="Gateway group routes traffic for this subsystem."
+ i18n-helperText
+ [disabled]="true">
+ Gateway group
+ <input cdsText
+ type="text"
+ disabled
+ [value]="group"
+ [skeleton]="!group">
+ </cds-text-label>
+ </div>
+ </div>
+ </div>
+</form>
+
+<ng-template #nqnInvalidTemplate>
+@for (err of formGroup.get('nqn').errors | keyvalue; track err.key) {
+<span class="invalid-feedback">{{ INVALID_TEXTS[err.key] }}</span>
+}
+</ng-template>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SharedModule } from '~/app/shared/shared.module';
+import { NvmeofSubsystemsStepOneComponent } from './nvmeof-subsystem-step-1.component';
+import { FormHelper } from '~/testing/unit-test-helper';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { GridModule, InputModule } from 'carbon-components-angular';
+
+describe('NvmeofSubsystemsStepOneComponent', () => {
+ let component: NvmeofSubsystemsStepOneComponent;
+ let fixture: ComponentFixture<NvmeofSubsystemsStepOneComponent>;
+ let nvmeofService: NvmeofService;
+ let form: CdFormGroup;
+ let formHelper: FormHelper;
+ const mockGroupName = 'default';
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [NvmeofSubsystemsStepOneComponent],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ NgbTypeaheadModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ InputModule,
+ GridModule,
+ ToastrModule.forRoot()
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(NvmeofSubsystemsStepOneComponent);
+ component = fixture.componentInstance;
+ component.ngOnInit();
+ form = component.formGroup;
+ formHelper = new FormHelper(form);
+ fixture.detectChanges();
+ component.group = mockGroupName;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('should test form', () => {
+ beforeEach(() => {
+ nvmeofService = TestBed.inject(NvmeofService);
+ spyOn(nvmeofService, 'createSubsystem').and.stub();
+ });
+
+ it('should give error on invalid nqn', () => {
+ formHelper.setValue('nqn', 'nqn:2001-07.com.ceph:');
+ formHelper.expectError('nqn', 'nqnPattern');
+ });
+ });
+});
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
+
+@Component({
+ selector: 'cd-nvmeof-subsystem-step-one',
+ templateUrl: './nvmeof-subsystem-step-1.component.html',
+ styleUrls: ['./nvmeof-subsystem-step-1.component.scss'],
+ standalone: false
+})
+export class NvmeofSubsystemsStepOneComponent implements OnInit, TearsheetStep {
+ @Input() group!: string;
+ formGroup: CdFormGroup;
+ action: string;
+ pageURL: string;
+ INVALID_TEXTS = {
+ required: $localize`This field is required`,
+ nqnPattern: $localize`Expected NQN format is "nqn.$year-$month.$reverseDomainName:$utf8-string" or "nqn.2014-08.org.nvmexpress:uuid:$UUID-string"`,
+ notUnique: $localize`This NQN is already in use`,
+ maxLength: $localize`An NQN may not be more than 223 bytes in length.`
+ };
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ public activeModal: NgbActiveModal,
+ private nvmeofService: NvmeofService
+ ) {}
+
+ DEFAULT_NQN = 'nqn.2001-07.com.ceph:' + Date.now();
+ NQN_REGEX = /^nqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+(:[A-Za-z0-9-\.]+)*)$/;
+ NQN_REGEX_UUID = /^nqn\.2014-08\.org\.nvmexpress:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
+
+ customNQNValidator = CdValidators.custom(
+ 'nqnPattern',
+ (nqnInput: string) =>
+ !!nqnInput && !(this.NQN_REGEX.test(nqnInput) || this.NQN_REGEX_UUID.test(nqnInput))
+ );
+
+ ngOnInit() {
+ this.createForm();
+ }
+
+ createForm() {
+ this.formGroup = new CdFormGroup({
+ nqn: new UntypedFormControl(this.DEFAULT_NQN, {
+ validators: [
+ this.customNQNValidator,
+ Validators.required,
+ CdValidators.custom(
+ 'maxLength',
+ (nqnInput: string) => new TextEncoder().encode(nqnInput).length > 223
+ )
+ ],
+ asyncValidators: [
+ CdValidators.unique(
+ this.nvmeofService.isSubsystemPresent,
+ this.nvmeofService,
+ null,
+ null,
+ this.group
+ )
+ ]
+ })
+ });
+ }
+}
[steps]="steps"
[title]="title"
[description]="description"
- (submitRequested)="onSubmit()"
+ [isSubmitLoading]="isSubmitLoading"
+ (submitRequested)="onSubmit($event)"
>
<cd-tearsheet-step>
- <form name="subsystemForm"
- #formDir="ngForm"
- [formGroup]="subsystemForm"
- novalidate>
- <!-- NQN -->
- <div class="form-group row">
- <label class="cd-col-form-label"
- for="nqn">
- <span class="required"
- i18n>NQN</span>
- </label>
- <div class="cd-col-form-input">
- <input name="nqn"
- class="form-control"
- type="text"
- formControlName="nqn">
- <cd-help-text>
- A unique and permanent name for the lifetime of the subsystem.
- </cd-help-text>
- <span class="invalid-feedback"
- *ngIf="subsystemForm.showError('nqn', formDir, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="subsystemForm.showError('nqn', formDir, 'unique')"
- i18n>This NQN is already in use.</span>
- <span class="invalid-feedback"
- *ngIf="subsystemForm.showError('nqn', formDir, 'pattern')"
- i18n>Expected NQN format<br/><<code>nqn.$year-$month.$reverseDomainName:$utf8-string</code>".> or <br/><<code>nqn.2014-08.org.nvmexpress:uuid:$UUID-string</code>".></span>
- <span class="invalid-feedback"
- *ngIf="subsystemForm.showError('nqn', formDir, 'maxLength')"
- i18n>An NQN may not be more than 223 bytes in length.</span>
- </div>
- </div>
- <!-- Maximum Namespaces -->
- <div class="form-group row">
- <label class="cd-col-form-label"
- for="max_namespaces">
- <span i18n>Maximum Namespaces</span>
- </label>
- <div class="cd-col-form-input">
- <input id="max_namespaces"
- class="form-control"
- type="text"
- name="max_namespaces"
- formControlName="max_namespaces">
- <cd-help-text i18n>The maximum namespaces per subsystem. Default is {{defaultMaxNamespace}}</cd-help-text>
- <span class="invalid-feedback"
- *ngIf="subsystemForm.showError('max_namespaces', formDir, 'min')"
- i18n>The value must be at least 1.</span>
- <span class="invalid-feedback"
- *ngIf="subsystemForm.showError('max_namespaces', formDir, 'pattern')"
- i18n>The value must be a positive integer.</span>
- </div>
- </div>
- </form>
+ <cd-nvmeof-subsystem-step-one
+ #tearsheetStep
+ [group]="group"></cd-nvmeof-subsystem-step-one>
</cd-tearsheet-step>
</cd-tearsheet>
import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
-import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
import { SharedModule } from '~/app/shared/shared.module';
-import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form.component';
-import { FormHelper } from '~/testing/unit-test-helper';
import {
- DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM,
- NvmeofService
-} from '~/app/shared/api/nvmeof.service';
+ NvmeofSubsystemsFormComponent,
+ SubsystemPayload
+} from './nvmeof-subsystems-form.component';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { NvmeofSubsystemsStepOneComponent } from './nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component';
+import { GridModule, InputModule } from 'carbon-components-angular';
describe('NvmeofSubsystemsFormComponent', () => {
let component: NvmeofSubsystemsFormComponent;
let fixture: ComponentFixture<NvmeofSubsystemsFormComponent>;
let nvmeofService: NvmeofService;
- let form: CdFormGroup;
- let formHelper: FormHelper;
const mockTimestamp = 1720693470789;
const mockGroupName = 'default';
+ const mockPayload: SubsystemPayload = {
+ nqn: '',
+ gw_group: mockGroupName
+ };
beforeEach(async () => {
spyOn(Date, 'now').and.returnValue(mockTimestamp);
await TestBed.configureTestingModule({
- declarations: [NvmeofSubsystemsFormComponent],
+ declarations: [NvmeofSubsystemsFormComponent, NvmeofSubsystemsStepOneComponent],
providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
ReactiveFormsModule,
RouterTestingModule,
SharedModule,
+ InputModule,
+ GridModule,
ToastrModule.forRoot()
]
}).compileComponents();
fixture = TestBed.createComponent(NvmeofSubsystemsFormComponent);
component = fixture.componentInstance;
component.ngOnInit();
- form = component.subsystemForm;
- formHelper = new FormHelper(form);
fixture.detectChanges();
component.group = mockGroupName;
});
it('should be creating request correctly', () => {
const expectedNqn = 'nqn.2001-07.com.ceph:' + mockTimestamp;
- component.onSubmit();
+ mockPayload['nqn'] = expectedNqn;
+ component.onSubmit(mockPayload);
expect(nvmeofService.createSubsystem).toHaveBeenCalledWith({
nqn: expectedNqn,
- max_namespaces: DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM,
- enable_ha: true,
- gw_group: mockGroupName
+ gw_group: mockGroupName,
+ enable_ha: true
});
});
-
- it('should give error on invalid nqn', () => {
- formHelper.setValue('nqn', 'nqn:2001-07.com.ceph:');
- component.onSubmit();
- formHelper.expectError('nqn', 'pattern');
- });
-
- it('should give error on invalid max_namespaces', () => {
- formHelper.setValue('max_namespaces', -56);
- component.onSubmit();
- formHelper.expectError('max_namespaces', 'pattern');
- });
-
- it(`should not give error on max_namespaces greater than ${DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM}`, () => {
- const expectedNqn = 'nqn.2001-07.com.ceph:' + mockTimestamp;
- formHelper.setValue('max_namespaces', 600);
- component.onSubmit();
- expect(nvmeofService.createSubsystem).toHaveBeenCalledWith({
- nqn: expectedNqn,
- max_namespaces: DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM,
- enable_ha: true,
- gw_group: mockGroupName
- });
- });
-
- it('should give error on max_namespaces lesser than 1', () => {
- formHelper.setValue('max_namespaces', 0);
- component.onSubmit();
- formHelper.expectError('max_namespaces', 'min');
- });
});
});
-import { Component, DestroyRef, OnInit } from '@angular/core';
-import { FormControlStatus, UntypedFormControl, Validators } from '@angular/forms';
+import { Component, DestroyRef, OnInit, ViewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
-import { CdValidators } from '~/app/shared/forms/cd-validators';
-import { Permission } from '~/app/shared/models/permissions';
-import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
-import { FinishedTask } from '~/app/shared/models/finished-task';
import { ActivatedRoute, Router } from '@angular/router';
-import {
- DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM,
- NvmeofService
-} from '~/app/shared/api/nvmeof.service';
import { Step } from 'carbon-components-angular';
-import { startWith } from 'rxjs/operators';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { TearsheetComponent } from '~/app/shared/components/tearsheet/tearsheet.component';
+
+export type SubsystemPayload = {
+ nqn: string;
+ gw_group: string;
+};
@Component({
selector: 'cd-nvmeof-subsystems-form',
standalone: false
})
export class NvmeofSubsystemsFormComponent implements OnInit {
- permission: Permission;
subsystemForm: CdFormGroup;
action: string;
- resource: string;
- pageURL: string;
- defaultMaxNamespace: number = DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM;
group: string;
steps: Step[] = [
{
- label: $localize`Subsystem Details`,
+ label: $localize`Subsystem details`,
complete: false,
invalid: false
},
complete: false
},
{
- label: $localize`Advanced Options`,
+ label: $localize`Advanced options`,
complete: false,
secondaryLabel: $localize`Advanced`
}
];
title: string = $localize`Create Subsystem`;
description: string = $localize`Subsytems define how hosts connect to NVMe namespaces and ensure secure access to storage.`;
+ isSubmitLoading: boolean = false;
+
+ @ViewChild(TearsheetComponent) tearsheet!: TearsheetComponent;
constructor(
- private authStorageService: AuthStorageService,
public actionLabels: ActionLabelsI18n,
public activeModal: NgbActiveModal,
+ private route: ActivatedRoute,
+ private destroyRef: DestroyRef,
private nvmeofService: NvmeofService,
private taskWrapperService: TaskWrapperService,
- private router: Router,
- private route: ActivatedRoute,
- private destroyRef: DestroyRef
- ) {
- this.permission = this.authStorageService.getPermissions().nvmeof;
- this.resource = $localize`Subsystem`;
- this.pageURL = 'block/nvmeof/subsystems';
- }
-
- DEFAULT_NQN = 'nqn.2001-07.com.ceph:' + Date.now();
- NQN_REGEX = /^nqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+(:[A-Za-z0-9-\.]+)*)$/;
- NQN_REGEX_UUID = /^nqn\.2014-08\.org\.nvmexpress:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
-
- customNQNValidator = CdValidators.custom(
- 'pattern',
- (nqnInput: string) =>
- !!nqnInput && !(this.NQN_REGEX.test(nqnInput) || this.NQN_REGEX_UUID.test(nqnInput))
- );
+ private router: Router
+ ) {}
ngOnInit() {
this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
this.group = params?.['group'];
});
-
- this.createForm();
- this.action = this.actionLabels.CREATE;
-
- this.subsystemForm.statusChanges
- .pipe(startWith(this.subsystemForm.status), takeUntilDestroyed(this.destroyRef))
- .subscribe((status: FormControlStatus) => {
- const step = this.steps[0];
- step.invalid = status === 'INVALID';
- });
- }
-
- createForm() {
- this.subsystemForm = new CdFormGroup({
- nqn: new UntypedFormControl(this.DEFAULT_NQN, {
- validators: [
- this.customNQNValidator,
- Validators.required,
- this.customNQNValidator,
- CdValidators.custom(
- 'maxLength',
- (nqnInput: string) => new TextEncoder().encode(nqnInput).length > 223
- )
- ],
- asyncValidators: [
- CdValidators.unique(
- this.nvmeofService.isSubsystemPresent,
- this.nvmeofService,
- null,
- null,
- this.group
- )
- ]
- }),
- max_namespaces: new UntypedFormControl(this.defaultMaxNamespace, {
- validators: [CdValidators.number(false), Validators.min(1)]
- })
- });
}
- onSubmit() {
+ onSubmit(payload: SubsystemPayload) {
const component = this;
- const nqn: string = this.subsystemForm.getValue('nqn');
- const max_namespaces: number = Number(this.subsystemForm.getValue('max_namespaces'));
+ const pageURL = 'block/nvmeof/subsystems';
let taskUrl = `nvmeof/subsystem/${URLVerbs.CREATE}`;
-
- const request = {
- nqn,
- enable_ha: true,
- gw_group: this.group,
- max_namespaces
- };
-
- if (!max_namespaces) {
- delete request.max_namespaces;
- }
+ this.isSubmitLoading = true;
this.taskWrapperService
.wrapTaskAroundCall({
task: new FinishedTask(taskUrl, {
- nqn: nqn
+ nqn: payload.nqn
}),
- call: this.nvmeofService.createSubsystem(request)
+ call: this.nvmeofService.createSubsystem({ ...payload, enable_ha: true })
})
.subscribe({
error() {
- // instead have error message set, not setting form status INVALID
- // which will show input as false
- component.subsystemForm.setErrors({ cdSubmitButton: true });
+ component.isSubmitLoading = false;
},
complete: () => {
- this.router.navigate([this.pageURL, { outlets: { modal: null } }]);
+ component.isSubmitLoading = false;
+ this.router.navigate([pageURL, { outlets: { modal: null } }]);
}
});
}
return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}?gw_group=${group}`);
}
- createSubsystem(request: {
- nqn: string;
- enable_ha: boolean;
- gw_group: string;
- max_namespaces?: number;
- }) {
+ createSubsystem(request: { nqn: string; enable_ha: boolean; gw_group: string }) {
return this.http.post(`${API_PATH}/subsystem`, request, { observe: 'response' });
}
-import { Component, TemplateRef, ViewChild } from '@angular/core';
+import { Component, ContentChild, TemplateRef, ViewChild } from '@angular/core';
+import { TearsheetStep } from '../../models/tearsheet-step';
@Component({
selector: 'cd-tearsheet-step',
export class TearsheetStepComponent {
@ViewChild(TemplateRef, { static: true })
template!: TemplateRef<any>;
+
+ @ContentChild('tearsheetStep')
+ stepComponent!: TearsheetStep;
}
@if (currentStep === lastStep) {
<button cdsButton="primary"
size="xl"
- (click)="onSubmit()"
- i18n>{{submitButtonLabel}}</button>
+ class="tearsheet-footer-submit"
+ [disabled]="isSubmitLoading"
+ (click)="handleSubmit()"
+ i18n>
+ @if (isSubmitLoading) {
+ <cds-loading
+ [isActive]="isSubmitLoading"
+ [overlay]="false"
+ size="sm">
+ </cds-loading>
+ {{submitButtonLoadingLabel}}...
+ }
+ @else {
+ {{submitButtonLabel}}
+ }
+ </button>
}
@else {
<button cdsButton="primary"
// WIDE TEARSHEET
// No css variable to apply css to modal div hence using css ngdeep
// This is needed to set the width of tearsheet as per carbon standards.
-:host ::ng-deep .cds--modal-container {
- block-size: 100%;
- inset-block-start: auto;
- inline-size: calc(100% - 8rem);
- max-block-size: calc(100% - 5.5rem);
+:host ::ng-deep .cds--modal-container.cds--modal-container--lg {
+ position: fixed;
+
+ // 88px from top
+ inset-block-start: 88px;
+
+ // Fill remaining viewport height
+ block-size: calc(100vh - 88px);
+ max-block-size: calc(100vh - 88px);
+
+ // 64px margins left & right
+ inset-inline-start: 64px;
+ inset-inline-end: 64px;
+ inline-size: auto;
+ max-inline-size: none;
+ display: flex;
+ flex-direction: column;
}
// FULL TEARSHEET
margin-left: var(--cds-spacing-05);
}
+ &-submit {
+ display: flex;
+ justify-content: start;
+ }
+
&--full {
position: sticky;
bottom: 0;
}
}
+
+cds-loading {
+ margin-right: var(--cds-spacing-05);
+}
OnInit,
Output,
QueryList,
- AfterViewChecked
+ AfterViewInit,
+ DestroyRef,
+ OnDestroy
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Step } from 'carbon-components-angular';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { ConfirmationModalComponent } from '../confirmation-modal/confirmation-modal.component';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Subject } from 'rxjs';
+/**
+<cd-tearsheet
+ [steps]="steps"
+ [title]="title"
+ [isSubmitLoading]="isSubmitLoading"
+ [description]="description"
+ (submitRequested)="onSubmit()">
+ <cd-tearsheet-step>
+ <cd-step #tearsheetStep>
+ </cds-step>
+ </cd-tearsheet-step>
+ <cd-tearsheet-step>
+ step 2 form
+ <cd-tearsheet-step>
+</cd-tearsheet>
+
+-----------------
+
+@Component({
+ selector: 'cd-step',
+ template: `<form></form>,
+ standalone: false
+})
+export class StepComponent implements TearsheetStep {
+formgroup: CdFormGroup;
+}
+**/
@Component({
selector: 'cd-tearsheet',
standalone: false,
templateUrl: './tearsheet.component.html',
styleUrls: ['./tearsheet.component.scss']
})
-export class TearsheetComponent implements OnInit, AfterViewChecked {
+export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() title!: string;
@Input() steps!: Array<Step>;
@Input() description!: string;
- @Input() submitButtonLabel: string = $localize`Create`;
@Input() type: 'full' | 'wide' = 'wide';
+ @Input() submitButtonLabel: string = $localize`Create`;
+ @Input() submitButtonLoadingLabel: string = $localize`Creating`;
+ @Input() isSubmitLoading: boolean = true;
- @Output() submitRequested = new EventEmitter<void>();
+ @Output() submitRequested = new EventEmitter<any[]>();
@ContentChildren(TearsheetStepComponent)
stepContents!: QueryList<TearsheetStepComponent>;
lastStep: number = null;
isOpen: boolean = true;
hasModalOutlet: boolean = false;
+ private destroy$ = new Subject<void>();
constructor(
protected formBuilder: FormBuilder,
private changeDetectorRef: ChangeDetectorRef,
private cdsModalService: ModalCdsService,
private route: ActivatedRoute,
- private location: Location
+ private location: Location,
+ private destroyRef: DestroyRef
) {}
ngOnInit() {
}
}
- onSubmit() {
- if (!this.steps[this.currentStep].invalid) {
- this.submitRequested.emit();
- }
+ getMergedPayload(): any {
+ return this.stepContents.toArray().reduce((acc, wrapper) => {
+ const stepFormValue = wrapper.stepComponent.formGroup.value;
+ return { ...acc, ...stepFormValue };
+ }, {});
+ }
+
+ handleSubmit() {
+ if (this.steps[this.currentStep].invalid) return;
+
+ const mergedPayloads = this.getMergedPayload();
+
+ this.submitRequested.emit(mergedPayloads);
}
closeFullTearsheet() {
});
}
- ngAfterViewChecked() {
- this.changeDetectorRef.detectChanges();
+ ngAfterViewInit() {
+ // Checking Step validity, subscries ot all steps
+ if (!this.stepContents || !this.steps) return;
+
+ this.stepContents.forEach((wrapper, index) => {
+ const form = wrapper.stepComponent?.formGroup;
+ if (!form) return;
+
+ // Initialize invalid flag if missing
+ this.steps[index].invalid = form.invalid;
+
+ form.statusChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
+ this.steps[index].invalid = form.invalid;
+ this.changeDetectorRef.markForCheck();
+ });
+ });
+ }
+
+ ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
}
}
--- /dev/null
+import { FormGroup } from '@angular/forms';
+
+export interface TearsheetStep {
+ formGroup: FormGroup;
+}