mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
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:
parent
c762cef4e6
commit
cac679b119
9 changed files with 67 additions and 21 deletions
|
@ -3,10 +3,11 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import AccountContactField from './account-contact-field';
|
import AccountContactField from './account-contact-field';
|
||||||
import {Utils, Actions, AccountStore} from 'nylas-exports';
|
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 CollapsedParticipants from './collapsed-participants';
|
||||||
import ComposerHeaderActions from './composer-header-actions';
|
import ComposerHeaderActions from './composer-header-actions';
|
||||||
|
import SubjectTextField from './subject-text-field';
|
||||||
|
|
||||||
import Fields from './fields';
|
import Fields from './fields';
|
||||||
|
|
||||||
|
@ -118,8 +119,8 @@ export default class ComposerHeader extends React.Component {
|
||||||
Actions.draftParticipantsChanged(this.props.draft.clientId, changes);
|
Actions.draftParticipantsChanged(this.props.draft.clientId, changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChangeSubject = (event) => {
|
_onSubjectChange = (value) => {
|
||||||
this.props.session.changes.add({subject: event.target.value});
|
this.props.session.changes.add({subject: value});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFocusInParticipants = () => {
|
_onFocusInParticipants = () => {
|
||||||
|
@ -192,21 +193,22 @@ export default class ComposerHeader extends React.Component {
|
||||||
if (!this.state.enabledFields.includes(Fields.Subject)) {
|
if (!this.state.enabledFields.includes(Fields.Subject)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const {draft} = this.props
|
||||||
return (
|
return (
|
||||||
<div
|
<InjectedComponent
|
||||||
|
ref={Fields.Subject}
|
||||||
key="subject-wrap"
|
key="subject-wrap"
|
||||||
className="compose-subject-wrap"
|
matching={{role: 'Composer:SubjectTextField'}}
|
||||||
>
|
exposedProps={{
|
||||||
<input
|
draft,
|
||||||
type="text"
|
value: draft.subject,
|
||||||
name="subject"
|
draftClientId: draft.clientId,
|
||||||
ref={Fields.Subject}
|
onSubjectChange: this._onSubjectChange,
|
||||||
placeholder="Subject"
|
}}
|
||||||
value={this.props.draft.subject}
|
requiredMethods={['focus']}
|
||||||
onChange={this._onChangeSubject}
|
fallback={SubjectTextField}
|
||||||
/>
|
/>
|
||||||
</div>
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderFields = () => {
|
_renderFields = () => {
|
||||||
|
|
|
@ -192,6 +192,7 @@ export default class ComposerView extends React.Component {
|
||||||
return (
|
return (
|
||||||
<InjectedComponent
|
<InjectedComponent
|
||||||
ref={Fields.Body}
|
ref={Fields.Body}
|
||||||
|
className="body-field"
|
||||||
matching={{role: "Composer:Editor"}}
|
matching={{role: "Composer:Editor"}}
|
||||||
fallback={ComposerEditor}
|
fallback={ComposerEditor}
|
||||||
requiredMethods={[
|
requiredMethods={[
|
||||||
|
|
39
internal_packages/composer/lib/subject-text-field.jsx
Normal file
39
internal_packages/composer/lib/subject-text-field.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -253,7 +253,7 @@ body.platform-win32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-subject-wrap {
|
.composer-subject {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 23px;
|
margin: 0 23px;
|
||||||
border-bottom: 1px solid @border-color-divider;
|
border-bottom: 1px solid @border-color-divider;
|
||||||
|
|
|
@ -100,7 +100,7 @@ class Contenteditable extends React.Component
|
||||||
|
|
||||||
editor = new EditorAPI(@_editableNode())
|
editor = new EditorAPI(@_editableNode())
|
||||||
|
|
||||||
if not editor.currentSelection().isInScope()
|
if not editor.currentSelection().isInScope() and extraArgsObj.methodName isnt 'onBlur'
|
||||||
@_restoreSelection()
|
@_restoreSelection()
|
||||||
|
|
||||||
argsObj = _.extend(extraArgsObj, {editor})
|
argsObj = _.extend(extraArgsObj, {editor})
|
||||||
|
@ -202,6 +202,7 @@ class Contenteditable extends React.Component
|
||||||
ref="contenteditable"
|
ref="contenteditable"
|
||||||
contentEditable
|
contentEditable
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
placeholder={@props.placeholder}
|
||||||
dangerouslySetInnerHTML={__html: @props.value}
|
dangerouslySetInnerHTML={__html: @props.value}
|
||||||
{...@_eventHandlers()}></div>
|
{...@_eventHandlers()}></div>
|
||||||
</KeyCommandsRegion>
|
</KeyCommandsRegion>
|
||||||
|
@ -439,6 +440,7 @@ class Contenteditable extends React.Component
|
||||||
return if @_inCompositionEvent
|
return if @_inCompositionEvent
|
||||||
return if not extension[method]?
|
return if not extension[method]?
|
||||||
editingFunction = extension[method].bind(extension)
|
editingFunction = extension[method].bind(extension)
|
||||||
|
argsObj = _.extend(argsObj, {methodName: method})
|
||||||
@atomicEdit(editingFunction, argsObj)
|
@atomicEdit(editingFunction, argsObj)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ import React from 'react';
|
||||||
|
|
||||||
function ListensToFluxStore(ComposedComponent, {stores, getStateFromStores}) {
|
function ListensToFluxStore(ComposedComponent, {stores, getStateFromStores}) {
|
||||||
return class extends ComposedComponent {
|
return class extends ComposedComponent {
|
||||||
static displayName = ComposedComponent.displayName;
|
|
||||||
|
|
||||||
static containerRequired = false;
|
static containerRequired = false;
|
||||||
|
|
||||||
|
static propTypes = {}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this._unlisteners = [];
|
this._unlisteners = [];
|
||||||
|
|
|
@ -193,6 +193,7 @@ export default class ParticipantsTextField extends React.Component {
|
||||||
ref="textField"
|
ref="textField"
|
||||||
matching={{role: 'Composer:ParticipantsTextField'}}
|
matching={{role: 'Composer:ParticipantsTextField'}}
|
||||||
fallback={TokenizingTextField}
|
fallback={TokenizingTextField}
|
||||||
|
requiredMethods={['focus']}
|
||||||
exposedProps={{
|
exposedProps={{
|
||||||
tokens: this.props.participants[this.props.field],
|
tokens: this.props.participants[this.props.field],
|
||||||
tokenKey: (p) => p.email,
|
tokenKey: (p) => p.email,
|
||||||
|
|
|
@ -156,6 +156,7 @@ class NylasExports
|
||||||
@lazyLoad "DeprecateUtils", 'deprecate-utils'
|
@lazyLoad "DeprecateUtils", 'deprecate-utils'
|
||||||
@lazyLoad "VirtualDOMUtils", 'virtual-dom-utils'
|
@lazyLoad "VirtualDOMUtils", 'virtual-dom-utils'
|
||||||
@lazyLoad "NylasSpellchecker", 'nylas-spellchecker'
|
@lazyLoad "NylasSpellchecker", 'nylas-spellchecker'
|
||||||
|
@lazyLoad "EditorAPI", 'components/contenteditable/editor-api'
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
@lazyLoad "UndoManager", 'undo-manager'
|
@lazyLoad "UndoManager", 'undo-manager'
|
||||||
|
|
2
src/pro
2
src/pro
|
@ -1 +1 @@
|
||||||
Subproject commit 17af8764f710a944344d39a9f834006fd7a0f3d5
|
Subproject commit 8a3b33001749f74933bff9d800080279935cb768
|
Loading…
Reference in a new issue