Skip to content
This repository has been archived by the owner on Jan 19, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1305 from GuessWhoSamFoo/modal-component
Browse files Browse the repository at this point in the history
Add modal component
  • Loading branch information
wwitzel3 committed Sep 23, 2020
2 parents 1a76d93 + 5c8cd16 commit 5a38dc3
Show file tree
Hide file tree
Showing 34 changed files with 839 additions and 203 deletions.
1 change: 1 addition & 0 deletions changelogs/unreleased/1305-GuessWhoSamFoo
@@ -0,0 +1 @@
Added modal component and opening modals through buttons
2 changes: 2 additions & 0 deletions pkg/view/component/base.go
Expand Up @@ -48,6 +48,8 @@ const (
TypeLoading = "loading"
// TypeLogs is a logs component.
TypeLogs = "logs"
// TypeModal is a modal component.
TypeModal = "modal"
// TypePodStatus is a pod status component.
TypePodStatus = "podStatus"
// TypePort is a port component.
Expand Down
42 changes: 42 additions & 0 deletions pkg/view/component/button.go
Expand Up @@ -3,6 +3,8 @@ package component
import (
"encoding/json"

"github.com/pkg/errors"

"github.com/vmware-tanzu/octant/pkg/action"
)

Expand All @@ -25,11 +27,51 @@ func WithButtonConfirmation(title, body string) ButtonOption {
}
}

// WithModal configures a button to open a modal
func WithModal(modal *Modal) ButtonOption {
return func(button *Button) {
button.Modal = modal
}
}

func (b *Button) UnmarshalJSON(data []byte) error {
x := struct {
Name string `json:"name"`
Payload action.Payload `json:"payload"`
Confirmation *Confirmation `json:"confirmation,omitempty"`
Modal *TypedObject `json:"modal,omitempty"`
}{}

if err := json.Unmarshal(data, &x); err != nil {
return err
}

if x.Modal != nil {
component, err := x.Modal.ToComponent()
if err != nil {
return err
}

modal, ok := component.(*Modal)
if !ok {
return errors.New("item was not a modal")
}
b.Modal = modal
}

b.Name = x.Name
b.Payload = x.Payload
b.Confirmation = x.Confirmation

return nil
}

// Button is a button in a group.
type Button struct {
Name string `json:"name"`
Payload action.Payload `json:"payload"`
Confirmation *Confirmation `json:"confirmation,omitempty"`
Modal Component `json:"modal,omitempty"`
}

// NewButton creates an instance of Button.
Expand Down
3 changes: 3 additions & 0 deletions pkg/view/component/form.go
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -694,6 +696,7 @@ func (f *Form) UnmarshalJSON(data []byte) error {

f.Fields = append(f.Fields, ff)
}
f.Action = x.Action

return nil
}
Expand Down
110 changes: 110 additions & 0 deletions pkg/view/component/modal.go
@@ -0,0 +1,110 @@
package component

import (
"encoding/json"
)

type ModalSize string

const (
// ModalSizeSmall is the smallest modal
ModalSizeSmall ModalSize = "sm"
// ModalSizeLarge is a large modal
ModalSizeLarge ModalSize = "lg"
// ModalSizeExtraLarge is the largest modal
ModalSizeExtraLarge ModalSize = "xl"
)

// ModalConfig is a configuration for the modal component.
type ModalConfig struct {
Body Component `json:"body,omitempty"`
Form *Form `json:"form,omitempty"`
Opened bool `json:"opened"`
ModalSize ModalSize `json:"size,omitempty"`
Buttons []Button `json:"buttons,omitempty"`
}

// UnmarshalJSON unmarshals a modal config from JSON.
func (m *ModalConfig) UnmarshalJSON(data []byte) error {
x := struct {
Body *TypedObject `json:"body,omitempty"`
Form *Form `json:"form,omitempty"`
Opened bool `json:"opened"`
ModalSize ModalSize `json:"size,omitempty"`
Buttons []Button `json:"buttons,omitempty"`
}{}

if err := json.Unmarshal(data, &x); 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.Opened = x.Opened
m.ModalSize = x.ModalSize
m.Buttons = x.Buttons
return nil
}

// Modal is a modal component.
//
// +octant:component
type Modal struct {
Base
Config ModalConfig `json:"config"`
}

// NewModal creates a new modal.
func NewModal(title []TitleComponent) *Modal {
return &Modal{
Base: newBase(TypeModal, title),
}
}

var _ Component = (*Modal)(nil)

// SetBody sets the body of a modal.
func (m *Modal) SetBody(body Component) {
m.Config.Body = body
}

// AddForm adds a form to a modal. It is added after the body.
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) {
m.Config.ModalSize = size
}

// AddButton is a helper to add a custom button
func (m *Modal) AddButton(button Button) {
m.Config.Buttons = append(m.Config.Buttons, button)
}

// Open opens a modal. A modal is closed by default.
func (m *Modal) Open() {
m.Config.Opened = true
}

// Close closes a modal.
func (m *Modal) Close() {
m.Config.Opened = false
}

type modalMarshal Modal

// MarshalJSON marshal a modal to JSON.
func (m *Modal) MarshalJSON() ([]byte, error) {
k := modalMarshal(*m)
k.Metadata.Type = TypeModal
return json.Marshal(&k)
}
73 changes: 73 additions & 0 deletions pkg/view/component/modal_test.go
@@ -0,0 +1,73 @@
package component

import (
"testing"
)

func TestModal_SetBody(t *testing.T) {
modal := NewModal(TitleFromString("modal"))
body := NewText("body")
modal.SetBody(body)

expected := NewModal(TitleFromString("modal"))
expected.Config.Body = body

AssertEqual(t, expected, modal)
}

func TestModal_SetSize(t *testing.T) {
tests := []struct {
name string
size ModalSize
expected *Modal
}{
{
name: "small",
size: ModalSizeSmall,
expected: &Modal{
Base: newBase(TypeModal, TitleFromString("modal")),
Config: ModalConfig{
ModalSize: ModalSizeSmall,
},
},
},
{
name: "large",
size: ModalSizeLarge,
expected: &Modal{
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,
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
modal := NewModal(TitleFromString("modal"))
modal.SetSize(test.size)

AssertEqual(t, test.expected, modal)
})
}
}

func TestModal_Open(t *testing.T) {
modal := NewModal(TitleFromString("modal"))
modal.Open()

expected := NewModal(TitleFromString("modal"))
expected.Config.Opened = true
AssertEqual(t, expected, modal)
}
12 changes: 12 additions & 0 deletions pkg/view/component/testdata/config_modal.json
@@ -0,0 +1,12 @@
{
"body":{
"metadata":{
"type":"text"
},
"config":{
"value":"test"
}
},
"opened":true,
"size":"sm"
}
5 changes: 5 additions & 0 deletions pkg/view/component/unmarshal.go
Expand Up @@ -116,6 +116,11 @@ func unmarshal(to TypedObject) (Component, error) {
err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
"unmarshal logs config")
o = t
case TypeModal:
t := &Modal{Base: Base{Metadata: to.Metadata}}
err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
"unmarshal modal config")
o = t
case TypeQuadrant:
t := &Quadrant{Base: Base{Metadata: to.Metadata}}
err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
Expand Down
13 changes: 13 additions & 0 deletions pkg/view/component/unmarshal_test.go
Expand Up @@ -261,6 +261,19 @@ func Test_unmarshal(t *testing.T) {
},
},
},
{
name: "modal",
configFile: "config_modal.json",
objectType: TypeModal,
expected: &Modal{
Config: ModalConfig{
Body: NewText("test"),
Opened: true,
ModalSize: ModalSizeSmall,
},
Base: newBase(TypeModal, nil),
},
},
{
name: "quadrant",
configFile: "config_quadrant.json",
Expand Down
Expand Up @@ -2,12 +2,13 @@
<clr-button
*ngFor="let button of v.config.buttons; trackBy: trackByFn"
class="{{ class }}"
(click)="onClick(button.payload, button.confirmation)"
(click)="onClick(button.payload, button.confirmation, button.modal)"
>
{{ button.name }}
</clr-button>
</clr-button-group>

<app-view-modal *ngIf="modalView" [view]="modalView"></app-view-modal>
<clr-modal [(clrModalOpen)]="isModalOpen">
<h3 class="modal-title">{{ modalTitle }}</h3>
<div class="modal-body">
Expand Down
@@ -1,7 +1,13 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { ButtonGroupView, Confirmation } from '../../../models/content';
import {
ButtonGroupView,
Confirmation,
View,
ModalView,
} from '../../../models/content';
import { ActionService } from '../../../services/action/action.service';
import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
import { ModalService } from '../../../services/modal/modal.service';

@Component({
selector: 'app-button-group',
Expand All @@ -19,7 +25,12 @@ export class ButtonGroupComponent extends AbstractViewComponent<
payload = {};
class = '';

constructor(private actionService: ActionService) {
modalView: View;

constructor(
private actionService: ActionService,
private modalService: ModalService
) {
super();
}

Expand All @@ -31,11 +42,19 @@ export class ButtonGroupComponent extends AbstractViewComponent<
} else {
this.class = 'btn-outline btn-sm';
}
if (button.modal) {
this.modalView = button.modal;
const modal = this.modalView as ModalView;
this.modalService.setState(modal.config.opened);
}
});
}
}

onClick(payload: {}, confirmation?: Confirmation) {
onClick(payload: {}, confirmation?: Confirmation, modal?: View) {
if (modal) {
this.modalService.openModal();
}
if (confirmation) {
this.activateModal(payload, confirmation);
} else {
Expand Down

0 comments on commit 5a38dc3

Please sign in to comment.