mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 07:46:06 +08:00
fix(*) Closes 517,603,608, moves search-playground to edgehill-plugins
This commit is contained in:
parent
b488ffb6a9
commit
7f3816cb67
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
node_modules
|
Binary file not shown.
Before Width: | Height: | Size: 112 KiB |
|
@ -1,2 +0,0 @@
|
|||
'.search-bar input':
|
||||
'escape': 'search-bar:escape-search'
|
|
@ -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
|
|
@ -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'
|
|
@ -1,12 +0,0 @@
|
|||
Reflux = require 'reflux'
|
||||
|
||||
Actions = Reflux.createActions([
|
||||
"setRankNext",
|
||||
"clearRanks",
|
||||
"submitRanks"
|
||||
])
|
||||
|
||||
for key, action of Actions
|
||||
action.sync = true
|
||||
|
||||
module.exports = Actions
|
|
@ -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(@)
|
|
@ -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
|
|
@ -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()
|
|
@ -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" />
|
||||
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -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": {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -78,6 +78,7 @@ ThreadListStore = Reflux.createStore
|
|||
_onNamespaceChanged: -> @createView()
|
||||
|
||||
_onSearchCommitted: (query) ->
|
||||
return if @_searchQuery is query
|
||||
@_searchQuery = query
|
||||
@createView()
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) ->
|
||||
|
|
Loading…
Reference in a new issue