Commit 9f0083a9 authored by Vinnie Okada's avatar Vinnie Okada

Add task lists to issues and merge requests

Make the Markdown parser recognize "[x]" or "[ ]" at the beginning of a
list item and turn it into a checkbox input.  Users who can modify the
issue or MR can toggle the checkboxes directly or edit the Markdown to
manage the tasks.  Task status is also displayed in the MR and issue
lists.
parent ff435000
......@@ -6,4 +6,28 @@ class Issue
$(".issue-box .inline-update").on "change", "#issue_assignee_id", ->
$(this).submit()
if $("a.btn-close").length
$("li.task-list-item input:checkbox").prop("disabled", false)
$(".task-list-item input:checkbox").on "click", ->
is_checked = $(this).prop("checked")
if $(this).is(":checked")
state_event = "task_check"
else
state_event = "task_uncheck"
mr_url = $("form.edit-issue").first().attr("action")
mr_num = mr_url.match(/\d+$/)
task_num = 0
$("li.task-list-item input:checkbox").each( (index, e) =>
if e == this
task_num = index + 1
)
$.ajax
type: "PATCH"
url: mr_url
data: "issue[state_event]=" + state_event +
"&issue[task_num]=" + task_num
@Issue = Issue
......@@ -17,6 +17,8 @@ class MergeRequest
disableButtonIfEmptyField '#commit_message', '.accept_merge_request'
if $("a.close-mr-link").length
$("li.task-list-item input:checkbox").prop("disabled", false)
# Local jQuery finder
$: (selector) ->
......@@ -72,6 +74,27 @@ class MergeRequest
this.$('.remove_source_branch_in_progress').hide()
this.$('.remove_source_branch_widget.failed').show()
this.$(".task-list-item input:checkbox").on "click", ->
is_checked = $(this).prop("checked")
if $(this).is(":checked")
state_event = "task_check"
else
state_event = "task_uncheck"
mr_url = $("form.edit-merge_request").first().attr("action")
mr_num = mr_url.match(/\d+$/)
task_num = 0
$("li.task-list-item input:checkbox").each( (index, e) =>
if e == this
task_num = index + 1
)
$.ajax
type: "PATCH"
url: mr_url
data: "merge_request[state_event]=" + state_event +
"&merge_request[task_num]=" + task_num
activateTab: (action) ->
this.$('.merge-request-tabs li').removeClass 'active'
this.$('.tab-content').hide()
......
......@@ -356,3 +356,6 @@ table {
font-size: 42px;
}
.task-status {
margin-left: 10px;
}
......@@ -122,3 +122,7 @@ ul.bordered-list {
}
}
}
li.task-list-item {
list-style-type: none;
}
......@@ -152,7 +152,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description,
:milestone_id, :state_event, label_ids: []
:milestone_id, :state_event, :task_num, label_ids: []
)
end
end
......@@ -250,7 +250,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, label_ids: []
:state_event, :description, :task_num, label_ids: []
)
end
end
# Contains functionality for objects that can have task lists in their
# descriptions. Task list items can be added with Markdown like "* [x] Fix
# bugs".
#
# Used by MergeRequest and Issue
module Taskable
TASK_PATTERN_MD = /^(?<bullet> *[*-] *)\[(?<checked>[ xX])\]/.freeze
TASK_PATTERN_HTML = /^<li>\[(?<checked>[ xX])\]/.freeze
# Change the state of a task list item for this Taskable. Edit the object's
# description by finding the nth task item and changing its checkbox
# placeholder to "[x]" if +checked+ is true, or "[ ]" if it's false.
# Note: task numbering starts with 1
def update_nth_task(n, checked)
index = 0
check_char = checked ? 'x' : ' '
# Do this instead of using #gsub! so that ActiveRecord detects that a field
# has changed.
self.description = self.description.gsub(TASK_PATTERN_MD) do |match|
index += 1
case index
when n then "#{$LAST_MATCH_INFO[:bullet]}[#{check_char}]"
else match
end
end
save
end
# Return true if this object's description has any task list items.
def tasks?
description && description.match(TASK_PATTERN_MD)
end
# Return a string that describes the current state of this Taskable's task
# list items, e.g. "20 tasks (12 done, 8 unfinished)"
def task_status
return nil unless description
num_tasks = 0
num_done = 0
description.scan(TASK_PATTERN_MD) do
num_tasks += 1
num_done += 1 unless $LAST_MATCH_INFO[:checked] == ' '
end
"#{num_tasks} tasks (#{num_done} done, #{num_tasks - num_done} unfinished)"
end
end
......@@ -23,6 +23,7 @@ require 'file_size_validator'
class Issue < ActiveRecord::Base
include Issuable
include InternalId
include Taskable
ActsAsTaggableOn.strict_case_match = true
......
......@@ -25,6 +25,7 @@ require Rails.root.join("lib/static_model")
class MergeRequest < ActiveRecord::Base
include Issuable
include Taskable
include InternalId
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
......
......@@ -8,9 +8,14 @@ module Issues
Issues::ReopenService.new(project, current_user, {}).execute(issue)
when 'close'
Issues::CloseService.new(project, current_user, {}).execute(issue)
when 'task_check'
issue.update_nth_task(params[:task_num].to_i, true)
when 'task_uncheck'
issue.update_nth_task(params[:task_num].to_i, false)
end
if params.present? && issue.update_attributes(params.except(:state_event))
if params.present? && issue.update_attributes(params.except(:state_event,
:task_num))
issue.reset_events_cache
if issue.previous_changes.include?('milestone_id')
......@@ -28,5 +33,12 @@ module Issues
issue
end
private
def update_task(issue, params, checked)
issue.update_nth_task(params[:task_num].to_i, checked)
params.except!(:task_num)
end
end
end
......@@ -17,9 +17,15 @@ module MergeRequests
MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request)
when 'close'
MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request)
when 'task_check'
merge_request.update_nth_task(params[:task_num].to_i, true)
when 'task_uncheck'
merge_request.update_nth_task(params[:task_num].to_i, false)
end
if params.present? && merge_request.update_attributes(params.except(:state_event))
if params.present? && merge_request.update_attributes(
params.except(:state_event, :task_num)
)
merge_request.reset_events_cache
if merge_request.previous_changes.include?('milestone_id')
......
......@@ -26,6 +26,10 @@
%span
%i.fa.fa-clock-o
= issue.milestone.title
- if issue.tasks?
%span.task-status
= issue.task_status
.pull-right
%small updated #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_update_ago')}
......
......@@ -48,7 +48,7 @@
.description
.wiki
= preserve do
= markdown @issue.description
= markdown(@issue.description, parse_tasks: true)
.context
%cite.cgray
= render partial: 'issue_context', locals: { issue: @issue }
......
......@@ -27,7 +27,9 @@
%span
%i.fa.fa-clock-o
= merge_request.milestone.title
- if merge_request.tasks?
%span.task-status
= merge_request.task_status
.pull-right
%small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')}
......
......@@ -18,7 +18,7 @@
.description
.wiki
= preserve do
= markdown @merge_request.description
= markdown(@merge_request.description, parse_tasks: true)
.context
%cite.cgray
......
......@@ -33,6 +33,11 @@ module Gitlab
attr_reader :html_options
def gfm_with_tasks(text, project = @project, html_options = {})
text = gfm(text, project, html_options)
parse_tasks(text)
end
# Public: Parse the provided text with GitLab-Flavored Markdown
#
# text - the source text
......@@ -265,5 +270,24 @@ module Gitlab
)
link_to("#{prefix_text}##{identifier}", url, options)
end
# Turn list items that start with "[ ]" into HTML checkbox inputs.
def parse_tasks(text)
li_tag = '<li class="task-list-item">'
unchecked_box = '<input type="checkbox" value="on" disabled />'
checked_box = unchecked_box.sub(/\/>$/, 'checked="checked" />')
# Regexp captures don't seem to work when +text+ is an
# ActiveSupport::SafeBuffer, hence the `String.new`
String.new(text).gsub(Taskable::TASK_PATTERN_HTML) do
checked = $LAST_MATCH_INFO[:checked].downcase == 'x'
if checked
"#{li_tag}#{checked_box}"
else
"#{li_tag}#{unchecked_box}"
end
end
end
end
end
......@@ -47,6 +47,10 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
unless @template.instance_variable_get("@project_wiki") || @project.nil?
full_document = h.create_relative_links(full_document)
end
h.gfm(full_document)
if @options[:parse_tasks]
h.gfm_with_tasks(full_document)
else
h.gfm(full_document)
end
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment