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"