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 @setState toolbarVisible: false
_focusedOnToolbar: => _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 # This needs to be in the contenteditable area because we need to first
# restore the selection before calling the `execCommand` # restore the selection before calling the `execCommand`

View file

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

View file

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

View file

@ -1,3 +1,4 @@
@import 'ui-variables';
.mode-switch { .mode-switch {
z-index: 1000; z-index: 1000;
@ -8,3 +9,11 @@
transition: left .2s ease-out; 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: "" query: ""
focused: false focused: false
suggestions: [] suggestions: []
committedQuery: "" committedQuery: null
componentDidMount: => componentDidMount: =>
@unsubscribe = SearchSuggestionStore.listen @_onStoreChange @unsubscribe = SearchSuggestionStore.listen @_onStoreChange
@ -84,7 +84,7 @@ class SearchBar extends React.Component
classNames classNames
'focused': @state.focused 'focused': @state.focused
'showing-query': @state.query?.length > 0 'showing-query': @state.query?.length > 0
'committed-query': @state.committedQuery.length > 0 'committed-query': @state.committedQuery?.length > 0
'search-container': true 'search-container': true
'showing-suggestions': @state.suggestions?.length > 0 'showing-suggestions': @state.suggestions?.length > 0
@ -120,7 +120,7 @@ class SearchBar extends React.Component
Actions.searchQueryCommitted(item.value) Actions.searchQueryCommitted(item.value)
_onClearSearch: (event) => _onClearSearch: (event) =>
Actions.searchQueryCommitted('') Actions.searchQueryCommitted(null)
_clearAndBlur: => _clearAndBlur: =>
@_onClearSearch() @_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() _onNamespaceChanged: -> @createView()
_onSearchCommitted: (query) -> _onSearchCommitted: (query) ->
return if @_searchQuery is query
@_searchQuery = query @_searchQuery = query
@createView() @createView()

View file

@ -78,7 +78,10 @@ class MultiselectList extends React.Component
name: "" name: ""
resolver: (thread) => resolver: (thread) =>
toggle = (event) => 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() event.stopPropagation()
<div className="checkmark" onClick={toggle}><div className="inner"></div></div> <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 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 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 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 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@2x.png`, `image.png`, `image@1x.png` in that order. It uses a lookup table and caches
image names, so images generally resolve immediately. image names, so images generally resolve immediately.
### ###
class RetinaImg extends React.Component class RetinaImg extends React.Component
@displayName: 'RetinaImg' @displayName: 'RetinaImg'

View file

@ -237,6 +237,10 @@ class DatabaseView extends ModelView
query.include(attr) for attr in @_includes query.include(attr) for attr in @_includes
query.then (items) => 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 we've started reloading since we made our query, don't do any more work
if page.loadingStart isnt start if page.loadingStart isnt start
@log("Retrieval cancelled — out of date.") @log("Retrieval cancelled — out of date.")
@ -251,6 +255,7 @@ class DatabaseView extends ModelView
retrievePageMetadata: (idx, items) -> retrievePageMetadata: (idx, items) ->
start = Date.now() start = Date.now()
page = @_pages[idx] page = @_pages[idx]
page.loadingStart = start page.loadingStart = start
# This method can only be used once the page is loaded. If no page is present, # 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) @_setTag(tag)
_onSearchQueryCommitted: (query) -> _onSearchQueryCommitted: (query) ->
if query? and query isnt "" if query
@_oldTag = @_tag @_oldTag = @_tag
@_setTag(null) @_setTag(null)
else else if @_oldTag
@_setTag(@_oldTag) @_setTag(@_oldTag)
_setTag: (tag) -> _setTag: (tag) ->