From 85fc37ff9abeeb4a058dbe7c2316796b79efc342 Mon Sep 17 00:00:00 2001 From: Chris Thielen Date: Mon, 1 May 2017 09:09:18 -0700 Subject: [PATCH 1/5] refactor(core/entityTag): Refactor Entity Tags to React - Introduce imperative React Modal API - Introduce Formsy validation for React --- .../core/src/entityTag/EntityTagEditor.tsx | 322 ++++++++++++++++++ .../entityTag/addEntityTagLinks.component.ts | 56 ++- .../entityTag/clusterTargetBuilder.service.ts | 7 +- .../entityTag/entityTagDetails.component.less | 2 +- .../entityTag/entityTagEditor.controller.ts | 69 ---- .../src/entityTag/entityTagEditor.modal.less | 4 +- .../src/entityTag/entityUiTags.component.ts | 58 ++-- .../modules/core/src/entityTag/index.ts | 1 + .../modules/core/src/help/HelpField.tsx | 92 ++++- .../modules/core/src/help/help.contents.ts | 4 + app/scripts/modules/core/src/help/index.ts | 3 + .../modules/core/src/modal/modals.less | 2 +- .../src/presentation/HoverablePopover.tsx | 6 +- .../core/src/presentation/ModalService.tsx | 41 +++ .../presentation/formsy/BasicFieldLayout.tsx | 50 +++ .../src/presentation/formsy/FormComponent.ts | 130 +++++++ .../presentation/formsy/components/Input.tsx | 37 ++ .../formsy/components/TextArea.tsx | 69 ++++ .../presentation/formsy/components/index.ts | 2 + .../presentation/formsy/formFieldLayout.ts | 11 + .../src/presentation/formsy/formsy-react.d.ts | 35 ++ .../core/src/presentation/formsy/index.ts | 6 + .../modules/core/src/presentation/index.ts | 2 + .../modules/core/src/reactShims/ngReact.ts | 30 +- .../core/src/reactShims/react.injector.ts | 38 ++- .../core/src/task/monitor/TaskMonitor.tsx | 6 + .../src/task/monitor/taskMonitor.directive.js | 26 -- .../src/task/monitor/taskMonitor.directive.ts | 36 ++ .../src/task/monitor/taskMonitor.module.js | 2 +- .../src/task/monitor/taskMonitorService.js | 7 +- package.json | 7 +- tslint.json | 2 +- yarn.lock | 47 ++- 33 files changed, 996 insertions(+), 214 deletions(-) create mode 100644 app/scripts/modules/core/src/entityTag/EntityTagEditor.tsx delete mode 100644 app/scripts/modules/core/src/entityTag/entityTagEditor.controller.ts create mode 100644 app/scripts/modules/core/src/presentation/ModalService.tsx create mode 100644 app/scripts/modules/core/src/presentation/formsy/BasicFieldLayout.tsx create mode 100644 app/scripts/modules/core/src/presentation/formsy/FormComponent.ts create mode 100644 app/scripts/modules/core/src/presentation/formsy/components/Input.tsx create mode 100644 app/scripts/modules/core/src/presentation/formsy/components/TextArea.tsx create mode 100644 app/scripts/modules/core/src/presentation/formsy/components/index.ts create mode 100644 app/scripts/modules/core/src/presentation/formsy/formFieldLayout.ts create mode 100644 app/scripts/modules/core/src/presentation/formsy/formsy-react.d.ts create mode 100644 app/scripts/modules/core/src/presentation/formsy/index.ts create mode 100644 app/scripts/modules/core/src/task/monitor/TaskMonitor.tsx delete mode 100644 app/scripts/modules/core/src/task/monitor/taskMonitor.directive.js create mode 100644 app/scripts/modules/core/src/task/monitor/taskMonitor.directive.ts diff --git a/app/scripts/modules/core/src/entityTag/EntityTagEditor.tsx b/app/scripts/modules/core/src/entityTag/EntityTagEditor.tsx new file mode 100644 index 00000000000..fb3a2dee6c5 --- /dev/null +++ b/app/scripts/modules/core/src/entityTag/EntityTagEditor.tsx @@ -0,0 +1,322 @@ +import { IDeferred } from 'angular'; +import { $q } from 'ngimport'; +import * as Formsy from 'formsy-react'; +import * as marked from 'marked'; +import * as React from 'react'; +import { Modal } from 'react-bootstrap'; +import { IModalServiceInstance } from 'angular-ui-bootstrap'; +import autoBindMethods from 'class-autobind-decorator'; + +import { + UUIDGenerator, Application, EntityTagWriter, HelpField, IEntityRef, IEntityTag, + ReactInjector, TaskMonitor, TaskMonitorBuilder +} from 'core'; + +import { BasicFieldLayout, TextArea, ReactModal } from 'core/presentation'; +import { NgReact } from 'core/reactShims/ngReact'; +import { EntityRefBuilder } from './entityRef.builder'; + +import './entityTagEditor.modal.less'; + +export interface IOwnerOption { + label: string; + type: string; + owner: any; + isDefault: boolean; +} + +export interface IEntityTagEditorProps { + owner: any; + application: Application; + entityType: string; + tag: IEntityTag; + ownerOptions: IOwnerOption[]; + entityRef: IEntityRef; + isNew: boolean; + show?: boolean; + onHide?(event: any): void; + onUpdate?(): any; +} + +export interface IEntityTagEditorState { + taskMonitor: TaskMonitor; + message: string; + show: boolean; + isValid: boolean; + isSubmitting: boolean; + owner: any; + entityType: string; +} + +@autoBindMethods +export class EntityTagEditor extends React.Component { + public static defaultProps = { + onHide: () => null as any, + onUpdate: () => null as any, + }; + + private taskMonitorBuilder: TaskMonitorBuilder = ReactInjector.taskMonitorBuilder; + private entityTagWriter: EntityTagWriter = ReactInjector.entityTagWriter; + private $uibModalInstanceEmulation: IModalServiceInstance & { deferred?: IDeferred }; + + /** Shows the Entity Tag Editor modal */ + public static show(props: IEntityTagEditorProps): Promise { + return ReactModal.show(React.createElement(EntityTagEditor, props)); + } + + constructor(props: IEntityTagEditorProps) { + super(props); + + const { ownerOptions, tag } = this.props; + const owner = this.props.owner || (ownerOptions && ownerOptions.length && ownerOptions[0]); + tag.name = tag.name || `spinnaker_ui_${tag.value.type}:${UUIDGenerator.generateUuid()}`; + + this.state = { + taskMonitor: null, + message: props.tag && props.tag.value && props.tag.value.message, + show: true, + isValid: false, + isSubmitting: false, + owner: owner, + entityType: this.props.entityType, + }; + + const deferred = $q.defer(); + const promise = deferred.promise; + this.$uibModalInstanceEmulation = { + result: promise, + close: () => this.setState({ show: false }), + dismiss: () => this.setState({ show: false }), + } as IModalServiceInstance; + Object.assign(this.$uibModalInstanceEmulation, { deferred }); + } + + private handleMessageChanged(message: string): void { + this.setState({ message }); + } + + private handleOwnerOptionChanged(option: IOwnerOption): void { + this.setState({ owner: option.owner, entityType: option.type }); + } + + private onValid(): void { + this.setState({ isValid: true }); + } + + private onInvalid(): void { + this.setState({ isValid: false }); + } + + private onHide(): void { + this.setState({ show: false }); + this.props.onHide.apply(null, arguments); + this.$uibModalInstanceEmulation.deferred.resolve(); + } + + private upsertTag(data: { message: string; }): void { + const { application, isNew, tag } = this.props; + const { owner, entityType } = this.state; + const entityRef: IEntityRef = this.props.entityRef || EntityRefBuilder.getBuilder(entityType)(owner); + + tag.value.message = data.message; + + const taskMonitor = this.taskMonitorBuilder.buildTaskMonitor({ + application: application, + title: `${isNew ? 'Create' : 'Update'} ${this.props.tag.value.type} for ${entityRef.entityId}`, + modalInstance: this.$uibModalInstanceEmulation, + onTaskComplete: () => this.props.onUpdate(), + }); + + const submitMethod = () => { + const promise = this.entityTagWriter.upsertEntityTag(application, tag, entityRef, isNew); + const done = () => this.setState({ isSubmitting: false }); + promise.then(done, done); + return promise; + }; + + taskMonitor.submit(submitMethod); + + this.setState({ taskMonitor, isSubmitting: true }); + } + + public render(): React.ReactElement { + const { isNew, tag, ownerOptions } = this.props; + const message = this.state.message || ''; + + const closeButton = ( +
+ + + +
+ ); + + const { TaskMonitorWrapper } = NgReact; + + return ( + + + + + + +

{isNew ? 'Create' : 'Update'} {tag.value.type}

+ {closeButton} +
+ + + + + { ownerOptions && ownerOptions.length && ( + + ) } + + + + + + +
+ +
+ ); + } +} + + + +interface IEntityTagMessageProps { + message: string; + onMessageChanged(message: string): void; +} + +@autoBindMethods +class EntityTagMessage extends React.Component { + private handleTextareaChanged(event: React.FormEvent): void { + this.props.onMessageChanged(event.currentTarget.value); + } + + public render(): React.ReactElement { + const { message } = this.props; + + return ( +
+
+ + -
Markdown is okay
-
-
-
-
- Preview -
-
-
-
-
- - -
-
-
-
-
- Applies to -
-
-
-
- -
-
-
-
-
-
-
- - - - diff --git a/app/scripts/modules/core/src/presentation/formsy/BasicFieldLayout.tsx b/app/scripts/modules/core/src/presentation/formsy/BasicFieldLayout.tsx index 4605932aa81..53bc235ceb2 100644 --- a/app/scripts/modules/core/src/presentation/formsy/BasicFieldLayout.tsx +++ b/app/scripts/modules/core/src/presentation/formsy/BasicFieldLayout.tsx @@ -25,25 +25,25 @@ export class BasicFieldLayout extends React.Component } public render() { - const { _label, _input, _help, _error, showRequired, showError } = this.props; + const { Label, Input, Help, Error, showRequired, showError } = this.props; - const renderedLabel = _label &&
{_label}
; - const renderedHelp = _help &&
{_help}
; - const renderedError = _error &&
{_error}
; + const LabelDiv = Label &&
{Label}
; + const HelpDiv = Help &&
{Help}
; + const ErrorDiv = Error &&
{Error}
; - const renderedInputGroup = _input && ( + const InputGroup = Input && (
- {_input} - {renderedHelp} - {renderedError} + {Input} + {HelpDiv} + {ErrorDiv}
); const className = `form-group ${showRequired || showError ? 'ng-invalid' : ''}`; return (
- {renderedLabel} - {renderedInputGroup} + {LabelDiv} + {InputGroup}
); } diff --git a/app/scripts/modules/core/src/presentation/formsy/components/Input.tsx b/app/scripts/modules/core/src/presentation/formsy/components/Input.tsx index ac1c047dcf1..4c975395baa 100644 --- a/app/scripts/modules/core/src/presentation/formsy/components/Input.tsx +++ b/app/scripts/modules/core/src/presentation/formsy/components/Input.tsx @@ -8,10 +8,8 @@ export interface IProps extends IFormComponentProps { type: string; } -export interface IState extends IFormComponentState { } - /** A simple Formsy form component for validated tags (text or checkbox) */ -export class Input extends FormComponent { +export class Input extends FormComponent { public handleValueChanged(value: any) { this.setValue(value); } diff --git a/app/scripts/modules/core/src/presentation/formsy/components/TextArea.tsx b/app/scripts/modules/core/src/presentation/formsy/components/TextArea.tsx index dee55c83883..e203c2edc5a 100644 --- a/app/scripts/modules/core/src/presentation/formsy/components/TextArea.tsx +++ b/app/scripts/modules/core/src/presentation/formsy/components/TextArea.tsx @@ -1,17 +1,19 @@ import * as React from 'react'; -import { ChangeEvent, PropTypes } from 'react'; +import { ChangeEvent, PropTypes, ValidationMap } from 'react'; import autoBindMethods from 'class-autobind-decorator'; +import { IFormsyContext } from 'formsy-react'; import { FormComponent, IFormComponentProps, IFormComponentState } from '../FormComponent'; import { IFormFieldLayoutProps } from 'core/presentation'; +import { noop } from 'core/utils'; export interface IProps extends IFormComponentProps, React.HTMLAttributes { /** A react class that will layout the label, input, help, and validation error components */ - layout: React.ComponentClass; + Layout: React.ComponentClass; /** The label text for the textarea */ label?: string; /** (optional) The help or usage rollover markup */ - _help?: React.ReactElement; + Help?: React.ReactElement; /** The class string to place on the textarea */ className?: string; /** A callback for when the textarea value changes */ @@ -20,18 +22,22 @@ export interface IProps extends IFormComponentProps, React.HTMLAttributes { - public static contextTypes = { - formsy: PropTypes.any + public static contextTypes: ValidationMap = { + formsy: PropTypes.object, }; - public static defaultProps = { + public static defaultProps: Partial = { name: null as any, - onChange: () => null as any, + onChange: noop, className: '', }; @@ -41,27 +47,27 @@ export class TextArea extends FormComponent { } public render() { - const { label, _help, layout, name, className, rows } = this.props; + const { label, Help, Layout, name, className, rows } = this.props; - const _label = label && ; + const Label = label && ; const isInvalid = this.showError() || this.showRequired(); const isDirty = !this.isPristine(); const inputClass = `form-control ${className} ${isInvalid ? 'ng-invalid' : ''} ${isDirty ? 'ng-dirty' : ''}`; - const _input =