This commit is contained in:
Seth Call 2015-08-14 10:35:18 -05:00
parent 303e186eff
commit 45a8a6897c
7 changed files with 532 additions and 323 deletions

View File

@ -1,6 +1,28 @@
ALTER TABLE jam_tracks ADD COLUMN search_tsv tsvector;
ALTER TABLE jam_tracks ADD COLUMN artist_tsv tsvector;
ALTER TABLE jam_tracks ADD COLUMN name_tsv tsvector;
CREATE FUNCTION jam_tracks_update_tsv() RETURNS TRIGGER AS $$
BEGIN
new.search_tsv = to_tsvector('public.jamenglish', COALESCE(NEW.original_artist, '') || ' ' || COALESCE(NEW.name, '') || ' ' || COALESCE(NEW.additional_info, ''));
new.artist_tsv = to_tsvector('public.jamenglish', COALESCE(NEW.original_artist, ''));
new.name_tsv = to_tsvector('public.jamenglish', COALESCE(NEW.name, ''));
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON jam_tracks FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(search_tsv, 'public.jamenglish', original_artist, name, additional_info);
CREATE INDEX jam_track_search_tsv_index ON jam_tracks USING gin(search_tsv);
jam_tracks_update_tsv();
CREATE INDEX jam_tracks_search_tsv_index ON jam_tracks USING gin(search_tsv);
CREATE INDEX jam_tracks_artist_tsv_index ON jam_tracks USING gin(artist_tsv);
CREATE INDEX jam_tracks_name_tsv_index ON jam_tracks USING gin(name_tsv);
CREATE INDEX jam_tracks_name_key ON jam_tracks USING btree (name);
CREATE INDEX jam_tracks_original_artist_key ON jam_tracks USING btree (original_artist);
CREATE INDEX jam_tracks_status_key ON jam_tracks USING btree (status);
UPDATE jam_tracks SET original_artist=original_artist, name=name, additional_info=additional_info;

View File

@ -280,11 +280,17 @@ module JamRuby
end
if options[:artist_search]
query = query.where('original_artist ilike ?', "%#{options[:artist_search]}%")
tsquery = Search.create_tsquery(options[:artist_search])
if tsquery
query = query.where("(artist_tsv @@ to_tsquery('jamenglish', ?))", tsquery)
end
end
if options[:song_search]
query = query.where('name ilike ?', "%#{options[:song_search]}%")
tsquery = Search.create_tsquery(options[:song_search])
if tsquery
query = query.where("(name_tsv @@ to_tsquery('jamenglish', ?))", tsquery)
end
end
if options[:artist].present?
@ -369,7 +375,10 @@ module JamRuby
query = query.where("jam_tracks.status = ?", 'Production') unless user.admin
if options[:artist_search]
query = query.where('original_artist ilike ?', "%#{options[:artist_search]}%")
tsquery = Search.create_tsquery(options[:artist_search])
if tsquery
query = query.where("(artist_tsv @@ to_tsquery('jamenglish', ?))", tsquery)
end
end

View File

@ -83,7 +83,7 @@ module JamRuby
@search_type = :musicians
User.musicians
end
@results = rel.where("(name_c @@ to_tsquery('jamenglish', ?))", tsquery).limit(10)
@results = rel.where("(name_tsv @@ to_tsquery('jamenglish', ?))", tsquery).limit(10)
@results
end

View File

@ -11,6 +11,8 @@ MIX_MODES = context.JK.MIX_MODES
input: null
MAX_ARTIST_SHOW: 3
filterOption:() ->
true
render: () ->
@ -161,6 +163,7 @@ MIX_MODES = context.JK.MIX_MODES
if !@state.first_search
# only show the artists links if the user typed the results
if @state.type == 'user-input'
artistSection =
@ -186,20 +189,27 @@ MIX_MODES = context.JK.MIX_MODES
{jamtracks}
</tbody>
</table>
<div className="end-of-jamtrack-list end-of-list">No more Jamtracks</div>
</div>`
options = {}
searchValue = if @state.search == 'SEPARATOR' then '' else window.JamTrackSearchInput
`<div className="JamTrackSearchScreen">
<div className="controls">
<Select
placeholder="Search for JamTracks"
name="search-field"
asyncOptions={this.getOptions}
autoload={false}
value={window.JamTrackSearchInput}
value={searchValue}
onChange={this.onSelectChange}
onBlur={this.onSelectBlur}
className="autocompleter"
cacheAsyncResults={true}
cacheAsyncResults={false}
filterOption={this.filterOption}
/>
<button className={searchClasses} name="search" onClick={this.userSearch}>{searchText}</button>
@ -215,9 +225,9 @@ MIX_MODES = context.JK.MIX_MODES
{search: '', type: 'user-input', artists:[], jamtracks:[], show_all_artists: false, currentPage: 0, next: null, searching: false, first_search: true}
onSelectChange: (val) ->
@logger.debug("CHANGE #{val}")
#@logger.debug("CHANGE #{val}")
return unless val?
return false unless val?
search_type
if val.indexOf('ARTIST=') == 0
@ -230,6 +240,11 @@ MIX_MODES = context.JK.MIX_MODES
@search(search_type, song)
else
@logger.debug("user selected separator")
# this is to signal to the component that the separator was selected, and it has code in render to negate the selection
setTimeout((() =>
@setState({search:val})
), 1)
return false
@ -251,13 +266,15 @@ MIX_MODES = context.JK.MIX_MODES
@setState({currentPage: 0, next: null, show_all_artists: false, artists:[], jamtracks:[], type: 'user-input', searching:false, artist: null, song:null})
defaultQuery:() ->
defaultQuery:(extra) ->
query =
per_page: @LIMIT
page: @state.currentPage + 1
sort_by: 'jamtrack'
if @state.next
query.since = @state.next
query
$.extend(query, extra)
userSearch: (e) ->
@ -270,20 +287,31 @@ MIX_MODES = context.JK.MIX_MODES
window.JamTrackSearchInput = input
$root = $(@getDOMNode())
# disable scroll watching now that we've started a new search
@logger.debug("disabling infinite scroll")
$root.find('.content-body-scroller').off('scroll')
$root.find('.end-of-jamtrack-list').hide()
if input?
@rest.getJamTrackArtists({artist_search: input, limit:100})
.done((response) =>
@setState({artists:response.artists})
query = @defaultQuery()
# we have to make sure the query starts from page 1, and no 'next' from previous causes a 'since' to show up
query = @defaultQuery({page: 1})
delete query.since
@logger.debug("Search type", search_type)
if search_type == 'artist-select'
query.artist_search = input
query.artist_search = input # works with ilike
else if search_type == 'song-select'
query.song_search = input
query.song_search = input # works with ilike
else
query.search = input # works with tsv
@rest.getJamTracks(query)
.done((response) =>
@next = response.next
@setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false})
@setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1})
)
.fail(() =>
@app.notifyServerError jqXHR, 'Search Unavailable'
@ -295,7 +323,7 @@ MIX_MODES = context.JK.MIX_MODES
@setState({searching: false, first_search: false})
)
@setState({artists: [], jamtracks:[], searching: true, artist: input, song: input, type: search_type})
@setState({currentPage: 0, next: null, artists: [], jamtracks:[], searching: true, artist: input, song: input, type: search_type, search:input})
getOptions: (input, callback) =>
@ -305,7 +333,7 @@ MIX_MODES = context.JK.MIX_MODES
window.JamTrackSearchInput = input
if !input? || input.length == 0
callback(null, {options: [], complete: true})
callback(null, {options: [], complete: false})
return
@rest.autocompleteJamTracks({match:input, limit:5})
@ -316,13 +344,12 @@ MIX_MODES = context.JK.MIX_MODES
options.push { value: "ARTIST=#{artist.original_artist}", label: "Artist: #{artist.original_artist}" }
if options.length > 0 && autocomplete.songs.length > 0
options.push { value: null, label: "-------"}
options.push { value: 'SEPARATOR', label: "---------------"}
for jamtrack in autocomplete.songs
options.push { value: "SONG=#{jamtrack.name}", label: "Song: #{jamtrack.name}" }
@logger.debug("OPTIONS", options)
callback(null, {options: options, complete: true})
callback(null, {options: options, complete: false})
)
artistNavSelected: (e) ->
@ -335,6 +362,7 @@ MIX_MODES = context.JK.MIX_MODES
componentDidUpdate: ( ) ->
$root = $(this.getDOMNode())
$scroller = $root.find('.content-body-scroller')
for jamTrack in @state.jamtracks
jamtrackElement = $root.find("tbody .jamtrack-record[data-jamtrack-id=\"#{jamTrack.id}\"]")
@ -350,6 +378,49 @@ MIX_MODES = context.JK.MIX_MODES
@handleExpanded(jamtrackElement)
@registerEvents(jamtrackElement)
if @state.next == null
$scroller = $root.find('.content-body-scroller')
# if we less results than asked for, end searching
#$scroller.infinitescroll 'pause'
@logger.debug("disabling infinite scroll")
$scroller.off('scroll')
if @state.currentPage == 0 and @state.jamtracks.length == 0
#@content.append '<td colspan="3" class="no-jamtracks-msg\'>No JamTracks found.</div>'
@logger.debug("JamTrackSearch: empty search")
if @state.currentPage > 0
@logger.debug("end of search")
$noMoreJamtracks = $root.find('.end-of-jamtrack-list')
$noMoreJamtracks.show()
# there are bugs with infinitescroll not removing the 'loading'.
# it's most noticeable at the end of the list, so whack all such entries
else
@registerInfiniteScroll($scroller)
registerInfiniteScroll:($scroller) ->
@logger.debug("registering infinite scroll")
$scroller.off('scroll')
$scroller.on('scroll', () =>
# be sure to not fire off many refreshes when user hits the bottom
return if @refreshing
if $scroller.scrollTop() + $scroller.innerHeight() + 100 >= $scroller[0].scrollHeight
$scroller.append('<div class="infinite-scroll-loader-2">... Loading more JamTracks ...</div>')
@refreshing = true
@logger.debug("refreshing more jamtracks for infinite scroll")
@rest.getJamTracks(@defaultQuery({search:@state.search}))
.done((json) =>
@setState({jamtracks: @state.jamtracks.concat(json.jamtracks), next: json.next, searching: false, first_search: false, currentPage: @state.currentPage + 1})
)
.always(() =>
$scroller.find('.infinite-scroll-loader-2').remove()
@refreshing = false
)
)
playJamtrack:(e) ->
e.preventDefault()
@ -414,14 +485,15 @@ MIX_MODES = context.JK.MIX_MODES
afterShow: (data) ->
@logger.debug("afterShow")
#@logger.debug("afterShow")
beforeShow: () ->
@clearResults();
#@clearResults();
onAppInit: (@app) ->
window.JamTrackSearchInput = '' # need to be not null; otherwise react-select chokes
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@logger = context.JK.logger

View File

@ -521,6 +521,15 @@ textarea {
text-align: center;
margin:auto;
width:100%;
@include border_box_sizing;
}
.infinite-scroll-loader-2 {
height:14px;
text-align: center;
margin:auto;
@include border_box_sizing;
margin-top:10px;
}
// disable text selection for the in-session panel, ftue, and arbitrary elements marked with .no-selection-range

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,73 @@
function topPosition(domElt) {
if (!domElt) {
return 0;
}
return domElt.offsetTop + topPosition(domElt.offsetParent);
}
(function () {
if (React.addons && React.addons.InfiniteScroll) {
return React.addons.InfiniteScroll;
}
React.addons = React.addons || {};
var InfiniteScroll = React.addons.InfiniteScroll = React.createClass({
getDefaultProps: function () {
return {
pageStart: 0,
hasMore: false,
loadMore: function () {},
threshold: 250,
scrollNode: null
};
},
componentDidMount: function () {
this.pageLoaded = this.props.pageStart;
this.attachScrollListener();
},
shouldComponentUpdate: function(nextProps, nextState) {
return !_.isEqual(this.props.children, nextProps.children);
},
componentDidUpdate: function () {
this.attachScrollListener();
},
render: function () {
var props = this.props;
return React.DOM.tbody(null, props.children, props.hasMore && (props.loader || InfiniteScroll._defaultLoader));
},
scrollListener: function () {
var el = this.props.scrollNode ? $(this.getDOMNode()).closest(this.props.scrollNode).get(0) : this.getDOMNode();
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
console.log("scrollTop", scrollTop)
if (topPosition(el) + el.offsetHeight - scrollTop - window.innerHeight < Number(this.props.threshold)) {
this.detachScrollListener();
// call loadMore after detachScrollListener to allow
// for non-async loadMore functions
this.props.loadMore(this.pageLoaded += 1);
}
},
attachScrollListener: function () {
if (!this.props.hasMore) {
return;
}
console.log("attachScrollListener")
window.addEventListener('scroll', this.scrollListener);
window.addEventListener('resize', this.scrollListener);
setTimeout(
this.scrollListener,
1
);
},
detachScrollListener: function () {
window.removeEventListener('scroll', this.scrollListener);
window.removeEventListener('resize', this.scrollListener);
},
componentWillUnmount: function () {
this.detachScrollListener();
}
});
InfiniteScroll.setDefaultLoader = function (loader) {
InfiniteScroll._defaultLoader = loader;
};
return InfiniteScroll;
})();