From 455b9a3619590a97f898831dfdb0a24cc1e1fcd0 Mon Sep 17 00:00:00 2001 From: GuessWhoSamFoo Date: Sun, 30 Aug 2020 00:43:39 -0700 Subject: [PATCH] Allow adding form into a modal component; added storybook Signed-off-by: GuessWhoSamFoo --- pkg/view/component/form.go | 3 + pkg/view/component/modal.go | 33 ++--- pkg/view/component/modal_test.go | 24 ++-- .../presentation/modal/modal.component.html | 117 ++++++++++++++++- .../modal/modal.component.spec.ts | 5 +- .../presentation/modal/modal.component.ts | 89 +++++++++++-- .../presentation/stepper/stepper.component.ts | 1 - web/src/app/modules/shared/models/content.ts | 6 +- web/src/stories/modal.stories.mdx | 124 +++++++++++++++--- 9 files changed, 345 insertions(+), 57 deletions(-) diff --git a/pkg/view/component/form.go b/pkg/view/component/form.go index e7d6e004cb..2d50cb45ae 100644 --- a/pkg/view/component/form.go +++ b/pkg/view/component/form.go @@ -614,6 +614,7 @@ func (ff *FormFieldHidden) UnmarshalJSON(data []byte) error { type Form struct { Fields []FormField `json:"fields"` + Action string `json:"action,omitempty"` } func (f *Form) MarshalJSON() ([]byte, error) { @@ -651,6 +652,7 @@ func (f *Form) UnmarshalJSON(data []byte) error { Error string `json:"error"` Validators []string `json:"validators"` } `json:"fields"` + Action string `json:"action,omitempty"` }{} err := json.Unmarshal(data, &x) @@ -694,6 +696,7 @@ func (f *Form) UnmarshalJSON(data []byte) error { f.Fields = append(f.Fields, ff) } + f.Action = x.Action return nil } diff --git a/pkg/view/component/modal.go b/pkg/view/component/modal.go index ac340cb0b2..13b67bd03e 100644 --- a/pkg/view/component/modal.go +++ b/pkg/view/component/modal.go @@ -17,8 +17,8 @@ const ( // ModalConfig is a configuration for the modal component. type ModalConfig struct { - Body Component `json:"body"` - //Form Form `json:"form,omitempty"` + Body Component `json:"body,omitempty"` + Form *Form `json:"form,omitempty"` Opened bool `json:"opened"` ModalSize ModalSize `json:"size,omitempty"` } @@ -26,23 +26,25 @@ type ModalConfig struct { // UnmarshalJSON unmarshals a modal config from JSON. func (m *ModalConfig) UnmarshalJSON(data []byte) error { x := struct { - Body TypedObject `json:"body"` - //Form Form `json:"form,omitempty"` - Opened bool `json:"opened"` - ModalSize ModalSize `json:"size,omitempty"` + Body *TypedObject `json:"body,omitempty"` + Form *Form `json:"form,omitempty"` + Opened bool `json:"opened"` + ModalSize ModalSize `json:"size,omitempty"` }{} if err := json.Unmarshal(data, &x); err != nil { return err } - var err error - m.Body, err = x.Body.ToComponent() - if err != nil { - return err + if x.Body != nil { + var err error + m.Body, err = x.Body.ToComponent() + if err != nil { + return err + } } - //m.Form = x.Form + m.Form = x.Form m.Opened = x.Opened m.ModalSize = x.ModalSize return nil @@ -57,10 +59,9 @@ type Modal struct { } // NewModal creates a new modal. -func NewModal(title []TitleComponent, body Component) *Modal { +func NewModal(title []TitleComponent) *Modal { return &Modal{ Base: newBase(TypeModal, title), - Config: ModalConfig{Body: body}, } } @@ -72,9 +73,9 @@ func (m *Modal) SetBody(body Component) { } // AddForm adds a form to a modal. It is added after the body. -//func (m *Modal) AddForm(form Form) { -// m.Config.Form = form -//} +func (m *Modal) AddForm(form Form) { + m.Config.Form = &form +} // SetSize sets the size of a modal. Size is medium by default. func (m *Modal) SetSize(size ModalSize) { diff --git a/pkg/view/component/modal_test.go b/pkg/view/component/modal_test.go index 6c890b319a..e3a6713268 100644 --- a/pkg/view/component/modal_test.go +++ b/pkg/view/component/modal_test.go @@ -16,33 +16,39 @@ func TestModal_SetBody(t *testing.T) { } func TestModal_SetSize(t *testing.T) { - tests := []struct{ - name string - size ModalSize + tests := []struct { + name string + size ModalSize expected *Modal }{ { name: "small", size: ModalSizeSmall, expected: &Modal{ - Base: newBase(TypeModal, TitleFromString("modal")), - Config: ModalConfig{ModalSize: ModalSizeSmall}, + Base: newBase(TypeModal, TitleFromString("modal")), + Config: ModalConfig{ + ModalSize: ModalSizeSmall, + }, }, }, { name: "large", size: ModalSizeLarge, expected: &Modal{ - Base: newBase(TypeModal, TitleFromString("modal")), - Config: ModalConfig{ModalSize: ModalSizeLarge}, + Base: newBase(TypeModal, TitleFromString("modal")), + Config: ModalConfig{ + ModalSize: ModalSizeLarge, + }, }, }, { name: "extra large", size: ModalSizeExtraLarge, expected: &Modal{ - Base: newBase(TypeModal, TitleFromString("modal")), - Config: ModalConfig{ModalSize: ModalSizeExtraLarge}, + Base: newBase(TypeModal, TitleFromString("modal")), + Config: ModalConfig{ + ModalSize: ModalSizeExtraLarge, + }, }, }, } diff --git a/web/src/app/modules/shared/components/presentation/modal/modal.component.html b/web/src/app/modules/shared/components/presentation/modal/modal.component.html index 03d96b9e25..90a3c43023 100644 --- a/web/src/app/modules/shared/components/presentation/modal/modal.component.html +++ b/web/src/app/modules/shared/components/presentation/modal/modal.component.html @@ -4,10 +4,119 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/web/src/app/modules/shared/components/presentation/modal/modal.component.spec.ts b/web/src/app/modules/shared/components/presentation/modal/modal.component.spec.ts index e4da6a266c..42dcf15448 100644 --- a/web/src/app/modules/shared/components/presentation/modal/modal.component.spec.ts +++ b/web/src/app/modules/shared/components/presentation/modal/modal.component.spec.ts @@ -5,14 +5,17 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ModalComponent } from './modal.component'; import { SharedModule } from '../../../shared.module'; +import { ModalService } from '../../../services/modal/modal.service'; -describe('LoadingComponent', () => { +describe('ModalComponent', () => { let component: ModalComponent; let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [SharedModule], + declarations: [ModalComponent], + providers: [{ provide: ModalService }], }).compileComponents(); })); diff --git a/web/src/app/modules/shared/components/presentation/modal/modal.component.ts b/web/src/app/modules/shared/components/presentation/modal/modal.component.ts index be498f26f2..5490e1d109 100644 --- a/web/src/app/modules/shared/components/presentation/modal/modal.component.ts +++ b/web/src/app/modules/shared/components/presentation/modal/modal.component.ts @@ -1,33 +1,59 @@ -import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core'; +import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; import { AbstractViewComponent } from '../../abstract-view/abstract-view.component'; -import { TitleView, ModalView, View } from '../../../models/content'; +import { + ActionField, + TitleView, + ModalView, + View, + ActionForm, +} from '../../../models/content'; import { ModalService } from '../../../services/modal/modal.service'; import { Subscription } from 'rxjs'; +import { + FormBuilder, + FormGroup, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { ClrForm } from '@clr/angular'; +import { WebsocketService } from '../../../services/websocket/websocket.service'; + +interface Choice { + label: string; + value: string; + checked: boolean; +} @Component({ selector: 'app-view-modal', templateUrl: './modal.component.html', styleUrls: ['./modal.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ModalComponent extends AbstractViewComponent implements OnInit, OnDestroy{ +export class ModalComponent extends AbstractViewComponent + implements OnInit, OnDestroy { + @ViewChild(ClrForm) clrForm: ClrForm; + title: TitleView[]; body: View; + form: ActionForm; opened = false; size: string; + formGroup: FormGroup; + action: string; private modalSubscription: Subscription; - constructor(private modalService: ModalService, private cd: ChangeDetectorRef) { + constructor( + private formBuilder: FormBuilder, + private modalService: ModalService, + private websocketService: WebsocketService + ) { super(); } ngOnInit() { this.modalSubscription = this.modalService.isOpened.subscribe(isOpened => { - if (this.opened !== isOpened) { - this.opened = isOpened; - this.cd.markForCheck(); - } + this.opened = isOpened; }); } @@ -41,5 +67,50 @@ export class ModalComponent extends AbstractViewComponent implements this.title = this.v.metadata.title as TitleView[]; this.body = this.v.config.body; this.size = this.v.config.size; + this.form = this.v.config.form; + this.opened = this.v.config.opened; + this.modalService.setState(this.opened); + + if (this.form) { + const controls: { [name: string]: any } = {}; + this.form.fields.forEach(field => { + controls[field.name] = [ + field.value, + this.getValidators(field.validators), + ]; + }); + this.action = this.form.action; + this.formGroup = this.formBuilder.group(controls); + } + } + + getValidators(validators: string[]): ValidatorFn[] { + if (validators) { + const vFn: ValidatorFn[] = []; + validators.forEach(v => { + vFn.push(Validators[v]); + }); + return vFn; + } + return []; + } + + trackByFn(index, _) { + return index; + } + + fieldChoices(field: ActionField) { + return field.configuration.choices as Choice[]; + } + + onFormSubmit() { + if (this.formGroup.invalid) { + this.clrForm.markAsTouched(); + } else { + this.websocketService.sendMessage('action.octant.dev/performAction', { + action: this.action, + formGroup: this.formGroup.value, + }); + } } } diff --git a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts index af14b3cef8..b5f66fb6a2 100644 --- a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts +++ b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts @@ -34,7 +34,6 @@ export class StepperComponent extends AbstractViewComponent { constructor( private formBuilder: FormBuilder, - private modalService: ModalService, private websocketService: WebsocketService ) { super(); diff --git a/web/src/app/modules/shared/models/content.ts b/web/src/app/modules/shared/models/content.ts index a6ff08dd61..d8981931ca 100644 --- a/web/src/app/modules/shared/models/content.ts +++ b/web/src/app/modules/shared/models/content.ts @@ -188,9 +188,10 @@ export interface SingleStatView extends View { export interface ModalView extends View { config: { - body: View; + body?: View; opened: boolean; - size: string; + size?: string; + form?: ActionForm; }; } @@ -288,6 +289,7 @@ export interface ActionField { export interface ActionForm { fields: ActionField[]; + action?: string; } export interface Action { diff --git a/web/src/stories/modal.stories.mdx b/web/src/stories/modal.stories.mdx index 772f190016..1c587adc4d 100644 --- a/web/src/stories/modal.stories.mdx +++ b/web/src/stories/modal.stories.mdx @@ -1,27 +1,121 @@ -import { moduleMetadata } from '@storybook/angular'; -import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks'; +import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs/blocks'; import { object, withKnobs } from '@storybook/addon-knobs'; -import { ModalComponent } from '../app/modules/shared/presentation/modal/modal.component`; +import { ModalComponent } from '../app/modules/shared/components/presentation/modal/modal.component'; -export const modalDocs = { - source; { code: `` }, -} +export const modalDocs = { source: { code: `modal := component.NewModal(component.TitleFromString("Modal Title")) + modal.SetBody(component.NewText("Modal Body")) + modal.SetSize(component.ModalSizeLarge) + modal.Open()` +}} -export const modalView = {} +export const modalFormDocs = { source: { code: `modal := component.NewModal(component.TitleFromString("Modal Title")) + modal.SetBody(component.NewText("Modal Body")) + fft := component.NewFormFieldText("textFormLabel", "textFormName", "") + fft.AddValidator("placeholder", "this is an error", []string{"required"}) + form := component.Form{ + Fields: []component.FormField{ + fft, + }, + } + modal.AddForm(form) + modal.SetSize(component.ModalSizeLarge) + modal.Open()` +}} + +export const textView = { + config: { + value: 'Modal Title', + }, + metadata: { + type: 'text', + }, +}; + +export const bodyView = { + config: { + value: 'Modal body', + }, + metadata: { + type: 'text', + }, +}; + +export const modalView = { + metadata: { + type: 'modal', + title: [textView], + }, + config: { + body: bodyView, + opened: true, + size: 'lg', + }, +}; + +export const formFields = { + fields: [ + { + configuration: {}, + error: 'this is an error', + label: 'textFormLabel', + name: 'textFormName', + placeholder: 'placeholder', + type: 'text', + validators: ["required"], + value: '', + }, + ], +}; + +export const modalFormView = { + metadata: { + type: 'modal', + title: [textView], + }, + config: { + body: bodyView, + form: formFields, + opened: true, + size: 'lg', + }, +};

Modal component

Description

The modal component provides a modal

+

Example

+ + + + + + {{ + props: { + view: object('View', modalView), + }, + component: ModalComponent, + }} + + + +

Props

+ + +

+Modal Component with a form +

- - + + + {{ + props: { + view: object('View', modalFormView), + }, + component: ModalComponent, + }} - +

Props

- \ No newline at end of file + \ No newline at end of file