diff --git a/app/controllers/application.rb b/app/controllers/application.rb
index df5fcd1f..497c2fa1 100644
--- a/app/controllers/application.rb
+++ b/app/controllers/application.rb
@@ -27,6 +27,7 @@ class ApplicationController < ActionController::Base
helper_method :current_user, :prefs
layout proc{ |controller| controller.mobile? ? "mobile" : "standard" }
+ exempt_from_layout /\.js\.erb$/
before_filter :set_session_expiration
before_filter :set_time_zone
@@ -129,14 +130,6 @@ class ApplicationController < ActionController::Base
RedCloth.new(text).to_html
end
- def build_default_project_context_name_map(projects)
- Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
- end
-
- def build_default_project_tags_map(projects)
- Hash[*projects.reject{ |p| p.default_tags.nil? }.map{ |p| [p.name, p.default_tags] }.flatten].to_json
- end
-
# Here's the concept behind this "mobile content negotiation" hack: In
# addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature
# 'mobile' version designed to be suitablef or use from a phone or PDA. It
diff --git a/app/controllers/contexts_controller.rb b/app/controllers/contexts_controller.rb
index 1c399935..5177c399 100644
--- a/app/controllers/contexts_controller.rb
+++ b/app/controllers/contexts_controller.rb
@@ -92,7 +92,7 @@ class ContextsController < ApplicationController
if @context.save
if boolean_param('wants_render')
- @context_state_changed = (@orgininal_context_hidden != @context.hidden?)
+ @context_state_changed = (@original_context_hidden != @context.hidden?)
@new_state = (@context.hidden? ? "hidden" : "active") if @context_state_changed
respond_to do |format|
format.js
@@ -110,6 +110,13 @@ class ContextsController < ApplicationController
end
end
+ def edit
+ @context = Context.find(params[:id])
+ respond_to do |format|
+ format.js
+ end
+ end
+
# Fairly self-explanatory; deletes the context If the context contains
# actions, you'll get a warning dialogue. If you choose to go ahead, any
# actions in the context will also be deleted.
@@ -124,11 +131,12 @@ class ContextsController < ApplicationController
# Methods for changing the sort order of the contexts in the list
#
def order
- list = params["list-contexts-hidden"] || params["list-contexts-active"]
- list.each_with_index do |id, position|
- current_user.contexts.update(id, :position => position + 1)
- end
+ context_ids = params["container_context"]
+ @projects = current_user.contexts.update_positions( context_ids )
render :nothing => true
+ rescue
+ notify :error, $!
+ redirect_to :action => 'index'
end
protected
@@ -218,8 +226,6 @@ class ContextsController < ApplicationController
@projects = current_user.projects
@count = @not_done_todos.size
- @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
- @default_project_tags_map = build_default_project_tags_map(@projects).to_json
end
end
diff --git a/app/controllers/integrations_controller.rb b/app/controllers/integrations_controller.rb
index 26f5f99d..93989c71 100644
--- a/app/controllers/integrations_controller.rb
+++ b/app/controllers/integrations_controller.rb
@@ -1,6 +1,6 @@
class IntegrationsController < ApplicationController
- skip_before_filter :login_required, :only => :search_plugin
+ skip_before_filter :login_required, :only => [:search_plugin, :google_gadget]
def index
@page_title = 'TRACKS::Integrations'
@@ -32,4 +32,8 @@ class IntegrationsController < ApplicationController
render :layout => false
end
+ def google_gadget
+ render :layout => false, :content_type => Mime::XML
+ end
+
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3e2b9939..56da269e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -47,14 +47,14 @@ 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_project_context_name_map = build_default_project_context_name_map(current_user.projects).to_json
- @default_project_tags_map = build_default_project_tags_map(current_user.projects).to_json
+ @default_tags = @project.default_tags
respond_to do |format|
format.html
format.m &render_project_mobile
@@ -92,7 +92,7 @@ class ProjectsController < ApplicationController
elsif @project.new_record?
render_failure @project.errors.full_messages.join(', ')
else
- head :created, :location => project_url(@project)
+ head :created, :location => project_url(@project), :text => @project.id
end
end
end
@@ -127,6 +127,7 @@ class ProjectsController < ApplicationController
@active_projects_count = current_user.projects.active.count
@hidden_projects_count = current_user.projects.hidden.count
@completed_projects_count = current_user.projects.completed.count
+ init_data_for_sidebar
render :template => 'projects/update.js.rjs'
return
elsif boolean_param('update_status')
@@ -173,7 +174,7 @@ class ProjectsController < ApplicationController
end
def order
- project_ids = params["list-active-projects"] || params["list-hidden-projects"] || params["list-completed-projects"]
+ project_ids = params["container_project"]
@projects = current_user.projects.update_positions( project_ids )
render :nothing => true
rescue
@@ -186,6 +187,7 @@ class ProjectsController < ApplicationController
@projects = current_user.projects.alphabetize(:state => @state) if @state
@contexts = current_user.contexts
init_not_done_counts(['project'])
+ init_project_hidden_todo_counts(['project']) if @state == 'hidden'
end
def actionize
@@ -193,6 +195,7 @@ class ProjectsController < ApplicationController
@projects = current_user.projects.actionize(current_user.id, :state => @state) if @state
@contexts = current_user.contexts
init_not_done_counts(['project'])
+ init_project_hidden_todo_counts(['project']) if @state == 'hidden'
end
protected
@@ -293,4 +296,4 @@ class ProjectsController < ApplicationController
project_description
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/recurring_todos_controller.rb b/app/controllers/recurring_todos_controller.rb
index 2bf54576..be659b3a 100644
--- a/app/controllers/recurring_todos_controller.rb
+++ b/app/controllers/recurring_todos_controller.rb
@@ -251,8 +251,6 @@ class RecurringTodosController < ApplicationController
@xth_day = [['first',1],['second',2],['third',3],['fourth',4],['last',5]]
@projects = current_user.projects.find(:all, :include => [:default_context])
@contexts = current_user.contexts.find(:all)
- @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
- @default_project_tags_map = build_default_project_tags_map(@projects).to_json
end
def get_recurring_todo_from_param
@@ -265,4 +263,4 @@ class RecurringTodosController < ApplicationController
recurring_todos.each { |rt| rt.toggle_completion! if rt.todos.not_completed.count == 0}
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb
index 2784be62..95e29b8c 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) }
@@ -50,9 +50,11 @@ class TodosController < ApplicationController
def create
@source_view = params['_source_view'] || 'todo'
+ @tag_name = params['_tag_name']
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)
@@ -69,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
@@ -94,6 +104,7 @@ class TodosController < ApplicationController
@projects = current_user.projects.find(:all) if @new_project_created
@initial_context_name = params['default_context_name']
@initial_project_name = params['default_project_name']
+ @default_tags = @todo.project.default_tags unless @todo.project.nil?
render :action => 'create'
end
format.xml do
@@ -128,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
#
@@ -140,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
@@ -188,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'
@@ -229,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?
@@ -295,16 +366,32 @@ 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
+ @pending_to_activate = []
+ @todo.pending_successors.each do |successor|
+ successor.uncompleted_predecessors.delete(@todo)
+ if successor.uncompleted_predecessors.empty?
+ successor.activate!
+ @pending_to_activate << successor
+ 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
@@ -354,11 +441,9 @@ 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
- @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
- @default_project_tags_map = build_default_project_tags_map(@projects).to_json
respond_to do |format|
format.html
@@ -409,6 +494,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
@@ -421,16 +509,19 @@ class TodosController < ApplicationController
@projects = current_user.projects
@contexts = current_user.contexts
@contexts_to_show = @contexts.reject {|x| x.hide? }
+
+ # Set defaults for new_action
+ @initial_tag_name = @tag_name
+ unless @not_done_todos.empty?
+ @context = current_user.contexts.find_by_id(@not_done_todos[0].context_id)
+ end
# Set count badge to number of items with this tag
@not_done_todos.empty? ? @count = 0 : @count = @not_done_todos.size
@down_count = @count
respond_to do |format|
- format.html {
- @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
- @default_project_tags_map = build_default_project_tags_map(@projects).to_json
- }
+ format.html
format.m {
cookies[:mobile_url]= {:value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']}
render :action => "mobile_tag"
@@ -464,8 +555,6 @@ class TodosController < ApplicationController
@page_title = "TRACKS::Calendar"
@projects = current_user.projects.find(:all)
- @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
- @default_project_tags_map = build_default_project_tags_map(@projects).to_json
due_today_date = Time.zone.now
due_this_week_date = Time.zone.now.end_of_week
@@ -506,12 +595,52 @@ class TodosController < ApplicationController
def auto_complete_for_tag
@items = Tag.find(:all,
- :conditions => [ "name LIKE ?", '%' + params['tag_list'] + '%' ],
- :order => "name ASC",
- :limit => 10)
+ :conditions => [ "name LIKE ?", '%' + params['tag_list'] + '%' ],
+ :order => "name ASC",
+ :limit => 10)
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[:q].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[:q].downcase + '%' ],
+ :order => 'description ASC',
+ :limit => 10
+ )
+ end
+ render :inline => "<%= auto_complete_result2(@items) %>"
+ end
+
private
def get_todo_from_params
@@ -669,6 +798,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
@@ -718,10 +848,15 @@ class TodosController < ApplicationController
end
def determine_deferred_tag_count(tag)
- tag_collection = Tag.find_by_name(tag).todos
- @deferred_tag_count = tag_collection.count(:all,
- :conditions => ['todos.user_id = ? and state = ?', current_user.id, 'deferred'],
- :order => 'show_from ASC, todos.created_at DESC')
+ tags = Tag.find_by_name(tag)
+ if tags.nil?
+ # should normally not happen, but is a workaround for #929
+ @deferred_tag_count = 0
+ else
+ @deferred_tag_count = tags.todos.count(:all,
+ :conditions => ['todos.user_id = ? and state = ?', current_user.id, 'deferred'],
+ :order => 'show_from ASC, todos.created_at DESC')
+ end
end
def render_todos_html
@@ -745,9 +880,6 @@ class TodosController < ApplicationController
end
end
- @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
- @default_project_tags_map = build_default_project_tags_map(@projects).to_json
-
render
end
end
@@ -953,6 +1085,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/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 33176298..55d38ad7 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,8 +3,7 @@ class UsersController < ApplicationController
skip_before_filter :login_required, :only => [ :new, :create ]
prepend_before_filter :login_optional, :only => [ :new, :create ]
- # GET /users
- # GET /users.xml
+ # GET /users GET /users.xml
def index
@users = User.find(:all, :order => 'login')
respond_to do |format|
@@ -12,18 +11,17 @@ class UsersController < ApplicationController
@page_title = "TRACKS::Manage Users"
@users = User.paginate :page => params[:page], :order => 'login ASC'
@total_users = User.count
- # When we call users/signup from the admin page
- # we store the URL so that we get returned here when signup is successful
+ # When we call users/signup from the admin page we store the URL so that
+ # we get returned here when signup is successful
store_location
end
format.xml { render :xml => @users.to_xml(:except => [ :password ]) }
end
end
- # GET /users/somelogin
- # GET /users/somelogin.xml
+ # GET /users/id GET /users/id.xml
def show
- @user = User.find_by_login(params[:id])
+ @user = User.find_by_id(params[:id])
render :xml => @user.to_xml(:except => [ :password ])
end
@@ -46,13 +44,13 @@ class UsersController < ApplicationController
render :layout => "login"
end
- # Example usage: curl -H 'Accept: application/xml' -H 'Content-Type: application/xml'
+ # Example usage: curl -H 'Accept: application/xml' -H 'Content-Type:
+ # application/xml'
# -u admin:up2n0g00d
# -d 'username abc123 '
# http://our.tracks.host/users
#
- # POST /users
- # POST /users.xml
+ # POST /users POST /users.xml
def create
if params['exception']
render_failure "Expected post format is valid xml like so: username abc123 ."
@@ -107,10 +105,9 @@ class UsersController < ApplicationController
end
end
- # DELETE /users/somelogin
- # DELETE /users/somelogin.xml
+ # DELETE /users/id DELETE /users/id.xml
def destroy
- @deleted_user = User.find_by_login(params[:id])
+ @deleted_user = User.find_by_id(params[:id])
@saved = @deleted_user.destroy
@total_users = User.find(:all).size
@@ -150,9 +147,8 @@ class UsersController < ApplicationController
if (params[:open_id_complete] || (params[:user][:auth_type] == 'open_id')) && openid_enabled?
authenticate_with_open_id do |result, identity_url|
if result.successful?
- # Success means that the transaction completed without
- # error. If info is nil, it means that the user cancelled
- # the verification.
+ # Success means that the transaction completed without error. If info
+ # is nil, it means that the user cancelled the verification.
@user.auth_type = 'open_id'
@user.open_id_url = identity_url
if @user.save
@@ -207,5 +203,4 @@ class UsersController < ApplicationController
return true
end
-
-end
+end
\ No newline at end of file
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 61ad8209..26bff1b3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -166,4 +166,17 @@ module ApplicationHelper
return rt+rp+rts
end
+ def date_format_for_date_picker()
+ standard_format = current_user.prefs.date_format
+ translations = [
+ ['%m', 'mm'],
+ ['%d', 'dd'],
+ ['%Y', 'yy'],
+ ['%y', 'y']
+ ]
+ translations.inject(standard_format) do |str, translation|
+ str.gsub(*translation)
+ end
+ end
+
end
diff --git a/app/helpers/recurring_todos_helper.rb b/app/helpers/recurring_todos_helper.rb
index b7f7e25d..5376a9cb 100644
--- a/app/helpers/recurring_todos_helper.rb
+++ b/app/helpers/recurring_todos_helper.rb
@@ -11,19 +11,15 @@ module RecurringTodosHelper
end
def recurring_todo_remote_delete_icon
- str = link_to( image_tag_for_delete,
+ link_to( image_tag_for_delete,
recurring_todo_path(@recurring_todo), :id => "delete_icon_"+@recurring_todo.id.to_s,
:class => "icon delete_icon", :title => "delete the recurring action '#{@recurring_todo.description}'")
- set_behavior_for_delete_icon
- str
end
def recurring_todo_remote_star_icon
- str = link_to( image_tag_for_star(@recurring_todo),
+ link_to( image_tag_for_star(@recurring_todo),
toggle_star_recurring_todo_path(@recurring_todo),
:class => "icon star_item", :title => "star the action '#{@recurring_todo.description}'")
- set_behavior_for_star_icon
- str
end
def recurring_todo_remote_edit_icon
@@ -31,7 +27,6 @@ module RecurringTodosHelper
str = link_to( image_tag_for_edit(@recurring_todo),
edit_recurring_todo_path(@recurring_todo),
:class => "icon edit_icon")
- set_behavior_for_edit_icon
else
str = '' + image_tag("blank.png") + " "
end
@@ -40,7 +35,6 @@ module RecurringTodosHelper
def recurring_todo_remote_toggle_checkbox
str = check_box_tag('item_id', toggle_check_recurring_todo_path(@recurring_todo), @recurring_todo.completed?, :class => 'item-checkbox')
- set_behavior_for_toggle_checkbox
str
end
@@ -53,26 +47,4 @@ module RecurringTodosHelper
def image_tag_for_edit(todo)
image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(todo, 'edit_icon'))
end
-
- def set_behavior_for_delete_icon
- parameters = "_source_view=#{@source_view}"
- parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
- apply_behavior '.item-container a.delete_icon:click', :prevent_default => true do |page|
- page.confirming "'Are you sure that you want to ' + this.title + '?'" do
- page << "itemContainer = this.up('.item-container'); itemContainer.startWaiting();"
- page << remote_to_href(:method => 'delete', :with => "'#{parameters}'", :complete => "itemContainer.stopWaiting();")
- end
- end
- end
-
- def set_behavior_for_edit_icon
- parameters = "_source_view=#{@source_view}"
- parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
- apply_behavior '.item-container a.edit_icon:click', :prevent_default => true do |page|
- page << "Effect.Pulsate(this);"
- page << remote_to_href(:method => 'get', :with => "'#{parameters}'")
- end
- end
-
-
-end
\ No newline at end of file
+end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 9b886679..d8be9d1f 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -8,52 +8,41 @@ module TodosHelper
end
def form_remote_tag_edit_todo( &block )
- form_tag(
- todo_path(@todo), {
+ form_remote_tag(
+ :url => todo_path(@todo),
+ :loading => "$('#submit_todo_#{@todo.id}').block({message: null})",
+ :html => {
:method => :put,
:id => dom_id(@todo, 'form'),
:class => dom_id(@todo, 'form') + " inline-form edit_todo_form" },
&block )
- apply_behavior 'form.edit_todo_form', make_remote_form(
- :method => :put,
- :before => "todoSpinner = this.down('button.positive'); todoSpinner.startWaiting()",
- :loaded => "todoSpinner.stopWaiting()",
- :condition => "!(this.down('button.positive').isWaiting())"),
- :prevent_default => true
end
-
- def set_behavior_for_star_icon
- apply_behavior '.item-container a.star_item:click',
- remote_to_href(:method => 'put', :with => "{ _source_view : '#{@source_view}' }"),
- :prevent_default => true
- end
def remote_star_icon
- str = link_to( image_tag_for_star(@todo),
+ link_to( image_tag_for_star(@todo),
toggle_star_todo_path(@todo),
:class => "icon star_item", :title => "star the action '#{@todo.description}'")
- set_behavior_for_star_icon
- str
end
def remote_edit_menu_item(parameters, todo)
return link_to_remote(
- image_tag("edit_off.png", :mouseover => "edit_on.png", :alt => "", :align => "absmiddle")+" Edit",
+ image_tag("edit_off.png", :mouseover => "edit_on.png", :alt => "Edit", :align => "absmiddle", :id => 'edit_icon_todo_'+todo.id.to_s)+" Edit",
:url => {:controller => 'todos', :action => 'edit', :id => todo.id},
:method => 'get',
:with => "'#{parameters}'",
:before => todo_start_waiting_js(todo),
- :complete => todo_stop_waiting_js)
+ :complete => todo_stop_waiting_js(todo))
end
def remote_delete_menu_item(parameters, todo)
return link_to_remote(
- image_tag("delete_off.png", :mouseover => "delete_on.png", :alt => "", :align => "absmiddle")+" Delete",
+ image_tag("delete_off.png", :mouseover => "delete_on.png", :alt => "Delete", :align => "absmiddle")+" Delete",
:url => {:controller => 'todos', :action => 'destroy', :id => todo.id},
:method => 'delete',
:with => "'#{parameters}'",
:before => todo_start_waiting_js(todo),
- :complete => todo_stop_waiting_js)
+ :complete => todo_stop_waiting_js(todo),
+ :confirm => "Are you sure that you want to delete the action '#{todo.description}'?")
end
def remote_defer_menu_item(days, todo)
@@ -64,24 +53,28 @@ module TodosHelper
futuredate = (@todo.show_from || @todo.user.date) + days.days
if @todo.due && futuredate > @todo.due
return link_to_function(
- image_tag("defer_#{days}_off.png", :mouseover => "defer_#{days}.png", :alt => "", :align => "absmiddle")+" Defer #{pluralize(days, "day")}",
+ image_tag("defer_#{days}_off.png", :mouseover => "defer_#{days}.png", :alt => "Defer #{pluralize(days, "day")}", :align => "absmiddle")+" Defer #{pluralize(days, "day")}",
"alert('Defer date is after due date. Please edit and adjust due date before deferring.')"
)
else
return link_to_remote(
- image_tag("defer_#{days}_off.png", :mouseover => "defer_#{days}.png", :alt => "", :align => "absmiddle")+" Defer #{pluralize(days, "day")}",
+ image_tag("defer_#{days}_off.png", :mouseover => "defer_#{days}.png", :alt => "Defer #{pluralize(days, "day")}", :align => "absmiddle")+" Defer #{pluralize(days, "day")}",
:url => url,
:before => todo_start_waiting_js(todo),
- :complete => todo_stop_waiting_js)
+ :complete => todo_stop_waiting_js(todo))
end
end
-
+
def todo_start_waiting_js(todo)
- return "$('ul#{dom_id(todo)}').hide(); itemContainer = $('#{dom_id(todo)}'); itemContainer.startWaiting()"
+ return "$('#ul#{dom_id(todo)}').css('visibility', 'hidden'); $('##{dom_id(todo)}').block({message: null})"
end
- def todo_stop_waiting_js
- return "itemContainer.stopWaiting();"
+ def successor_start_waiting_js(successor)
+ return "$('##{dom_id(successor, "successor")}').block({message: null})"
+ end
+
+ def todo_stop_waiting_js(todo)
+ return "$('##{dom_id(todo)}').unblock();enable_rich_interaction();"
end
def image_tag_for_recurring_todo(todo)
@@ -91,23 +84,17 @@ module TodosHelper
:class => "recurring_icon", :title => recurrence_pattern_as_text(todo.recurring_todo))
end
- def set_behavior_for_toggle_checkbox
- parameters = "_source_view=#{@source_view}"
- parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
- apply_behavior '.item-container input.item-checkbox:click',
- remote_function(:url => javascript_variable('this.value'), :method => 'put',
- :with => "'#{parameters}'")
- end
def remote_toggle_checkbox
- str = check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox')
- set_behavior_for_toggle_checkbox
- str
+ 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
@@ -115,6 +102,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
@@ -135,6 +138,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)})"
@@ -213,23 +220,25 @@ module TodosHelper
end
def calendar_setup( input_field )
- str = "Calendar.setup({ ifFormat:\"#{prefs.date_format}\""
- str << ",firstDay:#{prefs.week_starts},showOthers:true,range:[2004, 2010]"
- str << ",step:1,inputField:\"" + input_field + "\",cache:true,align:\"TR\" })\n"
- javascript_tag str
+ # TODO:jQuery
+ #str = "Calendar.setup({ ifFormat:\"#{prefs.date_format}\""
+ #str << ",firstDay:#{prefs.week_starts},showOthers:true,range:[2004, 2010]"
+ #str << ",step:1,inputField:\"" + input_field + "\",cache:true,align:\"TR\" })\n"
+ #javascript_tag str
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}"
+ return "c#{todo.context_id}items"
end
def should_show_new_item
- if @todo.project.nil? == false
+ unless @todo.project.nil?
# do not show new actions that were added to hidden or completed projects
# on home page and context page
return false if source_view_is(:todo) && (@todo.project.hidden? || @todo.project.completed?)
@@ -240,6 +249,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
@@ -266,6 +277,20 @@ module TodosHelper
array_or_string_for_javascript( current_user.contexts.collect{|c| escape_javascript(c.name) } )
end
+ def tag_names_for_autocomplete
+ array_or_string_for_javascript( Tag.all.collect{|c| escape_javascript(c.name) } )
+ end
+
+ def default_contexts_for_autocomplete
+ projects = current_user.projects.find(:all, :conditions => ['default_context_id is not null'])
+ Hash[*projects.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
+ end
+
+ def default_tags_for_autocomplete
+ projects = current_user.projects.find(:all, :conditions => ["default_tags != ''"])
+ Hash[*projects.map{ |p| [p.name, p.default_tags] }.flatten].to_json
+ end
+
def format_ical_notes(notes)
split_notes = notes.split(/\n/)
joined_notes = split_notes.join("\\n")
@@ -286,4 +311,9 @@ 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 entries.map{|e| e.specification()}.join("\n") rescue ''
+ 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 ce96c267..30c5a6fb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -10,6 +10,13 @@ class User < ActiveRecord::Base
def find_by_params(params)
find(params['id'] || params['context_id']) || nil
end
+ def update_positions(context_ids)
+ context_ids.each_with_index do |id, position|
+ context = self.detect { |c| c.id == id.to_i }
+ raise "Context id #{id} not associated with user id #{@user.id}." if context.nil?
+ context.update_attribute(:position, position + 1)
+ end
+ end
end
has_many :projects,
:order => 'projects.position ASC',
@@ -91,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/contexts/_context.rhtml b/app/views/contexts/_context.rhtml
index fafe99fe..c95c93b9 100644
--- a/app/views/contexts/_context.rhtml
+++ b/app/views/contexts/_context.rhtml
@@ -3,31 +3,9 @@
<% if collapsible -%>
<%= image_tag("collapse.png") %>
- <% apply_behavior '.container_toggle:click', :prevent_default => true do |page|
- page << " /* only handle the click if a previous click had finished its animation */
- if (todoItems.lastEffect == null || todoItems.lastEffect.state=='finished') {
- containerElem = this.up('.container')
- toggleTarget = containerElem.down('.toggle_target')
- if (Element.visible(toggleTarget))
- {
- todoItems.collapseNextActionListing(this, toggleTarget);
- todoItems.contextCollapseCookieManager.setCookie(todoItems.buildCookieName(containerElem), true)
- }
- else
- {
- todoItems.expandNextActionListing(this, toggleTarget);
- todoItems.contextCollapseCookieManager.clearCookie(todoItems.buildCookieName(containerElem))
- }
- }
- "
- end
- -%>
<% end -%>
<% if source_view_is :context %>
- <%= context.name %>
- <%= in_place_editor 'context_name_in_place_editor', {
- :url => { :controller => 'contexts', :action => 'update', :id => context.id, :field => 'name', :update_context_name => true, :escape => false},
- :options=>"{method:'put'}", :script => true } %>
+ <%= context.name %>
<% else %>
<%= link_to_context( context ) %>
<% end %>
@@ -37,11 +15,5 @@
Currently there are no incomplete actions in this context
<%= render :partial => "todos/todo", :collection => @not_done, :locals => { :parent_container_type => "context" } %>
- <% if @not_done.empty?
- # fix (hack) for #713
- set_behavior_for_star_icon
- set_behavior_for_toggle_checkbox
- end
- -%>
-
\ No newline at end of file
+
diff --git a/app/views/contexts/_context_form.rhtml b/app/views/contexts/_context_form.rhtml
index 68e3dde0..fc6d18c5 100644
--- a/app/views/contexts/_context_form.rhtml
+++ b/app/views/contexts/_context_form.rhtml
@@ -1,35 +1,28 @@
<% context = context_form
@context = context-%>
-
- <% form_tag(context_path(context), {:id => dom_id(context, 'edit_form'), :class => "inline-form "+dom_id(context, 'edit_form')+"-edit-context-form edit-context-form", :method => :put}) do -%>
- <%= error_messages_for 'context' %>
-
-
Context name
- <%= text_field('context', 'name', :class => 'context-name') %>
+<% form_remote_tag(:url => context_path(context), :html => {:id => dom_id(context, 'edit_form'), :class => "inline-form "+dom_id(context, 'edit_form')+"-edit-context-form edit-context-form", :method => :put}) do -%>
+ <%= error_messages_for 'context' %>
-
Hide from front page?
- <%= check_box('context', 'hide', :class => 'context-hide') %>
-
-
-
-
+
Context name
+ <%= text_field('context', 'name', :class => 'context-name') %>
+
+
Hide from front page?
+ <%= check_box('context', 'hide', :class => 'context-hide') %>
+
+
+
+
-
-
- <% end %>
- <%= apply_behavior ".edit-context-form", make_remote_form(
- :before => "this.up('div.edit-form').down('button.positive').startWaiting()",
- :condition => "!(this.up('div.edit-form').down('button.positive')).isWaiting()"),
- :external => true
-@context = nil %>
-
+
+
+
+ <% end %>
diff --git a/app/views/contexts/_context_listing.rhtml b/app/views/contexts/_context_listing.rhtml
index 32335457..3be6d091 100644
--- a/app/views/contexts/_context_listing.rhtml
+++ b/app/views/contexts/_context_listing.rhtml
@@ -19,23 +19,25 @@
<% else %>
VISIBLE
<% end %>
-
<%= image_tag( "blank.png", :title => "Delete context", :class=>"delete_item") %>
- <%= apply_behavior "a.delete_context_button:click", { :prevent_default => true, :external => true} do |page, element|
- page.confirming "'Are you sure that you want to ' + this.title + '?'" do
- element.up('.context').start_waiting
- page << remote_to_href(:method => 'delete')
- end
- end -%>
-
<%= image_tag( "blank.png", :title => "Edit context", :class=>"edit_item") %>
- <%= apply_behavior 'a.edit_context_button:click', :prevent_default => true do |page, element|
- element.up('.context').toggle
- editform = element.up('.list').down('.edit-form')
- editform.toggle
- editform.visual_effect(:appear)
- editform.down('input').focus
- end
- -%>
+ <%= link_to_remote(
+ image_tag( "blank.png", :title => "Delete context", :class=>"delete_item"),
+ :url => {:controller => 'contexts', :action => 'destroy', :id => context.id},
+ :method => 'delete',
+ :with => "'_source_view=#{@source_view}'",
+ :before => "$('#{dom_id(context)}').block({message:null});",
+ :complete => "$('#{dom_id(context)}').unblock();",
+ :confirm => "Are you sure that you want to delete the context '#{context.name}'?"
+ ) %>
+ <%= link_to_remote(
+ image_tag( "blank.png", :title => "Edit context", :class=>"edit_item"),
+ :url => {:controller => 'contexts', :action => 'edit', :id => context.id},
+ :method => 'get',
+ :with => "'_source_view=#{@source_view}'",
+ :before => "$('#{dom_id(context)}').block({message:null});",
+ :complete => "$('#{dom_id(context)}').unblock();"
+ ) %>
- <%= render :partial => 'contexts/context_form', :object => context %>
-
\ No newline at end of file
+
+
+
diff --git a/app/views/contexts/create.js.rjs b/app/views/contexts/create.js.rjs
index 89ee5d93..a4755f81 100644
--- a/app/views/contexts/create.js.rjs
+++ b/app/views/contexts/create.js.rjs
@@ -1,12 +1,12 @@
if @saved
+ container_name = 'list-contexts-' + (@context.hidden? ? 'hidden' : 'active')
page.hide 'contexts-empty-nd'
- page.insert_html :bottom, "list-contexts", :partial => 'context_listing', :locals => { :context_listing => @context }
- page.sortable "list-contexts", get_listing_sortable_options
+ page.insert_html :bottom, container_name, :partial => 'context_listing', :locals => { :context_listing => @context }
page.hide 'status'
page['badge_count'].replace_html @down_count
- page.call "Form.reset", "context-form"
- page.call "Form.focusFirstElement", "context-form"
+ page << '$("#context-form").clearForm();'
+ page << '$("#context-form input:text:first").focus();'
else
page.show 'status'
page.replace_html 'status', "#{error_messages_for('context')}"
-end
\ No newline at end of file
+end
diff --git a/app/views/contexts/edit.js.rjs b/app/views/contexts/edit.js.rjs
new file mode 100644
index 00000000..d9846e6c
--- /dev/null
+++ b/app/views/contexts/edit.js.rjs
@@ -0,0 +1,4 @@
+page[dom_id(@context, 'edit')].replace_html :partial => 'context_form', :locals => { :context_form => @context }
+page[@context].hide
+page[dom_id(@context, 'edit')].show
+page[dom_id(@context, 'edit_form')].find('input.context-name').focus
diff --git a/app/views/contexts/index.html.erb b/app/views/contexts/index.html.erb
index 418e637d..edfcc710 100644
--- a/app/views/contexts/index.html.erb
+++ b/app/views/contexts/index.html.erb
@@ -8,12 +8,6 @@
« Hide form
- <% apply_behavior '#toggle_context_new a:click', :prevent_default => true do |page|
- page << "TracksForm.toggle('toggle_context_new', 'context_new', 'context-form',
- '« Hide form', 'Hide new context form',
- 'Create a new context »', 'Add a context');"
- end
- %>
@@ -21,9 +15,8 @@
:url => contexts_path,
:method => :post,
:html=> { :id => 'context-form', :name => 'context', :class => 'inline-form'},
- :before => "$('context_new_submit').startWaiting()",
- :complete => "$('context_new_submit').stopWaiting()",
- :condition => "!$('context_new_submit').isWaiting()") do -%>
+ :before => "$('#context_new_submit').block({message: null})",
+ :complete => "$('#context_new_submit').unblock()") do -%>
<%= error_messages_for('context') %>
@@ -49,8 +42,3 @@
sortable_element 'list-contexts-active', get_listing_sortable_options
sortable_element 'list-contexts-hidden', get_listing_sortable_options
-%>
-
diff --git a/app/views/contexts/update.js.rjs b/app/views/contexts/update.js.rjs
index 33c633da..b7c0e0aa 100644
--- a/app/views/contexts/update.js.rjs
+++ b/app/views/contexts/update.js.rjs
@@ -1,12 +1,9 @@
status_message = 'Context saved'
page.notify :notice, status_message, 5.0
if @context_state_changed
- page << "jQuery('##{dom_id(@context, 'edit')}').hide();"
+ page.remove dom_id(@context, 'container')
page.insert_html :bottom, "list-contexts-#{@new_state}", :partial => 'context_listing', :object => @context
else
page.replace_html dom_id(@context, 'container'), :partial => 'context_listing', :object => @context
end
-page.sortable "list-contexts-active", get_listing_sortable_options
-page.sortable "list-contexts-hidden", get_listing_sortable_options
-
-page.hide "busy"
\ No newline at end of file
+page.visual_effect :highlight, dom_id(@context), :duration => 3
diff --git a/app/views/data/index.html.erb b/app/views/data/index.html.erb
index f4f8688d..414526a1 100644
--- a/app/views/data/index.html.erb
+++ b/app/views/data/index.html.erb
@@ -34,9 +34,3 @@
-
-
diff --git a/app/views/data/yaml_form.html.erb b/app/views/data/yaml_form.html.erb
index edc13098..1b89a9db 100644
--- a/app/views/data/yaml_form.html.erb
+++ b/app/views/data/yaml_form.html.erb
@@ -15,9 +15,3 @@
-
-
diff --git a/app/views/feedlist/index.html.erb b/app/views/feedlist/index.html.erb
index 74a45880..bb417c7e 100644
--- a/app/views/feedlist/index.html.erb
+++ b/app/views/feedlist/index.html.erb
@@ -125,11 +125,3 @@
<%= render :file => "sidebar/sidebar.html.erb" %>
-
-
diff --git a/app/views/integrations/google_gadget.erb b/app/views/integrations/google_gadget.erb
new file mode 100644
index 00000000..b08e89fa
--- /dev/null
+++ b/app/views/integrations/google_gadget.erb
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/integrations/index.html.erb b/app/views/integrations/index.html.erb
index be2a5a0c..d99f0066 100644
--- a/app/views/integrations/index.html.erb
+++ b/app/views/integrations/index.html.erb
@@ -1,27 +1,23 @@
<% has_contexts = !current_user.contexts.empty? -%>
Integrations
-Tracks can be integrated with a number of other tools... whatever it takes to help you get things done! This page has information on setting up some of these. Not all of these are applicable to all platforms, and some require more technical knowledge than others. See also <%= link_to "developer documentation for Tracks' REST API", url_for(:action => 'rest_api') %>.
-Contents:
-
+Tracks can be integrated with a number of other tools...
+ whatever it takes to help you get things done!
+ This page has information on setting up some of these.
+ Not all of these are applicable to all platforms, and some require more
+ technical knowledge than others.
+ See also <%= link_to "developer documentation for Tracks' REST API", url_for(:action => 'rest_api') %>.
+Contents:
+
-
-Do you have one of your own to add? Tell us about it in our Tips and Tricks forum
- and we may include it on this page in a future versions of Tracks.
-
-
-Integrated email/SMS receiver
-
-If Tracks is running on the same server as your mail server, you can use the integrated mail handler built into tracks. Steps to set it up:
-
-You can also use the Rich Todo API to send in tasks like "do laundry @ Home" or "Call Bill > project X". The subject of the message will fill description, context, and project, while the body will populate the tasks's note.
+ Integrate Tracks with an email server to be able to send an action through email to Tracks
+ Add Tracks as a Google Gmail gadget
+
+Do you have one of your own to add?
+ Tell us about
+ it in our Tips and Tricks forum and we may include it on this page in a future versions of Tracks.
@@ -29,22 +25,22 @@ You can also use the Rich Todo API to send in tasks like "do laundry @ Home" or
This is a simple script that pops up a dialog box asking for a description, and then sends that to Tracks with a hard-coded context.
<% if has_contexts -%>
-
- Choose the context you want to add actions to: <%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %>
- <%= observe_field "applescript1-contexts", :update => "applescript1",
- :with => 'context_id',
- :url => { :controller => "integrations", :action => "get_applescript1" },
- :before => "$('applescript1').startWaiting()",
- :complete => "$('applescript1').stopWaiting()"
- %>
-
- Copy the Applescript below to the clipboard.
-
-
-
- Open Script Editor and paste the script into a new document.
- Compile and save the script. Run it as necessary.
-
+
+ Choose the context you want to add actions to: <%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %>
+ <%= observe_field "applescript1-contexts", :update => "applescript1",
+ :with => 'context_id',
+ :url => { :controller => "integrations", :action => "get_applescript1" },
+ :before => "$('applescript1').startWaiting()",
+ :complete => "$('applescript1').stopWaiting()"
+ %>
+
+ Copy the Applescript below to the clipboard.
+
+
+
+ Open Script Editor and paste the script into a new document.
+ Compile and save the script. Run it as necessary.
+
<% else %>
You do not have any context yet. The script will be available after you add your first context
<% end %>
@@ -54,23 +50,23 @@ You can also use the Rich Todo API to send in tasks like "do laundry @ Home" or
This script takes the sender and subject of the selected email(s) in Mail and creates a new action for each one, with the description, "Email [sender] about [subject]". The description gets truncated to 100 characters (the validation limit for the field) if it is longer than that. It also has Growl notifications if you have Growl installed.
<% if has_contexts -%>
-
- Choose the context you want to add actions to: <%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %>
- <%= observe_field "applescript2-contexts", :update => "applescript2",
- :with => 'context_id',
- :url => { :controller => "integrations", :action => "get_applescript2" },
- :before => "$('applescript2').startWaiting()",
- :complete => "$('applescript2').stopWaiting()"
- %>
-
- Copy the Applescript below to the clipboard.
-
-
-
- Open Script Editor and paste the script into a new document.
- Compile and save the script to the ~/Library/Scriipts/Mail Scripts directory.
- For more information on using AppleScript with Mail.app, see this overview .
-
+
+ Choose the context you want to add actions to: <%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %>
+ <%= observe_field "applescript2-contexts", :update => "applescript2",
+ :with => 'context_id',
+ :url => { :controller => "integrations", :action => "get_applescript2" },
+ :before => "$('applescript2').startWaiting()",
+ :complete => "$('applescript2').stopWaiting()"
+ %>
+
+ Copy the Applescript below to the clipboard.
+
+
+
+ Open Script Editor and paste the script into a new document.
+ Compile and save the script to the ~/Library/Scriipts/Mail Scripts directory.
+ For more information on using AppleScript with Mail.app, see this overview .
+
<% else %>
You do not have any context yet. The script will be available after you add your first context
<% end %>
@@ -81,28 +77,28 @@ You can also use the Rich Todo API to send in tasks like "do laundry @ Home" or
This integration will allow you to add actions to Tracks via Quicksilver .
<% if has_contexts -%>
-
- Choose the context you want to add actions to: <%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %>
- <%= observe_field "quicksilver-contexts", :update => "quicksilver",
- :with => 'context_id',
- :url => { :controller => "integrations", :action => "get_quicksilver_applescript" },
- :before => "$('quicksilver').startWaiting()",
- :complete => "$('quicksilver').stopWaiting()"
- %>
-
-Copy the Applescript below to the clipboard.
-
-
-
- Open Script Editor and paste the script into a new document.
- Compile and save the script as "Add to Tracks.scpt" in ~/Library/Application Support/Quicksilver/Actions/ (you may need to create the Actions directory)
- Restart Quicksilver
- Activate Quicksilver (Ctrl+Space by default)
- Press "." to put quicksilver into text mode
- Type the description of the next action you want to add
- Press tab to switch to the action pane.
- By typing or scrolling, choose the "Add to Tracks" action.
-
+
+ Choose the context you want to add actions to: <%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %>
+ <%= observe_field "quicksilver-contexts", :update => "quicksilver",
+ :with => 'context_id',
+ :url => { :controller => "integrations", :action => "get_quicksilver_applescript" },
+ :before => "$('quicksilver').startWaiting()",
+ :complete => "$('quicksilver').stopWaiting()"
+ %>
+
+ Copy the Applescript below to the clipboard.
+
+
+
+ Open Script Editor and paste the script into a new document.
+ Compile and save the script as "Add to Tracks.scpt" in ~/Library/Application Support/Quicksilver/Actions/ (you may need to create the Actions directory)
+ Restart Quicksilver
+ Activate Quicksilver (Ctrl+Space by default)
+ Press "." to put quicksilver into text mode
+ Type the description of the next action you want to add
+ Press tab to switch to the action pane.
+ By typing or scrolling, choose the "Add to Tracks" action.
+
<% else %>
You do not have any context yet. The script will be available after you add your first context
<% end %>
@@ -115,3 +111,38 @@ You can also use the Rich Todo API to send in tasks like "do laundry @ Home" or
You can of course use other text <%= link_to 'feeds provided by Tracks', feeds_path %> -- why not email a list of next actions in a particular project to a group of colleagues who are working on the project?
+
+
+Integrated email/SMS receiver
+
+ If Tracks is running on the same server as your mail server, you can use the integrated mail handler built into tracks. Steps to set it up:
+
+You can also use the Rich Todo API to send in tasks like "do laundry @ Home"
+ or "Call Bill > project X". The subject of the message will fill description,
+ context, and project, while the body will populate the tasks's note.
+
+
+
+Add Tracks as a Google Gmail gadget
+
+ You can now manage your projects/actions inside Gmail using Tracks Gmail Gadget.
+ Add Tracks Gmail gadget to the sidebar of Gmail and track your next actions
+ or add new action without explicitly open new browser tab for Tracks. Steps to set it up:
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/layouts/login.html.erb b/app/views/layouts/login.html.erb
index 3e30e8f0..b550297c 100644
--- a/app/views/layouts/login.html.erb
+++ b/app/views/layouts/login.html.erb
@@ -3,7 +3,8 @@
<%= stylesheet_link_tag "scaffold" %>
- <%= javascript_include_tag :defaults %>
+ <%= javascript_include_tag 'jquery' %>
+ <%= javascript_include_tag 'jquery.cookie' %>
<%= @page_title -%>
diff --git a/app/views/layouts/standard.html.erb b/app/views/layouts/standard.html.erb
index 651691e7..07fac6bb 100644
--- a/app/views/layouts/standard.html.erb
+++ b/app/views/layouts/standard.html.erb
@@ -6,22 +6,35 @@
;url=<%= request.request_uri %>">
<% end -%>
<% bundle :name => "tracks_css" do %>
- <%= stylesheet_link_tag *%w[ standard superfish calendar-system niftyCorners] %>
+ <%= stylesheet_link_tag *%w[ standard superfish niftyCorners jquery-ui jquery.autocomplete] %>
<% end %>
<%= stylesheet_link_tag "print", :media => "print" %>
<% bundle :name => "jquery" do %>
- <%= javascript_include_tag "jquery-1.2.6.min" %>
+ <%= javascript_include_tag 'jquery' %>
+ <%= javascript_include_tag 'jquery-ui' %>
+ <%= javascript_include_tag 'jquery.cookie' %>
+ <%= javascript_include_tag 'jquery.blockUI' %>
+ <%= javascript_include_tag 'jquery.jeditable' %>
+ <%= javascript_include_tag 'jquery.autocomplete' %>
<% end %>
<% bundle :name => "tracks_js" do %>
<%= javascript_include_tag *%w[
- hoverIntent superfish prototype
- effects dragdrop controls application
- calendar calendar-en calendar-setup
- accesskey-hints todo-items niftycube
- protoload flashobject lowpro
- ] %>
+ hoverIntent superfish application
+ accesskey-hints niftycube flashobject ] %>
<% end %>
- <%= javascript_include_tag :unobtrusive %>
+ <%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? %>
+ <%= javascript_tag "var SOURCE_VIEW = '#{@source_view}';" %>
+ <%= javascript_tag "var TAG_NAME = '#{@tag_name}';" if @tag_name %>
+
<%= auto_discovery_link_tag(:rss, {:controller => "todos", :action => "index", :format => 'rss', :token => "#{current_user.token}"}, {:title => "RSS feed of next actions"}) %>
@@ -54,14 +67,16 @@
<%= navigation_link( "Contexts", contexts_path, {:accesskey=>"c", :title=>"Contexts"} ) %>
<%= navigation_link( "Notes", notes_path, {:accesskey => "o", :title => "Show all notes"} ) %>
<%= navigation_link( "Repeating todos", {:controller => "recurring_todos", :action => "index"}, :title => "Manage recurring actions" ) %>
-
+
+
View
-
- <%= navigation_link( "Calendar", calendar_path, :title => "Calendar of due actions" ) %>
- <%= navigation_link( "Done", done_path, {:accesskey=>"d", :title=>"Completed"} ) %>
- <%= navigation_link( "Feeds", {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %>
- <%= navigation_link( "Statistics", {:controller => "stats", :action => "index"}, :title => "See your statistics" ) %>
-
+
+ <%= navigation_link( "Calendar", calendar_path, :title => "Calendar of due actions" ) %>
+ <%= navigation_link( "Done", done_path, {:accesskey=>"d", :title=>"Completed"} ) %>
+ <%= navigation_link( "Feeds", {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %>
+ <%= navigation_link( "Statistics", {:controller => "stats", :action => "index"}, :title => "See your statistics" ) %>
+
+
Admin
<%= navigation_link( "Preferences", preferences_path, {:accesskey => "u", :title => "Show my preferences"} ) %>
@@ -69,19 +84,21 @@
<% if current_user.is_admin? -%>
<%= navigation_link("Manage users", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %>
<% end -%>
-
+
+
?
<%= link_to 'Integrate Tracks', integrations_path %>
<%= link_to 'REST API Docs', url_for(:controller => 'integrations', :action => 'rest_api') %>
-
+
+
<%= navigation_link(image_tag("system-search.png", :size => "16X16", :border => 0), {:controller => "search", :action => "index"}, :title => "Search All Items" ) %>
<%= render_flash %>
-
+
<% unless @controller_name == 'feed' or session['noexpiry'] == "on" -%>
<%= periodically_call_remote( :url => {:controller => "login", :action => "check_expiry"},
:frequency => (5*60)) %>
@@ -93,36 +110,5 @@
<%= render :partial => "shared/footer" %>
-
-