feat(download-all): Adds download-all button + style updates, more ES6
Summary: - Adds initial version of download all button - Converts attachments plugin to ES6 and adds updated styling - Updates quoted text button - #905, #1712 Test Plan: - Unit + manual Reviewers: evan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2769
|
@ -1,97 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
React = require 'react'
|
||||
{RetinaImg, Flexbox} = require 'nylas-component-kit'
|
||||
{Actions, Utils, FileDownloadStore} = require 'nylas-exports'
|
||||
|
||||
class AttachmentComponent extends React.Component
|
||||
@displayName: 'AttachmentComponent'
|
||||
|
||||
@propTypes:
|
||||
file: React.PropTypes.object.isRequired
|
||||
download: React.PropTypes.object
|
||||
removable: React.PropTypes.bool
|
||||
targetPath: React.PropTypes.string
|
||||
messageClientId: React.PropTypes.string
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = progressPercent: 0
|
||||
|
||||
render: =>
|
||||
<div className="inner" onDoubleClick={@_onClickView} onDragStart={@_onDragStart} draggable="true">
|
||||
<span className={"progress-bar-wrap state-#{@props.download?.state ? ""}"}>
|
||||
<span className="progress-background"></span>
|
||||
<span className="progress-foreground" style={@_downloadProgressStyle()}></span>
|
||||
</span>
|
||||
|
||||
<Flexbox direction="row" style={alignItems: 'center'}>
|
||||
<RetinaImg className="file-icon"
|
||||
fallback="file-fallback.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
name="file-#{@props.file.displayExtension()}.png"/>
|
||||
<span className="file-name">{@props.file.displayName()}</span>
|
||||
{@_renderFileActions()}
|
||||
</Flexbox>
|
||||
</div>
|
||||
|
||||
_renderFileActions: =>
|
||||
if @props.removable
|
||||
<div className="file-action-icon" onClick={@_onClickRemove}>
|
||||
{@_renderRemoveIcon()}
|
||||
</div>
|
||||
else if @_isDownloading() and @_canAbortDownload()
|
||||
<div className="file-action-icon" onClick={@_onClickAbort}>
|
||||
{@_renderRemoveIcon()}
|
||||
</div>
|
||||
else
|
||||
<div className="file-action-icon" onClick={@_onClickDownload}>
|
||||
{@_renderDownloadButton()}
|
||||
</div>
|
||||
|
||||
_downloadProgressStyle: =>
|
||||
width: "#{@props.download?.percent ? 0}%"
|
||||
|
||||
_canAbortDownload: -> true
|
||||
|
||||
_canClickToView: => not @props.removable
|
||||
|
||||
_isDownloading: => @props.download?.state is "downloading"
|
||||
|
||||
_renderRemoveIcon: ->
|
||||
<RetinaImg name="remove-attachment.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
_renderDownloadButton: ->
|
||||
<RetinaImg name="icon-attachment-download.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
_onDragStart: (event) =>
|
||||
filePath = FileDownloadStore.pathForFile(@props.file)
|
||||
if fs.existsSync(filePath)
|
||||
# Note: From trial and error, it appears that the second param /MUST/ be the
|
||||
# same as the last component of the filePath URL, or the download fails.
|
||||
DownloadURL = "#{@props.file.contentType}:#{path.basename(filePath)}:file://#{filePath}"
|
||||
event.dataTransfer.setData("DownloadURL", DownloadURL)
|
||||
event.dataTransfer.setData("text/nylas-file-url", DownloadURL)
|
||||
else
|
||||
event.preventDefault()
|
||||
return
|
||||
|
||||
_onClickView: =>
|
||||
Actions.fetchAndOpenFile(@props.file) if @_canClickToView()
|
||||
|
||||
_onClickRemove: (event) =>
|
||||
Actions.removeFile
|
||||
file: @props.file
|
||||
messageClientId: @props.messageClientId
|
||||
event.stopPropagation() # Prevent 'onClickView'
|
||||
|
||||
_onClickDownload: (event) =>
|
||||
Actions.fetchAndSaveFile(@props.file)
|
||||
event.stopPropagation() # Prevent 'onClickView'
|
||||
|
||||
_onClickAbort: (event) =>
|
||||
Actions.abortFetchFile(@props.file)
|
||||
event.stopPropagation() # Prevent 'onClickView'
|
||||
|
||||
|
||||
module.exports = AttachmentComponent
|
154
internal_packages/attachments/lib/attachment-component.jsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {RetinaImg, Flexbox} from 'nylas-component-kit'
|
||||
import {Actions, FileDownloadStore} from 'nylas-exports'
|
||||
|
||||
|
||||
class AttachmentComponent extends Component {
|
||||
static displayName = 'AttachmentComponent';
|
||||
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
download: PropTypes.object,
|
||||
removable: PropTypes.bool,
|
||||
targetPath: PropTypes.string,
|
||||
messageClientId: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {progressPercent: 0}
|
||||
}
|
||||
|
||||
static containerRequired = false;
|
||||
|
||||
_isDownloading() {
|
||||
const {download} = this.props
|
||||
const state = download ? download.state : null
|
||||
return state === 'downloading'
|
||||
}
|
||||
|
||||
_canClickToView() {
|
||||
return !this.props.removable
|
||||
}
|
||||
|
||||
_canAbortDownload() {
|
||||
return true
|
||||
}
|
||||
|
||||
_downloadProgressStyle() {
|
||||
const {download} = this.props
|
||||
const percent = download ? download.percent || 0 : 0;
|
||||
return {
|
||||
width: `${percent}%`,
|
||||
}
|
||||
}
|
||||
|
||||
_onDragStart = (event) => {
|
||||
const {file} = this.props
|
||||
const filePath = FileDownloadStore.pathForFile(file)
|
||||
if (fs.existsSync(filePath)) {
|
||||
// Note: From trial and error, it appears that the second param /MUST/ be the
|
||||
// same as the last component of the filePath URL, or the download fails.
|
||||
const DownloadURL = `${file.contentType}:${path.basename(filePath)}:file://${filePath}`
|
||||
event.dataTransfer.setData("DownloadURL", DownloadURL)
|
||||
event.dataTransfer.setData("text/nylas-file-url", DownloadURL)
|
||||
} else {
|
||||
event.preventDefault()
|
||||
}
|
||||
};
|
||||
|
||||
_onClickView = () => {
|
||||
if (this._canClickToView()) {
|
||||
Actions.fetchAndOpenFile(this.props.file)
|
||||
}
|
||||
};
|
||||
|
||||
_onClickRemove = (event) => {
|
||||
Actions.removeFile({
|
||||
file: this.props.file,
|
||||
messageClientId: this.props.messageClientId,
|
||||
})
|
||||
event.stopPropagation() // Prevent 'onClickView'
|
||||
};
|
||||
|
||||
_onClickDownload = (event) => {
|
||||
Actions.fetchAndSaveFile(this.props.file)
|
||||
event.stopPropagation() // Prevent 'onClickView'
|
||||
};
|
||||
|
||||
_onClickAbort = (event) => {
|
||||
Actions.abortFetchFile(this.props.file)
|
||||
event.stopPropagation() // Prevent 'onClickView'
|
||||
};
|
||||
|
||||
_renderRemoveIcon() {
|
||||
return (
|
||||
<RetinaImg
|
||||
name="remove-attachment.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
_renderDownloadButton() {
|
||||
return (
|
||||
<RetinaImg
|
||||
name="icon-attachment-download.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
_renderFileActionIcon() {
|
||||
if (this.props.removable) {
|
||||
return (
|
||||
<div className="file-action-icon" onClick={this._onClickRemove}>
|
||||
{this._renderRemoveIcon()}
|
||||
</div>
|
||||
)
|
||||
} else if (this._isDownloading() && this._canAbortDownload()) {
|
||||
return (
|
||||
<div className="file-action-icon" onClick={this._onClickAbort}>
|
||||
{this._renderRemoveIcon()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="file-action-icon" onClick={this._onClickDownload}>
|
||||
{this._renderDownloadButton()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {file, download} = this.props;
|
||||
const downloadState = download ? download.state || "" : "";
|
||||
|
||||
return (
|
||||
<div className="inner" onDoubleClick={this._onClickView} onDragStart={this._onDragStart} draggable="true">
|
||||
<span className={`progress-bar-wrap state-${downloadState}`}>
|
||||
<span className="progress-background" />
|
||||
<span className="progress-foreground" style={this._downloadProgressStyle()} />
|
||||
</span>
|
||||
|
||||
<Flexbox direction="row" style={{alignItems: 'center'}}>
|
||||
<div className="file-info-wrap">
|
||||
<RetinaImg
|
||||
className="file-icon"
|
||||
fallback="file-fallback.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
name={`file-${file.displayExtension()}.png`}
|
||||
/>
|
||||
<span className="file-name">{file.displayName()}</span>
|
||||
<span className="file-size">{file.displayFileSize()}</span>
|
||||
</div>
|
||||
{this._renderFileActionIcon()}
|
||||
</Flexbox>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default AttachmentComponent
|
|
@ -1,44 +0,0 @@
|
|||
path = require 'path'
|
||||
React = require 'react'
|
||||
AttachmentComponent = require './attachment-component'
|
||||
{RetinaImg, Spinner, DraggableImg} = require 'nylas-component-kit'
|
||||
|
||||
class ImageAttachmentComponent extends AttachmentComponent
|
||||
@displayName: 'ImageAttachmentComponent'
|
||||
|
||||
render: =>
|
||||
<div>
|
||||
<span className={"progress-bar-wrap state-#{@props.download?.state ? ""}"}>
|
||||
<span className="progress-background"></span>
|
||||
<span className="progress-foreground" style={@_downloadProgressStyle()}></span>
|
||||
</span>
|
||||
|
||||
{@_renderFileActions()}
|
||||
|
||||
<div className="file-preview" onDoubleClick={@_onClickView}>
|
||||
<div className="file-name-container">
|
||||
<div className="file-name">{@props.file.displayName()}</div>
|
||||
</div>
|
||||
{@_imgOrLoader()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_canAbortDownload: -> false
|
||||
|
||||
_renderRemoveIcon: ->
|
||||
<RetinaImg name="image-cancel-button.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
_renderDownloadButton: ->
|
||||
<RetinaImg name="image-download-button.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
_imgOrLoader: ->
|
||||
if @props.download and @props.download.percent <= 5
|
||||
<div style={width: "100%", height: "100px"}>
|
||||
<Spinner visible={true} />
|
||||
</div>
|
||||
else if @props.download and @props.download.percent < 100
|
||||
<DraggableImg src={"#{@props.targetPath}?percent=#{@props.download.percent}"} />
|
||||
else
|
||||
<DraggableImg src={@props.targetPath} />
|
||||
|
||||
module.exports = ImageAttachmentComponent
|
|
@ -0,0 +1,77 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {RetinaImg, Spinner, DraggableImg} from 'nylas-component-kit'
|
||||
import AttachmentComponent from './attachment-component'
|
||||
|
||||
|
||||
class ImageAttachmentComponent extends AttachmentComponent {
|
||||
static displayName = 'ImageAttachmentComponent';
|
||||
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
download: PropTypes.object,
|
||||
targetPath: PropTypes.string,
|
||||
};
|
||||
|
||||
static containerRequired = false;
|
||||
|
||||
_canAbortDownload() {
|
||||
return false
|
||||
}
|
||||
|
||||
_imgOrLoader() {
|
||||
const {download, targetPath} = this.props
|
||||
if (download && download.percent <= 5) {
|
||||
return (
|
||||
<div style={{width: "100%", height: "100px"}}>
|
||||
<Spinner visible />
|
||||
</div>
|
||||
)
|
||||
} else if (download && download.percent < 100) {
|
||||
return (
|
||||
<DraggableImg src={`${targetPath}?percent=${download.percent}`} />
|
||||
)
|
||||
}
|
||||
return <DraggableImg src={targetPath} />
|
||||
}
|
||||
|
||||
_renderRemoveIcon() {
|
||||
return (
|
||||
<RetinaImg
|
||||
name="image-cancel-button.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
_renderDownloadButton() {
|
||||
return (
|
||||
<RetinaImg
|
||||
name="image-download-button.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {download, file} = this.props
|
||||
const state = download ? download.state || "" : ""
|
||||
const displayName = file.displayName()
|
||||
return (
|
||||
<div>
|
||||
<span className={`progress-bar-wrap state-${state}`}>
|
||||
<span className="progress-background"></span>
|
||||
<span className="progress-foreground" style={this._downloadProgressStyle()}></span>
|
||||
</span>
|
||||
{this._renderFileActions()}
|
||||
<div className="file-preview" onDoubleClick={this._onClickView}>
|
||||
<div className="file-name-container">
|
||||
<div className="file-name">{displayName}</div>
|
||||
</div>
|
||||
{this._imgOrLoader()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageAttachmentComponent
|
|
@ -1,18 +0,0 @@
|
|||
{ComponentRegistry} = require 'nylas-exports'
|
||||
|
||||
AttachmentComponent = require "./attachment-component"
|
||||
ImageAttachmentComponent = require "./image-attachment-component"
|
||||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
ComponentRegistry.register AttachmentComponent,
|
||||
role: 'Attachment'
|
||||
|
||||
ComponentRegistry.register ImageAttachmentComponent,
|
||||
role: 'Attachment:Image'
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister(AttachmentComponent)
|
||||
ComponentRegistry.unregister(ImageAttachmentComponent)
|
||||
|
||||
serialize: -> @state
|
14
internal_packages/attachments/lib/main.es6
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {ComponentRegistry} from 'nylas-exports'
|
||||
import AttachmentComponent from "./attachment-component"
|
||||
import ImageAttachmentComponent from "./image-attachment-component"
|
||||
|
||||
|
||||
export function activate() {
|
||||
ComponentRegistry.register(AttachmentComponent, {role: 'Attachment'})
|
||||
ComponentRegistry.register(ImageAttachmentComponent, {role: 'Attachment:Image'})
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
ComponentRegistry.unregister(AttachmentComponent)
|
||||
ComponentRegistry.unregister(ImageAttachmentComponent)
|
||||
}
|
|
@ -12,12 +12,11 @@
|
|||
-webkit-user-drag: element;
|
||||
|
||||
.inner {
|
||||
border-radius: 4px;
|
||||
border-radius: 2px;
|
||||
color: @text-color;
|
||||
background: @background-off-primary;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.09);
|
||||
padding: 0 @spacing-standard;
|
||||
height:46px;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -25,6 +24,9 @@
|
|||
.inner {
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
.file-action-icon {
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
&.file-upload {
|
||||
|
@ -80,20 +82,46 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
margin-right: 10px;
|
||||
flex-shrink:0;
|
||||
}
|
||||
.file-name {
|
||||
font-weight: @font-weight-medium;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.file-info-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: @spacing-half + 1;
|
||||
width: 100%;
|
||||
|
||||
.file-icon {
|
||||
margin-right: 10px;
|
||||
flex-shrink:0;
|
||||
}
|
||||
.file-name {
|
||||
font-weight: @font-weight-medium;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.file-size {
|
||||
@file-size-color: #b8b8b8;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: @spacing-three-quarters;
|
||||
color: @file-size-color;
|
||||
}
|
||||
}
|
||||
|
||||
.file-action-icon {
|
||||
margin-left: 10px;
|
||||
flex-shrink:0;
|
||||
@file-icon-color: #c7c7c7;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
padding-top: 1px;
|
||||
height: 100%;
|
||||
width: 37px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.09);
|
||||
img {
|
||||
background-color: @file-icon-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,6 +151,9 @@
|
|||
.file-action-icon, .file-name-container, .file-name {
|
||||
display: block;
|
||||
}
|
||||
.file-action-icon {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-action-icon {
|
||||
|
@ -132,6 +163,10 @@
|
|||
top: -8px;
|
||||
width: 26px;
|
||||
border-radius: 0 0 0 3px;
|
||||
border-left: none;
|
||||
img {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
|
@ -170,7 +205,7 @@
|
|||
z-index: 1;
|
||||
max-width: 100%;
|
||||
background: url(../static/images/attachments/transparency-background.png) top left repeat;
|
||||
background-size:8px;
|
||||
background-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -364,9 +364,8 @@ class ComposerView extends React.Component
|
|||
|
||||
_renderQuotedTextControl: ->
|
||||
if QuotedHTMLTransformer.hasQuotedHTML(@state.body)
|
||||
text = if @state.showQuotedText then "Hide" else "Show"
|
||||
<a className="quoted-text-control" onClick={@_onToggleQuotedText}>
|
||||
<span className="dots">•••</span>{text} previous
|
||||
<span className="dots">•••</span>
|
||||
</a>
|
||||
else return []
|
||||
|
||||
|
|
|
@ -109,9 +109,6 @@ describe "Composer Quoted Text", ->
|
|||
it 'should be rendered', ->
|
||||
expect(@toggle).toBeDefined()
|
||||
|
||||
it 'prompts to hide the quote', ->
|
||||
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Hide previous"
|
||||
|
||||
describe 'when showQuotedText is false', ->
|
||||
beforeEach ->
|
||||
@composer.setState
|
||||
|
@ -151,6 +148,3 @@ describe "Composer Quoted Text", ->
|
|||
|
||||
it 'should be rendered', ->
|
||||
expect(@toggle).toBeDefined()
|
||||
|
||||
it 'prompts to hide the quote', ->
|
||||
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Show previous"
|
||||
|
|
|
@ -68,9 +68,8 @@ class MessageItemBody extends React.Component
|
|||
|
||||
_renderQuotedTextControl: =>
|
||||
return null unless QuotedHTMLTransformer.hasQuotedHTML(@props.message.body)
|
||||
text = if @state.showQuotedText then "Hide" else "Show"
|
||||
<a className="quoted-text-control" onClick={@_toggleQuotedText}>
|
||||
<span className="dots">•••</span>{text} previous
|
||||
<span className="dots">•••</span>
|
||||
</a>
|
||||
|
||||
_toggleQuotedText: =>
|
||||
|
|
|
@ -157,12 +157,38 @@ class MessageItem extends React.Component
|
|||
el = el.parentElement
|
||||
@_toggleCollapsed()
|
||||
|
||||
_onDownloadAll: =>
|
||||
Actions.fetchAndSaveAllFiles(@props.message.files)
|
||||
|
||||
_renderDownloadAllButton: =>
|
||||
<div className="download-all" onClick={@_onDownloadAll}>
|
||||
<div className="attachment-number">
|
||||
<RetinaImg
|
||||
name="ic-attachments-all-clippy.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
/>
|
||||
<span>{@props.message.files.length} attachments</span>
|
||||
</div>
|
||||
<div className="separator">-</div>
|
||||
<div className="download-all-action">
|
||||
<RetinaImg
|
||||
name="ic-attachments-download-all.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
/>
|
||||
<span>Download all</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
_renderAttachments: =>
|
||||
attachments = @_attachmentComponents()
|
||||
if attachments.length > 0
|
||||
<div className="attachments-area">{attachments}</div>
|
||||
<div>
|
||||
{if attachments.length > 1 then @_renderDownloadAllButton()}
|
||||
<div className="attachments-area">{attachments}</div>
|
||||
</div>
|
||||
else
|
||||
<div></div>
|
||||
<div />
|
||||
|
||||
_renderHeaderSideItems: ->
|
||||
styles =
|
||||
|
|
|
@ -196,9 +196,6 @@ describe "MessageItem", ->
|
|||
it 'should be rendered', ->
|
||||
expect(@toggle).toBeDefined()
|
||||
|
||||
it 'prompts to hide the quote', ->
|
||||
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Show previous"
|
||||
|
||||
it "should be initialized to true if the message contains `Forwarded`...", ->
|
||||
@message.body = """
|
||||
Hi guys, take a look at this. Very relevant. -mg
|
||||
|
@ -249,9 +246,6 @@ describe "MessageItem", ->
|
|||
it 'should be rendered', ->
|
||||
expect(@toggle).toBeDefined()
|
||||
|
||||
it 'prompts to hide the quote', ->
|
||||
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Hide previous"
|
||||
|
||||
it "should pass the value into the EmailFrame", ->
|
||||
frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)
|
||||
expect(frame.props.showQuotedText).toBe(true)
|
||||
|
|
|
@ -532,8 +532,43 @@ body.platform-win32 {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
.download-all {
|
||||
@download-btn-color: fadeout(#929292, 20%);
|
||||
@download-hover-color: fadeout(@component-active-color, 20%);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: @download-btn-color;
|
||||
font-size: 0.9em;
|
||||
cursor: default;
|
||||
margin-top: @spacing-three-quarters;
|
||||
|
||||
.separator {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.attachment-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
margin-right: @spacing-half;
|
||||
background-color: @download-btn-color;
|
||||
}
|
||||
|
||||
.download-all-action:hover {
|
||||
color: @download-hover-color;
|
||||
img {
|
||||
background-color: @download-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachments-area {
|
||||
padding-top: @spacing-standard;
|
||||
padding-top: @spacing-half + 2;
|
||||
|
||||
// attachments are padded on both sides so that things like the remove "X" can
|
||||
// overhang them. To make the attachments line up with the body, we need to outdent
|
||||
|
|
|
@ -475,6 +475,7 @@ class Actions
|
|||
|
||||
@fetchAndOpenFile: ActionScopeWindow
|
||||
@fetchAndSaveFile: ActionScopeWindow
|
||||
@fetchAndSaveAllFiles: ActionScopeWindow
|
||||
@fetchFile: ActionScopeWindow
|
||||
@abortFetchFile: ActionScopeWindow
|
||||
|
||||
|
|
|
@ -67,6 +67,10 @@ class File extends Model
|
|||
else
|
||||
return "Unnamed Attachment"
|
||||
|
||||
safeDisplayName: ->
|
||||
RegExpUtils = require '../../regexp-utils'
|
||||
return @displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-')
|
||||
|
||||
# Public: Returns the file extension that should be used for this file.
|
||||
# Note that asking for the displayExtension is more accurate than trying to read
|
||||
# the extension directly off the filename. The returned extension may be based
|
||||
|
@ -77,4 +81,20 @@ class File extends Model
|
|||
displayExtension: ->
|
||||
path.extname(@displayName().toLowerCase())[1..-1]
|
||||
|
||||
displayFileSize: (bytes = @size) ->
|
||||
threshold = 1000000000
|
||||
units = ['B', 'KB', 'MB', 'GB']
|
||||
idx = units.length - 1
|
||||
|
||||
result = bytes / threshold
|
||||
while result < 1 and idx >= 0
|
||||
threshold /= 1000
|
||||
result = bytes / threshold
|
||||
idx--
|
||||
|
||||
# parseFloat will remove trailing zeros
|
||||
decimalPoints = if idx >= 2 then 1 else 0
|
||||
rounded = parseFloat(result.toFixed(decimalPoints))
|
||||
return "#{rounded} #{units[idx]}"
|
||||
|
||||
module.exports = File
|
||||
|
|
|
@ -98,6 +98,7 @@ FileDownloadStore = Reflux.createStore
|
|||
@listenTo Actions.fetchFile, @_fetch
|
||||
@listenTo Actions.fetchAndOpenFile, @_fetchAndOpen
|
||||
@listenTo Actions.fetchAndSaveFile, @_fetchAndSave
|
||||
@listenTo Actions.fetchAndSaveAllFiles, @_fetchAndSaveAll
|
||||
@listenTo Actions.abortFetchFile, @_abortFetchFile
|
||||
@listenTo Actions.didPassivelyReceiveNewModels, @_newMailReceived
|
||||
|
||||
|
@ -113,9 +114,7 @@ FileDownloadStore = Reflux.createStore
|
|||
#
|
||||
pathForFile: (file) ->
|
||||
return undefined unless file
|
||||
|
||||
filesafeName = file.displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-')
|
||||
path.join(@_downloadDirectory, file.id, filesafeName)
|
||||
path.join(@_downloadDirectory, file.id, file.safeDisplayName())
|
||||
|
||||
downloadDataForFile: (fileId) ->
|
||||
@_downloads[fileId]?.data()
|
||||
|
@ -202,6 +201,13 @@ FileDownloadStore = Reflux.createStore
|
|||
.catch =>
|
||||
@_presentError(file)
|
||||
|
||||
_saveDownload: (download, savePath) =>
|
||||
return new Promise (resolve, reject) =>
|
||||
stream = fs.createReadStream(download.targetPath)
|
||||
stream.pipe(fs.createWriteStream(savePath))
|
||||
stream.on 'error', (err) -> reject(err)
|
||||
stream.on 'end', -> resolve()
|
||||
|
||||
_fetchAndSave: (file) ->
|
||||
defaultPath = @_defaultSavePath(file)
|
||||
defaultExtension = path.extname(defaultPath)
|
||||
|
@ -215,12 +221,36 @@ FileDownloadStore = Reflux.createStore
|
|||
if didLoseExtension
|
||||
savePath = savePath + defaultExtension
|
||||
|
||||
defaultPath = NylasEnv.savedState.lastDownloadDirectory
|
||||
@_runDownload(file).then (download) ->
|
||||
stream = fs.createReadStream(download.targetPath)
|
||||
stream.pipe(fs.createWriteStream(savePath))
|
||||
stream.on 'end', ->
|
||||
shell.showItemInFolder(savePath)
|
||||
@_runDownload(file)
|
||||
.then (download) => @_saveDownload(download, savePath)
|
||||
.then => shell.showItemInFolder(savePath)
|
||||
.catch =>
|
||||
@_presentError(file)
|
||||
|
||||
_fetchAndSaveAll: (files) ->
|
||||
defaultPath = @_defaultSaveDir()
|
||||
options = {
|
||||
defaultPath,
|
||||
properties: ['openDirectory'],
|
||||
}
|
||||
|
||||
NylasEnv.showOpenDialog options, (selected) =>
|
||||
return unless selected
|
||||
dirPath = selected[0]
|
||||
return unless dirPath
|
||||
NylasEnv.savedState.lastDownloadDirectory = dirPath
|
||||
|
||||
lastSavePath = null
|
||||
savePromises = files.map (file) =>
|
||||
savePath = path.join(dirPath, file.safeDisplayName())
|
||||
@_runDownload(file)
|
||||
.then (download) => @_saveDownload(download, savePath)
|
||||
.then ->
|
||||
lastSavePath = savePath
|
||||
|
||||
Promise.all(savePromises)
|
||||
.then =>
|
||||
shell.showItemInFolder(lastSavePath) if lastSavePath
|
||||
.catch =>
|
||||
@_presentError(file)
|
||||
|
||||
|
@ -234,7 +264,7 @@ FileDownloadStore = Reflux.createStore
|
|||
fs.exists downloadPath, (exists) ->
|
||||
fs.unlink(downloadPath) if exists
|
||||
|
||||
_defaultSavePath: (file) ->
|
||||
_defaultSaveDir: ->
|
||||
if process.platform is 'win32'
|
||||
home = process.env.USERPROFILE
|
||||
else
|
||||
|
@ -248,8 +278,11 @@ FileDownloadStore = Reflux.createStore
|
|||
if fs.existsSync(NylasEnv.savedState.lastDownloadDirectory)
|
||||
downloadDir = NylasEnv.savedState.lastDownloadDirectory
|
||||
|
||||
filesafeName = file.displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-')
|
||||
path.join(downloadDir, filesafeName)
|
||||
return downloadDir
|
||||
|
||||
_defaultSavePath: (file) ->
|
||||
downloadDir = @_defaultSaveDir()
|
||||
path.join(downloadDir, file.safeDisplayName())
|
||||
|
||||
_presentError: (file) ->
|
||||
remote.dialog.showMessageBox
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
border: 1px solid fade(@text-color-very-subtle, 15%);
|
||||
border-radius: 3px;
|
||||
line-height: 10px;
|
||||
padding: 6px 10px;
|
||||
padding: 1px 5px;
|
||||
font-weight: 600;
|
||||
font-size: @font-size-smaller;
|
||||
margin: 5px 0 3px 0;
|
||||
|
@ -23,7 +23,6 @@
|
|||
font-size: @font-size-smaller * 0.8;
|
||||
top:-1px;
|
||||
position:relative;
|
||||
padding-right:8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
BIN
static/images/attachments/ic-attachments-all-clippy@1x.png
Normal file
After Width: | Height: | Size: 390 B |
BIN
static/images/attachments/ic-attachments-all-clippy@2x.png
Normal file
After Width: | Height: | Size: 731 B |
BIN
static/images/attachments/ic-attachments-download-all@1x.png
Normal file
After Width: | Height: | Size: 355 B |
BIN
static/images/attachments/ic-attachments-download-all@2x.png
Normal file
After Width: | Height: | Size: 570 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 463 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 708 B |
Before Width: | Height: | Size: 407 B After Width: | Height: | Size: 15 KiB |
BIN
static/images/attachments/remove-attachment@2x.png
Normal file
After Width: | Height: | Size: 15 KiB |