-
Notifications
You must be signed in to change notification settings - Fork 903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor(core/entityTag): Refactor Entity Tags to React #3717
Changes from all commits
85fc37f
a408272
6c1ae46
4b1b067
cb5f853
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
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 * as DOMPurify from 'dompurify'; | ||
|
||
import { | ||
UUIDGenerator, Application, EntityTagWriter, HelpField, IEntityRef, IEntityTag, | ||
ReactInjector, TaskMonitor, TaskMonitorBuilder, SubmitButton | ||
} from 'core'; | ||
|
||
import { BasicFieldLayout, TextArea, ReactModal } from 'core/presentation'; | ||
import { NgReact } from 'core/reactShims/ngReact'; | ||
import { EntityRefBuilder } from './entityRef.builder'; | ||
import { noop } from 'core/utils'; | ||
|
||
import './entityTagEditor.modal.less'; | ||
import { Form } from 'formsy-react'; | ||
|
||
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<IEntityTagEditorProps, IEntityTagEditorState> { | ||
public static defaultProps: Partial<IEntityTagEditorProps> = { | ||
onHide: noop, | ||
onUpdate: noop, | ||
}; | ||
|
||
private taskMonitorBuilder: TaskMonitorBuilder = ReactInjector.taskMonitorBuilder; | ||
private entityTagWriter: EntityTagWriter = ReactInjector.entityTagWriter; | ||
private $uibModalInstanceEmulation: IModalServiceInstance & { deferred?: IDeferred<any> }; | ||
private form: Form; | ||
|
||
/** Shows the Entity Tag Editor modal */ | ||
public static show(props: IEntityTagEditorProps): Promise<void> { | ||
return ReactModal.show(React.createElement(EntityTagEditor, props)); | ||
} | ||
|
||
constructor(props: IEntityTagEditorProps) { | ||
super(props); | ||
|
||
const { ownerOptions, tag, entityType } = 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: tag.value && tag.value.message, | ||
show: true, | ||
isValid: false, | ||
isSubmitting: false, | ||
owner: owner, | ||
entityType: 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, onUpdate } = 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: () => 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 }); | ||
} | ||
|
||
private refCallback(form: Form): void { | ||
this.form = form; | ||
} | ||
|
||
private submit(): void { | ||
this.form.submit(); | ||
} | ||
|
||
public render() { | ||
const { isNew, tag, ownerOptions } = this.props; | ||
const { isValid, isSubmitting } = this.state; | ||
const message = this.state.message || ''; | ||
|
||
const closeButton = ( | ||
<div className="modal-close close-button pull-right"> | ||
<a className="btn btn-link" onClick={this.onHide}> | ||
<span className="glyphicon glyphicon-remove" /> | ||
</a> | ||
</div> | ||
); | ||
|
||
const submitLabel = `${isNew ? ' Create' : ' Update'} ${tag.value.type}`; | ||
const cancelButton = <button type="button" className="btn btn-default" onClick={this.onHide}>Cancel</button>; | ||
|
||
const { TaskMonitorWrapper } = NgReact; | ||
|
||
return ( | ||
<Modal show={this.state.show} onHide={this.onHide} dialogClassName="entity-tag-editor-modal"> | ||
|
||
<TaskMonitorWrapper monitor={this.state.taskMonitor} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm shocked that this works There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right?! |
||
|
||
<Formsy.Form | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Formsy code. Declare a formsy form. It adds |
||
ref={this.refCallback} | ||
role="form" | ||
name="form" | ||
className="form-horizontal" | ||
onSubmit={this.upsertTag} | ||
onValid={this.onValid} | ||
onInvalid={this.onInvalid} | ||
> | ||
<Modal.Header> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. alternatively, |
||
<h3>{isNew ? 'Create' : 'Update'} {tag.value.type}</h3> | ||
{closeButton} | ||
</Modal.Header> | ||
|
||
<Modal.Body> | ||
<EntityTagMessage | ||
message={message} | ||
onMessageChanged={this.handleMessageChanged} | ||
/> | ||
|
||
{ ownerOptions && ownerOptions.length && ( | ||
<OwnerOptions | ||
selectedOwner={this.state.owner} | ||
ownerOptions={ownerOptions} | ||
onOwnerOptionChanged={this.handleOwnerOptionChanged} | ||
/> | ||
) } | ||
</Modal.Body> | ||
|
||
<Modal.Footer> | ||
{cancelButton} | ||
|
||
<SubmitButton | ||
onClick={this.submit} | ||
label={submitLabel} | ||
isDisabled={!isValid || isSubmitting} | ||
submitting={this.state.isSubmitting} | ||
/> | ||
|
||
</Modal.Footer> | ||
</Formsy.Form> | ||
|
||
</Modal> | ||
); | ||
} | ||
} | ||
|
||
|
||
|
||
interface IEntityTagMessageProps { | ||
message: string; | ||
onMessageChanged(message: string): void; | ||
} | ||
|
||
@autoBindMethods | ||
class EntityTagMessage extends React.Component<IEntityTagMessageProps, {}> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I split these components out, but left them in the same file. Good? Bad? Separate files? The option component is teeny and I didn't think deserved a file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fine by me There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small enough, only used here.... |
||
private handleTextareaChanged(event: React.FormEvent<HTMLTextAreaElement>): void { | ||
this.props.onMessageChanged(event.currentTarget.value); | ||
} | ||
|
||
public render() { | ||
const { message } = this.props; | ||
|
||
return ( | ||
<div className="row"> | ||
<div className="col-md-10 col-md-offset-1"> | ||
|
||
<TextArea | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each validation component type has to be written by us. The |
||
label="Message" | ||
Help={<div>Markdown is okay <HelpField id="markdown.examples"/></div>} | ||
Layout={BasicFieldLayout} | ||
name="message" | ||
required={true} | ||
validationErrors={{ isDefaultRequiredValue: 'Please enter a message' }} | ||
onChange={this.handleTextareaChanged} | ||
value={message} | ||
rows={5} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These props |
||
className="form-control input-sm" | ||
/> | ||
|
||
{ message && ( | ||
<div className="form-group preview"> | ||
<div className="col-md-3 sm-label-right"> | ||
<strong>Preview</strong> | ||
</div> | ||
<div className="col-md-9"> | ||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked(message)) }}/> | ||
</div> | ||
</div> | ||
) } | ||
</div> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
|
||
|
||
interface IOwnerOptionsProps { | ||
selectedOwner: any; | ||
ownerOptions: IOwnerOption[]; | ||
onOwnerOptionChanged(owner: IOwnerOption): void; | ||
} | ||
|
||
@autoBindMethods | ||
class OwnerOptions extends React.Component<IOwnerOptionsProps, void> { | ||
public handleOwnerOptionChanged(option: IOwnerOption): void { | ||
this.props.onOwnerOptionChanged(option); | ||
} | ||
|
||
public render() { | ||
const { ownerOptions, selectedOwner } = this.props; | ||
|
||
return ( | ||
<div className="row"> | ||
<div className="col-md-10 col-md-offset-1"> | ||
<div className="form-group"> | ||
<div className="col-md-3 sm-label-right"> | ||
<b>Applies to</b> | ||
</div> | ||
<div className="col-md-9"> | ||
{ ownerOptions.map((option: IOwnerOption) => ( | ||
<div key={option.label} className="radio"> | ||
<label> | ||
<OwnerOption | ||
option={option} | ||
selectedOwner={selectedOwner} | ||
onOwnerOptionChanged={this.handleOwnerOptionChanged} | ||
/> | ||
<span className="marked" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked(option.label)) }}/> | ||
</label> | ||
</div> | ||
)) } | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
interface IOwnerOptionProps { | ||
option?: IOwnerOption; | ||
selectedOwner?: any; | ||
onOwnerOptionChanged?(option: IOwnerOption): void; | ||
} | ||
|
||
@autoBindMethods | ||
class OwnerOption extends React.Component<IOwnerOptionProps, any> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This component is here simply to avoid an arrow function |
||
public handleOwnerChanged(): void { | ||
this.props.onOwnerOptionChanged(this.props.option); | ||
} | ||
|
||
public render() { | ||
const { option, selectedOwner } = this.props; | ||
return ( | ||
<input | ||
name="owner" | ||
type="radio" | ||
value={option.label} | ||
onChange={this.handleOwnerChanged} | ||
checked={option.owner === selectedOwner} | ||
/> | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do yall like or hate this syntax?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
works for me! doesn't look like there are arguments though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The caller can access the trigger event from the click event handler or whatever. IDK if it's necessary.
I used
onHide.apply
to propagate any argumentsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure!