diff --git a/app/assets/javascripts/fulcrum.js b/app/assets/javascripts/fulcrum.js index 0228e02d3..06d36013d 100644 --- a/app/assets/javascripts/fulcrum.js +++ b/app/assets/javascripts/fulcrum.js @@ -1,3 +1,20 @@ +_.mixin({ + queryParams: function(queryString) { + var params = {}; + var queries; + var temp; + + queries = queryString.split('&'); + + for (var i = 0, l = queries.length; i < l; i++) { + temp = queries[i].split('='); + params[temp[0]] = decodeURIComponent(temp[1].replace(/\+/g, ' ')); + } + + return params; + } +}); + $(function() { $('#add_story').click(function() { window.projectView.newStory(); diff --git a/app/assets/javascripts/models/story.js b/app/assets/javascripts/models/story.js index d51111719..b88c690e0 100644 --- a/app/assets/javascripts/models/story.js +++ b/app/assets/javascripts/models/story.js @@ -21,6 +21,30 @@ Fulcrum.Story = Backbone.Model.extend({ }, + matchesSearch: function(params) { + var matchesTags = true; + var matchesText = true; + + if (params.tags) { + var myTags = this.labels(); + + matchesTags = _.every(params.tags.split(','), function(tag) { + return _.contains(myTags, tag); + }) + } + + if (params.text) { + var description = this.get('description'); + var title = this.get('title'); + var text = params.text.toLowerCase(); + + matchesText = (description && description.toLowerCase().indexOf(text) > -1) || + (title && title.toLowerCase().indexOf(text) > -1); + } + + return matchesTags && matchesText; + }, + changeState: function(model, new_value) { if (new_value == "started" && !this.get('owned_by_id')) { model.set({owned_by_id: model.collection.project.current_user.id}, true); diff --git a/app/assets/javascripts/router.js b/app/assets/javascripts/router.js new file mode 100644 index 000000000..cd90759fc --- /dev/null +++ b/app/assets/javascripts/router.js @@ -0,0 +1,14 @@ +if (typeof Fulcrum == 'undefined') { + Fulcrum = {}; +} + +Fulcrum.Router = Backbone.Router.extend({ + + routes: { + 'search?:params': 'search', + '': 'home' + } + +}); + +Fulcrum.appRouter = new Fulcrum.Router(); diff --git a/app/assets/javascripts/templates/search.jst.ejs b/app/assets/javascripts/templates/search.jst.ejs new file mode 100644 index 000000000..6ad690ef6 --- /dev/null +++ b/app/assets/javascripts/templates/search.jst.ejs @@ -0,0 +1 @@ + diff --git a/app/assets/javascripts/templates/story.jst.ejs b/app/assets/javascripts/templates/story.jst.ejs index bfb8754c3..543fc0b2e 100644 --- a/app/assets/javascripts/templates/story.jst.ejs +++ b/app/assets/javascripts/templates/story.jst.ejs @@ -34,7 +34,9 @@
- <% if (story.get('labels')) { %><%= story.escape('labels') %><% } %> + <% _.each(story.labels(), function(label) { %> + <%= label %> + <% }); %> <%= story.escape('title') %> <% if (story.owned_by()) { %> diff --git a/app/assets/javascripts/views/project_view.js b/app/assets/javascripts/views/project_view.js index 7d31c8c06..427599557 100644 --- a/app/assets/javascripts/views/project_view.js +++ b/app/assets/javascripts/views/project_view.js @@ -33,6 +33,7 @@ Fulcrum.ProjectView = Backbone.View.extend({ if (_.isUndefined(column) || !_.isString(column)) { column = story.column; } + var view = new Fulcrum.StoryView({model: story}).render(); this.appendViewToColumn(view, column); view.setFocus(); diff --git a/app/assets/javascripts/views/search_view.js b/app/assets/javascripts/views/search_view.js new file mode 100644 index 000000000..98870e826 --- /dev/null +++ b/app/assets/javascripts/views/search_view.js @@ -0,0 +1,62 @@ +if (typeof Fulcrum == 'undefined') { + Fulcrum = {}; +} + +Fulcrum.SearchView = Backbone.View.extend({ + + TAGS_REGEX: /tags:([a-z,]*)/i, + + template: JST['templates/search'], + + events: { + 'change input': 'search' + }, + + initialize: function() { + var that = this; + + Fulcrum.appRouter.on('route:search', function(params) { + params = _.queryParams(params); + var search = ''; + + if (params.tags) { + search = 'tags:' + params.tags + ' '; + } + + if (params.text) { + search += params.text; + } + + that.input.val(search); + }); + }, + + render: function() { + this.$el.html(this.template); + this.input = this.$el.find('input'); + + return this; + }, + + search: function() { + var params = this.input.val(); + var tags = this.TAGS_REGEX.exec(params); + var options = {}; + + if (tags) { + options.tags = tags[1]; + params = params.replace(tags[0], ''); + } + + if (params) { + options.text = params.trim(); + } + + if (params || tags) { + Fulcrum.appRouter.navigate('search?' + $.param(options), {trigger: true}); + } else { + Fulcrum.appRouter.navigate('/', {trigger: true}); + } + } + +}); diff --git a/app/assets/javascripts/views/story_view.js b/app/assets/javascripts/views/story_view.js index d1d225d9d..3bb1b0019 100644 --- a/app/assets/javascripts/views/story_view.js +++ b/app/assets/javascripts/views/story_view.js @@ -11,7 +11,7 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({ initialize: function() { _.bindAll(this, "render", "highlight", "moveColumn", "setClassName", "transition", "estimate", "disableForm", "renderNotes", - "renderNotesCollection", "addEmptyNote"); + "renderNotesCollection", "addEmptyNote", 'searchFromRoute'); // Rerender on any relevant change to the views story this.model.bind("change", this.render); @@ -37,6 +37,8 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({ // remove itself from the page when destroy() gets called. this.model.view = this; + Fulcrum.appRouter.on('all', this.searchFromRoute); + if (this.model.id) { this.id = this.el.id = this.model.id; this.$el.attr('id', 'story-' + this.id); @@ -58,9 +60,27 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({ "click input.estimate": "estimate", "click #destroy": "clear", "click #edit-description": "editDescription", + "click .tag": 'tagClicked', "sortupdate": "sortUpdate" }, + searchFromRoute: function(route, params) { + if (route !== 'route:search') { + return this.$el.show(); + } + + var matched = this.model.matchesSearch(_.queryParams(params)); + this.$el.toggle(matched); + + if (matched) { + $(this.model.column).parent().show(); + } + }, + + tagClicked: function(event) { + event.stopPropagation(); + }, + // Triggered whenever a story is dropped to a new position sortUpdate: function(ev, ui) { @@ -334,8 +354,6 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({ }) ); - - this.$el.append( this.makeFormControl(function(div) { $(div).append(this.label("description", "Description")); @@ -361,13 +379,18 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({ ); this.initTags(); - this.renderNotes(); - } else { this.$el.removeClass('editing'); this.$el.html(this.template({story: this.model, view: this})); } + + var route = Backbone.history.getFragment().split('?'); + + if (route[0] === 'search') { + this.searchFromRoute('route:search', route[1]); + } + this.hoverBox(); return this; }, diff --git a/app/assets/stylesheets/screen.css.scss b/app/assets/stylesheets/screen.css.scss index 19399a738..499da3b26 100644 --- a/app/assets/stylesheets/screen.css.scss +++ b/app/assets/stylesheets/screen.css.scss @@ -121,9 +121,18 @@ ul#primary-nav li.secondary ul li { margin: 0; padding: 10px 1em; + #right-side { + float: right; + } + + #search { + display: inline-block; + margin-right: 15px; + } + div.velocity { + display: inline-block; font-size: 1.6em; - float: right; position: relative; div.velocity_override_container { @@ -490,7 +499,7 @@ a.button:hover { color: white; text-shadow: $darkgrey 0 1px 0; } -.tags{ +.tag{ color:$sky-blue-3; font-size:84%; } diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 8310495b4..56f39d29e 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -38,6 +38,10 @@ $(function() { new Fulcrum.ColumnVisibilityButtonView({columnView: chillyBinColumn, elementId: 'chilly_bin'}).render().el ); + new Fulcrum.SearchView({ + el: $('#search') + }).render(); + // FIXME Move to view // Connect up drag and drop behaviour $('#backlog').sortable('option', 'connectWith', '#chilly_bin,#in_progress'); @@ -68,11 +72,17 @@ $(function() { image: '<%= image_path('dialog-warning.png') %>' }); <% end %> + + Backbone.history.start(); }); <% content_for :title_bar do %> -
+
+ +
+
+ <%= render :partial => 'projects/nav', :locals => {:project => @project, :show_column_toggles => true} %> | <%= t('add story') %> diff --git a/config/locales/el.yml b/config/locales/el.yml index b489dc6ef..b75b56a38 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -230,6 +230,7 @@ el: accepted: γίνουν αποδεκτές rejected: απορριφθούν delivered: παραδοθούν + search: Αναζήτηση ... author unknown: "αγνωστος" add story: "νέα ιστορία" diff --git a/config/locales/en.yml b/config/locales/en.yml index 63d909c3f..669b7aabc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -20,6 +20,7 @@ en: points: "Points" saving: "Saving ..." expand: "Expand" + search: "Search..." author unknown: "Author Unknown" add story: "Add story" diff --git a/config/locales/es.yml b/config/locales/es.yml index bb6b404e1..981adbd40 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -9,6 +9,7 @@ es: back: "Atras" import: "Importa" export: "Exporta" + search: "Busca..." author unknown: "Autor desconocido" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 8da882644..75f16d347 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -20,6 +20,7 @@ ja: points: "ポイント" saving: "保存中…" expand: "広げる" + search: "検索..." author unknown: "作者不明" add story: "ストーリー追加" diff --git a/config/locales/nl.yml b/config/locales/nl.yml index ee0e0d505..5b38675a1 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -17,6 +17,7 @@ nl: points: "Punten" saving: "Opslaan ..." expand: "Uitvouwen" + search: "Zoeken ..." author unknown: "Auteur Onbekend" add story: "Voeg verhaal toe" diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 2bbc3f459..94b0ff9e0 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -17,6 +17,7 @@ pt-BR: points: "Pontos" saving: "Salvando ..." expand: "Expandir" + search: "Pesquisa..." author unknown: "Autor Desconhecido" add story: "Adicionar história"