feat(mail-merge): Add CSV imports, styling, and several fixes
Summary: Adds CSV imports, proper styles to mail merge plugin and fixes a handful of bugs Test Plan: TODO Reviewers: bengotow, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2925
|
@ -311,6 +311,20 @@ export default class ComposerView extends React.Component {
|
|||
return _.reject(files, Utils.shouldDisplayAsImage);
|
||||
}
|
||||
|
||||
_renderActionsWorkspaceRegion() {
|
||||
return (
|
||||
<InjectedComponentSet
|
||||
matching={{role: "Composer:ActionBarWorkspace"}}
|
||||
exposedProps={{
|
||||
draft: this.props.draft,
|
||||
threadId: this.props.draft.threadId,
|
||||
draftClientId: this.props.draft.clientId,
|
||||
session: this.props.session,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
_renderActionsRegion() {
|
||||
return (
|
||||
<div className="composer-action-bar-content">
|
||||
|
@ -648,6 +662,10 @@ export default class ComposerView extends React.Component {
|
|||
{this._renderContentScrollRegion()}
|
||||
</div>
|
||||
|
||||
<div className="composer-action-bar-workspace-wrap">
|
||||
{this._renderActionsWorkspaceRegion()}
|
||||
</div>
|
||||
|
||||
<div className="composer-action-bar-wrap">
|
||||
{this._renderActionsRegion()}
|
||||
</div>
|
||||
|
|
|
@ -51,7 +51,7 @@ describe 'ParticipantsTextField', ->
|
|||
visible={true}
|
||||
participants={@participants}
|
||||
draft={clientId: 'draft-1'}
|
||||
sessio={{}}
|
||||
session={{}}
|
||||
change={@propChange} />
|
||||
)
|
||||
@renderedInput = ReactDOM.findDOMNode(@renderedField).querySelector('input')
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import RetinaImg from './retina-img'
|
||||
import ReactDOM from 'react-dom'
|
||||
import SelectableTable, {SelectableCell} from './selectable-table'
|
||||
|
||||
|
@ -66,19 +67,19 @@ class EditableCell extends Component {
|
|||
|
||||
render() {
|
||||
const {rowIdx, colIdx, tableData: {rows}, isHeader, inputProps, InputRenderer} = this.props
|
||||
const cellValue = rows[rowIdx][colIdx]
|
||||
const cellValue = rows[rowIdx][colIdx] || ''
|
||||
|
||||
return (
|
||||
<SelectableCell ref="cell" {...this.props}>
|
||||
<div ref="inputContainer" tabIndex="0">
|
||||
<InputRenderer
|
||||
type="text"
|
||||
defaultValue={cellValue}
|
||||
rowIdx={rowIdx}
|
||||
colIdx={colIdx}
|
||||
isHeader={isHeader}
|
||||
onKeyDown={::this.onInputKeyDown}
|
||||
defaultValue={cellValue}
|
||||
onBlur={::this.onInputBlur}
|
||||
onKeyDown={::this.onInputKeyDown}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
|
@ -138,8 +139,18 @@ class EditableTable extends Component {
|
|||
<div className="editable-table-container">
|
||||
<SelectableTable {...tableProps} />
|
||||
<div className="column-actions">
|
||||
<div className="btn btn-small" onClick={onAddColumn}>+</div>
|
||||
<div className="btn btn-small" onClick={onRemoveColumn}>-</div>
|
||||
<div className="btn btn-small" onClick={onAddColumn}>
|
||||
<RetinaImg
|
||||
name="icon-column-plus.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
/>
|
||||
</div>
|
||||
<div className="btn btn-small" onClick={onRemoveColumn}>
|
||||
<RetinaImg
|
||||
name="icon-column-minus.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -23,14 +23,20 @@ export class SelectableCell extends Component {
|
|||
className: '',
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
this.props.tableData.rows[this.props.rowIdx][this.props.colIdx] !== nextProps.tableData.rows[nextProps.rowIdx][nextProps.colIdx] ||
|
||||
this.isSelected(this.props) !== this.isSelected(nextProps)
|
||||
)
|
||||
}
|
||||
|
||||
onClickCell() {
|
||||
const {selection, rowIdx, colIdx, onSetSelection} = this.props
|
||||
if (_.isEqual(selection, {row: rowIdx, col: colIdx})) { return }
|
||||
onSetSelection({row: rowIdx, col: colIdx, key: null})
|
||||
}
|
||||
|
||||
isSelected() {
|
||||
const {selection, rowIdx, colIdx} = this.props
|
||||
isSelected({selection, rowIdx, colIdx}) {
|
||||
return (
|
||||
selection && selection.row === rowIdx && selection.col === colIdx
|
||||
)
|
||||
|
@ -38,7 +44,7 @@ export class SelectableCell extends Component {
|
|||
|
||||
isSelectedUsingKey(key) {
|
||||
const {selection} = this.props
|
||||
return this.isSelected() && selection.key === key
|
||||
return this.isSelected(this.props) && selection.key === key
|
||||
}
|
||||
|
||||
isInLastRow() {
|
||||
|
@ -50,7 +56,7 @@ export class SelectableCell extends Component {
|
|||
const {className} = this.props
|
||||
const classes = classnames({
|
||||
[className]: true,
|
||||
'selected': this.isSelected(),
|
||||
'selected': this.isSelected(this.props),
|
||||
})
|
||||
return (
|
||||
<TableCell
|
||||
|
@ -76,15 +82,22 @@ export class SelectableRow extends Component {
|
|||
className: '',
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
this.props.tableData.rows[this.props.rowIdx] !== nextProps.tableData.rows[nextProps.rowIdx] ||
|
||||
this.isSelected(this.props) !== this.isSelected(nextProps) ||
|
||||
this.props.selection.col !== nextProps.selection.col
|
||||
)
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.isSelected()) {
|
||||
if (this.isSelected(this.props)) {
|
||||
ReactDOM.findDOMNode(this)
|
||||
.scrollIntoViewIfNeeded(false)
|
||||
}
|
||||
}
|
||||
|
||||
isSelected() {
|
||||
const {selection, rowIdx} = this.props
|
||||
isSelected({selection, rowIdx}) {
|
||||
return selection && selection.row === rowIdx
|
||||
}
|
||||
|
||||
|
@ -92,7 +105,7 @@ export class SelectableRow extends Component {
|
|||
const {className} = this.props
|
||||
const classes = classnames({
|
||||
[className]: true,
|
||||
'selected': this.isSelected(),
|
||||
'selected': this.isSelected(this.props),
|
||||
})
|
||||
return (
|
||||
<TableRow
|
||||
|
@ -126,6 +139,13 @@ class SelectableTable extends Component {
|
|||
CellRenderer: SelectableCell,
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
this.props.tableData !== nextProps.tableData ||
|
||||
this.props.selection !== nextProps.selection
|
||||
)
|
||||
}
|
||||
|
||||
onArrowUp({key}) {
|
||||
const {onShiftSelection} = this.props
|
||||
onShiftSelection({row: -1, key})
|
||||
|
@ -153,7 +173,7 @@ class SelectableTable extends Component {
|
|||
|
||||
onTab({key}) {
|
||||
const {tableData, selection, onShiftSelection} = this.props
|
||||
const colLen = tableData.columns.length
|
||||
const colLen = tableData.rows[0].length
|
||||
if (selection.col === colLen - 1) {
|
||||
onShiftSelection({row: 1, col: -(colLen - 1), key})
|
||||
} else {
|
||||
|
@ -163,7 +183,7 @@ class SelectableTable extends Component {
|
|||
|
||||
onShiftTab({key}) {
|
||||
const {tableData, selection, onShiftSelection} = this.props
|
||||
const colLen = tableData.columns.length
|
||||
const colLen = tableData.rows[0].length
|
||||
if (selection.col === 0) {
|
||||
onShiftSelection({row: -1, col: colLen - 1, key})
|
||||
} else {
|
||||
|
|
|
@ -12,7 +12,6 @@ const TablePropTypes = {
|
|||
renderer: RendererType,
|
||||
tableData: PropTypes.shape({
|
||||
rows: PropTypes.arrayOf(RowDataType),
|
||||
columns: RowDataType,
|
||||
}),
|
||||
}
|
||||
|
||||
|
@ -83,7 +82,7 @@ export class TableRow extends Component {
|
|||
</TableCell> :
|
||||
null
|
||||
}
|
||||
{_.times(tableData.columns.length, (colIdx) => {
|
||||
{_.times(tableData.rows[0].length, (colIdx) => {
|
||||
const cellProps = {tableData, rowIdx, colIdx, ...extraProps}
|
||||
return (
|
||||
<CellRenderer key={`cell-${rowIdx}-${colIdx}`} {...cellProps}>
|
||||
|
|
|
@ -46,6 +46,7 @@ class Token extends React.Component
|
|||
@displayName: "Token"
|
||||
|
||||
@propTypes:
|
||||
className: React.PropTypes.string,
|
||||
selected: React.PropTypes.bool,
|
||||
valid: React.PropTypes.bool,
|
||||
item: React.PropTypes.object,
|
||||
|
@ -53,6 +54,9 @@ class Token extends React.Component
|
|||
onEdited: React.PropTypes.func,
|
||||
onAction: React.PropTypes.func
|
||||
|
||||
@defaultProps:
|
||||
className: ''
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
editing: false
|
||||
|
@ -90,15 +94,17 @@ class Token extends React.Component
|
|||
"invalid": !@props.valid
|
||||
"selected": @props.selected
|
||||
|
||||
<div className={classes}
|
||||
<div className={"#{classes} #{@props.className}"}
|
||||
onDragStart={@_onDragStart}
|
||||
onDragEnd={@_onDragEnd}
|
||||
draggable="true"
|
||||
onDoubleClick={@_onDoubleClick}
|
||||
onClick={@_onSelect}>
|
||||
<button className="action" onClick={@_onAction} tabIndex={-1}>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentIsMask} name="composer-caret.png" />
|
||||
</button>
|
||||
{if @props.onAction
|
||||
<button className="action" onClick={@_onAction} tabIndex={-1}>
|
||||
<RetinaImg mode={RetinaImg.Mode.ContentIsMask} name="composer-caret.png" />
|
||||
</button>
|
||||
}
|
||||
{@props.children}
|
||||
</div>
|
||||
|
||||
|
@ -187,6 +193,8 @@ class TokenizingTextField extends React.Component
|
|||
# to display that individual token.
|
||||
tokenRenderer: React.PropTypes.func.isRequired
|
||||
|
||||
tokenClassNames: React.PropTypes.func
|
||||
|
||||
# The function responsible for providing a list of possible options
|
||||
# given the current input.
|
||||
#
|
||||
|
@ -244,7 +252,10 @@ class TokenizingTextField extends React.Component
|
|||
onEmptied: React.PropTypes.func
|
||||
|
||||
# Called when the secondary action of the token gets invoked.
|
||||
onTokenAction: React.PropTypes.func
|
||||
onTokenAction: React.PropTypes.oneOfType([
|
||||
React.PropTypes.func,
|
||||
React.PropTypes.bool,
|
||||
])
|
||||
|
||||
# Called when the input is focused
|
||||
onFocus: React.PropTypes.func
|
||||
|
@ -257,6 +268,7 @@ class TokenizingTextField extends React.Component
|
|||
|
||||
@defaultProps:
|
||||
className: ''
|
||||
tokenClassNames: -> ''
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
|
@ -348,14 +360,19 @@ class TokenizingTextField extends React.Component
|
|||
valid = @props.tokenIsValid(item)
|
||||
|
||||
TokenRenderer = @props.tokenRenderer
|
||||
onAction = if @props.onTokenAction is false
|
||||
null
|
||||
else
|
||||
@props.onTokenAction || @_showDefaultTokenMenu
|
||||
|
||||
<Token item={item}
|
||||
<Token className={@props.tokenClassNames(item)}
|
||||
item={item}
|
||||
key={key}
|
||||
valid={valid}
|
||||
selected={@state.selectedTokenKey is key}
|
||||
onSelected={@_selectToken}
|
||||
onEdited={@props.onEdit}
|
||||
onAction={@props.onTokenAction || @_showDefaultTokenMenu}>
|
||||
onAction={onAction}>
|
||||
<TokenRenderer token={item} />
|
||||
</Token>
|
||||
|
||||
|
|
|
@ -367,6 +367,7 @@ class Actions
|
|||
```
|
||||
###
|
||||
@sendDraft: ActionScopeWindow
|
||||
@sendDrafts: ActionScopeWindow
|
||||
@ensureDraftSynced: ActionScopeWindow
|
||||
|
||||
###
|
||||
|
|
|
@ -66,6 +66,7 @@ class DraftStore
|
|||
# window.
|
||||
@listenTo Actions.ensureDraftSynced, @_onEnsureDraftSynced
|
||||
@listenTo Actions.sendDraft, @_onSendDraft
|
||||
@listenTo Actions.sendDrafts, @_onSendDrafts
|
||||
@listenTo Actions.destroyDraft, @_onDestroyDraft
|
||||
|
||||
@listenTo Actions.removeFile, @_onRemoveFile
|
||||
|
@ -327,6 +328,19 @@ class DraftStore
|
|||
Actions.queueTask(new SyncbackDraftTask(draftClientId))
|
||||
|
||||
_onSendDraft: (draftClientId) =>
|
||||
@_sendDraft(draftClientId)
|
||||
.then =>
|
||||
if @_isPopout()
|
||||
NylasEnv.close()
|
||||
|
||||
_onSendDrafts: (draftClientIds) =>
|
||||
Promise.each(draftClientIds, (draftClientId) =>
|
||||
@_sendDraft(draftClientId)
|
||||
).then =>
|
||||
if @_isPopout()
|
||||
NylasEnv.close()
|
||||
|
||||
_sendDraft: (draftClientId) =>
|
||||
@_draftsSending[draftClientId] = true
|
||||
|
||||
@sessionForClientId(draftClientId).then (session) =>
|
||||
|
@ -336,9 +350,7 @@ class DraftStore
|
|||
@_queueDraftAssetTasks(session.draft())
|
||||
Actions.queueTask(new SendDraftTask(draftClientId))
|
||||
@_doneWithSession(session)
|
||||
|
||||
if @_isPopout()
|
||||
NylasEnv.close()
|
||||
Promise.resolve()
|
||||
|
||||
_queueDraftAssetTasks: (draft) =>
|
||||
if draft.files.length > 0 or draft.uploads.length > 0
|
||||
|
|
|
@ -4,15 +4,18 @@
|
|||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
padding-bottom: 5px;
|
||||
|
||||
.column-actions {
|
||||
display: flex;
|
||||
margin-left: @padding-small-horizontal;
|
||||
margin-top: @padding-base-vertical - 1;
|
||||
|
||||
.btn.btn-small {
|
||||
padding: 0;
|
||||
border-radius: 100%;
|
||||
text-align: center;
|
||||
margin-left: 3px;
|
||||
margin-left: @padding-large-vertical - 2;
|
||||
margin-top: 1px;
|
||||
width: 24px;
|
||||
line-height: 22px;
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
@import 'ui-variables';
|
||||
|
||||
@row-highlight-color: #e3effc;
|
||||
@row-highlight-text-color: #3a6fa7;
|
||||
@row-highlight-border-color: #c9dcf0;
|
||||
|
||||
.nylas-table {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
|
||||
.table-row {
|
||||
|
||||
.table-cell {
|
||||
border: 1px solid lighten(@border-color-secondary, 5%);
|
||||
color: @text-color-very-subtle;
|
||||
|
||||
&>div {
|
||||
min-height: 20px;
|
||||
min-width: 100px;
|
||||
}
|
||||
border: 1px solid lightgrey;
|
||||
|
||||
input {
|
||||
color: inherit;
|
||||
border: none;
|
||||
&:focus {
|
||||
border: none;
|
||||
|
@ -24,8 +32,6 @@
|
|||
|
||||
&.selected {
|
||||
// TODO
|
||||
background: green;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.numbered-cell {
|
||||
|
@ -36,9 +42,14 @@
|
|||
}
|
||||
|
||||
&.selected {
|
||||
background: fadeout(@accent-primary, 80%);
|
||||
.table-cell input {
|
||||
color: darken(@accent-primary-dark, 25%);
|
||||
background: @row-highlight-color;
|
||||
|
||||
.table-cell {
|
||||
color: @row-highlight-text-color;
|
||||
border: 1px double @row-highlight-border-color;
|
||||
input {
|
||||
color: @row-highlight-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
BIN
static/images/composer/icon-composer-mailmerge@1x.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
static/images/composer/icon-composer-mailmerge@2x.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/images/mail-merge/btn-column-minus@1x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/mail-merge/btn-column-minus@2x.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
static/images/mail-merge/btn-column-plus@1x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/mail-merge/btn-column-plus@2x.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
static/images/mail-merge/icon-column-minus@1x.png
Normal file
After Width: | Height: | Size: 203 B |
BIN
static/images/mail-merge/icon-column-minus@2x.png
Normal file
After Width: | Height: | Size: 200 B |
BIN
static/images/mail-merge/icon-column-plus@1x.png
Normal file
After Width: | Height: | Size: 217 B |
BIN
static/images/mail-merge/icon-column-plus@2x.png
Normal file
After Width: | Height: | Size: 220 B |
BIN
static/images/mail-merge/mailmerge-grabber@1x.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
static/images/mail-merge/mailmerge-grabber@2x.png
Normal file
After Width: | Height: | Size: 18 KiB |