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
This commit is contained in:
commit
6c32abc5f7
41 changed files with 543 additions and 507 deletions
|
@ -35,7 +35,7 @@ v 7.11.0 (unreleased)
|
|||
- Show incompatible projects in Google Code import status (Stan Hu)
|
||||
- Fix bug where commit data would not appear in some subdirectories (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)
|
||||
- Move snippets UI to fluid layout
|
||||
- Improve UI for sidebar. Increase separation between navigation and content
|
||||
|
|
22
Gemfile
22
Gemfile
|
@ -87,20 +87,17 @@ gem "six"
|
|||
# Seed data
|
||||
gem "seed-fu"
|
||||
|
||||
# Markup pipeline for GitLab
|
||||
# Markdown and HTML processing
|
||||
gem 'html-pipeline', '~> 1.11.0'
|
||||
|
||||
# Markdown to HTML
|
||||
gem "github-markup"
|
||||
|
||||
# Required markup gems by github-markdown
|
||||
gem 'redcarpet', '~> 3.2.3'
|
||||
gem 'task_list', '~> 1.0.0', require: 'task_list/railtie'
|
||||
gem 'github-markup'
|
||||
gem 'redcarpet', '~> 3.2.3'
|
||||
gem 'RedCloth'
|
||||
gem 'rdoc', '~>3.6'
|
||||
gem 'org-ruby', '= 0.9.12'
|
||||
gem 'creole', '~>0.3.6'
|
||||
gem 'wikicloth', '=0.8.1'
|
||||
gem 'asciidoctor', '= 0.1.4'
|
||||
gem 'rdoc', '~>3.6'
|
||||
gem 'org-ruby', '= 0.9.12'
|
||||
gem 'creole', '~>0.3.6'
|
||||
gem 'wikicloth', '=0.8.1'
|
||||
gem 'asciidoctor', '= 0.1.4'
|
||||
|
||||
# Diffs
|
||||
gem 'diffy', '~> 3.0.3'
|
||||
|
@ -251,7 +248,6 @@ group :development, :test do
|
|||
# PhantomJS driver for Capybara
|
||||
gem 'poltergeist', '~> 1.5.1'
|
||||
|
||||
gem 'jasmine', '~> 2.2.0'
|
||||
gem 'jasmine-rails'
|
||||
|
||||
gem "spring", '~> 1.3.1'
|
||||
|
|
|
@ -290,11 +290,6 @@ GEM
|
|||
i18n (0.7.0)
|
||||
ice_cube (0.11.1)
|
||||
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-rails (0.10.8)
|
||||
jasmine-core (>= 1.3, < 3.0)
|
||||
|
@ -597,6 +592,8 @@ GEM
|
|||
stamp (0.5.0)
|
||||
state_machine (1.2.0)
|
||||
stringex (2.5.2)
|
||||
task_list (1.0.2)
|
||||
html-pipeline
|
||||
temple (0.6.7)
|
||||
term-ansicolor (1.2.2)
|
||||
tins (~> 0.8)
|
||||
|
@ -724,7 +721,6 @@ DEPENDENCIES
|
|||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 1.11.0)
|
||||
httparty
|
||||
jasmine (~> 2.2.0)
|
||||
jasmine-rails
|
||||
jquery-atwho-rails (~> 1.0.0)
|
||||
jquery-rails
|
||||
|
@ -789,6 +785,7 @@ DEPENDENCIES
|
|||
spring-commands-spinach (= 1.0.0)
|
||||
stamp
|
||||
state_machine
|
||||
task_list (~> 1.0.0)
|
||||
test_after_commit
|
||||
thin
|
||||
tinder (~> 1.9.2)
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
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
|
|
@ -1,3 +1,7 @@
|
|||
#= require jquery
|
||||
#= require jquery.waitforimages
|
||||
#= require task_list
|
||||
|
||||
class @Issue
|
||||
constructor: ->
|
||||
$('.edit-issue.inline-update input[type="submit"]').hide()
|
||||
|
@ -6,11 +10,11 @@ class @Issue
|
|||
$(".context .inline-update").on "change", "#issue_assignee_id", ->
|
||||
$(this).submit()
|
||||
|
||||
if $("a.btn-close").length
|
||||
$("li.task-list-item input:checkbox").prop("disabled", false)
|
||||
# Prevent duplicate event bindings
|
||||
@disableTaskList()
|
||||
|
||||
$('.task-list-item input:checkbox').off('change')
|
||||
$('.task-list-item input:checkbox').change('issue', updateTaskState)
|
||||
if $("a.btn-close").length
|
||||
@initTaskList()
|
||||
|
||||
$('.issue-details').waitForImages ->
|
||||
$('.issuable-affix').affix offset:
|
||||
|
@ -22,3 +26,22 @@ class @Issue
|
|||
$(@).width($(@).outerWidth())
|
||||
.on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
|
||||
$(@).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
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
#= require jquery
|
||||
#= require bootstrap
|
||||
#= require task_list
|
||||
|
||||
class @MergeRequest
|
||||
constructor: (@opts) ->
|
||||
@initContextWidget()
|
||||
|
@ -17,8 +21,11 @@ class @MergeRequest
|
|||
|
||||
disableButtonIfEmptyField '#commit_message', '.accept_merge_request'
|
||||
|
||||
# Prevent duplicate event bindings
|
||||
@disableTaskList()
|
||||
|
||||
if $("a.btn-close").length
|
||||
$("li.task-list-item input:checkbox").prop("disabled", false)
|
||||
@initTaskList()
|
||||
|
||||
$('.merge-request-details').waitForImages ->
|
||||
$('.issuable-affix').affix offset:
|
||||
|
@ -77,9 +84,6 @@ class @MergeRequest
|
|||
this.$('.remove_source_branch_in_progress').hide()
|
||||
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) ->
|
||||
this.$('.merge-request-tabs li').removeClass 'active'
|
||||
this.$('.tab-content').hide()
|
||||
|
@ -156,3 +160,22 @@ class @MergeRequest
|
|||
else
|
||||
setTimeout(merge_request.mergeInProgress, 3000)
|
||||
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
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
#= require jquery
|
||||
#= require autosave
|
||||
#= require bootstrap
|
||||
#= require dropzone
|
||||
#= require dropzone_input
|
||||
#= require gfm_auto_complete
|
||||
#= require jquery.atwho
|
||||
#= require task_list
|
||||
|
||||
class @Notes
|
||||
@interval: null
|
||||
|
||||
|
@ -11,6 +20,7 @@ class @Notes
|
|||
@setupMainTargetNoteForm()
|
||||
@cleanBinding()
|
||||
@addBinding()
|
||||
@initTaskList()
|
||||
|
||||
addBinding: ->
|
||||
# add note to UI after creation
|
||||
|
@ -81,6 +91,9 @@ class @Notes
|
|||
$(document).off "click", ".js-note-target-reopen"
|
||||
$(document).off "click", ".js-note-target-close"
|
||||
|
||||
$('.note .js-task-list-container').taskList('disable')
|
||||
$(document).off 'tasklist:changed', '.note .js-task-list-container'
|
||||
|
||||
initRefresh: ->
|
||||
clearInterval(Notes.interval)
|
||||
Notes.interval = setInterval =>
|
||||
|
@ -114,6 +127,7 @@ class @Notes
|
|||
if @isNewNote(note)
|
||||
@note_ids.push(note.id)
|
||||
$('ul.main-notes-list').append(note.html)
|
||||
@initTaskList()
|
||||
|
||||
###
|
||||
Check if note does not exists on page
|
||||
|
@ -268,6 +282,8 @@ class @Notes
|
|||
note_li.replaceWith(note.html)
|
||||
note_li.find('.note-edit-form').hide()
|
||||
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
|
||||
|
@ -479,3 +495,13 @@ class @Notes
|
|||
else
|
||||
form.find('.js-note-target-reopen').text('Reopen')
|
||||
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 {
|
|||
position: relative;
|
||||
|
||||
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 {
|
||||
|
|
|
@ -62,6 +62,16 @@ ul.notes {
|
|||
word-wrap: break-word;
|
||||
@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 {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ module NotesHelper
|
|||
hidden_field_tag(:target_id, note.noteable.id)
|
||||
end
|
||||
|
||||
def note_editable?(note)
|
||||
note.editable? && can?(current_user, :admin_note, note)
|
||||
end
|
||||
|
||||
def link_to_commit_diff_line_note(note)
|
||||
if note.for_commit_diff_line?
|
||||
link_to(
|
||||
|
|
|
@ -1,51 +1,36 @@
|
|||
require 'task_list'
|
||||
|
||||
# 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>(?<p_tag>\s*<p>)?\[(?<checked>[ xX])\]/.freeze
|
||||
# Called by `TaskList::Summary`
|
||||
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
|
||||
# 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
|
||||
@task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item|
|
||||
# ItemPattern strips out the hyphen, but Item requires it. Rabble rabble.
|
||||
TaskList::Item.new("- #{item}")
|
||||
end
|
||||
end
|
||||
|
||||
save
|
||||
def tasks
|
||||
@tasks ||= TaskList.new(self)
|
||||
end
|
||||
|
||||
# Return true if this object's description has any task list items.
|
||||
def tasks?
|
||||
description && description.match(TASK_PATTERN_MD)
|
||||
tasks.summary.items?
|
||||
end
|
||||
|
||||
# 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
|
||||
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
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
- content_for :note_actions do
|
||||
- if can?(current_user, :modify_issue, @issue)
|
||||
- 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
|
||||
= 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'
|
||||
|
||||
|
@ -15,11 +15,11 @@
|
|||
%span= pluralize(@issue.participants(current_user).count, 'participant')
|
||||
- @issue.participants(current_user).each do |participant|
|
||||
= 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
|
||||
.issuable-affix
|
||||
.clearfix
|
||||
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
|
||||
%span.slead.has_tooltip{title: 'Cross-project reference'}
|
||||
= cross_project_reference(@project, @issue)
|
||||
%hr
|
||||
.context
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= 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
|
||||
.issuable-context-title
|
||||
%label
|
||||
|
|
|
@ -13,17 +13,17 @@
|
|||
|
||||
.pull-right
|
||||
- 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
|
||||
%i.fa.fa-plus
|
||||
= 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
|
||||
= icon('plus')
|
||||
New Issue
|
||||
- if can?(current_user, :modify_issue, @issue)
|
||||
- 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
|
||||
= 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
|
||||
%i.fa.fa-pencil-square-o
|
||||
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do
|
||||
= icon('pencil-square-o')
|
||||
Edit
|
||||
|
||||
%hr
|
||||
|
@ -31,11 +31,13 @@
|
|||
= gfm escape_once(@issue.title)
|
||||
%div
|
||||
- if @issue.description.present?
|
||||
.description
|
||||
.description{class: can?(current_user, :modify_issue, @issue) ? 'js-task-list-container' : ''}
|
||||
.wiki
|
||||
= preserve do
|
||||
= markdown(@issue.description, parse_tasks: true)
|
||||
= markdown(@issue.description)
|
||||
%textarea.hidden.js-task-list-field
|
||||
= @issue.description
|
||||
|
||||
%hr
|
||||
.issue-discussion
|
||||
= render "projects/issues/discussion"
|
||||
= render 'projects/issues/discussion'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= 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
|
||||
.issuable-context-title
|
||||
%label
|
||||
|
@ -19,13 +19,13 @@
|
|||
%span.back-to-milestone
|
||||
= link_to namespace_project_milestone_path(@project.namespace, @project, @merge_request.milestone) do
|
||||
%strong
|
||||
%i.fa.fa-clock-o
|
||||
= icon('clock-o')
|
||||
= @merge_request.milestone.title
|
||||
- else
|
||||
none
|
||||
.issuable-context-selectbox
|
||||
- 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
|
||||
= f.submit class: 'btn'
|
||||
|
||||
|
@ -35,13 +35,13 @@
|
|||
%label
|
||||
Subscription:
|
||||
%button.btn.btn-block.subscribe-button{:type => 'button'}
|
||||
%i.fa.fa-eye
|
||||
%span= @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe"
|
||||
- subscribtion_status = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed"
|
||||
.subscription-status{"data-status" => subscribtion_status}
|
||||
.description-block.unsubscribed{class: ( "hidden" if @merge_request.subscribed?(current_user) )}
|
||||
= icon('eye')
|
||||
%span= @merge_request.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
|
||||
- subscribtion_status = @merge_request.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
|
||||
.subscription-status{data: {status: subscribtion_status}}
|
||||
.description-block.unsubscribed{class: ( 'hidden' if @merge_request.subscribed?(current_user) )}
|
||||
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.
|
||||
|
||||
:coffeescript
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
%div
|
||||
- if @merge_request.description.present?
|
||||
.description
|
||||
.description{class: can?(current_user, :modify_merge_request, @merge_request) ? 'js-task-list-container' : ''}
|
||||
.wiki
|
||||
= preserve do
|
||||
= markdown(@merge_request.description, parse_tasks: true)
|
||||
= markdown(@merge_request.description)
|
||||
%textarea.hidden.js-task-list-field
|
||||
= @merge_request.description
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
.note-edit-form
|
||||
= 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)
|
||||
= render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do
|
||||
= render 'projects/zen', f: f, attr: :note,
|
||||
classes: 'note_text js-note-text'
|
||||
= render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do
|
||||
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
|
||||
|
||||
.comment-hints.clearfix
|
||||
.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 & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', 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 & dropping or #{link_to 'selecting them', '#', class: 'markdown-selector', tabindex: -1 }.
|
||||
|
||||
.note-form-actions
|
||||
.buttons
|
||||
= 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"
|
||||
= 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'
|
||||
|
|
|
@ -2,28 +2,28 @@
|
|||
.timeline-entry-inner
|
||||
.timeline-icon
|
||||
- if note.system
|
||||
%span.fa.fa-circle
|
||||
%span= icon('circle')
|
||||
- else
|
||||
= 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
|
||||
.note-header
|
||||
.note-actions
|
||||
= link_to "##{dom_id(note)}", name: dom_id(note) do
|
||||
%i.fa.fa-link
|
||||
= icon('link')
|
||||
Link here
|
||||
|
||||
- if can?(current_user, :admin_note, note) && note.editable?
|
||||
= link_to "#", title: "Edit comment", class: "js-note-edit" do
|
||||
%i.fa.fa-pencil-square-o
|
||||
- if note_editable?(note)
|
||||
= link_to '#', title: 'Edit comment', class: 'js-note-edit' do
|
||||
= icon('pencil-square-o')
|
||||
Edit
|
||||
|
||||
= 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
|
||||
= 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
|
||||
= icon('trash-o', class: 'cred')
|
||||
Remove
|
||||
- if note.system
|
||||
= 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)
|
||||
%span.author-username
|
||||
= '@' + note.author.username
|
||||
|
@ -33,24 +33,24 @@
|
|||
- if note.superceded?(@notes)
|
||||
- if note.upvote?
|
||||
%span.vote.upvote.label.label-gray.strikethrough
|
||||
%i.fa.fa-thumbs-up
|
||||
= icon('thumbs-up')
|
||||
\+1
|
||||
- if note.downvote?
|
||||
%span.vote.downvote.label.label-gray.strikethrough
|
||||
%i.fa.fa-thumbs-down
|
||||
= icon('thumbs-down')
|
||||
\-1
|
||||
- else
|
||||
- if note.upvote?
|
||||
%span.vote.upvote.label.label-success
|
||||
%i.fa.fa-thumbs-up
|
||||
= icon('thumbs-up')
|
||||
\+1
|
||||
- if note.downvote?
|
||||
%span.vote.downvote.label.label-danger
|
||||
%i.fa.fa-thumbs-down
|
||||
= icon('thumbs-down')
|
||||
\-1
|
||||
|
||||
|
||||
.note-body
|
||||
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
|
||||
.note-text
|
||||
= preserve do
|
||||
= markdown(note.note, {no_header_anchors: true})
|
||||
|
@ -62,10 +62,10 @@
|
|||
= link_to note.attachment.url, target: '_blank' do
|
||||
= image_tag note.attachment.url, class: 'note-image-attach'
|
||||
.attachment
|
||||
= link_to note.attachment.url, target: "_blank" do
|
||||
%i.fa.fa-paperclip
|
||||
= link_to note.attachment.url, target: '_blank' do
|
||||
= icon('paperclip')
|
||||
= note.attachment_identifier
|
||||
= 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
|
||||
%i.fa.fa-trash-o.cred
|
||||
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
|
||||
= icon('trash-o', class: 'cred')
|
||||
.clear
|
||||
|
|
|
@ -5,4 +5,5 @@ if Rails.env.development?
|
|||
Rack::MiniProfilerRails.initialize!(Rails.application)
|
||||
Rack::MiniProfiler.config.position = 'right'
|
||||
Rack::MiniProfiler.config.start_hidden = true
|
||||
Rack::MiniProfiler.config.skip_paths << '/specs'
|
||||
end
|
||||
|
|
|
@ -172,7 +172,7 @@ GFM will turn that reference into a link so you can navigate between them easily
|
|||
GFM will recognize the following:
|
||||
|
||||
| input | references |
|
||||
|-----------------------:|:---------------------------|
|
||||
|:-----------------------|:---------------------------|
|
||||
| `@user_name` | specific user |
|
||||
| `@group_name` | specific group |
|
||||
| `@all` | entire team |
|
||||
|
@ -189,7 +189,7 @@ GFM will recognize the following:
|
|||
GFM also recognizes certain cross-project references:
|
||||
|
||||
| input | references |
|
||||
|----------------------------------------:|:------------------------|
|
||||
|:----------------------------------------|:------------------------|
|
||||
| `namespace/project#123` | issue |
|
||||
| `namespace/project!123` | merge request |
|
||||
| `namespace/project$123` | snippet |
|
||||
|
@ -198,15 +198,23 @@ GFM also recognizes certain cross-project references:
|
|||
|
||||
## 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
|
||||
* [x] Completed task
|
||||
* [ ] Unfinished task
|
||||
* [x] Nested task
|
||||
- [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 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
|
||||
|
||||
|
@ -246,51 +254,38 @@ Alt-H2
|
|||
|
||||
### 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.
|
||||
|
||||
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
|
||||
2. from the result, remove all HTML tags, but keep their inner content
|
||||
3. convert all characters to lowercase
|
||||
4. convert all characters except `[a-z0-9_-]` into hyphens `-`
|
||||
5. transform multiple adjacent hyphens into a single hyphen
|
||||
6. remove trailing and heading hyphens
|
||||
1. All text is converted to lowercase
|
||||
1. All non-word text (e.g., punctuation, HTML) is removed
|
||||
1. All spaces are converted to hyphens
|
||||
1. Two or more hyphens in a row are converted to one
|
||||
1. If a header with the same ID has already been generated, a unique
|
||||
incrementing number is appended.
|
||||
|
||||
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)..
|
||||
1. `this-header-has-spaces-in-it`
|
||||
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`
|
||||
|
||||
will first be converted by step 1) into a string like:
|
||||
|
||||
```
|
||||
..Ab_c-d. e <a href="URL">anchor</a> <img src="URL" alt="alt text"/>..
|
||||
```
|
||||
|
||||
After removing the tags in step 2) we get:
|
||||
|
||||
```
|
||||
..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
|
||||
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.
|
||||
|
||||
## Emphasis
|
||||
|
||||
|
@ -322,8 +317,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~
|
|||
1. Ordered sub-list
|
||||
4. And another item.
|
||||
|
||||
Some text that should be aligned with the above item.
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Or minuses
|
||||
+ Or pluses
|
||||
|
@ -336,8 +329,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~
|
|||
1. Ordered sub-list
|
||||
4. And another item.
|
||||
|
||||
Some text that should be aligned with the above item.
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Or minuses
|
||||
+ Or pluses
|
||||
|
@ -432,7 +423,7 @@ Quote break.
|
|||
|
||||
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
|
||||
<dl>
|
||||
|
@ -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.
|
||||
|
||||
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
|
||||
|
||||
- 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
|
|||
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
|
||||
|
||||
@javascript
|
||||
Scenario: Issue notes should be editable with +1
|
||||
Given project "Shop" has "Tasks-open" open issue with task markdown
|
||||
When I visit issue page "Tasks-open"
|
||||
Given project "Shop" have "Release 0.4" open issue
|
||||
When I visit issue page "Release 0.4"
|
||||
And I leave a comment with a header containing "Comment with a header"
|
||||
Then The comment with the header should not have an ID
|
||||
And I edit the last comment with a +1
|
||||
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
|
||||
|
||||
@javascript
|
||||
|
@ -212,8 +179,8 @@ Feature: Project Issues
|
|||
|
||||
@javascript
|
||||
Scenario: I can unsubscribe from issue
|
||||
Given project "Shop" has "Tasks-open" open issue with task markdown
|
||||
When I visit issue page "Tasks-open"
|
||||
Given project "Shop" have "Release 0.4" open issue
|
||||
When I visit issue page "Release 0.4"
|
||||
Then I should see that I am subscribed
|
||||
When I click button "Unsubscribe"
|
||||
Then I should see that I am unsubscribed
|
||||
|
|
|
@ -96,16 +96,6 @@ 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
|
||||
|
@ -173,28 +163,6 @@ Feature: Project Merge Requests
|
|||
And I click on the Changes tab via Javascript
|
||||
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
|
||||
|
||||
@javascript
|
||||
|
|
|
@ -179,14 +179,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
|
|||
author: project.users.first)
|
||||
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
|
||||
create :empty_project, name: 'Empty Project', namespace: @user.namespace
|
||||
end
|
||||
|
|
|
@ -108,10 +108,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
|
|||
author: project.users.first)
|
||||
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
|
||||
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||
end
|
||||
|
|
|
@ -10,55 +10,10 @@ module SharedMarkdown
|
|||
find(:xpath, "#{node.path}/..").text.should == text
|
||||
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
|
||||
header_should_have_correct_id_and_link(1, 'Description header', 'description-header')
|
||||
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
|
||||
expect(find('.gfm-form .js-md-preview')).not_to be_visible
|
||||
end
|
||||
|
|
|
@ -122,20 +122,6 @@ module SharedNote
|
|||
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
|
||||
find(".note").hover
|
||||
find('.js-note-edit').click
|
||||
|
|
|
@ -323,16 +323,6 @@ module SharedPaths
|
|||
visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
|
||||
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
|
||||
project = Project.find_by(name: 'Shop')
|
||||
visit namespace_project_labels_path(project.namespace, project)
|
||||
|
@ -363,16 +353,6 @@ module SharedPaths
|
|||
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
|
||||
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
|
||||
visit namespace_project_merge_requests_path(project.namespace, project)
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require 'html/pipeline'
|
||||
require 'task_list/filter'
|
||||
|
||||
module Gitlab
|
||||
# Custom parser for GitLab-flavored Markdown
|
||||
|
@ -31,9 +32,9 @@ module Gitlab
|
|||
# Public: Parse the provided text with GitLab-Flavored Markdown
|
||||
#
|
||||
# text - the source text
|
||||
# options - parse_tasks - render tasks
|
||||
# - xhtml - output XHTML instead of HTML
|
||||
# - reference_only_path - Use relative path for reference links
|
||||
# options - A Hash of options used to customize output (default: {}):
|
||||
# :xhtml - output XHTML instead of HTML
|
||||
# :reference_only_path - Use relative path for reference links
|
||||
# project - the project
|
||||
# html_options - extra options for the reference links as given to link_to
|
||||
def gfm_with_options(text, options = {}, project = @project, html_options = {})
|
||||
|
@ -45,7 +46,6 @@ module Gitlab
|
|||
text = text.dup.to_str
|
||||
|
||||
options.reverse_merge!(
|
||||
parse_tasks: false,
|
||||
xhtml: false,
|
||||
reference_only_path: true
|
||||
)
|
||||
|
@ -76,10 +76,6 @@ module Gitlab
|
|||
|
||||
text = result[:output].to_html(save_with: save_options)
|
||||
|
||||
if options[:parse_tasks]
|
||||
text = parse_tasks(text)
|
||||
end
|
||||
|
||||
text.html_safe
|
||||
end
|
||||
|
||||
|
@ -106,28 +102,10 @@ module Gitlab
|
|||
Gitlab::Markdown::SnippetReferenceFilter,
|
||||
Gitlab::Markdown::CommitRangeReferenceFilter,
|
||||
Gitlab::Markdown::CommitReferenceFilter,
|
||||
Gitlab::Markdown::LabelReferenceFilter
|
||||
Gitlab::Markdown::LabelReferenceFilter,
|
||||
|
||||
TaskList::Filter
|
||||
]
|
||||
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'
|
||||
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
|
||||
|
|
|
@ -31,7 +31,7 @@ module Gitlab
|
|||
id = text.downcase
|
||||
id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation
|
||||
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]}" : ''
|
||||
headers[id] += 1
|
||||
|
|
12
lib/tasks/jasmine.rake
Normal file
12
lib/tasks/jasmine.rake
Normal file
|
@ -0,0 +1,12 @@
|
|||
# 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'
|
|||
# -> Rinku (http, https, ftp)
|
||||
# -> Other schemes
|
||||
# -> References
|
||||
# -> TaskList
|
||||
# -> `html_safe`
|
||||
# -> Template
|
||||
#
|
||||
|
@ -279,6 +280,15 @@ describe 'GitLab Markdown' do
|
|||
expect(body).to have_selector('a.gfm.gfm-label', count: 3)
|
||||
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
|
||||
|
||||
|
@ -289,9 +299,8 @@ end
|
|||
# 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.
|
||||
#
|
||||
# The class contains the raw Markup used in the test, dynamically substituting
|
||||
# real objects, created from factories and setup on-demand, when referenced in
|
||||
# the Markdown.
|
||||
# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
|
||||
# reference to the factory-created objects.
|
||||
class MarkdownFeature
|
||||
include FactoryGirl::Syntax::Methods
|
||||
|
||||
|
|
151
spec/features/task_lists_spec.rb
Normal file
151
spec/features/task_lists_spec.rb
Normal file
|
@ -0,0 +1,151 @@
|
|||
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
|
10
spec/fixtures/markdown.md.erb
vendored
10
spec/fixtures/markdown.md.erb
vendored
|
@ -184,3 +184,13 @@ References should be parseable even inside _!<%= merge_request.iid %>_ emphasis.
|
|||
- Label by name in quotes: ~"<%= label.name %>"
|
||||
- Ignored in code: `~<%= simple_label.name %>`
|
||||
- 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
|
|||
expect(gfm(actual)).to match(expected)
|
||||
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
|
||||
|
||||
describe '#link_to_gfm' do
|
||||
|
|
36
spec/javascripts/issue_spec.js.coffee
Normal file
36
spec/javascripts/issue_spec.js.coffee
Normal file
|
@ -0,0 +1,36 @@
|
|||
#= 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')
|
36
spec/javascripts/merge_request_spec.js.coffee
Normal file
36
spec/javascripts/merge_request_spec.js.coffee
Normal file
|
@ -0,0 +1,36 @@
|
|||
#= 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')
|
30
spec/javascripts/notes_spec.js.coffee
Normal file
30
spec/javascripts/notes_spec.js.coffee
Normal file
|
@ -0,0 +1,30 @@
|
|||
#= 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 @@
|
|||
# defaults to 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
|
||||
# relative path from spec_dir
|
||||
spec_files:
|
||||
|
|
|
@ -4,39 +4,29 @@
|
|||
# subject { Issue or MergeRequest }
|
||||
shared_examples 'a Taskable' do
|
||||
before do
|
||||
subject.description = <<EOT.gsub(/ {6}/, '')
|
||||
subject.description = <<-EOT.strip_heredoc
|
||||
* [ ] Task 1
|
||||
* [x] Task 2
|
||||
* [x] Task 3
|
||||
* [ ] Task 4
|
||||
* [ ] Task 5
|
||||
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')
|
||||
EOT
|
||||
end
|
||||
|
||||
it 'returns the correct task status' do
|
||||
expect(subject.task_status).to match('5 tasks')
|
||||
expect(subject.task_status).to match('2 done')
|
||||
expect(subject.task_status).to match('3 unfinished')
|
||||
expect(subject.task_status).to match('2 completed')
|
||||
expect(subject.task_status).to match('3 remaining')
|
||||
end
|
||||
|
||||
it 'knows if it has tasks' do
|
||||
expect(subject.tasks?).to be_truthy
|
||||
describe '#tasks?' do
|
||||
it 'returns true when object has tasks' do
|
||||
expect(subject.tasks?).to eq true
|
||||
end
|
||||
|
||||
subject.description = 'Now I have no tasks'
|
||||
expect(subject.tasks?).to be_falsey
|
||||
it 'returns false when object has no tasks' do
|
||||
subject.description = 'Now I have no tasks'
|
||||
expect(subject.tasks?).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
6
vendor/assets/javascripts/jasmine-fixture.js
vendored
Normal file → Executable file
6
vendor/assets/javascripts/jasmine-fixture.js
vendored
Normal file → Executable file
|
@ -1,4 +1,4 @@
|
|||
/* jasmine-fixture - 1.2.2
|
||||
/* jasmine-fixture - 1.3.1
|
||||
* Makes injecting HTML snippets into the DOM easy & clean!
|
||||
* https://github.com/searls/jasmine-fixture
|
||||
*/
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
(function($) {
|
||||
var ewwSideEffects, jasmineFixture, originalAffix, originalJasmineDotFixture, originalJasmineFixture, root, _, _ref;
|
||||
root = this;
|
||||
root = (1, eval)('this');
|
||||
originalJasmineFixture = root.jasmineFixture;
|
||||
originalJasmineDotFixture = (_ref = root.jasmine) != null ? _ref.fixture : void 0;
|
||||
originalAffix = root.affix;
|
||||
|
@ -33,7 +33,7 @@
|
|||
create = function(selectorOptions, attach) {
|
||||
var $top;
|
||||
$top = null;
|
||||
_(selectorOptions.split(/[ ](?=[^\]]*?(?:\[|$))/)).inject(function($parent, elementSelector) {
|
||||
_(selectorOptions.split(/[ ](?![^\{]*\})(?=[^\]]*?(?:\[|$))/)).inject(function($parent, elementSelector) {
|
||||
var $el;
|
||||
if (elementSelector === ">") {
|
||||
return $parent;
|
||||
|
|
Loading…
Reference in a new issue