diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index d62f6aa0..56da269e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -47,10 +47,11 @@ class ProjectsController < ApplicationController @not_done = @project.not_done_todos_including_hidden @deferred = @project.deferred_todos + @pending = @project.pending_todos @done = @project.todos.find_in_state(:all, :completed, :order => "todos.completed_at DESC", :limit => current_user.prefs.show_number_completed, :include => [:context]) @count = @not_done.size - @down_count = @count + @deferred.size + @down_count = @count + @deferred.size + @pending.size @next_project = current_user.projects.next_from(@project) @previous_project = current_user.projects.previous_from(@project) @default_tags = @project.default_tags diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 949d94bf..affd101b 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -6,9 +6,9 @@ class TodosController < ApplicationController prepend_before_filter :login_or_feed_token_required, :only => [:index, :calendar] append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred, :toggle_check, :toggle_star, - :edit, :update, :create, :calendar, :auto_complete_for_tag] - append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy ] - protect_from_forgery :except => [:auto_complete_for_tag] + :edit, :update, :create, :calendar, :auto_complete_for_tag, :auto_complete_for_predecessor, :remove_predecessor, :add_predecessor] + append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy, :remove_predecessor] + protect_from_forgery :except => [:auto_complete_for_tag, :auto_complete_for_predecessor] session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) } @@ -54,6 +54,7 @@ class TodosController < ApplicationController p = TodoCreateParamsHelper.new(params, prefs) p.parse_dates() unless mobile? tag_list = p.tag_list + predecessor_list = p.predecessor_list @todo = current_user.todos.build(p.attributes) @@ -70,13 +71,21 @@ class TodosController < ApplicationController @todo.context_id = context.id end + @todo.add_predecessor_list(predecessor_list) @todo.update_state_from_project @saved = @todo.save unless (@saved == false) || tag_list.blank? @todo.tag_with(tag_list) @todo.tags.reload end - + + unless (@aved == false) + unless @todo.uncompleted_predecessors.empty? || @todo.state == 'project_hidden' + @todo.state = 'pending' + end + @todo.save + end + respond_to do |format| format.html { redirect_to :action => "index" } format.m do @@ -130,6 +139,30 @@ class TodosController < ApplicationController format.xml { render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ) } end end + + def add_predecessor + @source_view = params['_source_view'] || 'todo' + @predecessor = Todo.find(params['predecessor']) + @todo = Todo.find(params['successor']) + @original_state = @todo.state + # Add predecessor + @todo.add_predecessor(@predecessor) + @todo.state = 'pending' + @saved = @todo.save + respond_to do |format| + format.js + end + end + + def remove_predecessor + @source_view = params['_source_view'] || 'todo' + @predecessor = Todo.find(params['predecessor']) + @successor = @todo + @removed = @successor.remove_predecessor(@predecessor) + respond_to do |format| + format.js + end + end # Toggles the 'done' status of the action # @@ -142,6 +175,18 @@ class TodosController < ApplicationController # check if this todo has a related recurring_todo. If so, create next todo @new_recurring_todo = check_for_next_todo(@todo) if @saved + if @todo.completed? + @pending_to_activate = @todo.pending_to_activate + @pending_to_activate.each do |t| + t.activate! + end + else + @active_to_block = @todo.active_to_block + @active_to_block.each do |t| + t.block! + end + end + respond_to do |format| format.js do if @saved @@ -190,7 +235,8 @@ class TodosController < ApplicationController @original_item_project_id = @todo.project_id @original_item_was_deferred = @todo.deferred? @original_item_due = @todo.due - @original_item_due_id = get_due_id_for_calendar(@todo.due) + @original_item_due_id = get_due_id_for_calendar(@todo.due) + @original_item_predecessor_list = @todo.predecessors.map{|t| t.specification}.join(', ') if params['todo']['project_id'].blank? && !params['project_name'].nil? if params['project_name'] == 'None' @@ -231,15 +277,38 @@ class TodosController < ApplicationController if params['done'] == '1' && !@todo.completed? @todo.complete! + @todo.pending_to_activate.each do |t| + t.activate! + end end # strange. if checkbox is not checked, there is no 'done' in params. # Therefore I've used the negation if !(params['done'] == '1') && @todo.completed? @todo.activate! + @todo.active_to_block.each do |t| + t.block! + end end @todo.attributes = params["todo"] + + @todo.add_predecessor_list(params[:predecessor_list]) @saved = @todo.save + if @saved && params[:predecessor_list] + if @original_item_predecessor_list != params[:predecessor_list] + # Possible state change with new dependencies + if @todo.uncompleted_predecessors.empty? + if @todo.state == 'pending' + @todo.activate! # Activate pending if no uncompleted predecessors + end + else + if @todo.state == 'active' + @todo.block! # Block active if we got uncompleted predecessors + end + end + end + @todo.save! + end @context_changed = @original_item_context_id != @todo.context_id @todo_was_activated_from_deferred_state = @original_item_was_deferred && @todo.active? @@ -297,16 +366,30 @@ class TodosController < ApplicationController @context_id = @todo.context_id @project_id = @todo.project_id + # activate successors if they only depend on this todo + activated_successor_count = 0 + @todo.pending_successors.each do |successor| + successor.uncompleted_predecessors.delete(@todo) + if successor.uncompleted_predecessors.empty? + successor.activate! + activated_successor_count += 1 + end + end + @saved = @todo.destroy # check if this todo has a related recurring_todo. If so, create next todo @new_recurring_todo = check_for_next_todo(@todo) if @saved - + respond_to do |format| format.html do if @saved - notify :notice, "Successfully deleted next action", 2.0 + message = "Successfully deleted next action" + if activated_successor_count > 0 + message += " activated #{pluralize(activated_successor_count, 'pending action')}" + end + notify :notice, message, 2.0 redirect_to :action => 'index' else notify :error, "Failed to delete the action", 2.0 @@ -356,7 +439,7 @@ class TodosController < ApplicationController @contexts_to_show = @contexts = current_user.contexts.find(:all, :include => [ :todos ]) current_user.deferred_todos.find_and_activate_ready - @not_done_todos = current_user.deferred_todos + @not_done_todos = current_user.deferred_todos + current_user.pending_todos @count = @not_done_todos.size @down_count = @count @@ -409,6 +492,9 @@ class TodosController < ApplicationController @deferred = tag_collection.find(:all, :conditions => ['todos.user_id = ? and state = ?', current_user.id, 'deferred'], :order => 'show_from ASC, todos.created_at DESC') + @pending = tag_collection.find(:all, + :conditions => ['todos.user_id = ? and state = ?', current_user.id, 'pending'], + :order => 'show_from ASC, todos.created_at DESC') # If you've set no_completed to zero, the completed items box isn't shown on # the tag page @@ -513,6 +599,46 @@ class TodosController < ApplicationController render :inline => "<%= auto_complete_result(@items, :name) %>" end + def auto_complete_for_predecessor + unless params['id'].nil? + get_todo_from_params + # Begin matching todos in current project + @items = current_user.todos.find(:all, + :select => 'description, project_id, context_id, created_at', + :conditions => [ '(todos.state = ? OR todos.state = ?) AND ' + + 'NOT (id = ?) AND lower(description) LIKE ? AND project_id = ?', + 'active', 'pending', + @todo.id, + '%' + params[:predecessor_list].downcase + '%', + @todo.project_id ], + :order => 'description ASC', + :limit => 10 + ) + if @items.empty? # Match todos in other projects + @items = current_user.todos.find(:all, + :select => 'description, project_id, context_id, created_at', + :conditions => [ '(todos.state = ? OR todos.state = ?) AND ' + + 'NOT (id = ?) AND lower(description) LIKE ?', + 'active', 'pending', + params[:id], '%' + params[:predecessor_list].downcase + '%' ], + :order => 'description ASC', + :limit => 10 + ) + end + else + # New todo - TODO: Filter on project + @items = current_user.todos.find(:all, + :select => 'description, project_id, context_id, created_at', + :conditions => [ '(todos.state = ? OR todos.state = ?) AND lower(description) LIKE ?', + 'active', 'pending', + '%' + params[:predecessor_list].downcase + '%' ], + :order => 'description ASC', + :limit => 10 + ) + end + render :inline => "<%= auto_complete_result2(@items) %>" + end + private def get_todo_from_params @@ -670,6 +796,7 @@ class TodosController < ApplicationController unless @todo.project_id == nil @down_count = current_user.projects.find(@todo.project_id).not_done_todos_including_hidden.count @deferred_count = current_user.projects.find(@todo.project_id).deferred_todos.count + @pending_count = current_user.projects.find(@todo.project_id).pending_todos.count end end from.deferred do @@ -956,6 +1083,10 @@ class TodosController < ApplicationController def tag_list @params['tag_list'] end + + def predecessor_list + @params['predecessor_list'] + end def parse_dates() @attributes['show_from'] = @prefs.parse_date(show_from) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index b39d5674..6721059c 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -64,7 +64,7 @@ module TodosHelper :complete => todo_stop_waiting_js(todo)) end end - + def todo_start_waiting_js(todo) return "$('#ul#{dom_id(todo)}').css('visibility', 'hidden'); $('##{dom_id(todo)}').block({message: null})" end @@ -82,12 +82,15 @@ module TodosHelper def remote_toggle_checkbox - check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox') + check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox', + :title => @todo.pending? ? 'Blocked by ' + @todo.uncompleted_predecessors.map(&:description).join(', ') : "", :readonly => @todo.pending?) end def date_span if @todo.completed? "#{format_date( @todo.completed_at )}" + elsif @todo.pending? + "Pending " elsif @todo.deferred? show_date( @todo.show_from ) else @@ -95,6 +98,22 @@ module TodosHelper end end + def successors_span + unless @todo.pending_successors.empty? + pending_count = @todo.pending_successors.length + title = "Has #{pluralize(pending_count, 'pending action')}: #{@todo.pending_successors.map(&:description).join(', ')}" + image_tag( 'successor_off.png', :width=>'10', :height=>'16', :border=>'0', :title => title ) + end + end + + def grip_span + unless @todo.completed? + image_tag('grip.png', :width => '7', :height => '16', :border => '0', + :title => 'Drag onto another action to make it depend on that action', + :class => 'grip') + end + end + def tag_list_text @todo.tags.collect{|t| t.name}.join(', ') end @@ -115,6 +134,10 @@ module TodosHelper if tag_list.empty? then "" else "#{tag_list}" end end + def predecessor_list_text + @todo.predecessors.map{|t| t.specification}.join(', ') + end + def deferred_due_date if @todo.deferred? && @todo.due "(action due on #{format_date(@todo.due)})" @@ -201,9 +224,10 @@ module TodosHelper end def item_container_id (todo) - if source_view_is :project - return "p#{todo.project_id}items" if todo.active? - return "tickler" if todo.deferred? + if todo.deferred? or todo.pending? + return "tickleritems" + elsif source_view_is :project + return "p#{todo.project_id}items" end return "c#{todo.context_id}items" end @@ -221,6 +245,8 @@ module TodosHelper return true if source_view_is(:project) && @todo.project.hidden? && @todo.project_hidden? return true if source_view_is(:project) && @todo.deferred? return true if !source_view_is(:deferred) && @todo.active? + return true if source_view_is(:project) && @todo.pending? + return true if source_view_is(:tag) && @todo.pending? return false end @@ -281,4 +307,14 @@ module TodosHelper class_str = todo.starred? ? "starred_todo" : "unstarred_todo" image_tag("blank.png", :title =>"Star action", :class => class_str) end + + def auto_complete_result2(entries, phrase = nil) + return unless entries + items = entries.map do |entry| + item = entry.specification() + content_tag("li", phrase ? highlight(h(item), phrase) : h(item)) + end + content_tag("ul", items.uniq) + end + end diff --git a/app/models/dependency.rb b/app/models/dependency.rb new file mode 100644 index 00000000..dd347c1f --- /dev/null +++ b/app/models/dependency.rb @@ -0,0 +1,7 @@ +class Dependency < ActiveRecord::Base + + belongs_to :predecessor, :foreign_key => 'predecessor_id', :class_name => 'Todo' + belongs_to :successor, :foreign_key => 'successor_id', :class_name => 'Todo' + +end + diff --git a/app/models/project.rb b/app/models/project.rb index b5dbd3c1..88053a1c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -20,6 +20,11 @@ class Project < ActiveRecord::Base :class_name => 'Todo', :conditions => ["todos.state = ? ", "deferred"], :order => "show_from" + has_many :pending_todos, + :include => [:context,:tags,:project], + :class_name => 'Todo', + :conditions => ["todos.state = ? ", "pending"], + :order => "show_from" has_many :notes, :dependent => :delete_all, :order => "created_at DESC" diff --git a/app/models/todo.rb b/app/models/todo.rb index d608e56f..df7aca4f 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -4,12 +4,26 @@ class Todo < ActiveRecord::Base belongs_to :project belongs_to :user belongs_to :recurring_todo + + has_many :predecessor_dependencies, :foreign_key => 'predecessor_id', :class_name => 'Dependency', :dependent => :destroy + has_many :successor_dependencies, :foreign_key => 'successor_id', :class_name => 'Dependency', :dependent => :destroy + has_many :predecessors, :through => :successor_dependencies + has_many :successors, :through => :predecessor_dependencies + has_many :uncompleted_predecessors, :through => :successor_dependencies, + :source => :predecessor, :conditions => ['NOT (state = ?)', 'completed'] + has_many :pending_successors, :through => :predecessor_dependencies, + :source => :successor, :conditions => ['state = ?', 'pending'] + + after_save :save_predecessors named_scope :active, :conditions => { :state => 'active' } named_scope :not_completed, :conditions => ['NOT (state = ? )', 'completed'] named_scope :are_due, :conditions => ['NOT (todos.due IS NULL)'] STARRED_TAG_NAME = "starred" + RE_TODO = /[^"]+/ + RE_CONTEXT = /[^"]+/ + RE_PROJECT = /[^"]+/ acts_as_state_machine :initial => :active, :column => 'state' @@ -19,6 +33,7 @@ class Todo < ActiveRecord::Base state :project_hidden state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil } state :deferred + state :pending event :defer do transitions :to => :deferred, :from => [:active] @@ -30,6 +45,8 @@ class Todo < ActiveRecord::Base event :activate do transitions :to => :active, :from => [:project_hidden, :completed, :deferred] + transitions :to => :active, :from => [:pending], :guard => :no_uncompleted_predecessors_or_deferral? + transitions :to => :deferred, :from => [:pending], :guard => :no_uncompleted_predecessors? end event :hide do @@ -40,6 +57,10 @@ class Todo < ActiveRecord::Base transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? } transitions :to => :active, :from => [:project_hidden] end + + event :block do + transitions :to => :pending, :from => [:active, :deferred] + end attr_protected :user @@ -51,15 +72,120 @@ class Todo < ActiveRecord::Base validates_presence_of :show_from, :if => :deferred? validates_presence_of :context + def initialize(*args) + super(*args) + @predecessor_array = nil # Used for deferred save of predecessors + end + + def no_uncompleted_predecessors_or_deferral? + return (show_from.blank? or Time.zone.now > show_from and uncompleted_predecessors.empty?) + end + + def no_uncompleted_predecessors? + return uncompleted_predecessors.empty? + end + + + # Returns a string with description + def specification + project_name = project.is_a?(NullProject) ? "(none)" : project.name + return "\"#{description}\" <\"#{context.title}\"; \"#{project_name}\">" + end + + def todo_from_specification(specification) + # Split specification into parts: description + re_parts = /"(#{RE_TODO})"\s<"(#{RE_CONTEXT})";\s"(#{RE_PROJECT})">/ + parts = specification.scan(re_parts) + return nil unless parts.length == 1 + return nil unless parts[0].length == 3 + todo_description = parts[0][0] + context_name = parts[0][1] + todos = Todo.all( + :joins => :context, + :conditions => { + :description => todo_description, + :contexts => {:name => context_name} + } + ) + return nil if todos.empty? + # todos now contains all todos with matching description and context + # TODO: Is this possible to do with a single query? + todos.each do |todo| + project_name = todo.project.is_a?(NullProject) ? "(none)" : todo.project.name + return todo if project_name == parts[0][2] + end + return nil + end + def validate if !show_from.blank? && show_from < user.date errors.add("show_from", "must be a date in the future") end + errors.add(:description, "may not contain \" characters") if /\"/.match(description) + unless @predecessor_array.nil? # Only validate predecessors if they changed + @predecessor_array.each do |specification| + t = todo_from_specification(specification) + if t.nil? + errors.add("Depends on:", "Could not find action '#{h(specification)}'") + else + errors.add("Depends on:", "Adding '#{h(specification)}' would create a circular dependency") if is_successor?(t) + end + end + end + end + + def save_predecessors + unless @predecessor_array.nil? # Only save predecessors if they changed + current_array = predecessors.map{|p| p.specification} + remove_array = current_array - @predecessor_array + add_array = @predecessor_array - current_array + + # This is probably a bit naive code... + remove_array.each do |specification| + t = todo_from_specification(specification) + self.predecessors.delete(t) unless t.nil? + end + # ... as is this? + add_array.each do |specification| + t = todo_from_specification(specification) + unless t.nil? + self.predecessors << t unless self.predecessors.include?(t) + else + logger.error "Could not find #{specification}" # Unexpected since validation passed + end + end + end + end + + def remove_predecessor(predecessor) + # remove predecessor and activate myself + predecessors.delete(predecessor) + self.activate! + end + + # Returns true if t is equal to self or a successor of self + def is_successor?(t) + if self == t + return true + elsif self.successors.empty? + return false + else + self.successors.each do |item| + if item.is_successor?(t) + return true + end + end + end + return false end def update_state_from_project if state == 'project_hidden' and !project.hidden? - self.state = 'active' + if self.uncompleted_predecessors.empty? + self.state = 'active' + else + self.state = 'pending' + end elsif state == 'active' and project.hidden? self.state = 'project_hidden' end @@ -92,7 +218,7 @@ class Todo < ActiveRecord::Base def project original_project.nil? ? Project.null_object : original_project end - + alias_method :original_set_initial_state, :set_initial_state def set_initial_state @@ -138,6 +264,28 @@ class Todo < ActiveRecord::Base def from_recurring_todo? return self.recurring_todo_id != nil end + + def add_predecessor_list(predecessor_list) + return unless predecessor_list.kind_of? String + # Split into list + re_specification = /"#{RE_TODO}"\s<"#{RE_CONTEXT}";\s"#{RE_PROJECT}">/ + @predecessor_array = predecessor_list.scan(re_specification) + end + + def add_predecessor(t) + @predecessor_array = predecessors.map{|p| p.specification} + @predecessor_array << t.specification + end + + # Return todos that should be activated if the current todo is completed + def pending_to_activate + return successors.find_all {|t| t.uncompleted_predecessors.empty?} + end + + # Return todos that should be blocked if the current todo is undone + def active_to_block + return successors.find_all {|t| t.active? or t.deferred?} + end # Rich Todo API diff --git a/app/models/user.rb b/app/models/user.rb index 6ce58aa8..30c5a6fb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -98,6 +98,10 @@ class User < ActiveRecord::Base find(:all, :conditions => ['show_from <= ?', Time.zone.now ]).collect { |t| t.activate! } end end + has_many :pending_todos, + :class_name => 'Todo', + :conditions => [ 'state = ?', 'pending' ], + :order => 'show_from ASC, todos.created_at DESC' has_many :completed_todos, :class_name => 'Todo', :conditions => ['todos.state = ? AND NOT(todos.completed_at IS NULL)', 'completed'], diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 0ed68cbd..efa84ab9 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -4,7 +4,7 @@ <%= render :partial => "projects/project", :locals => { :project => @project, :collapsible => false } %> - <%= render :partial => "todos/deferred", :locals => { :deferred => @deferred, :collapsible => false, :append_descriptor => "in this project", :parent_container_type => 'project' } %> + <%= render :partial => "todos/deferred", :locals => { :deferred => @deferred, :collapsible => false, :append_descriptor => "in this project", :parent_container_type => 'project', :pending => @pending } %> <% unless @max_completed==0 -%> <%= render :partial => "todos/completed", :locals => { :done => @done, :collapsible => false, :suppress_project => true, :append_descriptor => "in this project" } %> <% end -%> diff --git a/app/views/shared/_add_new_item_form.rhtml b/app/views/shared/_add_new_item_form.rhtml index ca2c2032..7b2f313f 100644 --- a/app/views/shared/_add_new_item_form.rhtml +++ b/app/views/shared/_add_new_item_form.rhtml @@ -51,6 +51,14 @@ <%= text_field("todo", "show_from", "size" => 12, "class" => "Date", "tabindex" => 7, "autocomplete" => "off") %> + + <%= text_field_tag "predecessor_list", nil, :size => 30, :tabindex => 8 %> + <%= content_tag("div", "", :id => "predecessor_list_auto_complete", :class => "auto_complete") %> + <%= auto_complete_field 'predecessor_list', { + :url => {:controller => 'todos', :action => 'auto_complete_for_predecessor', :id => nil}, + :tokens => [','] + } %> + <%= source_view_tag( @source_view ) %> <%= hidden_field_tag :_tag_name, @tag_name.underscore.gsub(/\s+/,'_') if source_view_is :tag %> diff --git a/app/views/todos/_deferred.rhtml b/app/views/todos/_deferred.rhtml index 58c16756..c38e1561 100644 --- a/app/views/todos/_deferred.rhtml +++ b/app/views/todos/_deferred.rhtml @@ -3,15 +3,16 @@ <% if collapsible %> <%= image_tag("collapse.png") %> <% end %> - Deferred actions <%= append_descriptor ? append_descriptor : '' %> + Deferred/pending actions <%= append_descriptor ? append_descriptor : '' %>
-
-

Currently there are no deferred actions

+
+

Currently there are no deferred or pending actions

<%= render :partial => "todos/todo", :collection => deferred, :locals => { :parent_container_type => parent_container_type } %> + <%= render :partial => "todos/todo", :collection => pending, :locals => { :parent_container_type => parent_container_type } %>
-
\ No newline at end of file + diff --git a/app/views/todos/_edit_form.rhtml b/app/views/todos/_edit_form.rhtml index e471984e..2f8e4bc5 100644 --- a/app/views/todos/_edit_form.rhtml +++ b/app/views/todos/_edit_form.rhtml @@ -46,13 +46,21 @@ + +<%= text_field_tag 'predecessor_list', predecessor_list_text, :id => dom_id(@todo, 'predecessor_list'), :size => 30, :tabindex => 15 %> +<%= content_tag("div", "", :id => dom_id(@todo, 'predecessor_list')+"_auto_complete", :class => "auto_complete") %> +<%= auto_complete_field dom_id(@todo, 'predecessor_list'), { + :url => {:controller => 'todos', :action => 'auto_complete_for_predecessor', :id => @todo.id}, + :tokens => [','] +} %> + <% if controller.controller_name == "project" || @todo.deferred? -%> <% end -%>
- diff --git a/app/views/todos/_successor.html.erb b/app/views/todos/_successor.html.erb new file mode 100644 index 00000000..49b2ac63 --- /dev/null +++ b/app/views/todos/_successor.html.erb @@ -0,0 +1,27 @@ +<% +suppress_context ||= false +suppress_project ||= false +suppress_dependencies ||= false +parameters = "_source_view=#{@source_view}" +parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag' +@z_index_counter = @z_index_counter - 1 # for IE z-index bug +%> +
+
+
+ <%= h sanitize(successor.description) %> + + <%= link_to_remote( + image_tag("blank.png", :title => "Remove dependency (does not delete the action)", :align => "absmiddle", :class => "delete_item"), + {:url => {:controller => 'todos', :action => 'remove_predecessor', :id => successor.id}, + :method => 'delete', + :with => "'#{parameters}&predecessor=#{predecessor.id}'", + :before => todo_start_waiting_js(dom_id(successor, 'successor')), + :complete => todo_stop_waiting_js}, + {:style => "background: transparent;"}) %> + + <%= render(:partial => "todos/toggle_successors", :locals => { :item => successor, :suppress_button => true }) unless successor.pending_successors.empty? %> +
+
+
+ diff --git a/app/views/todos/_todo.html.erb b/app/views/todos/_todo.html.erb index 25f8d372..85cfbc49 100644 --- a/app/views/todos/_todo.html.erb +++ b/app/views/todos/_todo.html.erb @@ -26,13 +26,16 @@ parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
+ <%= grip_span %> <%= date_span -%> <%= h sanitize(todo.description) %> + <% #= successors_span %> <%= image_tag_for_recurring_todo(todo) if @todo.from_recurring_todo? %> <%= tag_list %> <%= deferred_due_date %> <%= project_and_context_links( parent_container_type, :suppress_context => suppress_context, :suppress_project => suppress_project ) %> <%= render(:partial => "todos/toggle_notes", :locals => { :item => todo }) if todo.notes? %> + <%= render(:partial => "todos/toggle_successors", :locals => { :item => todo }) unless todo.pending_successors.empty? %>
+<%= +draggable_element(dom_id(todo), :revert => "'true'", :handle => "'grip'", :onDrop => "''") +%> +<%= +drop_receiving_element(dom_id(todo), + :url => {:controller => "todos", :action => "add_predecessor"}, + :with => "'#{parameters}&successor=' + encodeURIComponent(element.id.split('_').last()) + '&predecessor=' + encodeURIComponent(#{todo.id})", + :hoverclass => 'hover' +) +%> + diff --git a/app/views/todos/_toggle_successors.html.erb b/app/views/todos/_toggle_successors.html.erb new file mode 100644 index 00000000..47efc3df --- /dev/null +++ b/app/views/todos/_toggle_successors.html.erb @@ -0,0 +1,15 @@ +<% +suppress_button ||= false +%> +<%= link_to(image_tag( 'blank.png', :width=>'16', :height=>'16', :border=>'0' ), "#", {:class => 'show_successors', :title => 'Show successors'}) unless suppress_button %> + +
> + <%= render :partial => "todos/successor", + :collection => item.pending_successors, + :locals => { :todo => item, + :parent_container_type => parent_container_type, + :suppress_dependencies => true, + :predecessor => item } + %> +
+ diff --git a/app/views/todos/add_predecessor.js.rjs b/app/views/todos/add_predecessor.js.rjs new file mode 100644 index 00000000..714a5956 --- /dev/null +++ b/app/views/todos/add_predecessor.js.rjs @@ -0,0 +1,28 @@ +if @saved + # show update message + status_message = "Added #{@predecessor.description} as dependency." + unless @original_state == 'pending' + status_message += " #{@todo.description} set to pending" + end + # remove successor from page + page[@todo].remove + # regenerate predecessor to add arrow + page[@predecessor].replace_html :partial => 'todos/todo', :locals => { :todo => @predecessor, :parent_container_type => parent_container_type } + + # show in tickler box in project view + if source_view_is_one_of :project, :tag + page['tickler-empty-nd'].hide + page.replace "tickler", :partial => 'todos/deferred', :locals => { :deferred => @todo.project.deferred_todos, + :collapsible => false, + :append_descriptor => "in this project", + :parent_container_type => 'project', + :pending => @todo.project.pending_todos } + end + + page << "TodoBehavior.enableToggleNotes();" + page << "TodoBehavior.enableToggleSuccessors();" + page.notify :notice, status_message, 5.0 +else + page.replace_html "status", content_tag("div", content_tag("h2", "Unable to add dependency"), "id" => "errorExplanation", "class" => "errorExplanation") +end + diff --git a/app/views/todos/create.js.rjs b/app/views/todos/create.js.rjs index b82a7b24..61943463 100644 --- a/app/views/todos/create.js.rjs +++ b/app/views/todos/create.js.rjs @@ -2,6 +2,7 @@ if @saved page.hide 'status' status_message = 'Added new next action' status_message += ' to tickler' if @todo.deferred? + status_message += ' in pending state' if @todo.pending? status_message = 'Added new project / ' + status_message if @new_project_created status_message = 'Added new context / ' + status_message if @new_context_created page.notify :notice, status_message, 5.0 @@ -21,9 +22,20 @@ if @saved page.visual_effect :highlight, dom_id(@todo), :duration => 3 page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil? end - # make sure the behavior of the new/updated todo is enabled - page['tickler-empty-nd'].hide if source_view_is :deferred + if (source_view_is :project and @todo.pending?) or (source_view_is :deferred) + page['tickler-empty-nd'].hide # For some reason this does not work: page['tickler-empty-nd'].hide if (@todo.pending? or (source_view_is :deferred)) + end end + # Update predecessors (if they exist and are visible) + @todo.uncompleted_predecessors.each do |p| + page << "if ($(\'#{item_container_id(p)}\')) {" + page[p].replace_html :partial => 'todos/todo', + :locals => { :todo => p, :parent_container_type => parent_container_type } + page << "}" + end + # make sure the behavior of the new/updated todo is enabled + page << "TodoBehavior.enableToggleNotes()" + page << "TodoBehavior.enableToggleSuccessors()" else page.show 'status' page.replace_html 'status', "#{error_messages_for('todo', :object_name => 'action')}" diff --git a/app/views/todos/remove_predecessor.js.rjs b/app/views/todos/remove_predecessor.js.rjs new file mode 100644 index 00000000..ea190cea --- /dev/null +++ b/app/views/todos/remove_predecessor.js.rjs @@ -0,0 +1,27 @@ +if @removed + status_message = "Removed #{@successor.description} as dependency from #{@predecessor.description}." + page.notify :notice, status_message, 5.0 + + # replace old predecessor with one without the successor + page.replace dom_id(@predecessor), :partial => 'todos/todo', :locals => { + :todo => @predecessor, :parent_container_type => parent_container_type } + + # update display if pending->active + if @successor.active? + page[@successor].remove unless source_view_is_one_of(:todo, :context) + page.insert_html :bottom, item_container_id(@successor), :partial => 'todos/todo', :locals => { + :todo => @successor, :parent_container_type => parent_container_type } + page.visual_effect :highlight, dom_id(@successor, 'line'), {'startcolor' => "'#99ff99'"} + end + + # update display if pending->deferred + if @successor.deferred? + page.replace dom_id(@successor), :partial => 'todos/todo', :locals => { + :todo => @successor, :parent_container_type => parent_container_type } + end + + page << "TodoBehavior.enableToggleNotes()" + page << "TodoBehavior.enableToggleSuccessors()" +else + page.notify :error, "There was an error removing the dependency", 8.0 +end diff --git a/app/views/todos/tag.html.erb b/app/views/todos/tag.html.erb index c6418da5..7804c9b9 100644 --- a/app/views/todos/tag.html.erb +++ b/app/views/todos/tag.html.erb @@ -8,7 +8,13 @@ :locals => { :collapsible => true } %> <% unless @deferred.nil? -%> - <%= render :partial => "todos/deferred", :locals => { :deferred => @deferred, :collapsible => true, :append_descriptor => "tagged with ‘#{@tag_name}’", :parent_container_type => 'tag' } %> + <%= render :partial => "todos/deferred", :locals => { + :deferred => @deferred, + :pending => @pending, + :collapsible => true, + :append_descriptor => "tagged with ‘#{@tag_name}’", + :parent_container_type => 'tag' + } %> <% end -%> <% unless @hidden_todos.nil? -%> diff --git a/app/views/todos/toggle_check.js.rjs b/app/views/todos/toggle_check.js.rjs index 5f9e2734..bd2ff2b6 100644 --- a/app/views/todos/toggle_check.js.rjs +++ b/app/views/todos/toggle_check.js.rjs @@ -8,9 +8,16 @@ if @saved page.insert_html :top, "completed_containeritems", :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => "completed" } page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"} page[empty_container_msg_div_id].show if @down_count == 0 && !empty_container_msg_div_id.nil? - page.show 'tickler-empty-nd' if source_view_is(:project) && @deferred_count == 0 + page.show 'tickler-empty-nd' if source_view_is(:project) && @deferred_count == 0 && @pending_count == 0 page.hide 'empty-d' # If we've checked something as done, completed items can't be empty end + # Activate pending todos that are successors of the completed + @pending_to_activate.each do |t| + logger.debug "#300: Removing #{t.description} from pending block and adding it to active" + page[t].remove if source_view_is(:project) or source_view_is(:tag) + page.insert_html :bottom, item_container_id(t), :partial => 'todos/todo', :locals => { :todo => t, :parent_container_type => parent_container_type } + page.visual_effect :highlight, dom_id(t, 'line'), {'startcolor' => "'#99ff99'"} + end # remove container if empty if @remaining_in_context == 0 && source_view_is(:todo) @@ -44,6 +51,15 @@ if @saved page.show "empty-d" if @completed_count == 0 page.show "c"+@todo.context.id.to_s page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil? # If we've checked something as undone, incomplete items can't be empty + # If active todos are successors of the reactivated todo they will be blocked + @active_to_block.each do |t| + logger.debug "#300: Block #{t.description} that are a successor of #{@todo.description}" + page[t].remove # Remove it from active + if source_view_is(:project) or source_view_is(:tag) # Insert it in deferred/pending block if existing + logger.debug "Insert #{t.description} in #{item_container_id(t)} block" + page.insert_html :bottom, item_container_id(t), :partial => 'todos/todo', :locals => { :todo => t, :parent_container_type => parent_container_type } + end + end end page.hide "status" diff --git a/app/views/todos/update.js.rjs b/app/views/todos/update.js.rjs index 8a535028..6cdf7367 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -7,7 +7,7 @@ if @saved page.notify :notice, status_message, 5.0 if source_view_is_one_of(:todo, :context, :tag) - if @context_changed || @todo.deferred? + if @context_changed || @todo.deferred? || @todo.pending? page[@todo].remove if (@remaining_in_context == 0) @@ -87,7 +87,14 @@ if @saved page["p#{@todo.project_id}empty-nd"].hide page.replace_html "badge_count", @down_count else - page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type } + page.replace_html "p#{@todo.project_id}items", :partial => 'todos/todo', :collection => @todo.project.not_done_todos, + :locals => { :parent_container_type => parent_container_type } + page.replace "tickler", :partial => 'todos/deferred', :locals => { :deferred => @todo.project.deferred_todos, + :collapsible => false, + :append_descriptor => "in this project", + :parent_container_type => 'project', + :pending => @todo.project.pending_todos } + page['tickler-empty-nd'].show if (@deferred_count == 0 and @pending_count == 0) page.visual_effect :highlight, dom_id(@todo), :duration => 3 end elsif source_view_is :deferred @@ -135,6 +142,13 @@ if @saved else logger.error "unexpected source_view '#{params[:_source_view]}'" end + # Update predecessors (if they exist and are visible) + @todo.uncompleted_predecessors.each do |p| + page << "if ($(\'#{item_container_id(p)}\')) {" + page[p].replace_html :partial => 'todos/todo', + :locals => { :todo => p, :parent_container_type => parent_container_type } + page << "}" + end else page.show 'error_status' page.replace_html 'error_status', "#{error_messages_for('todo')}" diff --git a/db/migrate/20090516000646_add_todo_dependencies.rb b/db/migrate/20090516000646_add_todo_dependencies.rb new file mode 100644 index 00000000..2ca58dae --- /dev/null +++ b/db/migrate/20090516000646_add_todo_dependencies.rb @@ -0,0 +1,13 @@ +class AddTodoDependencies < ActiveRecord::Migration + def self.up + create_table :dependencies do |t| + t.integer :successor_id, :null => false + t.integer :predecessor_id, :null => false + t.string :relationship_type + end + end + + def self.down + drop_table :dependencies + end +end diff --git a/db/tracks-17-blank.db b/db/tracks-17-blank.db index 941c0958..fdbc4ca6 100644 Binary files a/db/tracks-17-blank.db and b/db/tracks-17-blank.db differ diff --git a/db/tracks-17-test.db b/db/tracks-17-test.db new file mode 100644 index 00000000..3f79ee53 Binary files /dev/null and b/db/tracks-17-test.db differ diff --git a/public/images/grip.png b/public/images/grip.png new file mode 100644 index 00000000..40e3d9e4 Binary files /dev/null and b/public/images/grip.png differ diff --git a/public/images/successor_off.png b/public/images/successor_off.png new file mode 100644 index 00000000..01497e64 Binary files /dev/null and b/public/images/successor_off.png differ diff --git a/public/images/successor_on.png b/public/images/successor_on.png new file mode 100644 index 00000000..ea0b2bf3 Binary files /dev/null and b/public/images/successor_on.png differ diff --git a/public/javascripts/application.js b/public/javascripts/application.js index ee22fac9..839b8852 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -247,6 +247,10 @@ $(document).ready(function() { $(this).next().toggle("fast"); return false; }); + $(".show_successors").live('click', function () { + $(this).next().toggle("fast"); return false; + }); + /* fade flashes and alerts in automatically */ $(".alert").fadeOut(8000); diff --git a/public/stylesheets/standard.css b/public/stylesheets/standard.css index 699f6b3e..997f2f2d 100644 --- a/public/stylesheets/standard.css +++ b/public/stylesheets/standard.css @@ -99,6 +99,9 @@ a.to_bottom:hover {background: transparent url(/images/bottom_on.png) no-repeat; a.show_notes, a.link_to_notes {background-image: url(/images/notes_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} a.show_notes:hover, a.link_to_notes:hover {background-image: url(/images/notes_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} +a.show_successors, a.link_to_successors {background-image: url(/images/successor_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} +a.show_successors:hover, a.link_to_successors:hover {background-image: url(/images/successor_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} + /* Structural divs */ #content { @@ -378,11 +381,7 @@ input.item-checkbox { .description, .stale_l1, .stale_l2, .stale_l3 { margin-left: 60px; - position:relative; -} - -.stale_l1, .stale_l2, .stale_l3 { - padding-left: 3px; + position:relative; } .stale_l1 { @@ -936,6 +935,15 @@ div.message { color: #666; } +.grip { + cursor: move; +} + +.hover { + background: #EAEAEA; + font-weight: bold; +} + /* Error message styles */ .fieldWithErrors { padding: 2px; @@ -1262,6 +1270,7 @@ div.auto_complete ul { list-style-type:none; } div.auto_complete ul li { + font-size: 1.0em; margin:0; padding:3px; list-style-type: none; diff --git a/test/selenium/project_detail/_add_deferred_todo.rsel b/test/selenium/project_detail/_add_deferred_todo.rsel index 15fd15ef..2e798195 100644 --- a/test/selenium/project_detail/_add_deferred_todo.rsel +++ b/test/selenium/project_detail/_add_deferred_todo.rsel @@ -1,5 +1,5 @@ type "todo_description", "choose era" type "todo_show_from", "1/1/2030" click "css=#todo-form-new-action .submit_box button" -wait_for_element_present "xpath=//div[@id='tickler'] //div[@class='item-container']" -wait_for_element_present "xpath=//div[@id='tickler'] //div[@class='item-container'] //a[@title='01/01/2030']" +wait_for_element_present "css=div#tickler div.item-container" +wait_for_element_present "css=div#tickler div.item-container a[title=01/01/2030]" diff --git a/test/selenium/project_detail/add_todo.rsel b/test/selenium/project_detail/add_todo.rsel index 7575a3d6..d3f60e88 100644 --- a/test/selenium/project_detail/add_todo.rsel +++ b/test/selenium/project_detail/add_todo.rsel @@ -5,7 +5,7 @@ open "/projects/1" # add new todo type "todo_description", "a brand new todo" click "css=#todo-form-new-action .submit_box button" -wait_for_element_present "xpath=//div[@id='p1items'] //div[@class='item-container']" +wait_for_element_present "css=div#p1items div.item-container" # wait for flash to mention that todo was added and verify existence of new todo wait_for_visible "flash" diff --git a/test/selenium/tickler/create_deferred_todo_with_existing_context.rsel b/test/selenium/tickler/create_deferred_todo_with_existing_context.rsel index 09fb6bc2..8f7fccb8 100644 --- a/test/selenium/tickler/create_deferred_todo_with_existing_context.rsel +++ b/test/selenium/tickler/create_deferred_todo_with_existing_context.rsel @@ -10,4 +10,4 @@ assert_context_count_incremented do assert_confirmation "New context \"errands\" will be also created. Are you sure?" end wait_for_not_visible "tickler-empty-nd" -wait_for_element_present "xpath=//div[@class='item-container'] //a[@title='01/01/2030']" +wait_for_element_present "css=div.item-container a[title=01/01/2030]"