Commit 6c32abc5 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'rs-task_list' into 'master'

Use task_list gem for task lists

Task Lists can now be used in comments, and they'll render in previews. 👏

Closes internal https://dev.gitlab.org/gitlab/gitlabhq/issues/2271

See merge request !599
parents 757084f7 da8d6feb
...@@ -35,7 +35,7 @@ v 7.11.0 (unreleased) ...@@ -35,7 +35,7 @@ v 7.11.0 (unreleased)
- Show incompatible projects in Google Code import status (Stan Hu) - Show incompatible projects in Google Code import status (Stan Hu)
- Fix bug where commit data would not appear in some subdirectories (Stan Hu) - Fix bug where commit data would not appear in some subdirectories (Stan Hu)
- Unescape branch names in compare commit (Stan Hu) - Unescape branch names in compare commit (Stan Hu)
- - Task lists are now usable in comments, and will show up in Markdown previews.
- Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu) - Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu)
- Move snippets UI to fluid layout - Move snippets UI to fluid layout
- Improve UI for sidebar. Increase separation between navigation and content - Improve UI for sidebar. Increase separation between navigation and content
......
...@@ -87,20 +87,17 @@ gem "six" ...@@ -87,20 +87,17 @@ gem "six"
# Seed data # Seed data
gem "seed-fu" gem "seed-fu"
# Markup pipeline for GitLab # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '~> 1.0.0', require: 'task_list/railtie'
# Markdown to HTML gem 'github-markup'
gem "github-markup" gem 'redcarpet', '~> 3.2.3'
# Required markup gems by github-markdown
gem 'redcarpet', '~> 3.2.3'
gem 'RedCloth' gem 'RedCloth'
gem 'rdoc', '~>3.6' gem 'rdoc', '~>3.6'
gem 'org-ruby', '= 0.9.12' gem 'org-ruby', '= 0.9.12'
gem 'creole', '~>0.3.6' gem 'creole', '~>0.3.6'
gem 'wikicloth', '=0.8.1' gem 'wikicloth', '=0.8.1'
gem 'asciidoctor', '= 0.1.4' gem 'asciidoctor', '= 0.1.4'
# Diffs # Diffs
gem 'diffy', '~> 3.0.3' gem 'diffy', '~> 3.0.3'
...@@ -251,7 +248,6 @@ group :development, :test do ...@@ -251,7 +248,6 @@ group :development, :test do
# PhantomJS driver for Capybara # PhantomJS driver for Capybara
gem 'poltergeist', '~> 1.5.1' gem 'poltergeist', '~> 1.5.1'
gem 'jasmine', '~> 2.2.0'
gem 'jasmine-rails' gem 'jasmine-rails'
gem "spring", '~> 1.3.1' gem "spring", '~> 1.3.1'
......
...@@ -290,11 +290,6 @@ GEM ...@@ -290,11 +290,6 @@ GEM
i18n (0.7.0) i18n (0.7.0)
ice_cube (0.11.1) ice_cube (0.11.1)
ice_nine (0.10.0) ice_nine (0.10.0)
jasmine (2.2.0)
jasmine-core (~> 2.2)
phantomjs
rack (>= 1.2.1)
rake
jasmine-core (2.2.0) jasmine-core (2.2.0)
jasmine-rails (0.10.8) jasmine-rails (0.10.8)
jasmine-core (>= 1.3, < 3.0) jasmine-core (>= 1.3, < 3.0)
...@@ -597,6 +592,8 @@ GEM ...@@ -597,6 +592,8 @@ GEM
stamp (0.5.0) stamp (0.5.0)
state_machine (1.2.0) state_machine (1.2.0)
stringex (2.5.2) stringex (2.5.2)
task_list (1.0.2)
html-pipeline
temple (0.6.7) temple (0.6.7)
term-ansicolor (1.2.2) term-ansicolor (1.2.2)
tins (~> 0.8) tins (~> 0.8)
...@@ -724,7 +721,6 @@ DEPENDENCIES ...@@ -724,7 +721,6 @@ DEPENDENCIES
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0) html-pipeline (~> 1.11.0)
httparty httparty
jasmine (~> 2.2.0)
jasmine-rails jasmine-rails
jquery-atwho-rails (~> 1.0.0) jquery-atwho-rails (~> 1.0.0)
jquery-rails jquery-rails
...@@ -789,6 +785,7 @@ DEPENDENCIES ...@@ -789,6 +785,7 @@ DEPENDENCIES
spring-commands-spinach (= 1.0.0) spring-commands-spinach (= 1.0.0)
stamp stamp
state_machine state_machine
task_list (~> 1.0.0)
test_after_commit test_after_commit
thin thin
tinder (~> 1.9.2) tinder (~> 1.9.2)
......
window.updateTaskState = (taskableType) ->
objType = taskableType.data
isChecked = $(this).prop("checked")
if $(this).is(":checked")
stateEvent = "task_check"
else
stateEvent = "task_uncheck"
taskableUrl = $("form.edit-" + objType).first().attr("action")
taskableNum = taskableUrl.match(/\d+$/)
taskNum = 0
$("li.task-list-item input:checkbox").each( (index, e) =>
if e == this
taskNum = index + 1
)
$.ajax
type: "PATCH"
url: taskableUrl
data: objType + "[state_event]=" + stateEvent +
"&" + objType + "[task_num]=" + taskNum
#= require jquery
#= require jquery.waitforimages
#= require task_list
class @Issue class @Issue
constructor: -> constructor: ->
$('.edit-issue.inline-update input[type="submit"]').hide() $('.edit-issue.inline-update input[type="submit"]').hide()
...@@ -6,11 +10,11 @@ class @Issue ...@@ -6,11 +10,11 @@ class @Issue
$(".context .inline-update").on "change", "#issue_assignee_id", -> $(".context .inline-update").on "change", "#issue_assignee_id", ->
$(this).submit() $(this).submit()
if $("a.btn-close").length # Prevent duplicate event bindings
$("li.task-list-item input:checkbox").prop("disabled", false) @disableTaskList()
$('.task-list-item input:checkbox').off('change') if $("a.btn-close").length
$('.task-list-item input:checkbox').change('issue', updateTaskState) @initTaskList()
$('.issue-details').waitForImages -> $('.issue-details').waitForImages ->
$('.issuable-affix').affix offset: $('.issuable-affix').affix offset:
...@@ -22,3 +26,22 @@ class @Issue ...@@ -22,3 +26,22 @@ class @Issue
$(@).width($(@).outerWidth()) $(@).width($(@).outerWidth())
.on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> .on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
$(@).width('') $(@).width('')
initTaskList: ->
$('.issue-details .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList
disableTaskList: ->
$('.issue-details .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.issue-details .js-task-list-container'
# TODO (rspeicher): Make the issue description inline-editable like a note so
# that we can re-use its form here
updateTaskList: ->
patchData = {}
patchData['issue'] = {'description': $('.js-task-list-field', this).val()}
$.ajax
type: 'PATCH'
url: $('form.js-issue-update').attr('action')
data: patchData
#= require jquery
#= require bootstrap
#= require task_list
class @MergeRequest class @MergeRequest
constructor: (@opts) -> constructor: (@opts) ->
@initContextWidget() @initContextWidget()
...@@ -17,8 +21,11 @@ class @MergeRequest ...@@ -17,8 +21,11 @@ class @MergeRequest
disableButtonIfEmptyField '#commit_message', '.accept_merge_request' disableButtonIfEmptyField '#commit_message', '.accept_merge_request'
# Prevent duplicate event bindings
@disableTaskList()
if $("a.btn-close").length if $("a.btn-close").length
$("li.task-list-item input:checkbox").prop("disabled", false) @initTaskList()
$('.merge-request-details').waitForImages -> $('.merge-request-details').waitForImages ->
$('.issuable-affix').affix offset: $('.issuable-affix').affix offset:
...@@ -77,9 +84,6 @@ class @MergeRequest ...@@ -77,9 +84,6 @@ class @MergeRequest
this.$('.remove_source_branch_in_progress').hide() this.$('.remove_source_branch_in_progress').hide()
this.$('.remove_source_branch_widget.failed').show() this.$('.remove_source_branch_widget.failed').show()
$('.task-list-item input:checkbox').off('change')
$('.task-list-item input:checkbox').change('merge_request', updateTaskState)
activateTab: (action) -> activateTab: (action) ->
this.$('.merge-request-tabs li').removeClass 'active' this.$('.merge-request-tabs li').removeClass 'active'
this.$('.tab-content').hide() this.$('.tab-content').hide()
...@@ -156,3 +160,22 @@ class @MergeRequest ...@@ -156,3 +160,22 @@ class @MergeRequest
else else
setTimeout(merge_request.mergeInProgress, 3000) setTimeout(merge_request.mergeInProgress, 3000)
dataType: 'json' dataType: 'json'
initTaskList: ->
$('.merge-request-details .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList
disableTaskList: ->
$('.merge-request-details .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container'
# TODO (rspeicher): Make the merge request description inline-editable like a
# note so that we can re-use its form here
updateTaskList: ->
patchData = {}
patchData['merge_request'] = {'description': $('.js-task-list-field', this).val()}
$.ajax
type: 'PATCH'
url: $('form.js-merge-request-update').attr('action')
data: patchData
#= require jquery
#= require autosave
#= require bootstrap
#= require dropzone
#= require dropzone_input
#= require gfm_auto_complete
#= require jquery.atwho
#= require task_list
class @Notes class @Notes
@interval: null @interval: null
...@@ -11,6 +20,7 @@ class @Notes ...@@ -11,6 +20,7 @@ class @Notes
@setupMainTargetNoteForm() @setupMainTargetNoteForm()
@cleanBinding() @cleanBinding()
@addBinding() @addBinding()
@initTaskList()
addBinding: -> addBinding: ->
# add note to UI after creation # add note to UI after creation
...@@ -81,6 +91,9 @@ class @Notes ...@@ -81,6 +91,9 @@ class @Notes
$(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close" $(document).off "click", ".js-note-target-close"
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
initRefresh: -> initRefresh: ->
clearInterval(Notes.interval) clearInterval(Notes.interval)
Notes.interval = setInterval => Notes.interval = setInterval =>
...@@ -114,6 +127,7 @@ class @Notes ...@@ -114,6 +127,7 @@ class @Notes
if @isNewNote(note) if @isNewNote(note)
@note_ids.push(note.id) @note_ids.push(note.id)
$('ul.main-notes-list').append(note.html) $('ul.main-notes-list').append(note.html)
@initTaskList()
### ###
Check if note does not exists on page Check if note does not exists on page
...@@ -268,6 +282,8 @@ class @Notes ...@@ -268,6 +282,8 @@ class @Notes
note_li.replaceWith(note.html) note_li.replaceWith(note.html)
note_li.find('.note-edit-form').hide() note_li.find('.note-edit-form').hide()
note_li.find('.note-body > .note-text').show() note_li.find('.note-body > .note-text').show()
note_li.find('js-task-list-container').taskList('enable')
@enableTaskList()
### ###
Called in response to clicking the edit note link Called in response to clicking the edit note link
...@@ -479,3 +495,13 @@ class @Notes ...@@ -479,3 +495,13 @@ class @Notes
else else
form.find('.js-note-target-reopen').text('Reopen') form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close') form.find('.js-note-target-close').text('Close')
initTaskList: ->
@enableTaskList()
$(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList
enableTaskList: ->
$('.note .js-task-list-container').taskList('enable')
updateTaskList: ->
$('form', this).submit()
...@@ -37,7 +37,9 @@ pre { ...@@ -37,7 +37,9 @@ pre {
position: relative; position: relative;
a.anchor { a.anchor {
display: none; // Setting `display: none` would prevent the anchor being scrolled to, so
// instead we set the height to 0 and it gets updated on hover.
height: 0;
} }
&:hover > a.anchor { &:hover > a.anchor {
......
...@@ -62,6 +62,16 @@ ul.notes { ...@@ -62,6 +62,16 @@ ul.notes {
word-wrap: break-word; word-wrap: break-word;
@include md-typography; @include md-typography;
// Reduce left padding of first ul element
ul.task-list:first-child {
padding-left: 10px;
// sub-lists should be padded normally
ul {
padding-left: 20px;
}
}
hr { hr {
margin: 10px 0; margin: 10px 0;
} }
......
...@@ -9,6 +9,10 @@ module NotesHelper ...@@ -9,6 +9,10 @@ module NotesHelper
hidden_field_tag(:target_id, note.noteable.id) hidden_field_tag(:target_id, note.noteable.id)
end end
def note_editable?(note)
note.editable? && can?(current_user, :admin_note, note)
end
def link_to_commit_diff_line_note(note) def link_to_commit_diff_line_note(note)
if note.for_commit_diff_line? if note.for_commit_diff_line?
link_to( link_to(
......
require 'task_list'
# Contains functionality for objects that can have task lists in their # Contains functionality for objects that can have task lists in their
# descriptions. Task list items can be added with Markdown like "* [x] Fix # descriptions. Task list items can be added with Markdown like "* [x] Fix
# bugs". # bugs".
# #
# Used by MergeRequest and Issue # Used by MergeRequest and Issue
module Taskable module Taskable
TASK_PATTERN_MD = /^(?<bullet> *[*-] *)\[(?<checked>[ xX])\]/.freeze # Called by `TaskList::Summary`
TASK_PATTERN_HTML = /^<li>(?<p_tag>\s*<p>)?\[(?<checked>[ xX])\]/.freeze def task_list_items
return [] if description.blank?
# 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 @task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item|
# has changed. # ItemPattern strips out the hyphen, but Item requires it. Rabble rabble.
self.description = self.description.gsub(TASK_PATTERN_MD) do |match| TaskList::Item.new("- #{item}")
index += 1
case index
when n then "#{$LAST_MATCH_INFO[:bullet]}[#{check_char}]"
else match
end
end end
end
save def tasks
@tasks ||= TaskList.new(self)
end end
# Return true if this object's description has any task list items. # Return true if this object's description has any task list items.
def tasks? def tasks?
description && description.match(TASK_PATTERN_MD) tasks.summary.items?
end end
# Return a string that describes the current state of this Taskable's task # Return a string that describes the current state of this Taskable's task
# list items, e.g. "20 tasks (12 done, 8 unfinished)" # list items, e.g. "20 tasks (12 completed, 8 remaining)"
def task_status def task_status
return nil unless description return '' if description.blank?
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)" sum = tasks.summary
"#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)"
end end
end end
- content_for :note_actions do - content_for :note_actions do
- if can?(current_user, :modify_issue, @issue) - if can?(current_user, :modify_issue, @issue)
- if @issue.closed? - if @issue.closed?
= link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue' = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
- else - else
= link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue'
= render 'shared/show_aside' = render 'shared/show_aside'
...@@ -15,11 +15,11 @@ ...@@ -15,11 +15,11 @@
%span= pluralize(@issue.participants(current_user).count, 'participant') %span= pluralize(@issue.participants(current_user).count, 'participant')
- @issue.participants(current_user).each do |participant| - @issue.participants(current_user).each do |participant|
= link_to_member(@project, participant, name: false, size: 24) = link_to_member(@project, participant, name: false, size: 24)
.voting_notes#notes= render "projects/notes/notes_with_form" .voting_notes#notes= render 'projects/notes/notes_with_form'
%aside.col-md-3 %aside.col-md-3
.issuable-affix .issuable-affix
.clearfix .clearfix
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} %span.slead.has_tooltip{title: 'Cross-project reference'}
= cross_project_reference(@project, @issue) = cross_project_reference(@project, @issue)
%hr %hr
.context .context
......
= form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f| = form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update js-issue-update'} do |f|
%div.prepend-top-20 %div.prepend-top-20
.issuable-context-title .issuable-context-title
%label %label
......
...@@ -13,17 +13,17 @@ ...@@ -13,17 +13,17 @@
.pull-right .pull-right
- if can?(current_user, :write_issue, @project) - if can?(current_user, :write_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-grouped new-issue-link", title: "New Issue", id: "new_issue_link" do = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do
%i.fa.fa-plus = icon('plus')
New Issue New Issue
- if can?(current_user, :modify_issue, @issue) - if can?(current_user, :modify_issue, @issue)
- if @issue.closed? - if @issue.closed?
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen'
- else - else
= link_to 'Close', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: "btn btn-grouped issuable-edit" do = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do
%i.fa.fa-pencil-square-o = icon('pencil-square-o')
Edit Edit
%hr %hr
...@@ -31,11 +31,13 @@ ...@@ -31,11 +31,13 @@
= gfm escape_once(@issue.title) = gfm escape_once(@issue.title)
%div %div
- if @issue.description.present? - if @issue.description.present?
.description .description{class: can?(current_user, :modify_issue, @issue) ? 'js-task-list-container' : ''}
.wiki .wiki
= preserve do = preserve do
= markdown(@issue.description, parse_tasks: true) = markdown(@issue.description)
%textarea.hidden.js-task-list-field
= @issue.description
%hr %hr
.issue-discussion .issue-discussion
= render "projects/issues/discussion" = render 'projects/issues/discussion'
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f| = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update js-merge-request-update'} do |f|
%div.prepend-top-20 %div.prepend-top-20
.issuable-context-title .issuable-context-title
%label %label
...@@ -19,13 +19,13 @@ ...@@ -19,13 +19,13 @@
%span.back-to-milestone %span.back-to-milestone
= link_to namespace_project_milestone_path(@project.namespace, @project, @merge_request.milestone) do = link_to namespace_project_milestone_path(@project.namespace, @project, @merge_request.milestone) do
%strong %strong
%i.fa.fa-clock-o = icon('clock-o')
= @merge_request.milestone.title = @merge_request.milestone.title
- else - else
none none
.issuable-context-selectbox .issuable-context-selectbox
- if can?(current_user, :modify_merge_request, @merge_request) - if can?(current_user, :modify_merge_request, @merge_request)
= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'})
= hidden_field_tag :merge_request_context = hidden_field_tag :merge_request_context
= f.submit class: 'btn' = f.submit class: 'btn'
...@@ -35,13 +35,13 @@ ...@@ -35,13 +35,13 @@
%label %label
Subscription: Subscription:
%button.btn.btn-block.subscribe-button{:type => 'button'} %button.btn.btn-block.subscribe-button{:type => 'button'}
%i.fa.fa-eye = icon('eye')
%span= @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" %span= @merge_request.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
- subscribtion_status = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed" - subscribtion_status = @merge_request.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
.subscription-status{"data-status" => subscribtion_status} .subscription-status{data: {status: subscribtion_status}}
.description-block.unsubscribed{class: ( "hidden" if @merge_request.subscribed?(current_user) )} .description-block.unsubscribed{class: ( 'hidden' if @merge_request.subscribed?(current_user) )}
You're not receiving notifications from this thread. You're not receiving notifications from this thread.
.description-block.subscribed{class: ( "hidden" unless @merge_request.subscribed?(current_user) )} .description-block.subscribed{class: ( 'hidden' unless @merge_request.subscribed?(current_user) )}
You're receiving notifications because you're subscribed to this thread. You're receiving notifications because you're subscribed to this thread.
:coffeescript :coffeescript
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
%div %div
- if @merge_request.description.present? - if @merge_request.description.present?
.description .description{class: can?(current_user, :modify_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki .wiki
= preserve do = preserve do
= markdown(@merge_request.description, parse_tasks: true) = markdown(@merge_request.description)
%textarea.hidden.js-task-list-field
= @merge_request.description
.note-edit-form .note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f| = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
= note_target_fields(note) = note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do = render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do
= render 'projects/zen', f: f, attr: :note, = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
classes: 'note_text js-note-text'
.comment-hints.clearfix .comment-hints.clearfix
.pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} .pull-left Comments are parsed with #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'),{ target: '_blank', tabindex: -1 }}
.pull-right Attach files by dragging &amp; dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. .pull-right Attach files by dragging &amp; dropping or #{link_to 'selecting them', '#', class: 'markdown-selector', tabindex: -1 }.
.note-form-actions .note-form-actions
.buttons .buttons
= f.submit 'Save Comment', class: "btn btn-primary btn-save btn-grouped js-comment-button" = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
= link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel" = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
\ No newline at end of file
...@@ -2,28 +2,28 @@ ...@@ -2,28 +2,28 @@
.timeline-entry-inner .timeline-entry-inner
.timeline-icon .timeline-icon
- if note.system - if note.system
%span.fa.fa-circle %span= icon('circle')
- else - else
= link_to user_path(note.author) do = link_to user_path(note.author) do
= image_tag avatar_icon(note.author_email), class: "avatar s40", alt: '' = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: ''
.timeline-content .timeline-content
.note-header .note-header
.note-actions .note-actions
= link_to "##{dom_id(note)}", name: dom_id(note) do = link_to "##{dom_id(note)}", name: dom_id(note) do
%i.fa.fa-link = icon('link')
Link here Link here
&nbsp; &nbsp;
- if can?(current_user, :admin_note, note) && note.editable? - if note_editable?(note)
= link_to "#", title: "Edit comment", class: "js-note-edit" do = link_to '#', title: 'Edit comment', class: 'js-note-edit' do
%i.fa.fa-pencil-square-o = icon('pencil-square-o')
Edit Edit
&nbsp; &nbsp;
= link_to namespace_project_note_path(@project.namespace, @project, note), title: "Remove comment", method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: "danger js-note-delete" do = link_to namespace_project_note_path(@project.namespace, @project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'danger js-note-delete' do
%i.fa.fa-trash-o.cred = icon('trash-o', class: 'cred')
Remove Remove
- if note.system - if note.system
= link_to user_path(note.author) do = link_to user_path(note.author) do
= image_tag avatar_icon(note.author_email), class: "avatar s16", alt: '' = image_tag avatar_icon(note.author_email), class: 'avatar s16', alt: ''
= link_to_member(@project, note.author, avatar: false) = link_to_member(@project, note.author, avatar: false)
%span.author-username %span.author-username
= '@' + note.author.username = '@' + note.author.username
...@@ -33,24 +33,24 @@ ...@@ -33,24 +33,24 @@
- if note.superceded?(@notes) - if note.superceded?(@notes)
- if note.upvote? - if note.upvote?
%span.vote.upvote.label.label-gray.strikethrough %span.vote.upvote.label.label-gray.strikethrough
%i.fa.fa-thumbs-up = icon('thumbs-up')
\+1 \+1
- if note.downvote? - if note.downvote?
%span.vote.downvote.label.label-gray.strikethrough %span.vote.downvote.label.label-gray.strikethrough
%i.fa.fa-thumbs-down = icon('thumbs-down')
\-1 \-1
- else - else
- if note.upvote? - if note.upvote?
%span.vote.upvote.label.label-success %span.vote.upvote.label.label-success
%i.fa.fa-thumbs-up = icon('thumbs-up')
\+1 \+1
- if note.downvote? - if note.downvote?
%span.vote.downvote.label.label-danger %span.vote.downvote.label.label-danger
%i.fa.fa-thumbs-down = icon('thumbs-down')
\-1 \-1
.note-body .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text .note-text
= preserve do = preserve do
= markdown(note.note, {no_header_anchors: true}) = markdown(note.note, {no_header_anchors: true})
...@@ -62,10 +62,10 @@ ...@@ -62,10 +62,10 @@
= link_to note.attachment.url, target: '_blank' do = link_to note.attachment.url, target: '_blank' do
= image_tag note.attachment.url, class: 'note-image-attach' = image_tag note.attachment.url, class: 'note-image-attach'
.attachment .attachment
= link_to note.attachment.url, target: "_blank" do = link_to note.attachment.url, target: '_blank' do
%i.fa.fa-paperclip = icon('paperclip')
= note.attachment_identifier = note.attachment_identifier
= link_to delete_attachment_namespace_project_note_path(@project.namespace, @project, note), = link_to delete_attachment_namespace_project_note_path(@project.namespace, @project, note),
title: "Delete this attachment", method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: "danger js-note-attachment-delete" do title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
%i.fa.fa-trash-o.cred = icon('trash-o', class: 'cred')
.clear .clear
...@@ -5,4 +5,5 @@ if Rails.env.development? ...@@ -5,4 +5,5 @@ if Rails.env.development?
Rack::MiniProfilerRails.initialize!(Rails.application) Rack::MiniProfilerRails.initialize!(Rails.application)
Rack::MiniProfiler.config.position = 'right' Rack::MiniProfiler.config.position = 'right'
Rack::MiniProfiler.config.start_hidden = true Rack::MiniProfiler.config.start_hidden = true
Rack::MiniProfiler.config.skip_paths << '/specs'
end end
...@@ -172,7 +172,7 @@ GFM will turn that reference into a link so you can navigate between them easily ...@@ -172,7 +172,7 @@ GFM will turn that reference into a link so you can navigate between them easily
GFM will recognize the following: GFM will recognize the following:
| input | references | | input | references |
|-----------------------:|:---------------------------| |:-----------------------|:---------------------------|
| `@user_name` | specific user | | `@user_name` | specific user |
| `@group_name` | specific group | | `@group_name` | specific group |
| `@all` | entire team | | `@all` | entire team |
...@@ -189,7 +189,7 @@ GFM will recognize the following: ...@@ -189,7 +189,7 @@ GFM will recognize the following:
GFM also recognizes certain cross-project references: GFM also recognizes certain cross-project references:
| input | references | | input | references |
|----------------------------------------:|:------------------------| |:----------------------------------------|:------------------------|
| `namespace/project#123` | issue | | `namespace/project#123` | issue |
| `namespace/project!123` | merge request | | `namespace/project!123` | merge request |
| `namespace/project$123` | snippet | | `namespace/project$123` | snippet |
...@@ -198,15 +198,23 @@ GFM also recognizes certain cross-project references: ...@@ -198,15 +198,23 @@ GFM also recognizes certain cross-project references:
## Task Lists ## 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: You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
```no-highlight ```no-highlight
* [x] Completed task - [x] Completed task
* [ ] Unfinished task - [ ] Incomplete task
* [x] Nested task - [ ] Sub-task 1
- [x] Sub-task 2
- [ ] Sub-task 3
``` ```
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. - [x] Completed task
- [ ] Incomplete task
- [ ] Sub-task 1
- [x] Sub-task 2
- [ ] Sub-task 3
Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
# Standard Markdown # Standard Markdown
...@@ -246,51 +254,38 @@ Alt-H2 ...@@ -246,51 +254,38 @@ Alt-H2
### Header IDs and links ### Header IDs and links
All markdown rendered headers automatically get IDs, except for comments. All Markdown-rendered headers automatically get IDs, except in comments.
On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else. On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
The IDs are generated from the content of the header according to the following rules: The IDs are generated from the content of the header according to the following rules:
1. remove the heading hashes `#` and process the rest of the line as it would be processed if it were not a header 1. All text is converted to lowercase
2. from the result, remove all HTML tags, but keep their inner content 1. All non-word text (e.g., punctuation, HTML) is removed
3. convert all characters to lowercase 1. All spaces are converted to hyphens
4. convert all characters except `[a-z0-9_-]` into hyphens `-` 1. Two or more hyphens in a row are converted to one
5. transform multiple adjacent hyphens into a single hyphen 1. If a header with the same ID has already been generated, a unique
6. remove trailing and heading hyphens incrementing number is appended.
For example: For example:
``` ```
###### ..Ab_c-d. e [anchor](URL) ![alt text](URL).. # This header has spaces in it
## This header has a :thumbsup: in it
# This header has Unicode in it: 한글
## This header has spaces in it
### This header has spaces in it
``` ```
which renders as: Would generate the following link IDs:
###### ..Ab_c-d. e [anchor](URL) ![alt text](URL)..
will first be converted by step 1) into a string like:
``` 1. `this-header-has-spaces-in-it`
..Ab_c-d. e &lt;a href="URL">anchor&lt;/a> &lt;img src="URL" alt="alt text"/>.. 1. `this-header-has-a-in-it`
``` 1. `this-header-has-unicode-in-it-한글`
1. `this-header-has-spaces-in-it-1`
1. `this-header-has-spaces-in-it-2`
After removing the tags in step 2) we get: Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
```
..Ab_c-d. e anchor ..
```
And applying all the other steps gives the id:
```
ab_c-d-e-anchor
```
Note in particular how:
- for markdown anchors `[text](URL)`, only the `text` is used
- markdown images `![alt](URL)` are completely ignored
## Emphasis ## Emphasis
...@@ -322,8 +317,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~ ...@@ -322,8 +317,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~
1. Ordered sub-list 1. Ordered sub-list
4. And another item. 4. And another item.
Some text that should be aligned with the above item.
* Unordered list can use asterisks * Unordered list can use asterisks
- Or minuses - Or minuses
+ Or pluses + Or pluses
...@@ -336,8 +329,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~ ...@@ -336,8 +329,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~
1. Ordered sub-list 1. Ordered sub-list
4. And another item. 4. And another item.
Some text that should be aligned with the above item.
* Unordered list can use asterisks * Unordered list can use asterisks
- Or minuses - Or minuses
+ Or pluses + Or pluses
...@@ -432,7 +423,7 @@ Quote break. ...@@ -432,7 +423,7 @@ Quote break.
You can also use raw HTML in your Markdown, and it'll mostly work pretty well. You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows the `class`, `id`, and `style` attributes. See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements, as well as the `class`, and `id` attributes on all elements.
```no-highlight ```no-highlight
<dl> <dl>
...@@ -536,6 +527,20 @@ Code above produces next output: ...@@ -536,6 +527,20 @@ Code above produces next output:
The row of dashes between the table header and body must have at least three dashes in each column. The row of dashes between the table header and body must have at least three dashes in each column.
By including colons in the header row, you can align the text within that column:
```
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
```
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
## References ## References
- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
......
...@@ -134,48 +134,15 @@ Feature: Project Issues ...@@ -134,48 +134,15 @@ Feature: Project Issues
And I should see "Release 0.4" in issues And I should see "Release 0.4" in issues
And I should not see "Tweet control" 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
@javascript @javascript
Scenario: Issue notes should be editable with +1 Scenario: Issue notes should be editable with +1
Given project "Shop" has "Tasks-open" open issue with task markdown Given project "Shop" have "Release 0.4" open issue
When I visit issue page "Tasks-open" When I visit issue page "Release 0.4"
And I leave a comment with a header containing "Comment with a header" And I leave a comment with a header containing "Comment with a header"
Then The comment with the header should not have an ID Then The comment with the header should not have an ID
And I edit the last comment with a +1 And I edit the last comment with a +1
Then I should see +1 in the description Then I should see +1 in the description
# 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 the Taskable
# 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
# Issue description preview # Issue description preview
@javascript @javascript
...@@ -212,8 +179,8 @@ Feature: Project Issues ...@@ -212,8 +179,8 @@ Feature: Project Issues
@javascript @javascript
Scenario: I can unsubscribe from issue Scenario: I can unsubscribe from issue
Given project "Shop" has "Tasks-open" open issue with task markdown Given project "Shop" have "Release 0.4" open issue
When I visit issue page "Tasks-open" When I visit issue page "Release 0.4"
Then I should see that I am subscribed Then I should see that I am subscribed
When I click button "Unsubscribe" When I click button "Unsubscribe"
Then I should see that I am unsubscribed Then I should see that I am unsubscribed
...@@ -96,16 +96,6 @@ Feature: Project Merge Requests ...@@ -96,16 +96,6 @@ Feature: Project Merge Requests
And I leave a comment with a header containing "Comment with a header" And I leave a comment with a header containing "Comment with a header"
Then The comment with the header should not have an ID 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 # Toggling inline comments
@javascript @javascript
...@@ -173,28 +163,6 @@ Feature: Project Merge Requests ...@@ -173,28 +163,6 @@ Feature: Project Merge Requests
And I click on the Changes tab via Javascript And I click on the Changes tab via Javascript
Then I should see the proper Inline and Side-by-side links Then I should see the proper Inline and Side-by-side links
# Task status in issues list
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 the Taskable
# 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
# Description preview # Description preview
@javascript @javascript
......
...@@ -179,14 +179,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -179,14 +179,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
author: project.users.first) author: project.users.first)
end end
step 'project "Shop" has "Tasks-open" open issue with task markdown' do
create_taskable(:issue, 'Tasks-open')
end
step 'project "Shop" has "Tasks-closed" closed issue with task markdown' do
create_taskable(:closed_issue, 'Tasks-closed')
end
step 'empty project "Empty Project"' do step 'empty project "Empty Project"' do
create :empty_project, name: 'Empty Project', namespace: @user.namespace create :empty_project, name: 'Empty Project', namespace: @user.namespace
end end
......
...@@ -108,10 +108,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -108,10 +108,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
author: project.users.first) author: project.users.first)
end end
step 'project "Shop" has "MR-task-open" open MR with task markdown' do
create_taskable(:merge_request, 'MR-task-open')
end
step 'I switch to the diff tab' do step 'I switch to the diff tab' do
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end end
......
...@@ -10,55 +10,10 @@ module SharedMarkdown ...@@ -10,55 +10,10 @@ module SharedMarkdown
find(:xpath, "#{node.path}/..").text.should == text find(:xpath, "#{node.path}/..").text.should == text
end end
def create_taskable(type, title)
desc_text = <<EOT.gsub(/^ {6}/, '')
* [ ] Task 1
* [x] Task 2
EOT
case type
when :issue, :closed_issue
options = { project: project }
when :merge_request
options = { source_project: project, target_project: project }
end
create(
type,
options.merge(title: title,
author: project.users.first,
description: desc_text)
)
end
step 'Header "Description header" should have correct id and link' do step 'Header "Description header" should have correct id and link' do
header_should_have_correct_id_and_link(1, 'Description header', 'description-header') header_should_have_correct_id_and_link(1, 'Description header', 'description-header')
end end
step 'I should see task checkboxes in the description' do
expect(page).to have_selector(
'div.description li.task-list-item input[type="checkbox"]'
)
end
step 'I should see the task status for the Taskable' do
expect(find(:css, 'span.task-status').text).to eq(
'2 tasks (1 done, 1 unfinished)'
)
end
step 'Task checkboxes should be enabled' do
expect(page).to have_selector(
'div.description li.task-list-item input[type="checkbox"]:enabled'
)
end
step 'Task checkboxes should be disabled' do
expect(page).to have_selector(
'div.description li.task-list-item input[type="checkbox"]:disabled'
)
end
step 'I should not see the Markdown preview' do step 'I should not see the Markdown preview' do
expect(find('.gfm-form .js-md-preview')).not_to be_visible expect(find('.gfm-form .js-md-preview')).not_to be_visible
end end
......
...@@ -122,20 +122,6 @@ module SharedNote ...@@ -122,20 +122,6 @@ module SharedNote
end end
end end
step 'I leave a comment with task markdown' do
within('.js-main-target-form') do
fill_in 'note[note]', with: '* [x] Task item'
click_button 'Add Comment'
sleep 0.05
end
end
step 'I should not see task checkboxes in the comment' do
expect(page).not_to have_selector(
'li.note div.timeline-content input[type="checkbox"]'
)
end
step 'I edit the last comment with a +1' do step 'I edit the last comment with a +1' do
find(".note").hover find(".note").hover
find('.js-note-edit').click find('.js-note-edit').click
......
...@@ -323,16 +323,6 @@ module SharedPaths ...@@ -323,16 +323,6 @@ module SharedPaths
visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
end end
step 'I visit issue page "Tasks-open"' do
issue = Issue.find_by(title: 'Tasks-open')
visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
end
step 'I visit issue page "Tasks-closed"' do
issue = Issue.find_by(title: 'Tasks-closed')
visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
end
step 'I visit project "Shop" labels page' do step 'I visit project "Shop" labels page' do
project = Project.find_by(name: 'Shop') project = Project.find_by(name: 'Shop')
visit namespace_project_labels_path(project.namespace, project) visit namespace_project_labels_path(project.namespace, project)
...@@ -363,16 +353,6 @@ module SharedPaths ...@@ -363,16 +353,6 @@ module SharedPaths
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
end end
step 'I visit merge request page "MR-task-open"' do
mr = MergeRequest.find_by(title: 'MR-task-open')
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
end
step 'I visit merge request page "MR-task-closed"' do
mr = MergeRequest.find_by(title: 'MR-task-closed')
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
end
step 'I visit project "Shop" merge requests page' do step 'I visit project "Shop" merge requests page' do
visit namespace_project_merge_requests_path(project.namespace, project) visit namespace_project_merge_requests_path(project.namespace, project)
end end
......
require 'html/pipeline' require 'html/pipeline'
require 'task_list/filter'
module Gitlab module Gitlab
# Custom parser for GitLab-flavored Markdown # Custom parser for GitLab-flavored Markdown
...@@ -31,9 +32,9 @@ module Gitlab ...@@ -31,9 +32,9 @@ module Gitlab
# Public: Parse the provided text with GitLab-Flavored Markdown # Public: Parse the provided text with GitLab-Flavored Markdown
# #
# text - the source text # text - the source text
# options - parse_tasks - render tasks # options - A Hash of options used to customize output (default: {}):
# - xhtml - output XHTML instead of HTML # :xhtml - output XHTML instead of HTML
# - reference_only_path - Use relative path for reference links # :reference_only_path - Use relative path for reference links
# project - the project # project - the project
# html_options - extra options for the reference links as given to link_to # html_options - extra options for the reference links as given to link_to
def gfm_with_options(text, options = {}, project = @project, html_options = {}) def gfm_with_options(text, options = {}, project = @project, html_options = {})
...@@ -45,7 +46,6 @@ module Gitlab ...@@ -45,7 +46,6 @@ module Gitlab
text = text.dup.to_str text = text.dup.to_str
options.reverse_merge!( options.reverse_merge!(
parse_tasks: false,
xhtml: false, xhtml: false,
reference_only_path: true reference_only_path: true
) )
...@@ -76,10 +76,6 @@ module Gitlab ...@@ -76,10 +76,6 @@ module Gitlab
text = result[:output].to_html(save_with: save_options) text = result[:output].to_html(save_with: save_options)
if options[:parse_tasks]
text = parse_tasks(text)
end
text.html_safe text.html_safe
end end
...@@ -106,28 +102,10 @@ module Gitlab ...@@ -106,28 +102,10 @@ module Gitlab
Gitlab::Markdown::SnippetReferenceFilter, Gitlab::Markdown::SnippetReferenceFilter,
Gitlab::Markdown::CommitRangeReferenceFilter, Gitlab::Markdown::CommitRangeReferenceFilter,
Gitlab::Markdown::CommitReferenceFilter, Gitlab::Markdown::CommitReferenceFilter,
Gitlab::Markdown::LabelReferenceFilter Gitlab::Markdown::LabelReferenceFilter,
]
end
# Turn list items that start with "[ ]" into HTML checkbox inputs. TaskList::Filter
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'
p_tag = $LAST_MATCH_INFO[:p_tag]
if checked
"#{li_tag}#{p_tag}#{checked_box}"
else
"#{li_tag}#{p_tag}#{unchecked_box}"
end
end
end end
end end
end end
...@@ -31,7 +31,7 @@ module Gitlab ...@@ -31,7 +31,7 @@ module Gitlab
id = text.downcase id = text.downcase
id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation
id.gsub!(' ', '-') # replace spaces with dash id.gsub!(' ', '-') # replace spaces with dash
id.squeeze!(' -') # replace multiple spaces or dashes with one id.squeeze!('-') # replace multiple dashes with one
uniq = (headers[id] > 0) ? "-#{headers[id]}" : '' uniq = (headers[id] > 0) ? "-#{headers[id]}" : ''
headers[id] += 1 headers[id] += 1
......
# Since we no longer explicitly require the 'jasmine' gem, we lost the
# `jasmine:ci` task used by GitLab CI jobs.
#
# This provides a simple alias to run the `spec:javascript` task from the
# 'jasmine-rails' gem.
task jasmine: ['jasmine:ci']
namespace :jasmine do
task :ci do
Rake::Task['spec:javascript'].invoke
end
end
...@@ -24,6 +24,7 @@ require 'erb' ...@@ -24,6 +24,7 @@ require 'erb'
# -> Rinku (http, https, ftp) # -> Rinku (http, https, ftp)
# -> Other schemes # -> Other schemes
# -> References # -> References
# -> TaskList
# -> `html_safe` # -> `html_safe`
# -> Template # -> Template
# #
...@@ -279,6 +280,15 @@ describe 'GitLab Markdown' do ...@@ -279,6 +280,15 @@ describe 'GitLab Markdown' do
expect(body).to have_selector('a.gfm.gfm-label', count: 3) expect(body).to have_selector('a.gfm.gfm-label', count: 3)
end end
end end
describe 'Task Lists' do
it 'generates task lists' do
body = get_section('task-lists')
expect(body).to have_selector('ul.task-list', count: 2)
expect(body).to have_selector('li.task-list-item', count: 7)
expect(body).to have_selector('input[checked]', count: 3)
end
end
end end
end end
...@@ -289,9 +299,8 @@ end ...@@ -289,9 +299,8 @@ end
# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)` # once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
# block, so we fake it by encapsulating all the shared setup in this class. # block, so we fake it by encapsulating all the shared setup in this class.
# #
# The class contains the raw Markup used in the test, dynamically substituting # The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
# real objects, created from factories and setup on-demand, when referenced in # reference to the factory-created objects.
# the Markdown.
class MarkdownFeature class MarkdownFeature
include FactoryGirl::Syntax::Methods include FactoryGirl::Syntax::Methods
......
require 'spec_helper'
feature 'Task Lists' do
include Warden::Test::Helpers
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:markdown) do
<<-MARKDOWN.strip_heredoc
This is a task list:
- [ ] Incomplete entry 1
- [x] Complete entry 1
- [ ] Incomplete entry 2
- [x] Complete entry 2
- [ ] Incomplete entry 3
- [ ] Incomplete entry 4
MARKDOWN
end
before do
Warden.test_mode!
project.team << [user, :master]
project.team << [user2, :guest]
login_as(user)
end
def visit_issue(project, issue)
visit namespace_project_issue_path(project.namespace, project, issue)
end
describe 'for Issues' do
let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
it 'renders' do
visit_issue(project, issue)
expect(page).to have_selector('ul.task-list', count: 1)
expect(page).to have_selector('li.task-list-item', count: 6)
expect(page).to have_selector('ul input[checked]', count: 2)
end
it 'contains the required selectors' do
visit_issue(project, issue)
container = '.issue-details .description.js-task-list-container'
expect(page).to have_selector(container)
expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
expect(page).to have_selector("#{container} .js-task-list-field")
expect(page).to have_selector('form.js-issue-update')
expect(page).to have_selector('a.btn-close')
end
it 'is only editable by author' do
visit_issue(project, issue)
expect(page).to have_selector('.js-task-list-container')
logout(:user)
login_as(user2)
visit current_path
expect(page).not_to have_selector('.js-task-list-container')
end
it 'provides a summary on Issues#index' do
visit namespace_project_issues_path(project.namespace, project)
expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
end
end
describe 'for Notes' do
let!(:issue) { create(:issue, author: user, project: project) }
let!(:note) { create(:note, note: markdown, noteable: issue, author: user) }
it 'renders for note body' do
visit_issue(project, issue)
expect(page).to have_selector('.note ul.task-list', count: 1)
expect(page).to have_selector('.note li.task-list-item', count: 6)
expect(page).to have_selector('.note ul input[checked]', count: 2)
end
it 'contains the required selectors' do
visit_issue(project, issue)
expect(page).to have_selector('.note .js-task-list-container')
expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
end
it 'is only editable by author' do
visit_issue(project, issue)
expect(page).to have_selector('.js-task-list-container')
logout(:user)
login_as(user2)
visit current_path
expect(page).not_to have_selector('.js-task-list-container')
end
end
describe 'for Merge Requests' do
def visit_merge_request(project, merge)
visit namespace_project_merge_request_path(project.namespace, project, merge)
end
let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
it 'renders for description' do
visit_merge_request(project, merge)
expect(page).to have_selector('ul.task-list', count: 1)
expect(page).to have_selector('li.task-list-item', count: 6)
expect(page).to have_selector('ul input[checked]', count: 2)
end
it 'contains the required selectors' do
visit_merge_request(project, merge)
container = '.merge-request-details .description.js-task-list-container'
expect(page).to have_selector(container)
expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
expect(page).to have_selector("#{container} .js-task-list-field")
expect(page).to have_selector('form.js-merge-request-update')
expect(page).to have_selector('a.btn-close')
end
it 'is only editable by author' do
visit_merge_request(project, merge)
expect(page).to have_selector('.js-task-list-container')
logout(:user)
login_as(user2)
visit current_path
expect(page).not_to have_selector('.js-task-list-container')
end
it 'provides a summary on MergeRequests#index' do
visit namespace_project_merge_requests_path(project.namespace, project)
expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
end
end
end
...@@ -184,3 +184,13 @@ References should be parseable even inside _!<%= merge_request.iid %>_ emphasis. ...@@ -184,3 +184,13 @@ References should be parseable even inside _!<%= merge_request.iid %>_ emphasis.
- Label by name in quotes: ~"<%= label.name %>" - Label by name in quotes: ~"<%= label.name %>"
- Ignored in code: `~<%= simple_label.name %>` - Ignored in code: `~<%= simple_label.name %>`
- Ignored in links: [Link to ~<%= simple_label.id %>](#label-link) - Ignored in links: [Link to ~<%= simple_label.id %>](#label-link)
### Task Lists
- [ ] Incomplete task 1
- [x] Complete task 1
- [ ] Incomplete task 2
- [ ] Incomplete sub-task 1
- [ ] Incomplete sub-task 2
- [x] Complete sub-task 1
- [X] Complete task 2
...@@ -43,115 +43,6 @@ describe GitlabMarkdownHelper do ...@@ -43,115 +43,6 @@ describe GitlabMarkdownHelper do
expect(gfm(actual)).to match(expected) expect(gfm(actual)).to match(expected)
end end
end end
context 'parse_tasks: true' do
before(:all) do
@source_text_asterisk = <<-EOT.strip_heredoc
* [ ] valid unchecked task
* [x] valid lowercase checked task
* [X] valid uppercase checked task
* [ ] valid unchecked nested task
* [x] valid checked nested task
[ ] not an unchecked task - no list item
[x] not a checked task - no list item
* [ ] not an unchecked task - too many spaces
* [x ] not a checked task - too many spaces
* [] not an unchecked task - no spaces
* Not a task [ ] - not at beginning
EOT
@source_text_dash = <<-EOT.strip_heredoc
- [ ] valid unchecked task
- [x] valid lowercase checked task
- [X] valid uppercase checked task
- [ ] valid unchecked nested task
- [x] valid checked nested task
EOT
end
it 'should render checkboxes at beginning of asterisk list items' do
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/)
expect(rendered_text).to match(
/<input.*checkbox.*valid lowercase checked task/
)
expect(rendered_text).to match(
/<input.*checkbox.*valid uppercase checked task/
)
end
it 'should render checkboxes at beginning of dash list items' do
rendered_text = markdown(@source_text_dash, parse_tasks: true)
expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/)
expect(rendered_text).to match(
/<input.*checkbox.*valid lowercase checked task/
)
expect(rendered_text).to match(
/<input.*checkbox.*valid uppercase checked task/
)
end
it 'should render checkboxes for nested tasks' do
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
expect(rendered_text).to match(
/<input.*checkbox.*valid unchecked nested task/
)
expect(rendered_text).to match(
/<input.*checkbox.*valid checked nested task/
)
end
it 'should not be confused by whitespace before bullets' do
rendered_text_asterisk = markdown(@source_text_asterisk, parse_tasks: true)
rendered_text_dash = markdown(@source_text_dash, parse_tasks: true)
expect(rendered_text_asterisk).to match(
/<input.*checkbox.*valid unchecked nested task/
)
expect(rendered_text_asterisk).to match(
/<input.*checkbox.*valid checked nested task/
)
expect(rendered_text_dash).to match(
/<input.*checkbox.*valid unchecked nested task/
)
expect(rendered_text_dash).to match(
/<input.*checkbox.*valid checked nested task/
)
end
it 'should not render checkboxes outside of list items' do
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
expect(rendered_text).not_to match(
/<input.*checkbox.*not an unchecked task - no list item/
)
expect(rendered_text).not_to match(
/<input.*checkbox.*not a checked task - no list item/
)
end
it 'should not render checkboxes with invalid formatting' do
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
expect(rendered_text).not_to match(
/<input.*checkbox.*not an unchecked task - too many spaces/
)
expect(rendered_text).not_to match(
/<input.*checkbox.*not a checked task - too many spaces/
)
expect(rendered_text).not_to match(
/<input.*checkbox.*not an unchecked task - no spaces/
)
expect(rendered_text).not_to match(
/Not a task.*<input.*checkbox.*not at beginning/
)
end
end
end end
describe '#link_to_gfm' do describe '#link_to_gfm' do
......
#= require jquery
#= require jasmine-fixture
#= require issue
describe 'Issue', ->
describe 'task lists', ->
selectors = {
container: '.issue-details .description.js-task-list-container'
item: '.wiki ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}'
textarea: '.wiki textarea.js-task-list-field{- [ ] Task List Item}'
form: 'form.js-issue-update[action="/foo"]'
close: 'a.btn-close'
}
beforeEach ->
$container = affix(selectors.container)
# # These two elements are siblings inside the container
$container.find('.js-task-list-container').append(affix(selectors.item))
$container.find('.js-task-list-container').append(affix(selectors.textarea))
# Task lists don't get initialized unless this button exists. Not ideal.
$container.append(affix(selectors.close))
# This form is used to get the `update` URL. Not ideal.
$container.append(affix(selectors.form))
@issue = new Issue()
it 'submits an ajax request on tasklist:changed', ->
spyOn($, 'ajax').and.callFake (req) ->
expect(req.type).toBe('PATCH')
expect(req.url).toBe('/foo')
expect(req.data.issue.description).not.toBe(null)
$('.js-task-list-field').trigger('tasklist:changed')
#= require jquery
#= require jasmine-fixture
#= require merge_request
describe 'MergeRequest', ->
describe 'task lists', ->
selectors = {
container: '.merge-request-details .description.js-task-list-container'
item: '.wiki ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}'
textarea: '.wiki textarea.js-task-list-field{- [ ] Task List Item}'
form: 'form.js-merge-request-update[action="/foo"]'
close: 'a.btn-close'
}
beforeEach ->
$container = affix(selectors.container)
# # These two elements are siblings inside the container
$container.find('.js-task-list-container').append(affix(selectors.item))
$container.find('.js-task-list-container').append(affix(selectors.textarea))
# Task lists don't get initialized unless this button exists. Not ideal.
$container.append(affix(selectors.close))
# This form is used to get the `update` URL. Not ideal.
$container.append(affix(selectors.form))
@merge = new MergeRequest({})
it 'submits an ajax request on tasklist:changed', ->
spyOn($, 'ajax').and.callFake (req) ->
expect(req.type).toBe('PATCH')
expect(req.url).toBe('/foo')
expect(req.data.merge_request.description).not.toBe(null)
$('.js-task-list-field').trigger('tasklist:changed')
#= require jquery
#= require jasmine-fixture
#= require notes
window.gon = {}
window.disableButtonIfEmptyField = -> null
describe 'Notes', ->
describe 'task lists', ->
selectors = {
container: 'li.note .js-task-list-container'
item: '.note-text ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}'
textarea: '.note-edit-form form textarea.js-task-list-field{- [ ] Task List Item}'
}
beforeEach ->
$container = affix(selectors.container)
# These two elements are siblings inside the container
$container.find('.js-task-list-container').append(affix(selectors.item))
$container.find('.js-task-list-container').append(affix(selectors.textarea))
@notes = new Notes()
it 'submits the form on tasklist:changed', ->
submitted = false
$('form').on 'submit', (e) -> submitted = true; e.preventDefault()
$('.js-task-list-field').trigger('tasklist:changed')
expect(submitted).toBe(true)
...@@ -9,11 +9,6 @@ ...@@ -9,11 +9,6 @@
# defaults to spec/javascripts # defaults to spec/javascripts
spec_dir: spec/javascripts spec_dir: spec/javascripts
# list of file expressions to include as helpers into spec runner
# relative path from spec_dir
helpers:
- "helpers/**/*.{js.coffee,js,coffee}"
# list of file expressions to include as specs into spec runner # list of file expressions to include as specs into spec runner
# relative path from spec_dir # relative path from spec_dir
spec_files: spec_files:
......
...@@ -4,39 +4,29 @@ ...@@ -4,39 +4,29 @@
# subject { Issue or MergeRequest } # subject { Issue or MergeRequest }
shared_examples 'a Taskable' do shared_examples 'a Taskable' do
before do before do
subject.description = <<EOT.gsub(/ {6}/, '') subject.description = <<-EOT.strip_heredoc
* [ ] Task 1 * [ ] Task 1
* [x] Task 2 * [x] Task 2
* [x] Task 3 * [x] Task 3
* [ ] Task 4 * [ ] Task 4
* [ ] Task 5 * [ ] Task 5
EOT EOT
end
it 'updates the Nth task correctly' do
subject.update_nth_task(1, true)
expect(subject.description).to match(/\[x\] Task 1/)
subject.update_nth_task(2, true)
expect(subject.description).to match('\[x\] Task 2')
subject.update_nth_task(3, false)
expect(subject.description).to match('\[ \] Task 3')
subject.update_nth_task(4, false)
expect(subject.description).to match('\[ \] Task 4')
end end
it 'returns the correct task status' do it 'returns the correct task status' do
expect(subject.task_status).to match('5 tasks') expect(subject.task_status).to match('5 tasks')
expect(subject.task_status).to match('2 done') expect(subject.task_status).to match('2 completed')
expect(subject.task_status).to match('3 unfinished') expect(subject.task_status).to match('3 remaining')
end end
it 'knows if it has tasks' do describe '#tasks?' do
expect(subject.tasks?).to be_truthy it 'returns true when object has tasks' do
expect(subject.tasks?).to eq true
end
subject.description = 'Now I have no tasks' it 'returns false when object has no tasks' do
expect(subject.tasks?).to be_falsey subject.description = 'Now I have no tasks'
expect(subject.tasks?).to eq false
end
end end
end end
/* jasmine-fixture - 1.2.2 /* jasmine-fixture - 1.3.1
* Makes injecting HTML snippets into the DOM easy & clean! * Makes injecting HTML snippets into the DOM easy & clean!
* https://github.com/searls/jasmine-fixture * https://github.com/searls/jasmine-fixture
*/ */
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
(function($) { (function($) {
var ewwSideEffects, jasmineFixture, originalAffix, originalJasmineDotFixture, originalJasmineFixture, root, _, _ref; var ewwSideEffects, jasmineFixture, originalAffix, originalJasmineDotFixture, originalJasmineFixture, root, _, _ref;
root = this; root = (1, eval)('this');
originalJasmineFixture = root.jasmineFixture; originalJasmineFixture = root.jasmineFixture;
originalJasmineDotFixture = (_ref = root.jasmine) != null ? _ref.fixture : void 0; originalJasmineDotFixture = (_ref = root.jasmine) != null ? _ref.fixture : void 0;
originalAffix = root.affix; originalAffix = root.affix;
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
create = function(selectorOptions, attach) { create = function(selectorOptions, attach) {
var $top; var $top;
$top = null; $top = null;
_(selectorOptions.split(/[ ](?=[^\]]*?(?:\[|$))/)).inject(function($parent, elementSelector) { _(selectorOptions.split(/[ ](?![^\{]*\})(?=[^\]]*?(?:\[|$))/)).inject(function($parent, elementSelector) {
var $el; var $el;
if (elementSelector === ">") { if (elementSelector === ">") {
return $parent; return $parent;
......
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