mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-05 20:24:26 +08:00
fix(*) Closes 517,603,608, moves search-playground to edgehill-plugins
This commit is contained in:
parent
b488ffb6a9
commit
7f3816cb67
25 changed files with 34 additions and 692 deletions
|
@ -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`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
_onNamespaceChanged: -> @createView()
|
||||||
|
|
||||||
_onSearchCommitted: (query) ->
|
_onSearchCommitted: (query) ->
|
||||||
|
return if @_searchQuery is query
|
||||||
@_searchQuery = query
|
@_searchQuery = query
|
||||||
@createView()
|
@createView()
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
Loading…
Add table
Reference in a new issue