feat(mail-merge): Add ability to drop tokens in subject

Summary:
Adds ability to drop tokens in subject via a custom rendered subject field which
renders a contenteditable instead of an input.
Decided to completely replace the subject field via injected components for a
few resons:
  - That's the way we are currently extending the functionality of the participant fields, so it keeps the plugin code consistent (at the cost of potentially more code)
  - Completely replacing the subject for a contenteditable means we hace to do extra work to clean up the html before sending.
  - Reusing our Contenteditable.cjsx class for the subject is overkill, but using a vanilla contenteditable meant duplicating a bunch of the code in that class if we want to add

Test Plan: Unit tests

Reviewers: bengotow, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2949
This commit is contained in:
Juan Tejada 2016-05-12 10:47:22 -07:00
parent c762cef4e6
commit cac679b119
9 changed files with 67 additions and 21 deletions

View file

@ -3,10 +3,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import AccountContactField from './account-contact-field';
import {Utils, Actions, AccountStore} from 'nylas-exports';
import {KeyCommandsRegion, ParticipantsTextField, ListensToFluxStore} from 'nylas-component-kit';
import {InjectedComponent, KeyCommandsRegion, ParticipantsTextField, ListensToFluxStore} from 'nylas-component-kit';
import CollapsedParticipants from './collapsed-participants';
import ComposerHeaderActions from './composer-header-actions';
import SubjectTextField from './subject-text-field';
import Fields from './fields';
@ -118,8 +119,8 @@ export default class ComposerHeader extends React.Component {
Actions.draftParticipantsChanged(this.props.draft.clientId, changes);
}
_onChangeSubject = (event) => {
this.props.session.changes.add({subject: event.target.value});
_onSubjectChange = (value) => {
this.props.session.changes.add({subject: value});
}
_onFocusInParticipants = () => {
@ -192,21 +193,22 @@ export default class ComposerHeader extends React.Component {
if (!this.state.enabledFields.includes(Fields.Subject)) {
return false;
}
const {draft} = this.props
return (
<div
<InjectedComponent
ref={Fields.Subject}
key="subject-wrap"
className="compose-subject-wrap"
>
<input
type="text"
name="subject"
ref={Fields.Subject}
placeholder="Subject"
value={this.props.draft.subject}
onChange={this._onChangeSubject}
/>
</div>
);
matching={{role: 'Composer:SubjectTextField'}}
exposedProps={{
draft,
value: draft.subject,
draftClientId: draft.clientId,
onSubjectChange: this._onSubjectChange,
}}
requiredMethods={['focus']}
fallback={SubjectTextField}
/>
)
}
_renderFields = () => {

View file

@ -192,6 +192,7 @@ export default class ComposerView extends React.Component {
return (
<InjectedComponent
ref={Fields.Body}
className="body-field"
matching={{role: "Composer:Editor"}}
fallback={ComposerEditor}
requiredMethods={[

View file

@ -0,0 +1,39 @@
import React, {Component, PropTypes} from 'react'
import {findDOMNode} from 'react-dom'
export default class SubjectTextField extends Component {
static displayName = 'SubjectTextField'
static containerRequired = false
static propTypes = {
value: PropTypes.string,
onSubjectChange: PropTypes.func,
}
onInputChange = ({target: {value}}) => {
this.props.onSubjectChange(value)
}
focus() {
findDOMNode(this.refs.input).focus()
}
render() {
const {value} = this.props
return (
<div className="composer-subject subject-field">
<input
ref="input"
type="text"
name="subject"
placeholder="Subject"
value={value}
onChange={this.onInputChange}
/>
</div>
);
}
}

View file

@ -253,7 +253,7 @@ body.platform-win32 {
}
}
.compose-subject-wrap {
.composer-subject {
position: relative;
margin: 0 23px;
border-bottom: 1px solid @border-color-divider;

View file

@ -100,7 +100,7 @@ class Contenteditable extends React.Component
editor = new EditorAPI(@_editableNode())
if not editor.currentSelection().isInScope()
if not editor.currentSelection().isInScope() and extraArgsObj.methodName isnt 'onBlur'
@_restoreSelection()
argsObj = _.extend(extraArgsObj, {editor})
@ -202,6 +202,7 @@ class Contenteditable extends React.Component
ref="contenteditable"
contentEditable
spellCheck={false}
placeholder={@props.placeholder}
dangerouslySetInnerHTML={__html: @props.value}
{...@_eventHandlers()}></div>
</KeyCommandsRegion>
@ -439,6 +440,7 @@ class Contenteditable extends React.Component
return if @_inCompositionEvent
return if not extension[method]?
editingFunction = extension[method].bind(extension)
argsObj = _.extend(argsObj, {methodName: method})
@atomicEdit(editingFunction, argsObj)

View file

@ -2,10 +2,10 @@ import React from 'react';
function ListensToFluxStore(ComposedComponent, {stores, getStateFromStores}) {
return class extends ComposedComponent {
static displayName = ComposedComponent.displayName;
static containerRequired = false;
static propTypes = {}
constructor(props) {
super(props);
this._unlisteners = [];

View file

@ -193,6 +193,7 @@ export default class ParticipantsTextField extends React.Component {
ref="textField"
matching={{role: 'Composer:ParticipantsTextField'}}
fallback={TokenizingTextField}
requiredMethods={['focus']}
exposedProps={{
tokens: this.props.participants[this.props.field],
tokenKey: (p) => p.email,

View file

@ -156,6 +156,7 @@ class NylasExports
@lazyLoad "DeprecateUtils", 'deprecate-utils'
@lazyLoad "VirtualDOMUtils", 'virtual-dom-utils'
@lazyLoad "NylasSpellchecker", 'nylas-spellchecker'
@lazyLoad "EditorAPI", 'components/contenteditable/editor-api'
# Services
@lazyLoad "UndoManager", 'undo-manager'

@ -1 +1 @@
Subproject commit 17af8764f710a944344d39a9f834006fd7a0f3d5
Subproject commit 8a3b33001749f74933bff9d800080279935cb768