Commit 0fed1b58 authored by Marin Jankovski's avatar Marin Jankovski
Browse files

Merge pull request #7964 from mr-vinn/task-lists

Add task lists to issues and merge requests
......@@ -18,6 +18,7 @@ v 7.4.0
- Add Pushover service integration (Sullivan Senechal)
- Add select field type for services options (Sullivan Senechal)
- Add cross-project references to the Markdown parser (Vinnie Okada)
- Add task lists to issue and merge request descriptions (Vinnie Okada)
v 7.3.2
- Fix creating new file via web editor
......
......@@ -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 @@ def redirect_old
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 @@ def merge_request_params
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 @@
class Issue < ActiveRecord::Base
include Issuable
include InternalId
include Taskable
ActsAsTaggableOn.strict_case_match = true
......
......@@ -25,6 +25,7 @@
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 @@ def execute(issue)
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 @@ def execute(issue)
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 @@ def execute(merge_request)
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
......
......@@ -10,6 +10,7 @@
* [Code and Syntax Highlighting](#code-and-syntax-highlighting)
* [Emoji](#emoji)
* [Special GitLab references](#special-gitlab-references)
* [Task lists](#task-lists)
**[Standard Markdown](#standard-markdown)**
......@@ -183,6 +184,18 @@ GFM also recognizes references to commits, issues, and merge requests in other p
- namespace/project!123 : for merge requests
- namespace/project@1234567 : for commits
## Task Lists
You can add task lists to merge request and issue descriptions to keep track of to-do items. To create a task, add an unordered list to the description in an issue or merge request, formatted like so:
```no-highlight
* [x] Completed task
* [ ] Unfinished task
* [x] Nested task
```
Task lists can only be created in descriptions, not in titles or comments. Task item state can be managed by editing the description's Markdown or by clicking the rendered checkboxes.
# Standard Markdown
## Headers
......
......@@ -126,3 +126,36 @@ Feature: Project Issues
When I click label 'bug'
And I should see "Release 0.4" in issues
And I should not see "Tweet control" in issues
Scenario: Issue description should render task checkboxes
Given project "Shop" has "Tasks-open" open issue with task markdown
When I visit issue page "Tasks-open"
Then I should see task checkboxes in the description
@javascript
Scenario: Issue notes should not render task checkboxes
Given project "Shop" has "Tasks-open" open issue with task markdown
When I visit issue page "Tasks-open"
And I leave a comment with task markdown
Then I should not see task checkboxes in the comment
# Task status in issues list
Scenario: Issues list should display task status
Given project "Shop" has "Tasks-open" open issue with task markdown
When I visit project "Shop" issues page
Then I should see the task status for issue "Tasks-open"
# Toggling task items
@javascript
Scenario: Task checkboxes should be enabled for an open issue
Given project "Shop" has "Tasks-open" open issue with task markdown
When I visit issue page "Tasks-open"
Then Task checkboxes should be enabled
@javascript
Scenario: Task checkboxes should be disabled for a closed issue
Given project "Shop" has "Tasks-closed" closed issue with task markdown
When I visit issue page "Tasks-closed"
Then Task checkboxes should be disabled
......@@ -96,6 +96,16 @@ Feature: Project Merge Requests
And I leave a comment with a header containing "Comment with a header"
Then The comment with the header should not have an ID
Scenario: Merge request description should render task checkboxes
Given project "Shop" has "MR-task-open" open MR with task markdown
When I visit merge request page "MR-task-open"
Then I should see task checkboxes in the description
Scenario: Merge request notes should not render task checkboxes
Given project "Shop" has "MR-task-open" open MR with task markdown
When I visit merge request page "MR-task-open"
Then I should not see task checkboxes in the comment
# Toggling inline comments
@javascript
......@@ -155,3 +165,26 @@ Feature: Project Merge Requests
And I leave a comment like "Line is wrong" on line 39 of the second file
And I click Side-by-side Diff tab
Then I should see comments on the side-by-side diff page
# Task status in issues list
@now
Scenario: Merge requests list should display task status
Given project "Shop" has "MR-task-open" open MR with task markdown
When I visit project "Shop" merge requests page
Then I should see the task status for merge request "MR-task-open"
# Toggling task items
@javascript
Scenario: Task checkboxes should be enabled for an open merge request
Given project "Shop" has "MR-task-open" open MR with task markdown
When I visit merge request page "MR-task-open"
Then Task checkboxes should be enabled
@javascript
Scenario: Task checkboxes should be disabled for a closed merge request
Given project "Shop" has "MR-task-open" open MR with task markdown
And I visit merge request page "MR-task-open"
And I click link "Close"
Then Task checkboxes should be disabled
......@@ -153,6 +153,32 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
author: project.users.first)
end
step 'project "Shop" has "Tasks-open" open issue with task markdown' do
desc_text = <<EOT.gsub(/^ {6}/, '')
* [ ] Task 1
* [x] Task 2
EOT
create(:issue,
title: 'Tasks-open',
project: project,
author: project.users.first,
description: desc_text
)
end
step 'project "Shop" has "Tasks-closed" closed issue with task markdown' do
desc_text = <<EOT.gsub(/^ {6}/, '')
* [ ] Task 1
* [x] Task 2
EOT
create(:closed_issue,
title: 'Tasks-closed',
project: project,
author: project.users.first,
description: desc_text
)
end
step 'empty project "Empty Project"' do
create :empty_project, name: 'Empty Project', namespace: @user.namespace
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