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
This commit is contained in:
Juan Tejada 2016-04-26 14:18:30 -07:00
parent 7bf842fb0b
commit 71353cdaf1
22 changed files with 127 additions and 35 deletions

View file

@ -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>

View file

@ -51,7 +51,7 @@ describe 'ParticipantsTextField', ->
visible={true}
participants={@participants}
draft={clientId: 'draft-1'}
sessio={{}}
session={{}}
change={@propChange} />
)
@renderedInput = ReactDOM.findDOMNode(@renderedField).querySelector('input')

View file

@ -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>
)

View file

@ -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 {

View file

@ -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}>

View file

@ -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>

View file

@ -367,6 +367,7 @@ class Actions
```
###
@sendDraft: ActionScopeWindow
@sendDrafts: ActionScopeWindow
@ensureDraftSynced: ActionScopeWindow
###

View file

@ -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

View file

@ -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;

View file

@ -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;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB