mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-10-16 16:17:14 +08:00
refactor(package): remove old, unused, or private package. Adds examples
Moved to git@github.com:nylas/N1-private-packages.git Also started an "examples" folder at the root level with an example Github package
This commit is contained in:
parent
3b342b2e20
commit
626a76622c
39 changed files with 0 additions and 1356 deletions
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
1
internal_packages/calendar-bar/.gitignore
vendored
1
internal_packages/calendar-bar/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
node_modules
|
|
|
@ -1 +0,0 @@
|
||||||
# React version of thread list
|
|
|
@ -1,72 +0,0 @@
|
||||||
Reflux = require 'reflux'
|
|
||||||
_ = require 'underscore'
|
|
||||||
{DatabaseStore,
|
|
||||||
AccountStore,
|
|
||||||
Actions,
|
|
||||||
Event,
|
|
||||||
Calendar,
|
|
||||||
NylasAPI} = require 'nylas-exports'
|
|
||||||
moment = require 'moment'
|
|
||||||
|
|
||||||
CalendarBarEventStore = Reflux.createStore
|
|
||||||
init: ->
|
|
||||||
@_setStoreDefaults()
|
|
||||||
@_registerListeners()
|
|
||||||
@_populate()
|
|
||||||
@trigger(@)
|
|
||||||
|
|
||||||
########### PUBLIC #####################################################
|
|
||||||
|
|
||||||
events: ->
|
|
||||||
@_events
|
|
||||||
|
|
||||||
range: ->
|
|
||||||
@_range
|
|
||||||
|
|
||||||
########### PRIVATE ####################################################
|
|
||||||
|
|
||||||
_setStoreDefaults: ->
|
|
||||||
@_events = []
|
|
||||||
|
|
||||||
_registerListeners: ->
|
|
||||||
@listenTo DatabaseStore, @_onDataChanged
|
|
||||||
@listenTo AccountStore, @_onAccountChanged
|
|
||||||
|
|
||||||
_populate: ->
|
|
||||||
@_range =
|
|
||||||
start: moment({hour: 0, milliseconds: -1}).valueOf() / 1000.0
|
|
||||||
end: moment({hour: 24, milliseconds: 1}).valueOf() / 1000.0
|
|
||||||
|
|
||||||
account = AccountStore.current()
|
|
||||||
return unless account
|
|
||||||
|
|
||||||
DatabaseStore.findAll(Event, accountId: account.id).where([
|
|
||||||
Event.attributes.end.greaterThan(@_range.start),
|
|
||||||
Event.attributes.start.lessThan(@_range.end)
|
|
||||||
]).order(Event.attributes.start.ascending()).then (events) =>
|
|
||||||
@_events = events
|
|
||||||
@trigger(@)
|
|
||||||
|
|
||||||
_refetchFromAPI: ->
|
|
||||||
account = AccountStore.current()
|
|
||||||
return unless account
|
|
||||||
|
|
||||||
# Trigger a request to the API
|
|
||||||
oneDayAgo = Math.round(moment({hour: 0, milliseconds: -1}).valueOf() / 1000.0)
|
|
||||||
DatabaseStore.findAll(Calendar, accountId: account.id).then (calendars) ->
|
|
||||||
calendars.forEach (calendar) ->
|
|
||||||
NylasAPI.getCollection(account.id, 'events', {calendar_id: calendar.id, ends_after: oneDayAgo})
|
|
||||||
|
|
||||||
# Inbound Events
|
|
||||||
|
|
||||||
_onAccountChanged: ->
|
|
||||||
@_refetchFromAPI()
|
|
||||||
@_populate()
|
|
||||||
|
|
||||||
_onDataChanged: (change) ->
|
|
||||||
if change.objectClass == Calendar.name
|
|
||||||
@_refetchFromAPI()
|
|
||||||
if change.objectClass == Event.name
|
|
||||||
@_populate()
|
|
||||||
|
|
||||||
module.exports = CalendarBarEventStore
|
|
|
@ -1,37 +0,0 @@
|
||||||
React = require 'react'
|
|
||||||
{Actions} = require("nylas-exports")
|
|
||||||
moment = require 'moment'
|
|
||||||
|
|
||||||
# TODO: This file is out of date!
|
|
||||||
return
|
|
||||||
|
|
||||||
class CalendarBarItem extends React.Component
|
|
||||||
render: =>
|
|
||||||
style =
|
|
||||||
left: @props.item.xPercent
|
|
||||||
top: @props.item.yPercent
|
|
||||||
width: @props.item.wPercent
|
|
||||||
height: @props.item.hPercent
|
|
||||||
zIndex: @props.item.z
|
|
||||||
<div className="event" style={style} id={@props.item.event.id}>
|
|
||||||
<span className="title">{@props.item.event.title}</span>
|
|
||||||
<span className="time">{@_time()}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
_time: =>
|
|
||||||
w = @props.item.event.when
|
|
||||||
if w.start_time
|
|
||||||
return moment.unix(w.start_time).format('h:mm a')
|
|
||||||
else if w.time
|
|
||||||
return moment.unix(w.time).format('h:mm a')
|
|
||||||
else if w.start_date
|
|
||||||
return moment.unix(w.start_date).format('MMMM Do')
|
|
||||||
else
|
|
||||||
return ""
|
|
||||||
|
|
||||||
_onClick: (event) =>
|
|
||||||
event.preventDefault()
|
|
||||||
Actions.focusMailView(@props.tag)
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = CalendarBarItem
|
|
|
@ -1,127 +0,0 @@
|
||||||
React = require 'react'
|
|
||||||
{Actions} = require("nylas-exports")
|
|
||||||
CalendarBarItem = require("./calendar-bar-item")
|
|
||||||
CalendarBarEventStore = require ("./calendar-bar-event-store")
|
|
||||||
|
|
||||||
class CalendarBarRow
|
|
||||||
constructor: (initialItem = null) ->
|
|
||||||
@items = []
|
|
||||||
@last = 0
|
|
||||||
if initialItem
|
|
||||||
@last = initialItem.event.end
|
|
||||||
@items.push(initialItem)
|
|
||||||
|
|
||||||
canHoldItem: (item) =>
|
|
||||||
item.event.start > @last
|
|
||||||
|
|
||||||
addItem: (item) =>
|
|
||||||
@last = item.event.end
|
|
||||||
@items.push(item)
|
|
||||||
|
|
||||||
class CalendarBarMarker extends React.Component
|
|
||||||
@displayName: "CalendarBarMarker"
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
classname = "marker"
|
|
||||||
classname += " now" if @props.marker.now
|
|
||||||
<div className={classname} style={left: @props.marker.xPercent} id={@props.marker.xPercent}/>
|
|
||||||
|
|
||||||
class CalendarBar extends React.Component
|
|
||||||
@displayName: "CalendarBar"
|
|
||||||
|
|
||||||
constructor: (@props) ->
|
|
||||||
@state = @_getStateFromStores()
|
|
||||||
|
|
||||||
componentDidMount: =>
|
|
||||||
@unsubscribe = CalendarBarEventStore.listen @_onStoreChange
|
|
||||||
|
|
||||||
# It's important that every React class explicitly stops listening to
|
|
||||||
# atom events before it unmounts. Thank you event-kit
|
|
||||||
# This can be fixed via a Reflux mixin
|
|
||||||
componentWillUnmount: =>
|
|
||||||
@unsubscribe() if @unsubscribe
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
markers = @_getMarkers().map (marker) ->
|
|
||||||
<CalendarBarMarker marker={marker}/>
|
|
||||||
|
|
||||||
items = @_getItemsForEvents(@state.events)
|
|
||||||
items = items.map (item) ->
|
|
||||||
<CalendarBarItem item={item}/>
|
|
||||||
|
|
||||||
<div className="calendar-bar-inner">
|
|
||||||
{markers}
|
|
||||||
{items}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
_onStoreChange: =>
|
|
||||||
@setState @_getStateFromStores()
|
|
||||||
|
|
||||||
_getStateFromStores: =>
|
|
||||||
events: CalendarBarEventStore.events()
|
|
||||||
range: CalendarBarEventStore.range()
|
|
||||||
|
|
||||||
_getMarkers: =>
|
|
||||||
range = @state.range
|
|
||||||
now = (new Date).getTime()/1000 - range.start
|
|
||||||
markers = []
|
|
||||||
for hour in [0..24]
|
|
||||||
time = 60*60*hour
|
|
||||||
markers.push
|
|
||||||
xPercent: (time * 100) / (range.end - range.start) + "%"
|
|
||||||
markers.push
|
|
||||||
now: true
|
|
||||||
xPercent: (now * 100) / (range.end - range.start) + "%"
|
|
||||||
markers
|
|
||||||
|
|
||||||
_getItemsForEvents: (events) =>
|
|
||||||
# Create an array of items with additional metadata needed for our view.
|
|
||||||
# We compute the X and width of elements using their durations as a fraction
|
|
||||||
# of the displayed range
|
|
||||||
range = @state.range
|
|
||||||
items = events.map (event) ->
|
|
||||||
{
|
|
||||||
event: event,
|
|
||||||
z: event.start - range.start
|
|
||||||
xPercent: (event.start - range.start) * 100 / (range.end - range.start) + "%",
|
|
||||||
wPercent: (event.end - event.start) * 100 / (range.end - range.start) + "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Compute the number of rows we need by assigning events to rows. This works by
|
|
||||||
# creating virtual "row" objects which hold a series of non-overlapping events and
|
|
||||||
# have a "last" timestamp. For each item, we iterate through the rows:
|
|
||||||
#
|
|
||||||
# - If the event fits in more than one row, we delete all but one of the rows.
|
|
||||||
# This ensures that if we have two overlapping events, the next event that
|
|
||||||
# does not overlap goes back to taking all of the available height. (Rows no
|
|
||||||
# longer necessary)
|
|
||||||
#
|
|
||||||
# - If the event does not fit in any rows, we create a new row, and tell all of
|
|
||||||
# the items in existing rows that they're now sharing space with a new row.
|
|
||||||
|
|
||||||
rows = [new CalendarBarRow]
|
|
||||||
for item in items
|
|
||||||
for x in [rows.length-1..0] by -1
|
|
||||||
if rows[x].canHoldItem(item)
|
|
||||||
rows.splice(item.rowIndex, 1) unless item.rowIndex is undefined
|
|
||||||
rows[x].addItem(item)
|
|
||||||
item.rowIndex = x
|
|
||||||
|
|
||||||
if item.rowIndex is undefined
|
|
||||||
rows.push(new CalendarBarRow(item))
|
|
||||||
item.rowIndex = rows.length - 1
|
|
||||||
for row in rows
|
|
||||||
for item in row.items
|
|
||||||
item.rowCount += 1
|
|
||||||
|
|
||||||
item.rowCount = rows.length
|
|
||||||
|
|
||||||
# Now that each item knows what row it's in and how many rows are being displayed
|
|
||||||
# alongside it, we can assign fractional positions to them.
|
|
||||||
for item in items
|
|
||||||
item.yPercent = (item.rowIndex / item.rowCount) * 100 + "%"
|
|
||||||
item.hPercent = (100.0 / item.rowCount) + "%"
|
|
||||||
|
|
||||||
items
|
|
||||||
|
|
||||||
module.exports = CalendarBar
|
|
|
@ -1,9 +0,0 @@
|
||||||
React = require "react"
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
activate: (@state) ->
|
|
||||||
# Package turned off for now
|
|
||||||
|
|
||||||
deactivate: ->
|
|
||||||
|
|
||||||
serialize: -> @state
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"name": "calendar-bar",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"main": "./lib/main",
|
|
||||||
"description": "Footer bar that shows your daily availability",
|
|
||||||
"license": "Proprietary",
|
|
||||||
"private": true,
|
|
||||||
"engines": {
|
|
||||||
"atom": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
@import "ui-variables";
|
|
||||||
@import "ui-mixins";
|
|
||||||
|
|
||||||
|
|
||||||
#calendar-bar {
|
|
||||||
height: 45px;
|
|
||||||
overflow: hidden;
|
|
||||||
order: 50;
|
|
||||||
box-shadow: @standard-shadow-up;
|
|
||||||
z-index:2; // allows shadow over other elements
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.marker {
|
|
||||||
position: absolute;
|
|
||||||
border-left:1px solid @border-color-subtle;
|
|
||||||
width:1px;
|
|
||||||
height:100%;
|
|
||||||
top:0;
|
|
||||||
z-index:0;
|
|
||||||
&.now {
|
|
||||||
width:4px;
|
|
||||||
z-index:1000;
|
|
||||||
border-right:1px solid white;
|
|
||||||
border-left:1px solid white;
|
|
||||||
background-color:@background-color-success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.event {
|
|
||||||
position: absolute;
|
|
||||||
border:2px solid white;
|
|
||||||
background-color: @background-color-accent;
|
|
||||||
padding:3px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
float:left;
|
|
||||||
font-weight: normal;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow:hidden;
|
|
||||||
color: @text-color-inverse;
|
|
||||||
width:100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.title {
|
|
||||||
font-size:12px;
|
|
||||||
}
|
|
||||||
span.time {
|
|
||||||
font-size:10px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
1
internal_packages/file-list/.gitignore
vendored
1
internal_packages/file-list/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
node_modules
|
|
|
@ -1,55 +0,0 @@
|
||||||
Reflux = require 'reflux'
|
|
||||||
_ = require 'underscore'
|
|
||||||
fs = require 'fs'
|
|
||||||
|
|
||||||
{WorkspaceStore,
|
|
||||||
FocusedContentStore,
|
|
||||||
FileDownloadStore,
|
|
||||||
Actions} = require 'nylas-exports'
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
FileFrameStore = Reflux.createStore
|
|
||||||
init: ->
|
|
||||||
@_resetInstanceVars()
|
|
||||||
@_afterViewUpdate = []
|
|
||||||
|
|
||||||
@listenTo FocusedContentStore, @_onFocusedContentChange
|
|
||||||
@listenTo FileDownloadStore, @_onFileDownloadChange
|
|
||||||
|
|
||||||
file: ->
|
|
||||||
@_file
|
|
||||||
|
|
||||||
ready: ->
|
|
||||||
@_ready
|
|
||||||
|
|
||||||
download: ->
|
|
||||||
@_download
|
|
||||||
|
|
||||||
_resetInstanceVars: ->
|
|
||||||
@_file = null
|
|
||||||
@_download = null
|
|
||||||
@_ready = false
|
|
||||||
|
|
||||||
_update: ->
|
|
||||||
|
|
||||||
_onFileDownloadChange: ->
|
|
||||||
@_download = FileDownloadStore.downloadDataForFile(@_file.id) if @_file
|
|
||||||
if @_file and @_ready is false and not @_download
|
|
||||||
@_ready = true
|
|
||||||
@trigger()
|
|
||||||
|
|
||||||
_onFocusedContentChange: (change) ->
|
|
||||||
return unless change.impactsCollection('file')
|
|
||||||
|
|
||||||
@_file = FocusedContentStore.focused('file')
|
|
||||||
if @_file
|
|
||||||
filepath = FileDownloadStore.pathForFile(@_file)
|
|
||||||
fs.exists filepath, (exists) =>
|
|
||||||
Actions.fetchFile(@_file) if not exists
|
|
||||||
@_download = FileDownloadStore.downloadDataForFile(@_file.id)
|
|
||||||
@_ready = not @_download
|
|
||||||
@trigger()
|
|
||||||
else
|
|
||||||
@_ready = false
|
|
||||||
@_download = null
|
|
||||||
@trigger()
|
|
|
@ -1,39 +0,0 @@
|
||||||
React = require 'react'
|
|
||||||
_ = require "underscore"
|
|
||||||
{Utils, FileDownloadStore, Actions} = require 'nylas-exports'
|
|
||||||
{Spinner, EventedIFrame} = require 'nylas-component-kit'
|
|
||||||
FileFrameStore = require './file-frame-store'
|
|
||||||
|
|
||||||
class FileFrame extends React.Component
|
|
||||||
@displayName: 'FileFrame'
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
src = if @state.ready then @state.filepath else ''
|
|
||||||
if @state.file
|
|
||||||
<div className="file-frame-container">
|
|
||||||
<EventedIFrame src={src} />
|
|
||||||
<Spinner visible={!@state.ready} />
|
|
||||||
</div>
|
|
||||||
else
|
|
||||||
<div></div>
|
|
||||||
|
|
||||||
constructor: (@props) ->
|
|
||||||
@state = @getStateFromStores()
|
|
||||||
|
|
||||||
componentDidMount: =>
|
|
||||||
@_unsubscribers = []
|
|
||||||
@_unsubscribers.push FileFrameStore.listen @_onChange
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
|
||||||
unsubscribe() for unsubscribe in @_unsubscribers
|
|
||||||
|
|
||||||
getStateFromStores: =>
|
|
||||||
file: FileFrameStore.file()
|
|
||||||
filepath: FileDownloadStore.pathForFile(FileFrameStore.file())
|
|
||||||
ready: FileFrameStore.ready()
|
|
||||||
|
|
||||||
_onChange: =>
|
|
||||||
@setState(@getStateFromStores())
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = FileFrame
|
|
|
@ -1,20 +0,0 @@
|
||||||
Reflux = require 'reflux'
|
|
||||||
_ = require 'underscore'
|
|
||||||
{File,
|
|
||||||
DatabaseStore,
|
|
||||||
DatabaseView} = require 'nylas-exports'
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
FileListStore = Reflux.createStore
|
|
||||||
init: ->
|
|
||||||
@listenTo DatabaseStore, @_onDataChanged
|
|
||||||
|
|
||||||
@_view = new DatabaseView(File, matchers: [File.attributes.filename.not('')])
|
|
||||||
@listenTo @_view, => @trigger({})
|
|
||||||
|
|
||||||
view: ->
|
|
||||||
@_view
|
|
||||||
|
|
||||||
_onDataChanged: (change) ->
|
|
||||||
return unless change.objectClass is File.name
|
|
||||||
@_view.invalidate({shallow: true, changed: change.objects})
|
|
|
@ -1,51 +0,0 @@
|
||||||
_ = require 'underscore'
|
|
||||||
React = require 'react'
|
|
||||||
{ListTabular, MultiselectList} = require 'nylas-component-kit'
|
|
||||||
{Actions,
|
|
||||||
DatabaseStore,
|
|
||||||
ComponentRegistry} = require 'nylas-exports'
|
|
||||||
FileListStore = require './file-list-store'
|
|
||||||
|
|
||||||
class FileList extends React.Component
|
|
||||||
@displayName: 'FileList'
|
|
||||||
|
|
||||||
@containerRequired: false
|
|
||||||
|
|
||||||
componentWillMount: =>
|
|
||||||
prettySize = (size) ->
|
|
||||||
units = ['GB', 'MB', 'KB', 'bytes']
|
|
||||||
while size > 1024
|
|
||||||
size /= 1024
|
|
||||||
units.pop()
|
|
||||||
size = "#{(Math.ceil(size * 10) / 10)}"
|
|
||||||
pretty = units.pop()
|
|
||||||
"#{size} #{pretty}"
|
|
||||||
|
|
||||||
c1 = new ListTabular.Column
|
|
||||||
name: "Name"
|
|
||||||
flex: 1
|
|
||||||
resolver: (file) =>
|
|
||||||
<div>{file.displayName()}</div>
|
|
||||||
|
|
||||||
c2 = new ListTabular.Column
|
|
||||||
name: "Size"
|
|
||||||
width: '100px'
|
|
||||||
resolver: (file) =>
|
|
||||||
<div>{prettySize(file.size)}</div>
|
|
||||||
|
|
||||||
@columns = [c1, c2]
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
<MultiselectList
|
|
||||||
dataStore={FileListStore}
|
|
||||||
columns={@columns}
|
|
||||||
commands={{}}
|
|
||||||
onDoubleClick={@_onDoubleClick}
|
|
||||||
itemPropsProvider={ -> {} }
|
|
||||||
className="file-list"
|
|
||||||
collection="file" />
|
|
||||||
|
|
||||||
_onDoubleClick: (item) =>
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = FileList
|
|
|
@ -1,14 +0,0 @@
|
||||||
React = require "react/addons"
|
|
||||||
FileListStore = require './file-list-store'
|
|
||||||
{MultiselectActionBar} = require 'nylas-component-kit'
|
|
||||||
|
|
||||||
class FileSelectionBar extends React.Component
|
|
||||||
@displayName: 'FileSelectionBar'
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
<MultiselectActionBar
|
|
||||||
dataStore={FileListStore}
|
|
||||||
collection="file" />
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = FileSelectionBar
|
|
|
@ -1,30 +0,0 @@
|
||||||
# FileFrame = require "./file-frame"
|
|
||||||
# FileList = require './file-list'
|
|
||||||
# FileSelectionBar = require './file-selection-bar'
|
|
||||||
# {ComponentRegistry,
|
|
||||||
# WorkspaceStore} = require 'nylas-exports'
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
|
|
||||||
activate: (@state={}) ->
|
|
||||||
# WorkspaceStore.defineSheet 'Files', {root: true, supportedModes: ['list'], name: 'Files'},
|
|
||||||
# list: ['RootSidebar', 'FileList']
|
|
||||||
#
|
|
||||||
# WorkspaceStore.defineSheet 'File', {supportedModes: ['list']},
|
|
||||||
# list: ['File']
|
|
||||||
#
|
|
||||||
# ComponentRegistry.register FileList,
|
|
||||||
# location: WorkspaceStore.Location.FileList
|
|
||||||
#
|
|
||||||
# ComponentRegistry.register FileSelectionBar,
|
|
||||||
# location: WorkspaceStore.Location.FileList.Toolbar
|
|
||||||
#
|
|
||||||
# ComponentRegistry.register FileFrame,
|
|
||||||
# location: WorkspaceStore.Location.File
|
|
||||||
|
|
||||||
deactivate: ->
|
|
||||||
# ComponentRegistry.unregister(FileList)
|
|
||||||
# ComponentRegistry.unregister(FileSelectionBar)
|
|
||||||
# ComponentRegistry.unregister(FileFrame)
|
|
||||||
# WorkspaceStore.undefineSheet('Files')
|
|
||||||
# WorkspaceStore.undefineSheet('File')
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"name": "file-list",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"main": "./lib/main",
|
|
||||||
"description": "View files",
|
|
||||||
"license": "Proprietary",
|
|
||||||
"private": true,
|
|
||||||
"engines": {
|
|
||||||
"atom": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
@import "ui-variables";
|
|
||||||
@import "ui-mixins";
|
|
||||||
|
|
||||||
@message-max-width: 800px;
|
|
||||||
|
|
||||||
.file-frame-container {
|
|
||||||
width:100%;
|
|
||||||
height:100%;
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
width:100%;
|
|
||||||
height:100%;
|
|
||||||
border:0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
{DraftStoreExtension} = require 'nylas-exports'
|
|
||||||
|
|
||||||
class TemplatesDraftStoreExtension extends DraftStoreExtension
|
|
||||||
|
|
||||||
@warningsForSending: (draft) ->
|
|
||||||
warnings = []
|
|
||||||
if draft.body.search(/<code[^>]*empty[^>]*>/i) > 0
|
|
||||||
warnings.push("with an empty template area")
|
|
||||||
warnings
|
|
||||||
|
|
||||||
@finalizeSessionBeforeSending: (session) ->
|
|
||||||
body = session.draft().body
|
|
||||||
clean = body.replace(/<\/?code[^>]*>/g, '')
|
|
||||||
if body != clean
|
|
||||||
session.changes.add(body: clean)
|
|
||||||
|
|
||||||
@onMouseUp: (editableNode, range, event) ->
|
|
||||||
parent = range.startContainer?.parentNode
|
|
||||||
parentCodeNode = null
|
|
||||||
|
|
||||||
while parent and parent isnt editableNode
|
|
||||||
if parent.classList?.contains('var') and parent.tagName is 'CODE'
|
|
||||||
parentCodeNode = parent
|
|
||||||
break
|
|
||||||
parent = parent.parentNode
|
|
||||||
|
|
||||||
isSinglePoint = range.startContainer is range.endContainer and range.startOffset is range.endOffset
|
|
||||||
|
|
||||||
if isSinglePoint and parentCodeNode
|
|
||||||
range.selectNode(parentCodeNode)
|
|
||||||
selection = document.getSelection()
|
|
||||||
selection.removeAllRanges()
|
|
||||||
selection.addRange(range)
|
|
||||||
|
|
||||||
@onFocusPrevious: (editableNode, range, event) ->
|
|
||||||
@onFocusShift(editableNode, range, event, -1)
|
|
||||||
|
|
||||||
@onFocusNext: (editableNode, range, event) ->
|
|
||||||
@onFocusShift(editableNode, range, event, 1)
|
|
||||||
|
|
||||||
@onFocusShift: (editableNode, range, event, delta) ->
|
|
||||||
return unless range
|
|
||||||
|
|
||||||
# Try to find the node that the selection range is
|
|
||||||
# currently intersecting with (inside, or around)
|
|
||||||
parentCodeNode = null
|
|
||||||
nodes = editableNode.querySelectorAll('code.var')
|
|
||||||
for node in nodes
|
|
||||||
if range.intersectsNode(node)
|
|
||||||
parentCodeNode = node
|
|
||||||
|
|
||||||
|
|
||||||
if parentCodeNode
|
|
||||||
if range.startOffset is range.endOffset and parentCodeNode.classList.contains('empty')
|
|
||||||
# If the current node is empty and it's a single insertion point,
|
|
||||||
# select the current node rather than advancing to the next node
|
|
||||||
selectNode = parentCodeNode
|
|
||||||
else
|
|
||||||
# advance to the next code node
|
|
||||||
matches = editableNode.querySelectorAll('code.var')
|
|
||||||
matchIndex = -1
|
|
||||||
for match, idx in matches
|
|
||||||
if match is parentCodeNode
|
|
||||||
matchIndex = idx
|
|
||||||
break
|
|
||||||
if matchIndex != -1 and matchIndex + delta >= 0 and matchIndex + delta < matches.length
|
|
||||||
selectNode = matches[matchIndex+delta]
|
|
||||||
|
|
||||||
else
|
|
||||||
# We're not currently intersecting a code node. Find the one we want
|
|
||||||
# to move to by scanning for the next one in the DOM. Traversing the
|
|
||||||
# structure of the email would be hard, so instead we look for the next
|
|
||||||
# one that is *visually* to the left or beneath the current one, vice
|
|
||||||
# versa for going back (delta -1 case)
|
|
||||||
rangeRect = range.getClientRects()[0]
|
|
||||||
if rangeRect
|
|
||||||
if delta is 1 # next
|
|
||||||
for node in nodes
|
|
||||||
nodeRect = node.getBoundingClientRect()
|
|
||||||
continue if nodeRect.top < rangeRect.top
|
|
||||||
if nodeRect.top > rangeRect.top or nodeRect.left > rangeRect.left
|
|
||||||
selectNode = node
|
|
||||||
break
|
|
||||||
else if delta is -1 # previous
|
|
||||||
for node in nodes by -1
|
|
||||||
nodeRect = node.getBoundingClientRect()
|
|
||||||
continue if nodeRect.top > rangeRect.top
|
|
||||||
if nodeRect.top < rangeRect.top or nodeRect.left < rangeRect.left
|
|
||||||
selectNode = node
|
|
||||||
break
|
|
||||||
|
|
||||||
if selectNode
|
|
||||||
range.selectNode(selectNode)
|
|
||||||
selection = document.getSelection()
|
|
||||||
selection.removeAllRanges()
|
|
||||||
selection.addRange(range)
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
@onInput: (editableNode, event) ->
|
|
||||||
selection = document.getSelection()
|
|
||||||
|
|
||||||
isWithinNode = (node) ->
|
|
||||||
test = selection.baseNode
|
|
||||||
while test isnt editableNode
|
|
||||||
return true if test is node
|
|
||||||
test = test.parentNode
|
|
||||||
return false
|
|
||||||
|
|
||||||
codeTags = editableNode.querySelectorAll('code.var.empty')
|
|
||||||
for codeTag in codeTags
|
|
||||||
if selection.containsNode(codeTag) or isWithinNode(codeTag)
|
|
||||||
codeTag.classList.remove('empty')
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = TemplatesDraftStoreExtension
|
|
|
@ -1,25 +0,0 @@
|
||||||
React = require "react"
|
|
||||||
{ComponentRegistry, DraftStore} = require 'nylas-exports'
|
|
||||||
TemplatePicker = require './template-picker'
|
|
||||||
TemplateStatusBar = require './template-status-bar'
|
|
||||||
Extension = require './draft-extension'
|
|
||||||
_ = require 'underscore'
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
item: null # The DOM item the main React component renders into
|
|
||||||
|
|
||||||
activate: (@state={}) ->
|
|
||||||
ComponentRegistry.register TemplatePicker,
|
|
||||||
role: 'Composer:ActionButton'
|
|
||||||
|
|
||||||
ComponentRegistry.register TemplateStatusBar,
|
|
||||||
role: 'Composer:Footer'
|
|
||||||
|
|
||||||
DraftStore.registerExtension(Extension)
|
|
||||||
|
|
||||||
deactivate: ->
|
|
||||||
ComponentRegistry.unregister(TemplatePicker)
|
|
||||||
ComponentRegistry.unregister(TemplateStatusBar)
|
|
||||||
DraftStore.unregisterExtension(Extension)
|
|
||||||
|
|
||||||
serialize: -> @state
|
|
|
@ -1,87 +0,0 @@
|
||||||
_ = require 'underscore'
|
|
||||||
React = require 'react'
|
|
||||||
TemplateStore = require './template-store'
|
|
||||||
{Actions, Message, DatabaseStore} = require 'nylas-exports'
|
|
||||||
{Popover, Menu, RetinaImg} = require 'nylas-component-kit'
|
|
||||||
|
|
||||||
class TemplatePicker extends React.Component
|
|
||||||
@displayName: 'TemplatePicker'
|
|
||||||
|
|
||||||
@containerStyles:
|
|
||||||
order:2
|
|
||||||
|
|
||||||
constructor: (@props) ->
|
|
||||||
@state =
|
|
||||||
searchValue: ""
|
|
||||||
templates: TemplateStore.items()
|
|
||||||
|
|
||||||
componentDidMount: =>
|
|
||||||
@unsubscribe = TemplateStore.listen @_onStoreChange
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
|
||||||
@unsubscribe() if @unsubscribe
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
button = <button className="btn btn-toolbar narrow">
|
|
||||||
<RetinaImg name="icon-composer-templates.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
|
||||||
|
|
||||||
<RetinaImg name="icon-composer-dropdown.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
headerComponents = [
|
|
||||||
<input type="text"
|
|
||||||
tabIndex="1"
|
|
||||||
key="textfield"
|
|
||||||
className="search"
|
|
||||||
value={@state.searchValue}
|
|
||||||
onChange={@_onSearchValueChange}/>
|
|
||||||
]
|
|
||||||
|
|
||||||
footerComponents = [
|
|
||||||
<div className="item" key="new" onMouseDown={@_onNewTemplate}>Save Draft as Template...</div>
|
|
||||||
<div className="item" key="manage" onMouseDown={@_onManageTemplates}>Open Templates Folder...</div>
|
|
||||||
]
|
|
||||||
|
|
||||||
<Popover ref="popover" className="template-picker pull-right" buttonComponent={button}>
|
|
||||||
<Menu ref="menu"
|
|
||||||
headerComponents={headerComponents}
|
|
||||||
footerComponents={footerComponents}
|
|
||||||
items={@state.templates}
|
|
||||||
itemKey={ (item) -> item.id }
|
|
||||||
itemContent={ (item) -> item.name }
|
|
||||||
onSelect={@_onChooseTemplate}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
|
|
||||||
_filteredTemplates: (search) =>
|
|
||||||
search ?= @state.searchValue
|
|
||||||
items = TemplateStore.items()
|
|
||||||
|
|
||||||
return items unless search.length
|
|
||||||
|
|
||||||
_.filter items, (t) ->
|
|
||||||
t.name.toLowerCase().indexOf(search.toLowerCase()) == 0
|
|
||||||
|
|
||||||
_onStoreChange: =>
|
|
||||||
@setState
|
|
||||||
templates: @_filteredTemplates()
|
|
||||||
|
|
||||||
_onSearchValueChange: =>
|
|
||||||
newSearch = event.target.value
|
|
||||||
@setState
|
|
||||||
searchValue: newSearch
|
|
||||||
templates: @_filteredTemplates(newSearch)
|
|
||||||
|
|
||||||
_onChooseTemplate: (template) =>
|
|
||||||
Actions.insertTemplateId({templateId:template.id, draftClientId: @props.draftClientId})
|
|
||||||
@refs.popover.close()
|
|
||||||
|
|
||||||
_onManageTemplates: =>
|
|
||||||
Actions.showTemplates()
|
|
||||||
|
|
||||||
_onNewTemplate: =>
|
|
||||||
Actions.createTemplate({draftClientId: @props.draftClientId})
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = TemplatePicker
|
|
|
@ -1,47 +0,0 @@
|
||||||
_ = require 'underscore'
|
|
||||||
React = require 'react'
|
|
||||||
{Actions, Message, DraftStore} = require 'nylas-exports'
|
|
||||||
|
|
||||||
class TemplateStatusBar extends React.Component
|
|
||||||
@displayName: 'TemplateStatusBar'
|
|
||||||
|
|
||||||
@containerStyles:
|
|
||||||
textAlign:'center'
|
|
||||||
width:530
|
|
||||||
margin:'auto'
|
|
||||||
|
|
||||||
@propTypes:
|
|
||||||
draftClientId: React.PropTypes.string
|
|
||||||
|
|
||||||
constructor: (@props) ->
|
|
||||||
@state = draft: null
|
|
||||||
|
|
||||||
componentDidMount: =>
|
|
||||||
DraftStore.sessionForClientId(@props.draftClientId).then (_proxy) =>
|
|
||||||
return if @_unmounted
|
|
||||||
return unless _proxy.draftClientId is @props.draftClientId
|
|
||||||
@_proxy = _proxy
|
|
||||||
@unsubscribe = @_proxy.listen(@_onDraftChange, @)
|
|
||||||
@_onDraftChange()
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
|
||||||
@_unmounted = true
|
|
||||||
@unsubscribe() if @unsubscribe
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
if @_draftUsesTemplate()
|
|
||||||
<div className="template-status-bar">
|
|
||||||
Press "tab" to quickly fill in the blanks - highlighting will not be visible to recipients.
|
|
||||||
</div>
|
|
||||||
else
|
|
||||||
<div></div>
|
|
||||||
|
|
||||||
_onDraftChange: =>
|
|
||||||
@setState(draft: @_proxy.draft())
|
|
||||||
|
|
||||||
_draftUsesTemplate: =>
|
|
||||||
return unless @state.draft
|
|
||||||
@state.draft.body.search(/<code[^>]*class="var[^>]*>/i) > 0
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = TemplateStatusBar
|
|
|
@ -1,104 +0,0 @@
|
||||||
Reflux = require 'reflux'
|
|
||||||
_ = require 'underscore'
|
|
||||||
{DatabaseStore, DraftStore, Actions, Message} = require 'nylas-exports'
|
|
||||||
shell = require 'shell'
|
|
||||||
path = require 'path'
|
|
||||||
fs = require 'fs-plus'
|
|
||||||
|
|
||||||
TemplateStore = Reflux.createStore
|
|
||||||
init: ->
|
|
||||||
@_setStoreDefaults()
|
|
||||||
@_registerListeners()
|
|
||||||
|
|
||||||
@_templatesDir = path.join(atom.getConfigDirPath(), 'templates')
|
|
||||||
|
|
||||||
# I know this is a bit of pain but don't do anything that
|
|
||||||
# could possibly slow down app launch
|
|
||||||
fs.exists @_templatesDir, (exists) =>
|
|
||||||
if exists
|
|
||||||
@_populate()
|
|
||||||
fs.watch @_templatesDir, => @_populate()
|
|
||||||
else
|
|
||||||
fs.mkdir @_templatesDir, =>
|
|
||||||
fs.watch @_templatesDir, => @_populate()
|
|
||||||
|
|
||||||
|
|
||||||
########### PUBLIC #####################################################
|
|
||||||
|
|
||||||
items: ->
|
|
||||||
@_items
|
|
||||||
|
|
||||||
templatesDirectory: ->
|
|
||||||
@_templatesDir
|
|
||||||
|
|
||||||
|
|
||||||
########### PRIVATE ####################################################
|
|
||||||
|
|
||||||
_setStoreDefaults: ->
|
|
||||||
@_items = []
|
|
||||||
|
|
||||||
_registerListeners: ->
|
|
||||||
@listenTo Actions.insertTemplateId, @_onInsertTemplateId
|
|
||||||
@listenTo Actions.createTemplate, @_onCreateTemplate
|
|
||||||
@listenTo Actions.showTemplates, @_onShowTemplates
|
|
||||||
|
|
||||||
_populate: ->
|
|
||||||
fs.readdir @_templatesDir, (err, filenames) =>
|
|
||||||
@_items = []
|
|
||||||
for filename in filenames
|
|
||||||
continue if filename[0] is '.'
|
|
||||||
displayname = path.basename(filename, path.extname(filename))
|
|
||||||
@_items.push
|
|
||||||
id: filename,
|
|
||||||
name: displayname,
|
|
||||||
path: path.join(@_templatesDir, filename)
|
|
||||||
@trigger(@)
|
|
||||||
|
|
||||||
_onCreateTemplate: ({draftClientId, name, contents} = {}) ->
|
|
||||||
if draftClientId
|
|
||||||
DraftStore.sessionForClientId(draftClientId).then (session) =>
|
|
||||||
draft = session.draft()
|
|
||||||
name ?= draft.subject
|
|
||||||
contents ?= draft.body
|
|
||||||
if not name or name.length is 0
|
|
||||||
return @_displayError("Give your draft a subject to name your template.")
|
|
||||||
if not contents or contents.length is 0
|
|
||||||
return @_displayError("To create a template you need to fill the body of the current draft.")
|
|
||||||
@_writeTemplate(name, contents)
|
|
||||||
|
|
||||||
else
|
|
||||||
if not name or name.length is 0
|
|
||||||
return @_displayError("You must provide a name for your template.")
|
|
||||||
if not contents or contents.length is 0
|
|
||||||
return @_displayError("You must provide contents for your template.")
|
|
||||||
@_writeTemplate(name, contents)
|
|
||||||
|
|
||||||
_onShowTemplates: ->
|
|
||||||
shell.showItemInFolder(@_items[0]?.path || @_templatesDir)
|
|
||||||
|
|
||||||
_displayError: (message) ->
|
|
||||||
dialog = require('remote').require('dialog')
|
|
||||||
dialog.showErrorBox('Template Creation Error', message)
|
|
||||||
|
|
||||||
_writeTemplate: (name, contents) ->
|
|
||||||
filename = "#{name}.html"
|
|
||||||
templatePath = path.join(@_templatesDir, filename)
|
|
||||||
fs.writeFile templatePath, contents, (err) =>
|
|
||||||
@_displayError(err) if err
|
|
||||||
shell.showItemInFolder(templatePath)
|
|
||||||
@_items.push
|
|
||||||
id: filename,
|
|
||||||
name: name,
|
|
||||||
path: templatePath
|
|
||||||
@trigger(@)
|
|
||||||
|
|
||||||
_onInsertTemplateId: ({templateId, draftClientId} = {}) ->
|
|
||||||
template = _.find @_items, (item) -> item.id is templateId
|
|
||||||
return unless template
|
|
||||||
|
|
||||||
fs.readFile template.path, (err, data) ->
|
|
||||||
body = data.toString()
|
|
||||||
DraftStore.sessionForClientId(draftClientId).then (session) ->
|
|
||||||
session.changes.add(body: body)
|
|
||||||
|
|
||||||
module.exports = TemplateStore
|
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"name": "message-templates",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"main": "./lib/main",
|
|
||||||
"description": "Template features galore!",
|
|
||||||
"license": "Proprietary",
|
|
||||||
"private": true,
|
|
||||||
"engines": {
|
|
||||||
"atom": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
},
|
|
||||||
"windowTypes": {
|
|
||||||
"default": true,
|
|
||||||
"composer": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
{Message, Actions, DatabaseStore, DraftStore} = require 'nylas-exports'
|
|
||||||
TemplateStore = require '../lib/template-store'
|
|
||||||
fs = require 'fs-plus'
|
|
||||||
shell = require 'shell'
|
|
||||||
|
|
||||||
stubTemplatesDir = TemplateStore.templatesDirectory()
|
|
||||||
|
|
||||||
stubTemplateFiles = {
|
|
||||||
'template1.html': '<p>bla1</p>',
|
|
||||||
'template2.html': '<p>bla2</p>'
|
|
||||||
}
|
|
||||||
|
|
||||||
stubTemplates = [
|
|
||||||
{id: 'template1.html', name: 'template1', path: "#{stubTemplatesDir}/template1.html"},
|
|
||||||
{id: 'template2.html', name: 'template2', path: "#{stubTemplatesDir}/template2.html"},
|
|
||||||
]
|
|
||||||
|
|
||||||
describe "TemplateStore", ->
|
|
||||||
beforeEach ->
|
|
||||||
spyOn(fs, 'mkdir')
|
|
||||||
spyOn(shell, 'showItemInFolder').andCallFake ->
|
|
||||||
spyOn(fs, 'writeFile').andCallFake (path, contents, callback) ->
|
|
||||||
callback(null)
|
|
||||||
spyOn(fs, 'readFile').andCallFake (path, callback) ->
|
|
||||||
filename = path.split('/').pop()
|
|
||||||
callback(null, stubTemplateFiles[filename])
|
|
||||||
|
|
||||||
it "should create the templates folder if it does not exist", ->
|
|
||||||
spyOn(fs, 'exists').andCallFake (path, callback) -> callback(false)
|
|
||||||
TemplateStore.init()
|
|
||||||
expect(fs.mkdir).toHaveBeenCalled()
|
|
||||||
|
|
||||||
it "should expose templates in the templates directory", ->
|
|
||||||
spyOn(fs, 'exists').andCallFake (path, callback) -> callback(true)
|
|
||||||
spyOn(fs, 'readdir').andCallFake (path, callback) -> callback(null, Object.keys(stubTemplateFiles))
|
|
||||||
TemplateStore.init()
|
|
||||||
expect(TemplateStore.items()).toEqual(stubTemplates)
|
|
||||||
|
|
||||||
it "should watch the templates directory and reflect changes", ->
|
|
||||||
watchCallback = null
|
|
||||||
watchFired = false
|
|
||||||
|
|
||||||
spyOn(fs, 'exists').andCallFake (path, callback) -> callback(true)
|
|
||||||
spyOn(fs, 'watch').andCallFake (path, callback) -> watchCallback = callback
|
|
||||||
spyOn(fs, 'readdir').andCallFake (path, callback) ->
|
|
||||||
if watchFired
|
|
||||||
callback(null, Object.keys(stubTemplateFiles))
|
|
||||||
else
|
|
||||||
callback(null, [])
|
|
||||||
|
|
||||||
TemplateStore.init()
|
|
||||||
expect(TemplateStore.items()).toEqual([])
|
|
||||||
|
|
||||||
watchFired = true
|
|
||||||
watchCallback()
|
|
||||||
expect(TemplateStore.items()).toEqual(stubTemplates)
|
|
||||||
|
|
||||||
describe "insertTemplateId", ->
|
|
||||||
it "should insert the template with the given id into the draft with the given id", ->
|
|
||||||
|
|
||||||
add = jasmine.createSpy('add')
|
|
||||||
spyOn(DraftStore, 'sessionForClientId').andCallFake ->
|
|
||||||
Promise.resolve(changes: {add})
|
|
||||||
|
|
||||||
runs ->
|
|
||||||
TemplateStore._onInsertTemplateId
|
|
||||||
templateId: 'template1.html',
|
|
||||||
draftClientId: 'localid-draft'
|
|
||||||
waitsFor ->
|
|
||||||
add.calls.length > 0
|
|
||||||
runs ->
|
|
||||||
expect(add).toHaveBeenCalledWith
|
|
||||||
body: stubTemplateFiles['template1.html']
|
|
||||||
|
|
||||||
describe "onCreateTemplate", ->
|
|
||||||
beforeEach ->
|
|
||||||
TemplateStore.init()
|
|
||||||
spyOn(DraftStore, 'sessionForClientId').andCallFake (draftClientId) ->
|
|
||||||
if draftClientId is 'localid-nosubject'
|
|
||||||
d = new Message(subject: '', body: '<p>Body</p>')
|
|
||||||
else
|
|
||||||
d = new Message(subject: 'Subject', body: '<p>Body</p>')
|
|
||||||
session =
|
|
||||||
draft: -> d
|
|
||||||
Promise.resolve(session)
|
|
||||||
|
|
||||||
it "should create a template with the given name and contents", ->
|
|
||||||
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
|
|
||||||
item = TemplateStore.items()?[0]
|
|
||||||
expect(item.id).toBe "123.html"
|
|
||||||
expect(item.name).toBe "123"
|
|
||||||
expect(item.path.split("/").pop()).toBe "123.html"
|
|
||||||
|
|
||||||
it "should display an error if no name is provided", ->
|
|
||||||
spyOn(TemplateStore, '_displayError')
|
|
||||||
TemplateStore._onCreateTemplate({contents: 'bla'})
|
|
||||||
expect(TemplateStore._displayError).toHaveBeenCalled()
|
|
||||||
|
|
||||||
it "should display an error if no content is provided", ->
|
|
||||||
spyOn(TemplateStore, '_displayError')
|
|
||||||
TemplateStore._onCreateTemplate({name: 'bla'})
|
|
||||||
expect(TemplateStore._displayError).toHaveBeenCalled()
|
|
||||||
|
|
||||||
it "should save the template file to the templates folder", ->
|
|
||||||
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
|
|
||||||
path = "#{stubTemplatesDir}/123.html"
|
|
||||||
expect(fs.writeFile).toHaveBeenCalled()
|
|
||||||
expect(fs.writeFile.mostRecentCall.args[0]).toEqual(path)
|
|
||||||
expect(fs.writeFile.mostRecentCall.args[1]).toEqual('bla')
|
|
||||||
|
|
||||||
it "should open the template so you can see it", ->
|
|
||||||
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
|
|
||||||
path = "#{stubTemplatesDir}/123.html"
|
|
||||||
expect(shell.showItemInFolder).toHaveBeenCalled()
|
|
||||||
|
|
||||||
describe "when given a draft id", ->
|
|
||||||
it "should create a template from the name and contents of the given draft", ->
|
|
||||||
spyOn(TemplateStore, 'trigger')
|
|
||||||
spyOn(TemplateStore, '_populate')
|
|
||||||
runs ->
|
|
||||||
TemplateStore._onCreateTemplate({draftClientId: 'localid-b'})
|
|
||||||
waitsFor ->
|
|
||||||
TemplateStore.trigger.callCount > 0
|
|
||||||
runs ->
|
|
||||||
expect(TemplateStore.items().length).toEqual(1)
|
|
||||||
|
|
||||||
it "should display an error if the draft has no subject", ->
|
|
||||||
spyOn(TemplateStore, '_displayError')
|
|
||||||
runs ->
|
|
||||||
TemplateStore._onCreateTemplate({draftClientId: 'localid-nosubject'})
|
|
||||||
waitsFor ->
|
|
||||||
TemplateStore._displayError.callCount > 0
|
|
||||||
runs ->
|
|
||||||
expect(TemplateStore._displayError).toHaveBeenCalled()
|
|
||||||
|
|
||||||
describe "onShowTemplates", ->
|
|
||||||
it "should open the templates folder in the Finder", ->
|
|
||||||
TemplateStore._onShowTemplates()
|
|
||||||
expect(shell.showItemInFolder).toHaveBeenCalled()
|
|
|
@ -1,38 +0,0 @@
|
||||||
@import "ui-variables";
|
|
||||||
@import "ui-mixins";
|
|
||||||
|
|
||||||
@code-bg-color: #fcf4db;
|
|
||||||
|
|
||||||
.template-picker {
|
|
||||||
.menu {
|
|
||||||
.content-container {
|
|
||||||
height:150px;
|
|
||||||
overflow-y:scroll;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-status-bar {
|
|
||||||
background-color: @code-bg-color;
|
|
||||||
color: darken(@code-bg-color, 70%);
|
|
||||||
border: 1.5px solid darken(@code-bg-color, 10%);
|
|
||||||
border-radius: @border-radius-small;
|
|
||||||
padding-top: @padding-small-vertical @padding-small-horizontal @padding-small-vertical @padding-small-horizontal;
|
|
||||||
font-size: @font-size-small;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-body #contenteditable {
|
|
||||||
code.var {
|
|
||||||
font: inherit;
|
|
||||||
padding:0;
|
|
||||||
padding-left:2px;
|
|
||||||
padding-right:2px;
|
|
||||||
border-bottom: 1.5px solid darken(@code-bg-color, 10%);
|
|
||||||
background-color: fade(@code-bg-color, 10%);
|
|
||||||
&.empty {
|
|
||||||
color:darken(@code-bg-color, 70%);
|
|
||||||
border-bottom: 1px solid darken(@code-bg-color, 14%);
|
|
||||||
background-color: @code-bg-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 5.9 MiB |
|
@ -1,17 +0,0 @@
|
||||||
TodayView = require "./today-view"
|
|
||||||
TodayIcon = require "./today-icon"
|
|
||||||
{ComponentRegistry,
|
|
||||||
WorkspaceStore} = require 'nylas-exports'
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
|
|
||||||
activate: (@state={}) ->
|
|
||||||
# WorkspaceStore.defineSheet 'Today', {root: true, supportedModes: ['list'], name: 'Today', icon: 'today.png'},
|
|
||||||
# list: ['RootSidebar', 'Today']
|
|
||||||
#
|
|
||||||
# ComponentRegistry.register TodayView,
|
|
||||||
# location: WorkspaceStore.Location.Today
|
|
||||||
|
|
||||||
deactivate: ->
|
|
||||||
# ComponentRegistry.unregister(TodayView)
|
|
||||||
# WorkspaceStore.undefineSheet('Today')
|
|
|
@ -1,32 +0,0 @@
|
||||||
React = require 'react'
|
|
||||||
_ = require "underscore"
|
|
||||||
moment = require 'moment'
|
|
||||||
classNames = require 'classnames'
|
|
||||||
|
|
||||||
class TodayIcon extends React.Component
|
|
||||||
@displayName: 'TodayIcon'
|
|
||||||
|
|
||||||
constructor: (@props) ->
|
|
||||||
@state =
|
|
||||||
moment: moment()
|
|
||||||
|
|
||||||
componentDidMount: =>
|
|
||||||
@_setTimeState()
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
|
||||||
clearInterval(@_timer)
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
classes = classNames
|
|
||||||
'today-icon': true
|
|
||||||
'selected': @props.selected
|
|
||||||
|
|
||||||
<div className={classes}>{@state.moment.format('D')}</div>
|
|
||||||
|
|
||||||
_setTimeState: =>
|
|
||||||
timeTillNextSecond = (60 - (new Date).getSeconds()) * 1000
|
|
||||||
@_timer = setTimeout(@_setTimeState, timeTillNextSecond)
|
|
||||||
@setState(moment: moment())
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = TodayIcon
|
|
|
@ -1,83 +0,0 @@
|
||||||
React = require 'react'
|
|
||||||
_ = require "underscore"
|
|
||||||
{Utils, Actions} = require 'nylas-exports'
|
|
||||||
{Spinner, EventedIFrame} = require 'nylas-component-kit'
|
|
||||||
moment = require 'moment'
|
|
||||||
|
|
||||||
class TodayViewDateTime extends React.Component
|
|
||||||
@displayName: 'TodayViewDateTime'
|
|
||||||
|
|
||||||
constructor: (@props) ->
|
|
||||||
@state =
|
|
||||||
moment: moment()
|
|
||||||
|
|
||||||
componentDidMount: =>
|
|
||||||
@_setTimeState()
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
|
||||||
clearInterval(@_timer)
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
<div className="centered">
|
|
||||||
<div className="time">{@state.moment.format('h:mm')}</div>
|
|
||||||
<div className="date">{@state.moment.format('dddd, MMM Do')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
_setTimeState: =>
|
|
||||||
timeTillNextSecond = (60 - (new Date).getSeconds()) * 1000
|
|
||||||
@_timer = setTimeout(@_setTimeState, timeTillNextSecond)
|
|
||||||
|
|
||||||
@setState(moment: moment())
|
|
||||||
|
|
||||||
|
|
||||||
class TodayViewBox extends React.Component
|
|
||||||
@displayName: 'TodayViewBox'
|
|
||||||
|
|
||||||
@propTypes:
|
|
||||||
name: React.PropTypes.string.isRequired
|
|
||||||
|
|
||||||
constructor: (@props) ->
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
<div className="box">
|
|
||||||
<h2>{@props.name}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
class TodayView extends React.Component
|
|
||||||
@displayName: 'TodayView'
|
|
||||||
|
|
||||||
constructor: (@props) ->
|
|
||||||
@state = @_getStateFromStores()
|
|
||||||
|
|
||||||
render: =>
|
|
||||||
<div className="today">
|
|
||||||
<div className="inner">
|
|
||||||
<TodayViewDateTime />
|
|
||||||
<div className="boxes">
|
|
||||||
<TodayViewBox name="Conversations">
|
|
||||||
</TodayViewBox>
|
|
||||||
<TodayViewBox name="Events">
|
|
||||||
</TodayViewBox>
|
|
||||||
<TodayViewBox name="Drafts">
|
|
||||||
</TodayViewBox>
|
|
||||||
</div>
|
|
||||||
<div className="to-the-inbox">
|
|
||||||
Inbox
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
componentDidMount: =>
|
|
||||||
@_unsubscribers = []
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
|
||||||
unsubscribe() for unsubscribe in @_unsubscribers
|
|
||||||
|
|
||||||
_getStateFromStores: =>
|
|
||||||
{}
|
|
||||||
|
|
||||||
_onChange: =>
|
|
||||||
@setState(@_getStateFromStores())
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = TodayView
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"name": "today",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"main": "./lib/main",
|
|
||||||
"description": "Today View",
|
|
||||||
"license": "Proprietary",
|
|
||||||
"private": true,
|
|
||||||
"engines": {
|
|
||||||
"atom": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"moment": "^2.8"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
@import "ui-variables";
|
|
||||||
@import "ui-mixins";
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Hurme';
|
|
||||||
font-style: normal;
|
|
||||||
src: url(nylas://today/assets/HurmeGeometricSans4Thin.otf);
|
|
||||||
}
|
|
||||||
|
|
||||||
.today-icon {
|
|
||||||
display:inline-block;
|
|
||||||
overflow:hidden;
|
|
||||||
width:16px;
|
|
||||||
height:16px;
|
|
||||||
color:@source-list-bg;
|
|
||||||
text-align:center;
|
|
||||||
font-weight:500;
|
|
||||||
font-size:11px;
|
|
||||||
line-height:16px;
|
|
||||||
position:relative;
|
|
||||||
top:5px;
|
|
||||||
background-color:@text-color-very-subtle;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color:@accent-primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.today {
|
|
||||||
background:url(nylas://today/assets/background.png) top center no-repeat;
|
|
||||||
background-size:100%;
|
|
||||||
overflow-y:scroll;
|
|
||||||
position:absolute;
|
|
||||||
width:100%;
|
|
||||||
height:100%;
|
|
||||||
|
|
||||||
.inner {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.to-the-inbox {
|
|
||||||
opacity:0.3;
|
|
||||||
position:absolute;
|
|
||||||
width:100%;
|
|
||||||
text-align:center;
|
|
||||||
bottom:10px;
|
|
||||||
font-weight:@font-weight-semi-bold;
|
|
||||||
}
|
|
||||||
.centered {
|
|
||||||
text-align:center;
|
|
||||||
opacity:0.6;
|
|
||||||
.time {
|
|
||||||
font-family: 'Hurme';
|
|
||||||
margin-top:70px;
|
|
||||||
font-size:100px;
|
|
||||||
line-height:96px;
|
|
||||||
}
|
|
||||||
.date {
|
|
||||||
font-family:@font-family-sans-serif;
|
|
||||||
font-weight:@font-weight-normal;
|
|
||||||
font-size:22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.boxes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction:row;
|
|
||||||
padding:15px;
|
|
||||||
position:absolute;
|
|
||||||
bottom:20px;
|
|
||||||
width:100%;
|
|
||||||
.box {
|
|
||||||
margin:15px;
|
|
||||||
border-radius: @border-radius-large;
|
|
||||||
background-color:white;
|
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
|
||||||
flex:1;
|
|
||||||
height:40vh;
|
|
||||||
h2 {
|
|
||||||
margin-top:4px;
|
|
||||||
padding:12px;
|
|
||||||
border-bottom:1px solid #ccc;
|
|
||||||
font-size:15px;
|
|
||||||
font-weight:@font-weight-semi-bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue