fix(*) Closes 517,603,608, moves search-playground to edgehill-plugins

This commit is contained in:
Ben Gotow 2015-04-27 18:26:04 -07:00
parent b488ffb6a9
commit 7f3816cb67
25 changed files with 34 additions and 692 deletions

View file

@ -577,7 +577,7 @@ class ContenteditableComponent extends React.Component
@setState toolbarVisible: false
_focusedOnToolbar: =>
React.findDOMNode(@refs.floatingToolbar).contains(document.activeElement)
React.findDOMNode(@refs.floatingToolbar)?.contains(document.activeElement)
# This needs to be in the contenteditable area because we need to first
# restore the selection before calling the `execCommand`

View file

@ -67,7 +67,8 @@ class MessageList extends React.Component
,100
render: =>
return <div></div> if not @state.currentThread?
if not @state.currentThread?
return <div className="message-list" id="message-list"></div>
wrapClass = classNames
"messages-wrap": true

View file

@ -21,11 +21,12 @@ ModeToggle = React.createClass
render: ->
return <div></div> unless @state.visible
<div className="mode-switch"
<div className="mode-toggle"
style={order:51, marginTop:10, marginRight:14}
onClick={@_onToggleMode}>
<RetinaImg
name="toolbar-icon-toggle-pane.png"
colorfill={@state.mode is 'split'}
onClick={@_onToggleMode} />
</div>

View file

@ -1,3 +1,4 @@
@import 'ui-variables';
.mode-switch {
z-index: 1000;
@ -8,3 +9,11 @@
transition: left .2s ease-out;
}
}
.mode-toggle {
z-index: 1000;
position: relative;
.colorfill {
background-color: @component-active-color;
}
}

View file

@ -14,7 +14,7 @@ class SearchBar extends React.Component
query: ""
focused: false
suggestions: []
committedQuery: ""
committedQuery: null
componentDidMount: =>
@unsubscribe = SearchSuggestionStore.listen @_onStoreChange
@ -84,7 +84,7 @@ class SearchBar extends React.Component
classNames
'focused': @state.focused
'showing-query': @state.query?.length > 0
'committed-query': @state.committedQuery.length > 0
'committed-query': @state.committedQuery?.length > 0
'search-container': true
'showing-suggestions': @state.suggestions?.length > 0
@ -120,7 +120,7 @@ class SearchBar extends React.Component
Actions.searchQueryCommitted(item.value)
_onClearSearch: (event) =>
Actions.searchQueryCommitted('')
Actions.searchQueryCommitted(null)
_clearAndBlur: =>
@_onClearSearch()

View file

@ -1 +0,0 @@
node_modules

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

View file

@ -1,2 +0,0 @@
'.search-bar input':
'escape': 'search-bar:escape-search'

View file

@ -1,19 +0,0 @@
moment = require "moment"
React = require 'react'
module.exports =
timestamp: (time) ->
diff = moment().diff(time, 'days', true)
if diff <= 1
format = "h:mm a"
else if diff > 1 and diff <= 365
format = "MMM D"
else
format = "MMM D YYYY"
moment(time).format(format)
subject: (subj) ->
if (subj ? "").trim().length is 0
return <span className="no-subject">(No Subject)</span>
else
return subj

View file

@ -1,41 +0,0 @@
path = require 'path'
require 'coffee-react/register'
React = require 'react'
{ComponentRegistry, WorkspaceStore} = require 'inbox-exports'
SearchPlaygroundBar = require './search-bar'
SearchPlaygroundSettingsBar = require './search-settings-bar'
SearchPlaygroundBottomBar = require './search-bottom-bar'
SearchResultsList = require './search-results-list'
module.exports =
configDefaults:
showOnRightSide: false
activate: (@state) ->
WorkspaceStore.defineSheet 'Search', {root: true, supportedModes: ['list'], name: 'Search'},
list: ['RootSidebar', 'SearchPlayground']
ComponentRegistry.register
view: SearchPlaygroundBar
name: 'SearchPlaygroundBar'
location: WorkspaceStore.Location.SearchPlayground
ComponentRegistry.register
view: SearchPlaygroundBottomBar
name: 'SearchPlaygroundBottomBar'
location: WorkspaceStore.Location.SearchPlayground
ComponentRegistry.register
view: SearchPlaygroundSettingsBar
name: 'SearchPlaygroundSettingsBar'
location: WorkspaceStore.Location.SearchPlayground
ComponentRegistry.register
view: SearchResultsList
name: 'SearchResultsList'
location: WorkspaceStore.Location.SearchPlayground
deactivate: ->
ComponentRegistry.unregister 'SearchBar'

View file

@ -1,12 +0,0 @@
Reflux = require 'reflux'
Actions = Reflux.createActions([
"setRankNext",
"clearRanks",
"submitRanks"
])
for key, action of Actions
action.sync = true
module.exports = Actions

View file

@ -1,74 +0,0 @@
Reflux = require 'reflux'
_ = require 'underscore-plus'
remote = require 'remote'
{NamespaceStore,
Contact,
Message,
Actions,
DatabaseStore} = require 'inbox-exports'
PlaygroundActions = require './playground-actions'
SearchStore = require './search-store'
module.exports =
RelevanceStore = Reflux.createStore
init: ->
@listenTo SearchStore, @_onSearchChanged
@listenTo PlaygroundActions.setRankNext, @_onSetRankNext
@listenTo PlaygroundActions.clearRanks, @_onClearRanks
@listenTo PlaygroundActions.submitRanks, @_onSubmitRanks
valueForId: (id) ->
@_values[id]
_onSubmitRanks: ->
v = SearchStore.view()
if @_valuesOrdered.length is 0
return
data =
namespaceId: NamespaceStore.current().id
query: SearchStore.searchQuery()
weights: SearchStore.searchWeights()
returned: [0..Math.min(9, v.count())].map (i) -> v.get(i)?.id
desired: @_valuesOrdered
draft = new Message
from: [NamespaceStore.current().me()]
to: [new Contact(name: "Nilas Team", email: "feedback@nilas.com")]
date: (new Date)
draft: true
subject: "Feedback - Search Result Ranking"
namespaceId: NamespaceStore.current().id
body: JSON.stringify(data, null, '\t')
DatabaseStore.persistModel(draft).then =>
DatabaseStore.localIdForModel(draft).then (localId) ->
Actions.sendDraft(localId)
dialog = remote.require('dialog')
dialog.showMessageBox remote.getCurrentWindow(), {
type: 'warning'
buttons: ['OK'],
message: "Thank you."
detail: "Your preferred ranking order for this query has been sent to the Edgehill team."
}
@_onClearRanks()
_onClearRanks: ->
@_values = {}
@_valuesOrdered = []
@_valueLast = 0
@trigger(@)
_onSetRankNext: (id) ->
@_values[id] = @_valueLast += 1
@_valuesOrdered.push(id)
@trigger(@)
_onSearchChanged: ->
v = SearchStore.view()
@_values = {}
@_valuesOrdered = []
@_valueLast = 0
@trigger(@)

View file

@ -1,152 +0,0 @@
React = require 'react/addons'
classNames = require 'classnames'
{Actions} = require 'inbox-exports'
{Menu, RetinaImg} = require 'ui-components'
SearchSuggestionStore = require './search-suggestion-store'
_ = require 'underscore-plus'
class SearchBar extends React.Component
@displayName = 'SearchBar'
constructor: (@props) ->
@state =
query: ""
focused: false
suggestions: []
committedQuery: ""
componentDidMount: =>
@unsubscribe = SearchSuggestionStore.listen @_onStoreChange
@body_unsubscriber = atom.commands.add 'body', {
'application:focus-search': @_onFocusSearch
}
@body_unsubscriber = atom.commands.add '.search-bar', {
'search-bar:escape-search': @_clearAndBlur
}
# 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()
@body_unsubscriber.dispose()
render: =>
inputValue = @_queryToString(@state.query)
inputClass = classNames
'input-bordered': true
'empty': inputValue.length is 0
headerComponents = [
<input type="text"
ref="searchInput"
key="input"
className={inputClass}
placeholder="Search all email"
value={inputValue}
onChange={@_onValueChange}
onFocus={@_onFocus}
onBlur={@_onBlur} />
<RetinaImg className="search-accessory search"
name="searchloupe.png"
key="accessory"
onClick={@_doSearch} />
<div className="search-accessory clear"
key="clear"
onClick={@_onClearSearch}><i className="fa fa-remove"></i></div>
]
itemContentFunc = (item) =>
if item.divider
<Menu.Item divider={item.divider} />
else if item.contact
<Menu.NameEmailItem name={item.contact.name} email={item.contact.email} />
else
item.label
<div className="search-bar">
<div className="header">Search Query</div>
<Menu ref="menu"
className={@_containerClasses()}
headerComponents={headerComponents}
items={@state.suggestions}
itemContent={itemContentFunc}
itemKey={ (item) -> item.label }
onSelect={@_onSelectSuggestion}
/>
</div>
_onFocusSearch: =>
React.findDOMNode(@refs.searchInput).focus()
_containerClasses: =>
classNames
'focused': @state.focused
'showing-query': @state.query?.length > 0
'committed-query': @state.committedQuery.length > 0
'search-container': true
'showing-suggestions': @state.suggestions?.length > 0
_queryToString: (query) =>
return "" unless query instanceof Array
str = ""
for term in query
for key,val of term
if key == "all"
str += val
else
str += "#{key}:#{val}"
_stringToQuery: (str) =>
return [] unless str
# note: right now this only works if there's one term. In the future,
# we'll make this whole search input a tokenizing field
[a,b] = str.split(':')
term = {}
if b
term[a] = b
else
term["all"] = a
[term]
_onValueChange: (event) =>
Actions.searchQueryChanged(@_stringToQuery(event.target.value))
if (event.target.value is '')
@_onClearSearch()
_onSelectSuggestion: (item) =>
Actions.searchQueryCommitted(item.value)
_onClearSearch: (event) =>
Actions.searchQueryCommitted('')
_clearAndBlur: =>
@_onClearSearch()
React.findDOMNode(@refs.searchInput)?.blur()
_onFocus: =>
@setState focused: true
_onBlur: =>
# Don't immediately hide the menu when the text input is blurred,
# because the user might have clicked an item in the menu. Wait to
# handle the touch event, then dismiss the menu.
setTimeout =>
Actions.searchBlurred()
@setState(focused: false)
, 150
_doSearch: =>
Actions.searchQueryCommitted(@state.query)
_onStoreChange: =>
@setState
query: SearchSuggestionStore.query()
suggestions: SearchSuggestionStore.suggestions()
committedQuery: SearchSuggestionStore.committedQuery()
module.exports = SearchBar

View file

@ -1,18 +0,0 @@
React = require 'react'
_ = require 'underscore-plus'
PlaygroundActions = require './playground-actions'
module.exports =
SearchBottomBar = React.createClass
render: ->
<div className="search-bottom-bar">
<button onClick={@_onClear} className="btn">Clear Ranking</button>
<button onClick={@_onSubmit} className="btn btn-emphasis">Submit Ranking</button>
</div>
_onClear: ->
PlaygroundActions.clearRanks()
_onSubmit: ->
PlaygroundActions.submitRanks()

View file

@ -1,91 +0,0 @@
_ = require 'underscore-plus'
React = require 'react'
PlaygroundActions = require './playground-actions'
{ListTabular, MultiselectList, Flexbox} = require 'ui-components'
{timestamp, subject} = require './formatting-utils'
{Utils,
Thread,
WorkspaceStore,
NamespaceStore} = require 'inbox-exports'
RelevanceStore = require './relevance-store'
SearchStore = require './search-store'
Relevance = React.createClass
getInitialState: ->
@_getStateFromStores()
componentDidMount: ->
@unlisten = RelevanceStore.listen @_onUpdate, @
componentWillUnmount: ->
@unlisten() if @unlisten
render: ->
<div onClick={@_onClick} style={backgroundColor:'#ccc', padding:3, borderRadius:3, textAlign:'center', width:50}>
{@state.relevance}
</div>
_onClick: (event) ->
PlaygroundActions.setRankNext(@props.threadId)
event.stopPropagation()
_onUpdate: ->
@setState(@_getStateFromStores())
_getStateFromStores: ->
relevance: RelevanceStore.valueForId(@props.threadId) ? '-'
module.exports =
SearchResultsList = React.createClass
displayName: 'SearchResultsList'
componentWillMount: ->
c2 = new ListTabular.Column
name: "Name"
width: 200
resolver: (thread) ->
list = thread.participants
return [] unless list and list instanceof Array
me = NamespaceStore.current().emailAddress
list = _.reject list, (p) -> p.email is me
list = _.map list, (p) -> if p.name and p.name.length then p.name else p.email
list = list.join(', ')
<span>{list}</span>
c3 = new ListTabular.Column
name: "Message"
flex: 4
resolver: (thread) ->
attachments = []
if thread.hasTagId('attachment')
attachments = <div className="thread-icon thread-icon-attachment"></div>
<span className="details">
<span className="subject">{subject(thread.subject)}</span>
<span className="snippet">{thread.snippet}</span>
{attachments}
</span>
c4 = new ListTabular.Column
name: "Date"
resolver: (thread) ->
<span className="timestamp">{timestamp(thread.lastMessageTimestamp)}</span>
c5 = new ListTabular.Column
name: "Relevance"
resolver: (thread) ->
<Relevance threadId={thread.id} />
@columns = [c2, c3, c4, c5]
@commands = {}
render: ->
<MultiselectList
dataStore={SearchStore}
columns={@columns}
itemPropsProvider={ -> {} }
commands={@commands}
className="thread-list"
collection="thread" />

View file

@ -1,40 +0,0 @@
React = require 'react'
{Actions} = require 'inbox-exports'
_ = require 'underscore-plus'
SearchStore = require './search-store'
module.exports =
SearchBar = React.createClass
getInitialState: ->
weights: SearchStore.searchWeights()
componentDidMount: ->
@unsubscribe = SearchStore.listen @_onStoreChange
componentWillUnmount: ->
@unsubscribe()
render: ->
<div className="search-settings-bar">
<div className="header">Search Weights</div>
<div className="field">
<strong>From:</strong>
<input type="range" name="from" value={@state.weights.from} onChange={@_onValueChange} min="0" max="10"/>
<input type="text" name="from" value={@state.weights.from} onChange={@_onValueChange} />
</div>
<div className="field">
<strong>Subject:</strong>
<input type="range" name="subject" value={@state.weights.subject} onChange={@_onValueChange} min="0" max="10"/>
<input type="text" name="subject" value={@state.weights.subject} onChange={@_onValueChange} />
</div>
</div>
_onValueChange: (event) ->
weights = SearchStore.searchWeights()
weights[event.target.name] = event.target.value
Actions.searchWeightsChanged(weights)
_onStoreChange: ->
@setState
weights: SearchStore.searchWeights()

View file

@ -1,91 +0,0 @@
Reflux = require 'reflux'
_ = require 'underscore-plus'
{DatabaseStore,
SearchView,
NamespaceStore,
FocusedContentStore,
Actions,
Utils,
Thread,
Message} = require 'inbox-exports'
module.exports =
SearchStore = Reflux.createStore
init: ->
@_resetInstanceVars()
@listenTo Actions.searchQueryCommitted, @_onSearchCommitted
@listenTo Actions.searchWeightsChanged, @_onSearchWeightsChanged
@listenTo DatabaseStore, @_onDataChanged
@listenTo NamespaceStore, @_onNamespaceChanged
_resetInstanceVars: ->
@_lastQuery = null
@_searchQuery = null
@_searchWeights = {"from": 4, "subject": 2}
view: ->
@_view
searchWeights: ->
@_searchWeights
searchQuery: ->
@_searchQuery
setView: (view) ->
@_viewUnlisten() if @_viewUnlisten
@_view = view
if view
@_viewUnlisten = view.listen ->
@trigger(@)
,@
@trigger(@)
createView: ->
namespaceId = NamespaceStore.current()?.id
if @_searchQuery
query = JSON.parse(JSON.stringify(@_searchQuery))
for term in query
if term['all']
term['weights'] = @_searchWeights
v = new SearchView(query, namespaceId)
v.setSortOrder('relevance')
@setView(v)
else
@setView(null)
Actions.focusInCollection(collection: 'thread', item: null)
# Inbound Events
_onNamespaceChanged: ->
@createView()
_onSearchCommitted: (query) ->
@_searchQuery = query
@createView()
_onSearchWeightsChanged: (weights) ->
@_searchWeights = weights
@createViewDebounced ?= _.debounce =>
@createView()
, 500
@createViewDebounced()
@trigger(@)
_onDataChanged: (change) ->
return unless @_view
if change.objectClass is Thread.name
@_view.invalidate({changed: change.objects, shallow: true})
if change.objectClass is Message.name
threadIds = _.uniq _.map change.objects, (m) -> m.threadId
@_view.invalidateMetadataFor(threadIds)

View file

@ -1,71 +0,0 @@
Reflux = require 'reflux'
{Actions,
Contact,
ContactStore} = require 'inbox-exports'
_ = require 'underscore-plus'
# Stores should closely match the needs of a particular part of the front end.
# For example, we might create a "MessageStore" that observes this store
# for changes in selectedThread, "DatabaseStore" for changes to the underlying database,
# and vends up the array used for that view.
SearchSuggestionStore = Reflux.createStore
init: ->
@_suggestions = []
@_query = ""
@_committedQuery = ""
@listenTo Actions.searchQueryChanged, @onSearchQueryChanged
@listenTo Actions.searchQueryCommitted, @onSearchQueryCommitted
@listenTo Actions.searchBlurred, @onSearchBlurred
onSearchQueryChanged: (query) ->
@_query = query
@repopulate()
onSearchQueryCommitted: (query) ->
@_query = query
@_committedQuery = query
@_suggestions = []
@trigger()
onSearchBlurred: ->
@_suggestions = []
@trigger()
repopulate: ->
@_suggestions = []
term = @_query?[0]
return @trigger(@) unless term
key = Object.keys(term)[0]
val = term[key]?.toLowerCase()
return @trigger(@) unless val
contactResults = ContactStore.searchContacts(val, limit:10)
@_suggestions.push
label: "Message Contains: #{val}"
value: [{"all": val}]
if contactResults.length
@_suggestions.push
divider: 'People'
_.each contactResults, (contact) =>
@_suggestions.push
contact: contact
value: [{"participants": contact.email}]
@trigger(@)
# Exposed Data
query: -> @_query
committedQuery: -> @_committedQuery
suggestions: ->
@_suggestions
module.exports = SearchSuggestionStore

View file

@ -1,13 +0,0 @@
{
"name": "search-playground",
"version": "0.1.0",
"main": "./lib/main",
"description": "Internal search playground for testing",
"license": "Proprietary",
"private": true,
"engines": {
"atom": "*"
},
"dependencies": {
}
}

View file

@ -1,53 +0,0 @@
@import "ui-variables";
@import "ui-mixins";
.search-bar,
.search-settings-bar {
.header {
color: @text-color-very-subtle;
font-weight: @font-weight-semi-bold;
font-size: @font-size-small;
padding-top:@padding-small-horizontal;
display:block;
}
}
.search-settings-bar {
padding:15px;
padding-bottom:25px;
border-bottom:1px solid #ccc;
background: url(nylas://search-playground/cloudine.png) top right no-repeat;
background-size:contain;
.field {
display:inline-block;
padding-right:15px;
}
input[type=range] {
margin-left:10px;
position:relative;
top:3px;
}
input[type=text] {
margin-left:10px;
width:40px;
padding-bottom:0;
}
}
.search-bottom-bar {
order:100;
text-align:right;
padding-top:15px;
padding-bottom:15px;
.btn {
margin-right:15px;
}
}
.search-playground {
.thread-list {
position: relative;
flex: 1;
}
}

View file

@ -78,6 +78,7 @@ ThreadListStore = Reflux.createStore
_onNamespaceChanged: -> @createView()
_onSearchCommitted: (query) ->
return if @_searchQuery is query
@_searchQuery = query
@createView()

View file

@ -78,7 +78,10 @@ class MultiselectList extends React.Component
name: ""
resolver: (thread) =>
toggle = (event) =>
props.dataStore.view().selection.toggle(thread)
if event.shiftKey
props.dataStore.view().selection.expandTo(thread)
else
props.dataStore.view().selection.toggle(thread)
event.stopPropagation()
<div className="checkmark" onClick={toggle}><div className="inner"></div></div>

View file

@ -19,11 +19,11 @@ StylesImpactedByZoom = [
###
Public: RetinaImg wraps the DOM's standard `<img`> tag and implements a `UIImage` style
interface. Rather than specifying an image `src`, RetinaImg allows you to provide
an image name. Like UIImage on iOS, it automatically finds the best image for the current
display based on pixel density. Given `image.png`, on a Retina screen, it looks for
`image@2x.png`, `image.png`, `image@1x.png` in that order. It uses a lookup table and caches
image names, so images generally resolve immediately.
interface. Rather than specifying an image `src`, RetinaImg allows you to provide
an image name. Like UIImage on iOS, it automatically finds the best image for the current
display based on pixel density. Given `image.png`, on a Retina screen, it looks for
`image@2x.png`, `image.png`, `image@1x.png` in that order. It uses a lookup table and caches
image names, so images generally resolve immediately.
###
class RetinaImg extends React.Component
@displayName: 'RetinaImg'

View file

@ -237,6 +237,10 @@ class DatabaseView extends ModelView
query.include(attr) for attr in @_includes
query.then (items) =>
# If the page is no longer in the cache at all, it may have fallen out of the
# retained range and been cleaned up.
return unless @_pages[idx]
# If we've started reloading since we made our query, don't do any more work
if page.loadingStart isnt start
@log("Retrieval cancelled — out of date.")
@ -251,6 +255,7 @@ class DatabaseView extends ModelView
retrievePageMetadata: (idx, items) ->
start = Date.now()
page = @_pages[idx]
page.loadingStart = start
# This method can only be used once the page is loaded. If no page is present,

View file

@ -28,10 +28,10 @@ FocusedTagStore = Reflux.createStore
@_setTag(tag)
_onSearchQueryCommitted: (query) ->
if query? and query isnt ""
if query
@_oldTag = @_tag
@_setTag(null)
else
else if @_oldTag
@_setTag(@_oldTag)
_setTag: (tag) ->