summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/pages/notes.scss52
-rw-r--r--app/helpers/notes_helper.rb8
-rw-r--r--app/views/projects/notes/_actions.html.haml6
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml14
-rw-r--r--app/views/snippets/notes/_actions.html.haml7
-rw-r--r--features/steps/project/merge_requests.rb6
-rw-r--r--features/steps/shared/note.rb16
-rw-r--r--lib/gitlab/url_builder.rb7
-rw-r--r--spec/features/issues/note_polling_spec.rb17
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb4
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb8
-rw-r--r--spec/features/reportable_note/commit_spec.rb33
-rw-r--r--spec/features/reportable_note/issue_spec.rb17
-rw-r--r--spec/features/reportable_note/merge_request_spec.rb26
-rw-r--r--spec/features/reportable_note/snippets_spec.rb33
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb12
-rw-r--r--spec/helpers/notes_helper_spec.rb10
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb11
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb36
-rw-r--r--spec/support/helpers/note_interaction_helpers.rb8
21 files changed, 314 insertions, 22 deletions
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5ab48b6c874..9c70f1a1ef2 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -201,6 +201,11 @@
width: 100%;
}
+ &.dropdown-open-left {
+ right: 0;
+ left: auto;
+ }
+
&.is-loading {
.dropdown-content {
display: none;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index f956e3757bf..e622e5c3f4b 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -38,9 +38,12 @@ ul.notes {
}
.discussion {
- overflow: hidden;
display: block;
position: relative;
+
+ .diff-content {
+ overflow: visible;
+ }
}
> li {
@@ -443,6 +446,53 @@ ul.notes {
.note-action-button {
margin-left: 8px;
}
+
+ .more-actions-toggle {
+ margin-left: 2px;
+ }
+}
+
+.more-actions {
+ display: inline;
+
+ .tooltip {
+ white-space: nowrap;
+ }
+}
+
+.more-actions-toggle {
+ padding: 0;
+ outline: none;
+
+ &:hover .icon,
+ &:focus .icon {
+ color: $blue-600;
+ }
+
+ .icon {
+ padding: 0 6px;
+ }
+}
+
+.more-actions-dropdown {
+ width: 180px;
+ min-width: 180px;
+ margin-top: $gl-btn-padding;
+
+ li > a,
+ li > .btn {
+ color: $gl-text-color;
+ padding: $gl-btn-padding;
+ width: 100%;
+ text-align: left;
+
+ &:hover,
+ &:focus {
+ color: $gl-text-color;
+ background-color: $blue-25;
+ border-radius: $border-radius-default;
+ }
+ }
}
.discussion-actions {
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 3d4802290b5..c59d8dafc83 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -90,14 +90,18 @@ module NotesHelper
end
end
- def note_url(note)
+ def note_url(note, project = @project)
if note.noteable.is_a?(PersonalSnippet)
snippet_note_path(note.noteable, note)
else
- namespace_project_note_path(@project.namespace, @project, note)
+ namespace_project_note_path(project.namespace, project, note)
end
end
+ def noteable_note_url(note)
+ Gitlab::UrlBuilder.build(note)
+ end
+
def form_resources
if @snippet.is_a?(PersonalSnippet)
[@note]
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 3e79dbec70c..9c42be4e0ff 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -37,8 +37,4 @@
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- - if note_editable
- = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip' do
- = icon('pencil', class: 'link-highlight')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger has-tooltip' do
- = icon('trash-o', class: 'danger-highlight')
+ = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
new file mode 100644
index 00000000000..e0d45054854
--- /dev/null
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -0,0 +1,14 @@
+.dropdown.more-actions
+ = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do
+ = icon('ellipsis-v', class: 'icon')
+ %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
+ %li
+ = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
+ %li.divider
+ %li
+ = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
+ Report as abuse
+ - if note_editable
+ %li
+ = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do
+ %span.text-danger Delete comment
diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml
index e8119642ab8..098a88c48c5 100644
--- a/app/views/snippets/notes/_actions.html.haml
+++ b/app/views/snippets/notes/_actions.html.haml
@@ -6,8 +6,5 @@
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- - if note_editable
- = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip' do
- = icon('pencil', class: 'link-highlight')
- = link_to snippet_note_path(note.noteable, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger has-tooltip' do
- = icon('trash-o', class: 'danger-highlight')
+
+ = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 7f1e9e693af..ce6d1a6b8d0 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -297,6 +297,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do
page.within('.diff-file:nth-of-type(5) .note') do
+ find('.more-actions').click
+ find('.more-actions .dropdown-menu li', match: :first)
+
find('.js-note-edit').click
page.within('.current-note-edit-form', visible: true) do
@@ -322,6 +325,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I delete the comment "Line is wrong" on diff' do
page.within('.diff-file:nth-of-type(5) .note') do
+ find('.more-actions').click
+ find('.more-actions .dropdown-menu li', match: :first)
+
find('.js-note-delete').click
end
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 44eb8f321dd..80187b83fee 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -8,7 +8,12 @@ module SharedNote
step 'I delete a comment' do
page.within('.main-notes-list') do
- find('.note').hover
+ note = find('.note')
+ note.hover
+
+ note.find('.more-actions').click
+ note.find('.more-actions .dropdown-menu li', match: :first)
+
find(".js-note-delete").click
end
end
@@ -139,8 +144,13 @@ module SharedNote
step 'I edit the last comment with a +1' do
page.within(".main-notes-list") do
- find(".note").hover
- find('.js-note-edit').click
+ note = find('.note')
+ note.hover
+
+ note.find('.more-actions').click
+ note.find('.more-actions .dropdown-menu li', match: :first)
+
+ note.find('.js-note-edit').click
end
page.within(".current-note-edit-form") do
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index ccb456bcc94..23af9318d1a 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -61,7 +61,12 @@ module Gitlab
elsif object.for_snippet?
snippet = Snippet.find(object.noteable_id)
- project_snippet_url(snippet, anchor: dom_id(object))
+
+ if snippet.is_a?(PersonalSnippet)
+ snippet_url(snippet, anchor: dom_id(object))
+ else
+ project_snippet_url(snippet, anchor: dom_id(object))
+ end
end
end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index 80f57906506..2c0a6ffd3cb 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Issue notes polling', :feature, :js do
+ include NoteInteractionHelpers
+
let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
@@ -48,7 +50,7 @@ feature 'Issue notes polling', :feature, :js do
end
it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
- find("#note_#{existing_note.id} .js-note-edit").click
+ click_edit_action(existing_note)
expect(page).to have_field("note[note]", with: note_text)
@@ -58,19 +60,18 @@ feature 'Issue notes polling', :feature, :js do
end
it 'when editing but you changed some things, and an update comes in, show a warning' do
- find("#note_#{existing_note.id} .js-note-edit").click
+ click_edit_action(existing_note)
expect(page).to have_field("note[note]", with: note_text)
find("#note_#{existing_note.id} .js-note-text").set('something random')
-
update_note(existing_note, updated_text)
expect(page).to have_selector(".alert")
end
it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do
- find("#note_#{existing_note.id} .js-note-edit").click
+ click_edit_action(existing_note)
expect(page).to have_field("note[note]", with: note_text)
@@ -128,4 +129,12 @@ feature 'Issue notes polling', :feature, :js do
note.update(note: new_text)
page.execute_script('notes.refresh();')
end
+
+ def click_edit_action(note)
+ note_element = find("#note_#{note.id}")
+
+ open_more_actions_dropdown(note)
+
+ note_element.find('.js-note-edit').click
+ end
end
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 854e2d1758f..e23dc2cd940 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Diff note avatars', feature: true, js: true do
+ include NoteInteractionHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
@@ -110,6 +112,8 @@ feature 'Diff note avatars', feature: true, js: true do
end
it 'removes avatar when note is deleted' do
+ open_more_actions_dropdown(note)
+
page.within find(".note-row-#{note.id}") do
find('.js-note-delete').click
end
diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index 06de072257a..22552529b9e 100644
--- a/spec/features/merge_requests/user_posts_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Merge requests > User posts notes', :js do
+ include NoteInteractionHelpers
+
let(:project) { create(:project) }
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
@@ -73,6 +75,8 @@ describe 'Merge requests > User posts notes', :js do
describe 'editing the note' do
before do
find('.note').hover
+ open_more_actions_dropdown(note)
+
find('.js-note-edit').click
end
@@ -100,6 +104,8 @@ describe 'Merge requests > User posts notes', :js do
wait_for_requests
find('.note').hover
+ open_more_actions_dropdown(note)
+
find('.js-note-edit').click
page.within('.current-note-edit-form') do
@@ -126,6 +132,8 @@ describe 'Merge requests > User posts notes', :js do
describe 'deleting an attachment' do
before do
find('.note').hover
+ open_more_actions_dropdown(note)
+
find('.js-note-edit').click
end
diff --git a/spec/features/reportable_note/commit_spec.rb b/spec/features/reportable_note/commit_spec.rb
new file mode 100644
index 00000000000..39b1c4acf52
--- /dev/null
+++ b/spec/features/reportable_note/commit_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe 'Reportable note on commit', :feature, :js do
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.add_master(user)
+ login_as user
+ end
+
+ context 'a normal note' do
+ let!(:note) { create(:note_on_commit, commit_id: sample_commit.id, project: project) }
+
+ before do
+ visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+ end
+
+ it_behaves_like 'reportable note'
+ end
+
+ context 'a diff note' do
+ let!(:note) { create(:diff_note_on_commit, commit_id: sample_commit.id, project: project) }
+
+ before do
+ visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+ end
+
+ it_behaves_like 'reportable note'
+ end
+end
diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb
new file mode 100644
index 00000000000..5f526818994
--- /dev/null
+++ b/spec/features/reportable_note/issue_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe 'Reportable note on issue', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let!(:note) { create(:note_on_issue, noteable: issue, project: project) }
+
+ before do
+ project.add_master(user)
+ login_as user
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like 'reportable note'
+end
diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb
new file mode 100644
index 00000000000..6d053d26626
--- /dev/null
+++ b/spec/features/reportable_note/merge_request_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'Reportable note on merge request', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ project.add_master(user)
+ login_as user
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'a normal note' do
+ let!(:note) { create(:note_on_merge_request, noteable: merge_request, project: project) }
+
+ it_behaves_like 'reportable note'
+ end
+
+ context 'a diff note' do
+ let!(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+
+ it_behaves_like 'reportable note'
+ end
+end
diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb
new file mode 100644
index 00000000000..3f1e0cf9097
--- /dev/null
+++ b/spec/features/reportable_note/snippets_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe 'Reportable note on snippets', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.add_master(user)
+ login_as user
+ end
+
+ describe 'on project snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project, author: user) }
+ let!(:note) { create(:note_on_project_snippet, noteable: snippet, project: project) }
+
+ before do
+ visit namespace_project_snippet_path(project.namespace, project, snippet)
+ end
+
+ it_behaves_like 'reportable note'
+ end
+
+ describe 'on personal snippet' do
+ let(:snippet) { create(:personal_snippet, :public, author: user) }
+ let!(:note) { create(:note_on_personal_snippet, noteable: snippet, author: user) }
+
+ before do
+ visit snippet_path(snippet)
+ end
+
+ it_behaves_like 'reportable note'
+ end
+end
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index f7afc174019..44b0c89fac7 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Comments on personal snippets', :js, feature: true do
+ include NoteInteractionHelpers
+
let!(:user) { create(:user) }
let!(:snippet) { create(:personal_snippet, :public) }
let!(:snippet_notes) do
@@ -22,6 +24,8 @@ describe 'Comments on personal snippets', :js, feature: true do
it 'contains notes for a snippet with correct action icons' do
expect(page).to have_selector('#notes-list li', count: 2)
+ open_more_actions_dropdown(snippet_notes[0])
+
# comment authored by current user
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
expect(page).to have_content(snippet_notes[0].note)
@@ -29,6 +33,8 @@ describe 'Comments on personal snippets', :js, feature: true do
expect(page).to have_selector('.note-emoji-button')
end
+ open_more_actions_dropdown(snippet_notes[1])
+
page.within("#notes-list li#note_#{snippet_notes[1].id}") do
expect(page).to have_content(snippet_notes[1].note)
expect(page).not_to have_selector('.js-note-delete')
@@ -68,6 +74,8 @@ describe 'Comments on personal snippets', :js, feature: true do
context 'when editing a note' do
it 'changes the text' do
+ open_more_actions_dropdown(snippet_notes[0])
+
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
click_on 'Edit comment'
end
@@ -89,8 +97,10 @@ describe 'Comments on personal snippets', :js, feature: true do
context 'when deleting a note' do
it 'removes the note from the snippet detail page' do
+ open_more_actions_dropdown(snippet_notes[0])
+
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
- click_on 'Remove comment'
+ click_on 'Delete comment'
end
wait_for_requests
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 355a4845afb..cc861af8533 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -256,4 +256,14 @@ describe NotesHelper do
expect(helper.form_resources).to eq([@project.namespace, @project, @note])
end
end
+
+ describe '#noteable_note_url' do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:note_on_issue, noteable: issue, project: project) }
+
+ it 'returns the noteable url with an anchor to the note' do
+ expect(noteable_note_url(note)).to match("/#{project.namespace.path}/#{project.path}/issues/#{issue.iid}##{dom_id(note)}")
+ end
+ end
end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 3fe8cf43934..e8a37e8d77b 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -97,6 +97,17 @@ describe Gitlab::UrlBuilder, lib: true do
end
end
+ context 'on a PersonalSnippet' do
+ it 'returns a proper URL' do
+ personal_snippet = create(:personal_snippet)
+ note = build_stubbed(:note_on_personal_snippet, noteable: personal_snippet)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/snippets/#{note.noteable_id}#note_#{note.id}"
+ end
+ end
+
context 'on another object' do
it 'returns a proper URL' do
project = build_stubbed(:empty_project)
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
new file mode 100644
index 00000000000..0d80c95e826
--- /dev/null
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+shared_examples 'reportable note' do
+ include NotesHelper
+
+ let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") }
+ let(:more_actions_selector) { '.more-actions.dropdown' }
+ let(:abuse_report_path) { new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) }
+
+ it 'has a `More actions` dropdown' do
+ expect(comment).to have_selector(more_actions_selector)
+ end
+
+ it 'dropdown has Edit, Report and Delete links' do
+ dropdown = comment.find(more_actions_selector)
+
+ dropdown.click
+ dropdown.find('.dropdown-menu li', match: :first)
+
+ expect(dropdown).to have_button('Edit comment')
+ expect(dropdown).to have_link('Report as abuse', href: abuse_report_path)
+ expect(dropdown).to have_link('Delete comment', href: note_url(note, project))
+ end
+
+ it 'Report button links to a report page' do
+ dropdown = comment.find(more_actions_selector)
+
+ dropdown.click
+ dropdown.find('.dropdown-menu li', match: :first)
+
+ dropdown.click_link('Report as abuse')
+
+ expect(find('#user_name')['value']).to match(note.author.username)
+ expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note))
+ end
+end
diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb
new file mode 100644
index 00000000000..551c759133c
--- /dev/null
+++ b/spec/support/helpers/note_interaction_helpers.rb
@@ -0,0 +1,8 @@
+module NoteInteractionHelpers
+ def open_more_actions_dropdown(note)
+ note_element = find("#note_#{note.id}")
+
+ note_element.find('.more-actions').click
+ note_element.find('.more-actions .dropdown-menu li', match: :first)
+ end
+end