From f7344338985338df3d0958ef9d362b9624723e0f Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 9 Jun 2017 01:49:49 +0300 Subject: [PATCH 001/243] Add Vuex as a dependency. --- package.json | 1 + yarn.lock | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/package.json b/package.json index fd944531a6a0..6117bbcbe2a9 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "vue-loader": "^11.3.4", "vue-resource": "^1.3.4", "vue-template-compiler": "^2.2.6", + "vuex": "^2.3.1", "webpack": "^2.6.1", "webpack-bundle-analyzer": "^2.8.2" }, diff --git a/yarn.lock b/yarn.lock index 98da6a984d1c..075bf70a6419 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5775,6 +5775,10 @@ vue@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed" +vuex@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.3.1.tgz#cde8e997c1f9957719bc7dea154f9aa691d981a6" + watchpack@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87" -- GitLab From cb2287df0ad9396d1f075bde1c4f6de481d908e6 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 9 Jun 2017 01:50:11 +0300 Subject: [PATCH 002/243] Notes bundle for the issue discussions refactor. --- app/assets/javascripts/notes/index.js | 8 ++++++++ app/views/projects/issues/_discussion.html.haml | 5 +++++ config/webpack.config.js | 2 ++ 3 files changed, 15 insertions(+) create mode 100644 app/assets/javascripts/notes/index.js diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js new file mode 100644 index 000000000000..4ee2cf19cf0e --- /dev/null +++ b/app/assets/javascripts/notes/index.js @@ -0,0 +1,8 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; + +Vue.use(Vuex); + +document.addEventListener('DOMContentLoaded', () => { + // instantiate Vue here... +}); diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 8b095f4ca109..f1711bf074eb 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -3,5 +3,10 @@ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' +#js-notes + - content_for :page_specific_javascripts do + = webpack_bundle_tag 'common_vue' + = webpack_bundle_tag 'notes' + #notes = render 'shared/notes/notes_with_form', :autocomplete => true diff --git a/config/webpack.config.js b/config/webpack.config.js index a7d92bc53b79..bcc64b1fccd5 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -51,6 +51,7 @@ var config = { monitoring: './monitoring/monitoring_bundle.js', network: './network/network_bundle.js', notebook_viewer: './blob/notebook_viewer.js', + notes: './notes/index.js', pdf_viewer: './blob/pdf_viewer.js', pipelines: './pipelines/pipelines_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js', @@ -166,6 +167,7 @@ var config = { 'merge_conflicts', 'monitoring', 'notebook_viewer', + 'notes', 'pdf_viewer', 'pipelines', 'pipelines_details', -- GitLab From 76c3d2d434d3c550c3de912abc0a5b1dc1455368 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Fri, 9 Jun 2017 16:24:54 -0500 Subject: [PATCH 003/243] Add full JSON endpoints for issue notes and discussions --- app/controllers/concerns/notes_actions.rb | 49 ++++++++++------ app/controllers/projects/issues_controller.rb | 8 +++ app/helpers/issues_helper.rb | 2 +- app/helpers/system_note_helper.rb | 8 ++- app/models/discussion.rb | 4 ++ app/serializers/award_emoji_entity.rb | 4 ++ app/serializers/discussion_entity.rb | 20 +++++++ app/serializers/discussion_serializer.rb | 3 + app/serializers/note_attachment_entity.rb | 5 ++ app/serializers/note_entity.rb | 58 +++++++++++++++++++ app/serializers/note_serializer.rb | 3 + app/serializers/user_entity.rb | 2 + app/views/discussions/_headline.html.haml | 2 +- config/routes/project.rb | 1 + lib/api/entities.rb | 4 +- 15 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 app/serializers/award_emoji_entity.rb create mode 100644 app/serializers/discussion_entity.rb create mode 100644 app/serializers/discussion_serializer.rb create mode 100644 app/serializers/note_attachment_entity.rb create mode 100644 app/serializers/note_entity.rb create mode 100644 app/serializers/note_serializer.rb diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index a57d9e6e6c0e..14e755c8d971 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -11,14 +11,16 @@ def index notes_json = { notes: [], last_fetched_at: current_fetched_at } - @notes = notes_finder.execute.inc_relations_for_view + @notes = notes_finder.execute.inc_relations_for_view.to_a + @notes.reject! { |n| n.cross_reference_not_visible_for?(current_user) } @notes = prepare_notes_for_rendering(@notes) - @notes.each do |note| - next if note.cross_reference_not_visible_for?(current_user) - - notes_json[:notes] << note_json(note) - end + notes_json[:notes] = + if params[:full_data] + note_serializer.represent(@notes) + else + @notes.map { |note| note_json(note) } + end render json: notes_json end @@ -80,22 +82,27 @@ def note_json(note) } if note.persisted? - attrs.merge!( - valid: true, - id: note.id, - discussion_id: note.discussion_id(noteable), - html: note_html(note), - note: note.note - ) + attrs[:valid] = true - discussion = note.to_discussion(noteable) - unless discussion.individual_note? + if params[:full_data] + attrs.merge!(note_serializer.represent(note)) + else attrs.merge!( - discussion_resolvable: discussion.resolvable?, - - diff_discussion_html: diff_discussion_html(discussion), - discussion_html: discussion_html(discussion) + id: note.id, + discussion_id: note.discussion_id(noteable), + html: note_html(note), + note: note.note ) + + discussion = note.to_discussion(noteable) + unless discussion.individual_note? + attrs.merge!( + discussion_resolvable: discussion.resolvable?, + + diff_discussion_html: diff_discussion_html(discussion), + discussion_html: discussion_html(discussion) + ) + end end else attrs.merge!( @@ -177,4 +184,8 @@ def last_fetched_at def notes_finder @notes_finder ||= NotesFinder.new(project, current_user, finder_params) end + + def note_serializer + NoteSerializer.new(project: project, noteable: noteable, current_user: current_user) + end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 0ac9da2ff0fa..153c490ce3f1 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -97,6 +97,14 @@ def show end end + def discussions + @discussions = @issue.discussions + @discussions.reject! { |d| d.individual_note? && d.first_note.cross_reference_not_visible_for?(current_user) } + prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + + render json: DiscussionSerializer.new(project: @project, noteable: @issue, current_user: current_user).represent(@discussions) + end + def create create_params = issue_params.merge(spammable_params).merge( merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of], diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 42b6cfdf02fe..034b4451b56d 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -119,7 +119,7 @@ def award_user_authored_class(award) end def awards_sort(awards) - awards.sort_by do |award, notes| + awards.sort_by do |award, award_emojis| if award == "thumbsup" 0 elsif award == "thumbsdown" diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index 209bd56b78ac..693668251247 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -21,8 +21,14 @@ module SystemNoteHelper 'outdated' => 'icon_edit' }.freeze + def system_note_icon_name(note) + ICON_NAMES_BY_ACTION[note.system_note_metadata&.action] + end + def icon_for_system_note(note) - icon_name = ICON_NAMES_BY_ACTION[note.system_note_metadata&.action] + icon_name = system_note_icon_name(note) custom_icon(icon_name) if icon_name end + + extend self end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index d1cec7613afa..b80da7b246a8 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -81,6 +81,10 @@ def last_updated_by last_note.author end + def updated? + last_updated_at != created_at + end + def id first_note.discussion_id(context_noteable) end diff --git a/app/serializers/award_emoji_entity.rb b/app/serializers/award_emoji_entity.rb new file mode 100644 index 000000000000..6e03cd02392b --- /dev/null +++ b/app/serializers/award_emoji_entity.rb @@ -0,0 +1,4 @@ +class AwardEmojiEntity < Grape::Entity + expose :name + expose :user, using: API::Entities::UserSafe +end diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb new file mode 100644 index 000000000000..cb6c3c238070 --- /dev/null +++ b/app/serializers/discussion_entity.rb @@ -0,0 +1,20 @@ +class DiscussionEntity < Grape::Entity + include RequestAwareEntity + + expose :id, :reply_id + expose :expanded?, as: :expanded + expose :author, using: UserEntity + + expose :created_at + + expose :last_updated_at, if: -> (discussion, _) { discussion.updated? } + expose :last_updated_by, if: -> (discussion, _) { discussion.updated? }, using: UserEntity + + expose :notes, using: NoteEntity + + expose :individual_note?, as: :individual_note + + expose :can_reply do |discussion| + can?(request.current_user, :create_note, discussion.project) + end +end diff --git a/app/serializers/discussion_serializer.rb b/app/serializers/discussion_serializer.rb new file mode 100644 index 000000000000..ed5e1224bb23 --- /dev/null +++ b/app/serializers/discussion_serializer.rb @@ -0,0 +1,3 @@ +class DiscussionSerializer < BaseSerializer + entity DiscussionEntity +end diff --git a/app/serializers/note_attachment_entity.rb b/app/serializers/note_attachment_entity.rb new file mode 100644 index 000000000000..1ad50568ab96 --- /dev/null +++ b/app/serializers/note_attachment_entity.rb @@ -0,0 +1,5 @@ +class NoteAttachmentEntity < Grape::Entity + expose :url + expose :filename + expose :image?, as: :image +end diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb new file mode 100644 index 000000000000..7a49ec4ef553 --- /dev/null +++ b/app/serializers/note_entity.rb @@ -0,0 +1,58 @@ +class NoteEntity < API::Entities::Note + include RequestAwareEntity + + expose :type + + expose :author, using: UserEntity + + expose :human_access do |note| + note.project.team.human_max_access(note.author_id) + end + + unexpose :note, as: :body + expose :note + + expose :redacted_note_html, as: :note_html + + expose :last_edited_at, if: -> (note, _) { note.is_edited? } + expose :last_edited_by, using: UserEntity, if: -> (note, _) { note.is_edited? } + + expose :can_edit do |note| + Ability.can_edit_note?(request.current_user, note) + end + + expose :system_note_icon_name, if: -> (note, _) { note.system? } do |note| + SystemNoteHelper.system_note_icon_name(note) + end + + expose :discussion_id do |note| + note.discussion_id(request.noteable) + end + + expose :emoji_awardable?, as: :emoji_awardable + expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity + expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note| + if note.for_personal_snippet? + toggle_award_emoji_snippet_note_path(note.noteable, note) + else + toggle_award_emoji_namespace_project_note_path(note.project.namespace, note.project, note.id) + end + end + + expose :report_abuse_path do |note| + new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note)) + end + + expose :path do |note| + if note.for_personal_snippet? + snippet_note_path(note.noteable, note) + else + namespace_project_note_path(note.project.namespace, note.project, note) + end + end + + expose :attachment, using: NoteAttachmentEntity + expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note| + delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note) + end +end diff --git a/app/serializers/note_serializer.rb b/app/serializers/note_serializer.rb new file mode 100644 index 000000000000..2afe40d7a34b --- /dev/null +++ b/app/serializers/note_serializer.rb @@ -0,0 +1,3 @@ +class NoteSerializer < BaseSerializer + entity NoteEntity +end diff --git a/app/serializers/user_entity.rb b/app/serializers/user_entity.rb index 876512b12dc6..3bb340065c44 100644 --- a/app/serializers/user_entity.rb +++ b/app/serializers/user_entity.rb @@ -1,6 +1,8 @@ class UserEntity < API::Entities::UserBasic include RequestAwareEntity + unexpose :web_url + expose :path do |user| user_path(user) end diff --git a/app/views/discussions/_headline.html.haml b/app/views/discussions/_headline.html.haml index c1dabeed3871..25e90924413f 100644 --- a/app/views/discussions/_headline.html.haml +++ b/app/views/discussions/_headline.html.haml @@ -5,7 +5,7 @@ by = link_to_member(@project, discussion.resolved_by, avatar: false) = time_ago_with_tooltip(discussion.resolved_at, placement: "bottom") -- elsif discussion.last_updated_at != discussion.created_at +- elsif discussion.updated? .discussion-headline-light.js-discussion-headline Last updated - if discussion.last_updated_by diff --git a/config/routes/project.rb b/config/routes/project.rb index 672b5a9a160a..d6fb309de8e2 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -308,6 +308,7 @@ get :can_create_branch get :realtime_changes post :create_merge_request + get :discussions, format: :json end collection do post :bulk_update diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 09a888690639..89ab2118dd51 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1,11 +1,11 @@ module API module Entities class UserSafe < Grape::Entity - expose :name, :username + expose :id, :name, :username end class UserBasic < UserSafe - expose :id, :state + expose :state expose :avatar_url do |user, options| user.avatar_url(only_path: false) end -- GitLab From 4b87ecd37d3ef0d0e2d0bbe59d6a4d0a409e19f9 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Fri, 9 Jun 2017 16:41:59 -0500 Subject: [PATCH 004/243] Add data-discussions-path to issues notes div --- app/views/projects/issues/_discussion.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index f1711bf074eb..6d09a171b53f 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -8,5 +8,5 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' -#notes +#notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json) } } = render 'shared/notes/notes_with_form', :autocomplete => true -- GitLab From 0273b1188b939b157d439247a97e6c2d24a9f0b4 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 10 Jun 2017 01:50:31 +0300 Subject: [PATCH 005/243] Initial version of main component, Vuex store and service of issue discussions refactor. --- .../notes/components/issue_notes.vue | 37 +++++++++++++++++++ app/assets/javascripts/notes/index.js | 14 ++++--- .../notes/services/issue_notes_service.js | 10 +++++ .../notes/stores/issue_notes_store.js | 37 +++++++++++++++++++ .../projects/issues/_discussion.html.haml | 5 ++- 5 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_notes.vue create mode 100644 app/assets/javascripts/notes/services/issue_notes_service.js create mode 100644 app/assets/javascripts/notes/stores/issue_notes_store.js diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue new file mode 100644 index 000000000000..1b7de0b740e9 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -0,0 +1,37 @@ +<script> +import Vue from 'vue'; +import Vuex from 'vuex'; +import storeOptions from '../stores/issue_notes_store'; + +Vue.use(Vuex); +const store = new Vuex.Store(storeOptions); + +export default { + name: 'IssueNotes', + store, + data() { + return { + isLoading: true, + }; + }, + mounted() { + const path = this.$el.parentNode.dataset.discussionsPath; + this.$store.dispatch('fetchNotes', path) + .finally(() => { + this.isLoading = false; + }); + }, +}; +</script> + +<template> + <div id="notes"> + <div + v-if="isLoading" + class="loading"> + <i + aria-hidden="true" + class="fa fa-spinner fa-spin" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 4ee2cf19cf0e..f0f94e2f5006 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,8 +1,10 @@ import Vue from 'vue'; -import Vuex from 'vuex'; +import IssueNotes from './components/issue_notes.vue'; -Vue.use(Vuex); - -document.addEventListener('DOMContentLoaded', () => { - // instantiate Vue here... -}); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#js-notes', + components: { IssueNotes }, + template: ` + <issue-notes /> + `, +})); diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js new file mode 100644 index 000000000000..810dec61b5b7 --- /dev/null +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -0,0 +1,10 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default { + fetchNotes(endpoint) { + return Vue.http.get(endpoint); + }, +}; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js new file mode 100644 index 000000000000..4b3c08e9ca8d --- /dev/null +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -0,0 +1,37 @@ +/* global Flash */ +/* eslint-disable no-param-reassign */ + +import service from '../services/issue_notes_service'; + +const state = { + notes: [], +}; + +const getters = {}; + +const mutations = { + setNotes(vmState, notes) { + vmState.notes = notes; + }, +}; + +const actions = { + fetchNotes(context, path) { + return service + .fetchNotes(path) + .then(res => res.json()) + .then((res) => { + context.commit('setNotes', res); + }) + .catch(() => { + new Flash('Something went while fetching issue comments. Please try again.'); // eslint-disable-line + }); + }, +}; + +export default { + state, + getters, + mutations, + actions, +}; diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 6d09a171b53f..d5ec7fdcb3e5 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -3,10 +3,11 @@ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' -#js-notes +%section{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json) } } + #js-notes - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' -#notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json) } } +#notes = render 'shared/notes/notes_with_form', :autocomplete => true -- GitLab From 16a5808e6061cce75f84de8cfb332ec2f5c81ef6 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 13 Jun 2017 20:01:59 +0300 Subject: [PATCH 006/243] =?UTF-8?q?Implement=20initial=20version=20of=20Vu?= =?UTF-8?q?e=20notes=20for=20issues.=20=F0=9F=8E=89=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notes/components/issue_discussion.vue | 112 ++++++++++++++++++ .../notes/components/issue_note.vue | 63 ++++++++++ .../notes/components/issue_note_actions.vue | 95 +++++++++++++++ .../notes/components/issue_note_body.vue | 18 +++ .../components/issue_note_edited_text.vue | 43 +++++++ .../notes/components/issue_note_header.vue | 79 ++++++++++++ .../notes/components/issue_notes.vue | 25 +++- .../icons/emoji_slightly_smiling_face.svg | 1 + .../javascripts/notes/icons/emoji_smile.svg | 1 + .../javascripts/notes/icons/emoji_smiley.svg | 1 + .../notes/stores/issue_notes_store.js | 17 ++- app/assets/stylesheets/pages/issuable.scss | 1 + .../shared/notes/_notes_with_form.html.haml | 6 +- 13 files changed, 454 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_discussion.vue create mode 100644 app/assets/javascripts/notes/components/issue_note.vue create mode 100644 app/assets/javascripts/notes/components/issue_note_actions.vue create mode 100644 app/assets/javascripts/notes/components/issue_note_body.vue create mode 100644 app/assets/javascripts/notes/components/issue_note_edited_text.vue create mode 100644 app/assets/javascripts/notes/components/issue_note_header.vue create mode 100644 app/assets/javascripts/notes/icons/emoji_slightly_smiling_face.svg create mode 100644 app/assets/javascripts/notes/icons/emoji_smile.svg create mode 100644 app/assets/javascripts/notes/icons/emoji_smiley.svg diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue new file mode 100644 index 000000000000..21f1f721a05e --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -0,0 +1,112 @@ +<script> +import IssueNote from './issue_note.vue'; +import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import IssueNoteHeader from './issue_note_header.vue'; +import IssueNoteActions from './issue_note_actions.vue'; +import IssueNoteEditedText from './issue_note_edited_text.vue'; + +export default { + props: { + note: { + type: Object, + required: true, + }, + }, + data() { + return { + registerLink: '#', + signInLink: '#', + } + }, + computed: { + discussion() { + return this.note.notes[0]; + }, + author() { + return this.discussion.author; + }, + }, + components: { + IssueNote, + UserAvatarLink, + IssueNoteHeader, + IssueNoteActions, + IssueNoteEditedText, + }, + mounted() { + // We need to grab the register and sign in links from DOM for the time being. + const registerLink = document.querySelector('.js-disabled-comment .js-register-link'); + const signInLink = document.querySelector('.js-disabled-comment .js-sign-in-link'); + + if (registerLink && signInLink) { + this.registerLink = registerLink.getAttribute('href'); + this.signInLink = signInLink.getAttribute('href'); + } + }, +} +</script> + +<template> + <li class="note note-discussion timeline-entry"> + <div class="timeline-entry-inner"> + <div class="timeline-icon"> + <user-avatar-link + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" /> + </div> + <div class="timeline-content"> + <div class="discussion"> + <div class="discussion-header"> + <issue-note-header + :author="author" + :createdAt="discussion.created_at" + :notePath="discussion.path" + :includeToggle="true" + :discussionId="note.id" + actionText="started a discussion" /> + <issue-note-edited-text + v-if="note.last_updated_by" + :editedAt="note.last_updated_at" + :editedBy="note.last_updated_by" + actionText="Last updated" + className="discussion-headline-light js-discussion-headline" /> + </div> + </div> + <div + v-if="note.expanded" + class="discussion-body"> + <div class="panel panel-default"> + <div class="discussion-notes"> + <ul class="notes"> + <issue-note + v-for="note in note.notes" + key="note.id" + :note="note" /> + </ul> + <div class="flash-container"></div> + <div class="discussion-reply-holder"> + <button + v-if="note.can_reply" + type="button" + class="btn btn-text-field js-discussion-reply-button" + title="Add a reply"></button> + <div + v-if="!note.can_reply" + class="disabled-comment text-center"> + Please + <a :href="registerLink">register</a> + or + <a :href="signInLink">sign in</a> + to reply + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue new file mode 100644 index 000000000000..7e4c6cc6eee9 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -0,0 +1,63 @@ +<script> +import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import IssueNoteHeader from './issue_note_header.vue'; +import IssueNoteActions from './issue_note_actions.vue'; +import IssueNoteBody from './issue_note_body.vue'; +import IssueNoteEditedText from './issue_note_edited_text.vue'; + +export default { + props: { + note: { + type: Object, + required: true, + }, + }, + components: { + UserAvatarLink, + IssueNoteHeader, + IssueNoteActions, + IssueNoteBody, + IssueNoteEditedText, + }, + computed: { + author() { + return this.note.author; + }, + }, +}; +</script> + +<template> + <li class="note timeline-entry"> + <div class="timeline-entry-inner"> + <div class="timeline-icon"> + <user-avatar-link + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" /> + </div> + <div class="timeline-content"> + <div class="note-header"> + <issue-note-header + :author="author" + :createdAt="note.created_at" + :notePath="note.path" + actionText="commented" /> + <issue-note-actions + :accessLevel="note.human_access" + :canAward="note.emoji_awardable" + :canEdit="note.can_edit" + :canDelete="note.can_edit" + :reportAbusePath="note.report_abuse_path" /> + </div> + <issue-note-body :note="note" /> + <issue-note-edited-text + v-if="note.last_edited_by" + :editedAt="note.last_edited_at" + :editedBy="note.last_edited_by" + actionText="Edited" /> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue new file mode 100644 index 000000000000..35828a959e16 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -0,0 +1,95 @@ +<script> +import emojiSmiling from '../icons/emoji_slightly_smiling_face.svg'; +import emojiSmile from '../icons/emoji_smile.svg'; +import emojiSmiley from '../icons/emoji_smiley.svg'; + +export default { + props: { + accessLevel: { + type: String, + required: true, + }, + reportAbusePath: { + type: String, + required: true, + }, + canEdit: { + type: Boolean, + required: true, + }, + canDelete: { + type: Boolean, + required: true, + }, + }, + data() { + return { + emojiSmiling, + emojiSmile, + emojiSmiley, + }; + }, +}; +</script> + +<template> + <div class="note-actions"> + <span class="note-role"> + {{accessLevel}} + </span> + <a + class="note-action-button note-emoji-button js-add-award js-note-emoji js-user-authored has-tooltip" data-position="right" + href="#" + title="Add reaction"> + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-spinner fa-spin"></i> + <span + v-html="emojiSmiling" + class="link-highlight award-control-icon-neutral"></span> + <span + v-html="emojiSmiley" + class="link-highlight award-control-icon-positive"></span> + <span + v-html="emojiSmile" + class="link-highlight award-control-icon-super-positive"></span> + </a> + <div class="dropdown more-actions"> + <button + type="button" + title="More actions" + class="note-action-button more-actions-toggle has-tooltip btn btn-transparent" + data-toggle="dropdown" + data-container="body"> + <i + aria-hidden="true" + class="fa fa-ellipsis-v icon"></i> + </button> + <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> + <template v-if="canEdit"> + <li> + <button + type="button" + class="js-note-edit btn btn-transparent"> + Edit comment + </button> + </li> + <li class="divider"></li> + </template> + <li v-if="reportAbusePath"> + <a :href="reportAbusePath"> + Report as abuse + </a> + </li> + <li> + <a class="js-note-delete"> + <span class="text-danger"> + Delete comment + </span> + </a> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue new file mode 100644 index 000000000000..8c12a81c2dd8 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -0,0 +1,18 @@ +<script> +export default { + props: { + note: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div class="note-body"> + <div + v-html="note.note_html" + class="note-text md"></div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue new file mode 100644 index 000000000000..f1b8362ad658 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue @@ -0,0 +1,43 @@ +<script> +import TimeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + +export default { + props: { + actionText: { + type: String, + required: true, + }, + editedAt: { + type: String, + required: true, + }, + editedBy: { + type: Object, + required: true, + }, + className: { + type: String, + required: false, + default: 'edited-text', + }, + }, + components: { + TimeAgoTooltip, + }, +} +</script> + +<template> + <div :class="className"> + <span>{{actionText}} </span> + <span> by </span> + <a + :href="editedBy.path" + class="author_link"> + <span>{{editedBy.name}}</span> + </a> + <time-ago-tooltip + :time="editedAt" + tooltipPlacement="bottom" /> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue new file mode 100644 index 000000000000..0ce07da8d3e6 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -0,0 +1,79 @@ +<script> +import TimeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + +export default { + props: { + author: { + type: Object, + required: true, + }, + createdAt: { + type: String, + required: true, + }, + actionText: { + type: String, + required: true, + }, + notePath: { + type: String, + required: true, + }, + includeToggle: { + type: Boolean, + required: false, + default: false, + }, + discussionId: { + type: String, + required: false, + } + }, + components: { + TimeAgoTooltip, + }, + methods: { + doShit() { + this.$store.commit('toggleDiscussion', { + discussionId: this.discussionId, + }); + }, + }, +}; +</script> + +<template> + <div class="note-header-info"> + <a :href="author.path"> + <span class="note-header-author-name"> + {{author.name}} + </span> + <span class="note-headline-light"> + @{{author.username}} + </span> + </a> + <span class="note-headline-light"> + <span class="note-headline-meta"> + {{actionText}} + <a :href="notePath"> + <time-ago-tooltip + :time="createdAt" + tooltipPlacement="bottom" /> + </a> + </span> + </span> + <div + v-if="includeToggle" + class="discussion-actions"> + <button + @click="doShit" + class="note-action-button discussion-toggle-button js-toggle-button" + type="button"> + <i + aria-hidden="true" + class="fa fa-chevron-up"></i> + Toggle discussion + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 1b7de0b740e9..98db5b1ec9b8 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -2,6 +2,8 @@ import Vue from 'vue'; import Vuex from 'vuex'; import storeOptions from '../stores/issue_notes_store'; +import IssueNote from './issue_note.vue'; +import IssueDiscussion from './issue_discussion.vue'; Vue.use(Vuex); const store = new Vuex.Store(storeOptions); @@ -14,6 +16,18 @@ export default { isLoading: true, }; }, + components: { + IssueNote, + IssueDiscussion, + }, + methods: { + component(note) { + return note.individual_note ? IssueNote : IssueDiscussion; + }, + componentData(note) { + return note.individual_note ? note.notes[0] : note; + } + }, mounted() { const path = this.$el.parentNode.dataset.discussionsPath; this.$store.dispatch('fetchNotes', path) @@ -31,7 +45,16 @@ export default { class="loading"> <i aria-hidden="true" - class="fa fa-spinner fa-spin" /> + class="fa fa-spinner fa-spin"></i> </div> + <ul + class="notes main-notes-list timeline" + id="notes-list"> + <component + v-for="note in $store.getters.notes" + :is="component(note)" + :note="componentData(note)" + :key="note.id" /> + </ul> </div> </template> diff --git a/app/assets/javascripts/notes/icons/emoji_slightly_smiling_face.svg b/app/assets/javascripts/notes/icons/emoji_slightly_smiling_face.svg new file mode 100644 index 000000000000..56dbad91554e --- /dev/null +++ b/app/assets/javascripts/notes/icons/emoji_slightly_smiling_face.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445c.195.625.556 1.131 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg> diff --git a/app/assets/javascripts/notes/icons/emoji_smile.svg b/app/assets/javascripts/notes/icons/emoji_smile.svg new file mode 100644 index 000000000000..ce645fee46fd --- /dev/null +++ b/app/assets/javascripts/notes/icons/emoji_smile.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg> diff --git a/app/assets/javascripts/notes/icons/emoji_smiley.svg b/app/assets/javascripts/notes/icons/emoji_smiley.svg new file mode 100644 index 000000000000..ddfae50e5669 --- /dev/null +++ b/app/assets/javascripts/notes/icons/emoji_smiley.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="nonzero"/></svg> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 4b3c08e9ca8d..8392f7bfab54 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -7,11 +7,20 @@ const state = { notes: [], }; -const getters = {}; +const getters = { + notes(storeState) { + return storeState.notes; + }, +}; const mutations = { - setNotes(vmState, notes) { - vmState.notes = notes; + setNotes(storeState, notes) { + storeState.notes = notes; + }, + toggleDiscussion(storeState, { discussionId }) { + const [ discussion ] = storeState.notes.filter((note) => note.id === discussionId); + + discussion.expanded = !discussion.expanded; }, }; @@ -24,7 +33,7 @@ const actions = { context.commit('setNotes', res); }) .catch(() => { - new Flash('Something went while fetching issue comments. Please try again.'); // eslint-disable-line + new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line }); }, }; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index aa04e4906498..286b6f56cf96 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -453,6 +453,7 @@ color: $gray-darkest; display: block; margin: 16px 0 0; + font-size: 85%; .author_link { color: $gray-darkest; diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index f0fcc414756b..51049acf042b 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -15,11 +15,11 @@ .timeline-content.timeline-content-form = render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete - elsif !current_user - .disabled-comment.text-center.prepend-top-default + .disabled-comment.text-center.prepend-top-default.js-disabled-comment Please - = link_to "register", new_session_path(:user, redirect_to_referer: 'yes') + = link_to "register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-register-link' or - = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes') + = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link' to comment :javascript -- GitLab From 57ccfb9a4f88a652b02ca89d7a98326006a24227 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 14 Jun 2017 05:13:03 +0300 Subject: [PATCH 007/243] Fix some ESLint errors. --- app/assets/javascripts/notes/components/issue_discussion.vue | 4 ++-- .../javascripts/notes/components/issue_note_edited_text.vue | 2 +- app/assets/javascripts/notes/components/issue_note_header.vue | 2 +- app/assets/javascripts/notes/components/issue_notes.vue | 2 +- app/assets/javascripts/notes/stores/issue_notes_store.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 21f1f721a05e..236154f4c718 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -16,7 +16,7 @@ export default { return { registerLink: '#', signInLink: '#', - } + }; }, computed: { discussion() { @@ -43,7 +43,7 @@ export default { this.signInLink = signInLink.getAttribute('href'); } }, -} +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue index f1b8362ad658..8ed35bdb2a0c 100644 --- a/app/assets/javascripts/notes/components/issue_note_edited_text.vue +++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue @@ -24,7 +24,7 @@ export default { components: { TimeAgoTooltip, }, -} +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 0ce07da8d3e6..8d7b2ee72310 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -27,7 +27,7 @@ export default { discussionId: { type: String, required: false, - } + }, }, components: { TimeAgoTooltip, diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 98db5b1ec9b8..4ff9525661cf 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -26,7 +26,7 @@ export default { }, componentData(note) { return note.individual_note ? note.notes[0] : note; - } + }, }, mounted() { const path = this.$el.parentNode.dataset.discussionsPath; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 8392f7bfab54..0c1c6836325d 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -18,7 +18,7 @@ const mutations = { storeState.notes = notes; }, toggleDiscussion(storeState, { discussionId }) { - const [ discussion ] = storeState.notes.filter((note) => note.id === discussionId); + const [discussion] = storeState.notes.filter(note => note.id === discussionId); discussion.expanded = !discussion.expanded; }, -- GitLab From 7374d2cc9b678ead83076b28b11c370bb6db5bd2 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 14 Jun 2017 05:13:14 +0300 Subject: [PATCH 008/243] Add missing button title. --- app/assets/javascripts/notes/components/issue_discussion.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 236154f4c718..e53e6d04b7e5 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -91,7 +91,7 @@ export default { v-if="note.can_reply" type="button" class="btn btn-text-field js-discussion-reply-button" - title="Add a reply"></button> + title="Add a reply">Reply...</button> <div v-if="!note.can_reply" class="disabled-comment text-center"> -- GitLab From 0df662feeee6c15ebb81963b0d739db5a1261720 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 14 Jun 2017 05:13:38 +0300 Subject: [PATCH 009/243] Implement note awards as a vue component. --- .../notes/components/issue_note.vue | 7 - .../components/issue_note_awards_list.vue | 124 ++++++++++++++++++ .../notes/components/issue_note_body.vue | 15 +++ .../projects/issues/_discussion.html.haml | 3 +- 4 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_note_awards_list.vue diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 7e4c6cc6eee9..1043776010ec 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -3,7 +3,6 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ import IssueNoteHeader from './issue_note_header.vue'; import IssueNoteActions from './issue_note_actions.vue'; import IssueNoteBody from './issue_note_body.vue'; -import IssueNoteEditedText from './issue_note_edited_text.vue'; export default { props: { @@ -17,7 +16,6 @@ export default { IssueNoteHeader, IssueNoteActions, IssueNoteBody, - IssueNoteEditedText, }, computed: { author() { @@ -52,11 +50,6 @@ export default { :reportAbusePath="note.report_abuse_path" /> </div> <issue-note-body :note="note" /> - <issue-note-edited-text - v-if="note.last_edited_by" - :editedAt="note.last_edited_at" - :editedBy="note.last_edited_by" - actionText="Edited" /> </div> </div> </li> diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue new file mode 100644 index 000000000000..1964ea4a0817 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -0,0 +1,124 @@ +<script> +import { glEmojiTag } from '~/behaviors/gl_emoji'; +import emojiSmiling from '../icons/emoji_slightly_smiling_face.svg'; +import emojiSmile from '../icons/emoji_smile.svg'; +import emojiSmiley from '../icons/emoji_smiley.svg'; + +export default { + props: { + awards: { + type: Array, + required: true, + }, + }, + data() { + return { + emojiSmiling, + emojiSmile, + emojiSmiley, + }; + }, + computed: { + // `this.awards` is an array with emojis but they are not grouped by emoji name. See below. + // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ] + // This method will group emojis by name their name as an Object. See below. + // { + // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ], + // bar: [ { name: bar, user: user1 } ] + // } + // We need to do this otherwise will will render the same emoji over and over again. + groupedAwards() { + const awards = {}; + + this.awards.forEach((award) => { + awards[award.name] = awards[award.name] || []; + awards[award.name].push(award); + }); + + return awards; + }, + }, + methods: { + getAwardHTML(name) { + return glEmojiTag(name); + }, + amIAwarded(awardList) { + const myUserId = window.gon.current_user_id; + const isAwarded = awardList.filter(award => award.user.id === myUserId); + + return isAwarded.length; + }, + awardTitle(awardsList) { + const amIAwarded = this.amIAwarded(awardsList); + const myUserId = window.gon.current_user_id; + const TOOLTIP_NAME_COUNT = amIAwarded ? 9 : 10; + let awardList = awardsList; + + if (amIAwarded) { + awardList = awardList.filter(award => award.user.id !== myUserId); + } + + const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); + const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); + + if (amIAwarded) { + namesToShow.unshift('You'); + } + + let title = ''; + + if (remainingAwardList.length) { + title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; + } else if (namesToShow.length > 1) { + title = namesToShow.slice(0, namesToShow.length - 1).join(', '); + title += namesToShow.length > 2 ? ',' : ''; + title += ` and ${namesToShow.slice(-1)}`; + } else { + title = namesToShow.join(' and '); + } + + return title; + }, + }, +}; +</script> + +<template> + <div class="note-awards"> + <div class="awards js-awards-block"> + <button + v-for="(awardList, awardName) in groupedAwards" + class="btn award-control has-tooltip" + :class="{ active: amIAwarded(awardList) }" + :title="awardTitle(awardList)" + data-placement="bottom" + type="button"> + <span v-html="getAwardHTML(awardName)"></span> + <span class="award-control-text"> + {{awardList.length}} + </span> + </button> + <div class="award-menu-holder"> + <button + aria-label="Add reaction" + class="award-control btn has-tooltip" + data-placement="bottom" + title="Add reaction" + type="button"> + <span + v-html="emojiSmiling" + class="award-control-icon award-control-icon-neutral"></span> + <span + v-html="emojiSmiley" + class="award-control-icon award-control-icon-positive"></span> + <span + v-html="emojiSmile" + class="award-control-icon award-control-icon-super-positive"></span> + <i + aria-hidden="true" + class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i> + </button> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 8c12a81c2dd8..08f367e04960 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -1,4 +1,7 @@ <script> +import IssueNoteEditedText from './issue_note_edited_text.vue'; +import IssueNoteAwardsList from './issue_note_awards_list.vue'; + export default { props: { note: { @@ -6,6 +9,10 @@ export default { required: true, }, }, + components: { + IssueNoteEditedText, + IssueNoteAwardsList, + }, }; </script> @@ -14,5 +21,13 @@ export default { <div v-html="note.note_html" class="note-text md"></div> + <issue-note-edited-text + v-if="note.last_edited_by" + :editedAt="note.last_edited_at" + :editedBy="note.last_edited_by" + actionText="Edited" /> + <issue-note-awards-list + v-if="note.award_emoji.length" + :awards="note.award_emoji" /> </div> </template> diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index d5ec7fdcb3e5..6ce7014095b7 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -9,5 +9,6 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' -#notes + +#notes{style: "margin-top: 150px"} = render 'shared/notes/notes_with_form', :autocomplete => true -- GitLab From 654355a0f0f3031cbd11bb4dff537ffb5a75b771 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 14 Jun 2017 05:29:19 +0300 Subject: [PATCH 010/243] Add comments to complicated awardTitle method. --- .../notes/components/issue_note_awards_list.vue | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 1964ea4a0817..dd39dcf7ee0e 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -54,26 +54,34 @@ export default { const TOOLTIP_NAME_COUNT = amIAwarded ? 9 : 10; let awardList = awardsList; + // Filter myself from list if I am awarded. if (amIAwarded) { awardList = awardList.filter(award => award.user.id !== myUserId); } + // Get only 9-10 usernames to show in tooltip text. const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); + + // Get the remaining list to use in `and x more` text. const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); + // Add myself to the begining of the list so title will start with You. if (amIAwarded) { namesToShow.unshift('You'); } let title = ''; + // We have 10+ awarded user, join them with comma and add `and x more`. if (remainingAwardList.length) { title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; } else if (namesToShow.length > 1) { + // Join all names with comma but not the last one, it will be added with and text. title = namesToShow.slice(0, namesToShow.length - 1).join(', '); + // If we have more than 2 users we need an extra comma before and text. title += namesToShow.length > 2 ? ',' : ''; - title += ` and ${namesToShow.slice(-1)}`; - } else { + title += ` and ${namesToShow.slice(-1)}`; // Append and text + } else { // We have only 2 users so join them with and. title = namesToShow.join(' and '); } -- GitLab From 0f6ecaa1a6df1c1b0e0605b367104653653fa090 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 14 Jun 2017 16:47:43 +0300 Subject: [PATCH 011/243] Implement canAward. --- .../notes/components/issue_note_awards_list.vue | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index dd39dcf7ee0e..9750f04e2079 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -16,6 +16,7 @@ export default { emojiSmiling, emojiSmile, emojiSmiley, + canAward: !!window.gon.current_user_id, }; }, computed: { @@ -42,6 +43,12 @@ export default { getAwardHTML(name) { return glEmojiTag(name); }, + getAwardClassBindings(awardList) { + return { + active: this.amIAwarded(awardList), + disabled: !this.canAward, + }; + }, amIAwarded(awardList) { const myUserId = window.gon.current_user_id; const isAwarded = awardList.filter(award => award.user.id === myUserId); @@ -97,7 +104,7 @@ export default { <button v-for="(awardList, awardName) in groupedAwards" class="btn award-control has-tooltip" - :class="{ active: amIAwarded(awardList) }" + :class="getAwardClassBindings(awardList)" :title="awardTitle(awardList)" data-placement="bottom" type="button"> @@ -106,7 +113,9 @@ export default { {{awardList.length}} </span> </button> - <div class="award-menu-holder"> + <div + v-if="canAward" + class="award-menu-holder"> <button aria-label="Add reaction" class="award-control btn has-tooltip" -- GitLab From 82f973070777f7738614ebf62d947d84ec7b23b3 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 15 Jun 2017 02:36:18 +0300 Subject: [PATCH 012/243] MarkdownField: Add extra prop to make spacing classes optional. --- .../javascripts/vue_shared/components/markdown/field.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 4e10bbc7408c..547e459d8e92 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -14,6 +14,11 @@ type: String, required: true, }, + addSpacingClasses: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -74,7 +79,8 @@ <template> <div - class="md-area prepend-top-default append-bottom-default js-vue-markdown-field" + class="md-area js-vue-markdown-field" + :class="{ 'prepend-top-default append-bottom-default': addSpacingClasses }" ref="gl-form"> <markdown-header :preview-markdown="previewMarkdown" -- GitLab From d110f38d9075f9061869d8427773dd499aee92b6 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 15 Jun 2017 02:36:53 +0300 Subject: [PATCH 013/243] IssueNotesRefactor: Implement note edit widget. --- .../notes/components/issue_note.vue | 29 ++++++- .../notes/components/issue_note_actions.vue | 7 +- .../notes/components/issue_note_body.vue | 32 +++++++- .../notes/components/issue_note_form.vue | 82 +++++++++++++++++++ .../notes/components/issue_note_header.vue | 4 +- 5 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_note_form.vue diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 1043776010ec..ef18ff4b45f7 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -11,6 +11,11 @@ export default { required: true, }, }, + data() { + return { + isEditing: false, + } + }, components: { UserAvatarLink, IssueNoteHeader, @@ -22,11 +27,24 @@ export default { return this.note.author; }, }, + methods: { + editHandler() { + this.isEditing = true; + }, + formUpdateHandler(data) { + console.log('update requested', data); + }, + formCancelHandler() { + this.isEditing = false; + }, + }, }; </script> <template> - <li class="note timeline-entry"> + <li + class="note timeline-entry" + :class="{ 'is-editing': isEditing }"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link @@ -47,9 +65,14 @@ export default { :canAward="note.emoji_awardable" :canEdit="note.can_edit" :canDelete="note.can_edit" - :reportAbusePath="note.report_abuse_path" /> + :reportAbusePath="note.report_abuse_path" + :editHandler="editHandler" /> </div> - <issue-note-body :note="note" /> + <issue-note-body + :note="note" + :isEditing="isEditing" + :formUpdateHandler="formUpdateHandler" + :formCancelHandler="formCancelHandler" /> </div> </div> </li> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 35828a959e16..126f844b3303 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -21,6 +21,10 @@ export default { type: Boolean, required: true, }, + editHandler: { + type: Function, + required: true, + }, }, data() { return { @@ -70,8 +74,9 @@ export default { <template v-if="canEdit"> <li> <button + @click="editHandler" type="button" - class="js-note-edit btn btn-transparent"> + class="btn btn-transparent"> Edit comment </button> </li> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 08f367e04960..e5c3ddf0d0c6 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -1,6 +1,7 @@ <script> import IssueNoteEditedText from './issue_note_edited_text.vue'; import IssueNoteAwardsList from './issue_note_awards_list.vue'; +import IssueNoteForm from './issue_note_form.vue'; export default { props: { @@ -8,19 +9,48 @@ export default { type: Object, required: true, }, + isEditing: { + type: Boolean, + required: false, + default: false, + }, + formUpdateHandler: { + type: Function, + required: true, + }, + formCancelHandler: { + type: Function, + required: true, + } }, components: { IssueNoteEditedText, IssueNoteAwardsList, + IssueNoteForm, + }, + methods: { + renderGFM() { + $(this.$refs['note-body']).renderGFM(); + }, + }, + mounted() { + this.renderGFM(); }, }; </script> <template> - <div class="note-body"> + <div + ref="note-body" + class="note-body"> <div v-html="note.note_html" class="note-text md"></div> + <issue-note-form + v-if="isEditing" + :updateHandler="formUpdateHandler" + :cancelHandler="formCancelHandler" + :noteBody="note.note" /> <issue-note-edited-text v-if="note.last_edited_by" :editedAt="note.last_edited_at" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue new file mode 100644 index 000000000000..93d4acbb20ae --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -0,0 +1,82 @@ +<script> +import MarkdownField from '../../vue_shared/components/markdown/field.vue'; + +export default { + props: { + noteBody: { + type: String, + required: true, + }, + updateHandler: { + type: Function, + required: true, + }, + cancelHandler: { + type: Function, + required: true, + }, + }, + data() { + return { + note: this.noteBody, + markdownPreviewUrl: '', + markdownDocsUrl: '', + } + }, + components: { + MarkdownField, + }, + methods: { + handleUpdate() { + this.updateHandler({ + note: this.note, + }); + }, + }, + mounted() { + const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); + const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); + const { markdownDocs, markdownPreviewUrl } = issueData; + + this.markdownDocsUrl = markdownDocs; + this.markdownPreviewUrl = markdownPreviewUrl; + }, +}; +</script> + +<template> + <div class="note-edit-form"> + <form class="edit-note common-note-form"> + <markdown-field + :markdown-preview-url="markdownPreviewUrl" + :markdown-docs="markdownDocsUrl" + :addSpacingClasses="false"> + <textarea + id="note-body" + class="note-textarea js-gfm-input js-autosize markdown-area" + data-supports-slash-commands="false" + aria-label="Description" + v-model="note" + ref="textarea" + slot="textarea" + placeholder="Write a comment or drag your files here..." + @keydown.meta.enter="handleUpdate"> + </textarea> + </markdown-field> + <div class="note-form-actions clearfix"> + <button + @click="handleUpdate" + type="button" + class="btn btn-nr btn-save"> + Save comment + </button> + <button + @click="cancelHandler" + class="btn btn-nr btn-cancel" + type="button"> + Cancel + </button> + </div> + </form> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 8d7b2ee72310..d267d1db7fa7 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -33,7 +33,7 @@ export default { TimeAgoTooltip, }, methods: { - doShit() { + toggle() { this.$store.commit('toggleDiscussion', { discussionId: this.discussionId, }); @@ -66,7 +66,7 @@ export default { v-if="includeToggle" class="discussion-actions"> <button - @click="doShit" + @click="toggle" class="note-action-button discussion-toggle-button js-toggle-button" type="button"> <i -- GitLab From 36f84ce79e6d1fefdfd4c3c1b18d76874160e66a Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 15 Jun 2017 03:38:24 +0300 Subject: [PATCH 014/243] IssueNotesRefactor: Always show :+1: :-1: emojis first. --- .../components/issue_note_awards_list.vue | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 9750f04e2079..5765445a73d6 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -22,21 +22,37 @@ export default { computed: { // `this.awards` is an array with emojis but they are not grouped by emoji name. See below. // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ] - // This method will group emojis by name their name as an Object. See below. + // This method will group emojis by their name as an Object. See below. // { // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ], // bar: [ { name: bar, user: user1 } ] // } - // We need to do this otherwise will will render the same emoji over and over again. + // We need to do this otherwise we will render the same emoji over and over again. groupedAwards() { const awards = {}; + const orderedAwards = {}; this.awards.forEach((award) => { awards[award.name] = awards[award.name] || []; awards[award.name].push(award); }); - return awards; + // Always show thumbsup and thumbsdown first + const { thumbsup, thumbsdown } = awards; + if (thumbsup) { + orderedAwards.thumbsup = thumbsup; + delete awards.thumbsup; + } + if (thumbsdown) { + orderedAwards.thumbsdown = thumbsdown; + delete awards.thumbsdown; + } + + for (let key in awards) { + orderedAwards[key] = awards[key]; + }; + + return orderedAwards; }, }, methods: { -- GitLab From 7433377adf6e716696f9cc7521053d8ec97810b7 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 15 Jun 2017 03:58:46 +0300 Subject: [PATCH 015/243] IssueNotesRefactor: Restrict :+1: and :-1: on your own note. --- .../notes/components/issue_note.vue | 6 +-- .../components/issue_note_awards_list.vue | 39 ++++++++++++++----- .../notes/components/issue_note_body.vue | 5 ++- .../notes/components/issue_note_form.vue | 2 +- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index ef18ff4b45f7..191555fd9792 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -14,7 +14,7 @@ export default { data() { return { isEditing: false, - } + }; }, components: { UserAvatarLink, @@ -31,8 +31,8 @@ export default { editHandler() { this.isEditing = true; }, - formUpdateHandler(data) { - console.log('update requested', data); + formUpdateHandler() { + // console.log('update requested', data); }, formCancelHandler() { this.isEditing = false; diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 5765445a73d6..825ebb7e7406 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -10,13 +10,20 @@ export default { type: Array, required: true, }, + noteAuthorId: { + type: Number, + required: true, + }, }, data() { + const userId = window.gon.current_user_id; + return { emojiSmiling, emojiSmile, emojiSmiley, - canAward: !!window.gon.current_user_id, + canAward: !!userId, + myUserId: userId, }; }, computed: { @@ -48,9 +55,11 @@ export default { delete awards.thumbsdown; } - for (let key in awards) { + // Because for-in forbidden + const keys = Object.keys(awards); + keys.forEach((key) => { orderedAwards[key] = awards[key]; - }; + }); return orderedAwards; }, @@ -59,27 +68,37 @@ export default { getAwardHTML(name) { return glEmojiTag(name); }, - getAwardClassBindings(awardList) { + getAwardClassBindings(awardList, awardName) { return { active: this.amIAwarded(awardList), - disabled: !this.canAward, + disabled: !this.canInteractWithEmoji(awardList, awardName), }; }, + canInteractWithEmoji(awardList, awardName) { + let isAllowed = true; + const restrictedEmojis = ['thumbsup', 'thumbsdown']; + const { myUserId, noteAuthorId } = this; + + // Users can not add :+1: and :-1: to their notes + if (myUserId === noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) { + isAllowed = false; + } + + return this.canAward && isAllowed; + }, amIAwarded(awardList) { - const myUserId = window.gon.current_user_id; - const isAwarded = awardList.filter(award => award.user.id === myUserId); + const isAwarded = awardList.filter(award => award.user.id === this.myUserId); return isAwarded.length; }, awardTitle(awardsList) { const amIAwarded = this.amIAwarded(awardsList); - const myUserId = window.gon.current_user_id; const TOOLTIP_NAME_COUNT = amIAwarded ? 9 : 10; let awardList = awardsList; // Filter myself from list if I am awarded. if (amIAwarded) { - awardList = awardList.filter(award => award.user.id !== myUserId); + awardList = awardList.filter(award => award.user.id !== this.myUserId); } // Get only 9-10 usernames to show in tooltip text. @@ -120,7 +139,7 @@ export default { <button v-for="(awardList, awardName) in groupedAwards" class="btn award-control has-tooltip" - :class="getAwardClassBindings(awardList)" + :class="getAwardClassBindings(awardList, awardName)" :title="awardTitle(awardList)" data-placement="bottom" type="button"> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index e5c3ddf0d0c6..4ce7d61251c7 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -21,7 +21,7 @@ export default { formCancelHandler: { type: Function, required: true, - } + }, }, components: { IssueNoteEditedText, @@ -58,6 +58,7 @@ export default { actionText="Edited" /> <issue-note-awards-list v-if="note.award_emoji.length" - :awards="note.award_emoji" /> + :awards="note.award_emoji" + :noteAuthorId="note.author.id" /> </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 93d4acbb20ae..86fd9c063e93 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -21,7 +21,7 @@ export default { note: this.noteBody, markdownPreviewUrl: '', markdownDocsUrl: '', - } + }; }, components: { MarkdownField, -- GitLab From c3a4fa4101bc2d71dba8dacac8154469175a7ab9 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 15 Jun 2017 04:24:52 +0300 Subject: [PATCH 016/243] IssueNotesRefactor: Initial template for system notes. --- app/assets/javascripts/notes/components/issue_notes.vue | 8 +++++++- .../javascripts/notes/components/issue_system_note.vue | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/notes/components/issue_system_note.vue diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 4ff9525661cf..890252fc54c3 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -4,6 +4,7 @@ import Vuex from 'vuex'; import storeOptions from '../stores/issue_notes_store'; import IssueNote from './issue_note.vue'; import IssueDiscussion from './issue_discussion.vue'; +import IssueSystemNote from './issue_system_note.vue'; Vue.use(Vuex); const store = new Vuex.Store(storeOptions); @@ -19,10 +20,15 @@ export default { components: { IssueNote, IssueDiscussion, + IssueSystemNote, }, methods: { component(note) { - return note.individual_note ? IssueNote : IssueDiscussion; + if (note.individual_note) { + return note.notes[0].system ? IssueSystemNote : IssueNote; + } + + return IssueDiscussion; }, componentData(note) { return note.individual_note ? note.notes[0] : note; diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue new file mode 100644 index 000000000000..6748cd4332c2 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -0,0 +1,3 @@ +<template> + <p>System note</p> +</template> -- GitLab From a4cb06f92478f1cd719cee9b2e2837c34af6e997 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 15 Jun 2017 18:13:52 +0300 Subject: [PATCH 017/243] IssueNotesRefactor: Complete system notes. --- .../notes/components/issue_note_header.vue | 16 +++++++- .../notes/components/issue_note_icons.js | 37 ++++++++++++++++++ .../notes/components/issue_system_note.vue | 39 ++++++++++++++++++- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_note_icons.js diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index d267d1db7fa7..f4136a129b61 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -13,7 +13,13 @@ export default { }, actionText: { type: String, - required: true, + required: false, + default: '', + }, + actionTextHtml: { + type: String, + required: false, + default: '', }, notePath: { type: String, @@ -54,7 +60,13 @@ export default { </a> <span class="note-headline-light"> <span class="note-headline-meta"> - {{actionText}} + <template v-if="actionText"> + {{actionText}} + </template> + <span + v-if="actionTextHtml" + v-html="actionTextHtml" + class="system-note-message"></span> <a :href="notePath"> <time-ago-tooltip :time="createdAt" diff --git a/app/assets/javascripts/notes/components/issue_note_icons.js b/app/assets/javascripts/notes/components/issue_note_icons.js new file mode 100644 index 000000000000..d8e3cb4bc01c --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_icons.js @@ -0,0 +1,37 @@ +import iconArrowCircle from 'icons/_icon_arrow_circle_o_right.svg'; +import iconCheck from 'icons/_icon_check_square_o.svg'; +import iconClock from 'icons/_icon_clock_o.svg'; +import iconCodeFork from 'icons/_icon_code_fork.svg'; +import iconComment from 'icons/_icon_comment_o.svg'; +import iconCommit from 'icons/_icon_commit.svg'; +import iconEdit from 'icons/_icon_edit.svg'; +import iconEye from 'icons/_icon_eye.svg'; +import iconEyeSlash from 'icons/_icon_eye_slash.svg'; +import iconMerge from 'icons/_icon_merge.svg'; +import iconMerged from 'icons/_icon_merged.svg'; +import iconRandom from 'icons/_icon_random.svg'; +import iconClosed from 'icons/_icon_status_closed.svg'; +import iconStatusOpen from 'icons/_icon_status_open.svg'; +import iconStopwatch from 'icons/_icon_stopwatch.svg'; +import iconTags from 'icons/_icon_tags.svg'; +import iconUser from 'icons/_icon_user.svg'; + +export default { + icon_arrow_circle_o_right: iconArrowCircle, + icon_check_square_o: iconCheck, + icon_clock_o: iconClock, + icon_code_fork: iconCodeFork, + icon_comment_o: iconComment, + icon_commit: iconCommit, + icon_edit: iconEdit, + icon_eye: iconEye, + icon_eye_slash: iconEyeSlash, + icon_merge: iconMerge, + icon_merged: iconMerged, + icon_random: iconRandom, + icon_status_closed: iconClosed, + icon_status_open: iconStatusOpen, + icon_stopwatch: iconStopwatch, + icon_tags: iconTags, + icon_user: iconUser, +}; diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index 6748cd4332c2..f8b37344cee4 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -1,3 +1,40 @@ +<script> +import iconsMap from './issue_note_icons'; +import IssueNoteHeader from './issue_note_header.vue'; + +export default { + props: { + note: { + type: Object, + required: true, + }, + }, + data() { + return { + svg: iconsMap[this.note.system_note_icon_name], + } + }, + components: { + IssueNoteHeader, + }, +} +</script> + <template> - <p>System note</p> + <li class="note system-note timeline-entry"> + <div class="timeline-entry-inner"> + <div class="timeline-icon"> + <span v-html="svg"></span> + </div> + <div class="timeline-content"> + <div class="note-header"> + <issue-note-header + :author="note.author" + :createdAt="note.created_at" + :notePath="note.path" + :actionTextHtml="note.note_html" /> + </div> + </div> + </div> + </li> </template> -- GitLab From ce80c46adceca97494db6c738caaeb3e06b9a233 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 15 Jun 2017 18:16:07 +0300 Subject: [PATCH 018/243] IssueNotesRefactor: Remove vendored svg icons and use from shared icons folder. --- .../javascripts/notes/components/issue_note_actions.vue | 6 +++--- .../javascripts/notes/components/issue_note_awards_list.vue | 6 +++--- .../javascripts/notes/icons/emoji_slightly_smiling_face.svg | 1 - app/assets/javascripts/notes/icons/emoji_smile.svg | 1 - app/assets/javascripts/notes/icons/emoji_smiley.svg | 1 - 5 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 app/assets/javascripts/notes/icons/emoji_slightly_smiling_face.svg delete mode 100644 app/assets/javascripts/notes/icons/emoji_smile.svg delete mode 100644 app/assets/javascripts/notes/icons/emoji_smiley.svg diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 126f844b3303..210743891fe0 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -1,7 +1,7 @@ <script> -import emojiSmiling from '../icons/emoji_slightly_smiling_face.svg'; -import emojiSmile from '../icons/emoji_smile.svg'; -import emojiSmiley from '../icons/emoji_smiley.svg'; +import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; +import emojiSmile from 'icons/_emoji_smile.svg'; +import emojiSmiley from 'icons/_emoji_smiley.svg'; export default { props: { diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 825ebb7e7406..f0e499aa4776 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -1,8 +1,8 @@ <script> import { glEmojiTag } from '~/behaviors/gl_emoji'; -import emojiSmiling from '../icons/emoji_slightly_smiling_face.svg'; -import emojiSmile from '../icons/emoji_smile.svg'; -import emojiSmiley from '../icons/emoji_smiley.svg'; +import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; +import emojiSmile from 'icons/_emoji_smile.svg'; +import emojiSmiley from 'icons/_emoji_smiley.svg'; export default { props: { diff --git a/app/assets/javascripts/notes/icons/emoji_slightly_smiling_face.svg b/app/assets/javascripts/notes/icons/emoji_slightly_smiling_face.svg deleted file mode 100644 index 56dbad91554e..000000000000 --- a/app/assets/javascripts/notes/icons/emoji_slightly_smiling_face.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445c.195.625.556 1.131 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg> diff --git a/app/assets/javascripts/notes/icons/emoji_smile.svg b/app/assets/javascripts/notes/icons/emoji_smile.svg deleted file mode 100644 index ce645fee46fd..000000000000 --- a/app/assets/javascripts/notes/icons/emoji_smile.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg> diff --git a/app/assets/javascripts/notes/icons/emoji_smiley.svg b/app/assets/javascripts/notes/icons/emoji_smiley.svg deleted file mode 100644 index ddfae50e5669..000000000000 --- a/app/assets/javascripts/notes/icons/emoji_smiley.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="nonzero"/></svg> -- GitLab From c77b2649ca0b49fb48730e99de4b070f2092e739 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 15 Jun 2017 19:02:13 +0300 Subject: [PATCH 019/243] IssueNotesRefactor: Fix accessLevel non existence case. --- .../notes/components/issue_note_actions.vue | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 210743891fe0..f9447062df93 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -7,7 +7,8 @@ export default { props: { accessLevel: { type: String, - required: true, + required: false, + default: '', }, reportAbusePath: { type: String, @@ -38,11 +39,14 @@ export default { <template> <div class="note-actions"> - <span class="note-role"> + <span + v-if="accessLevel" + class="note-role"> {{accessLevel}} </span> <a - class="note-action-button note-emoji-button js-add-award js-note-emoji js-user-authored has-tooltip" data-position="right" + class="note-action-button note-emoji-button js-add-award js-note-emoji js-user-authored has-tooltip" + data-position="right" href="#" title="Add reaction"> <i -- GitLab From 66f4af5c65a6721666f05ac75b07f7495f8e86d0 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 16 Jun 2017 01:36:11 +0300 Subject: [PATCH 020/243] IssueNotesRefactor: Refactor toggle to work with delegated handler. --- .../notes/components/issue_discussion.vue | 9 ++++++++- .../notes/components/issue_note_header.vue | 13 +++---------- .../javascripts/notes/stores/issue_notes_store.js | 6 +++++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index e53e6d04b7e5..954a6e3b7eed 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -43,6 +43,13 @@ export default { this.signInLink = signInLink.getAttribute('href'); } }, + methods: { + toggleDiscussion() { + this.$store.commit('toggleDiscussion', { + discussionId: this.note.id, + }); + } + }, }; </script> @@ -64,7 +71,7 @@ export default { :createdAt="discussion.created_at" :notePath="discussion.path" :includeToggle="true" - :discussionId="note.id" + :toggleHandler="toggleDiscussion" actionText="started a discussion" /> <issue-note-edited-text v-if="note.last_updated_by" diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index f4136a129b61..bf944732ede8 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -30,21 +30,14 @@ export default { required: false, default: false, }, - discussionId: { - type: String, + toggleHandler: { + type: Function, required: false, }, }, components: { TimeAgoTooltip, }, - methods: { - toggle() { - this.$store.commit('toggleDiscussion', { - discussionId: this.discussionId, - }); - }, - }, }; </script> @@ -78,7 +71,7 @@ export default { v-if="includeToggle" class="discussion-actions"> <button - @click="toggle" + @click="toggleHandler" class="note-action-button discussion-toggle-button js-toggle-button" type="button"> <i diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 0c1c6836325d..9fd95d892fa3 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -3,6 +3,10 @@ import service from '../services/issue_notes_service'; +const findNoteObjectById = (notes, id) => { + return notes.filter(n => n.id === id)[0]; +}; + const state = { notes: [], }; @@ -18,7 +22,7 @@ const mutations = { storeState.notes = notes; }, toggleDiscussion(storeState, { discussionId }) { - const [discussion] = storeState.notes.filter(note => note.id === discussionId); + const discussion = findNoteObjectById(storeState.notes, discussionId); discussion.expanded = !discussion.expanded; }, -- GitLab From 96ede7f73191909e68156fba3e9e2c3f9a8e723f Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 16 Jun 2017 01:37:06 +0300 Subject: [PATCH 021/243] IssueNotesRefactor: Implement logic for delete action. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No backend request for now. It’s just client side logic. --- .../notes/components/issue_discussion.vue | 2 +- .../notes/components/issue_note.vue | 23 ++++++++++++-- .../notes/components/issue_note_actions.vue | 9 +++++- .../notes/components/issue_system_note.vue | 4 +-- .../notes/services/issue_notes_service.js | 3 ++ .../notes/stores/issue_notes_store.js | 30 +++++++++++++++++-- 6 files changed, 62 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 954a6e3b7eed..1f7707a5208b 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -48,7 +48,7 @@ export default { this.$store.commit('toggleDiscussion', { discussionId: this.note.id, }); - } + }, }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 191555fd9792..3dc28d60ddd3 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -14,6 +14,7 @@ export default { data() { return { isEditing: false, + isDeleting: false, }; }, components: { @@ -26,11 +27,28 @@ export default { author() { return this.note.author; }, + classNameBindings() { + return { + 'is-editing': this.isEditing, + 'disabled-content': this.isDeleting, + }; + }, }, methods: { editHandler() { this.isEditing = true; }, + deleteHandler() { + this.isDeleting = true; + this.$store + .dispatch('deleteNote', this.note) + .then(() => { + this.isDeleting = false; + }) + .catch(() => { + this.isDeleting = false; + }); + }, formUpdateHandler() { // console.log('update requested', data); }, @@ -44,7 +62,7 @@ export default { <template> <li class="note timeline-entry" - :class="{ 'is-editing': isEditing }"> + :class="classNameBindings"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link @@ -66,7 +84,8 @@ export default { :canEdit="note.can_edit" :canDelete="note.can_edit" :reportAbusePath="note.report_abuse_path" - :editHandler="editHandler" /> + :editHandler="editHandler" + :deleteHandler="deleteHandler" /> </div> <issue-note-body :note="note" diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index f9447062df93..1f25421dd744 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -26,6 +26,10 @@ export default { type: Function, required: true, }, + deleteHandler: { + type: Function, + required: true, + }, }, data() { return { @@ -92,7 +96,10 @@ export default { </a> </li> <li> - <a class="js-note-delete"> + <a + @click.prevent="deleteHandler" + class="js-note-delete" + href="#"> <span class="text-danger"> Delete comment </span> diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index f8b37344cee4..a2ca4c828c19 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -12,12 +12,12 @@ export default { data() { return { svg: iconsMap[this.note.system_note_icon_name], - } + }; }, components: { IssueNoteHeader, }, -} +}; </script> <template> diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index 810dec61b5b7..c36b835c3830 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -7,4 +7,7 @@ export default { fetchNotes(endpoint) { return Vue.http.get(endpoint); }, + deleteNote(endpoint) { + return Vue.http.get(endpoint); + }, }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 9fd95d892fa3..eb387811443a 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -3,9 +3,7 @@ import service from '../services/issue_notes_service'; -const findNoteObjectById = (notes, id) => { - return notes.filter(n => n.id === id)[0]; -}; +const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; const state = { notes: [], @@ -26,6 +24,20 @@ const mutations = { discussion.expanded = !discussion.expanded; }, + deleteNote(storeState, note) { + const noteObj = findNoteObjectById(storeState.notes, note.discussion_id); + + if (noteObj.individual_note) { + storeState.notes.splice(storeState.notes.indexOf(noteObj), 1); + } else { + const comment = findNoteObjectById(noteObj.notes, note.id); + noteObj.notes.splice(noteObj.notes.indexOf(comment), 1); + + if (!noteObj.notes.length) { + storeState.notes.splice(storeState.notes.indexOf(noteObj), 1); + } + } + }, }; const actions = { @@ -40,6 +52,18 @@ const actions = { new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line }); }, + deleteNote(context, note) { + // FIXME: Implement request, remove fake delete timer... + return service + .deleteNote(`${document.location.href}.json`) + .then(res => res.json) + .then(() => { + context.commit('deleteNote', note); + }) + .catch(() => { + new Flash('Something went wrong while deleting your note. Please try again.'); // eslint-disable-line + }); + }, }; export default { -- GitLab From 696de46ace53f7022c23d03748ef2f6b284ed937 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 16 Jun 2017 20:16:40 +0300 Subject: [PATCH 022/243] IssueNotesRefactor: Implement note delete. --- .../notes/components/issue_discussion.vue | 8 ++--- .../notes/components/issue_note.vue | 31 +++++++++++-------- .../notes/components/issue_note_actions.vue | 1 + .../notes/services/issue_notes_service.js | 2 +- .../notes/stores/issue_notes_store.js | 4 +-- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 1f7707a5208b..b362ef4b53ed 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -58,10 +58,10 @@ export default { <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="author.name" - :img-size="40" /> + :linkHref="author.path" + :imgSrc="author.avatar_url" + :imgAlt="author.name" + :imgSize="40" /> </div> <div class="timeline-content"> <div class="discussion"> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 3dc28d60ddd3..9ec31589b247 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -39,15 +39,20 @@ export default { this.isEditing = true; }, deleteHandler() { - this.isDeleting = true; - this.$store - .dispatch('deleteNote', this.note) - .then(() => { - this.isDeleting = false; - }) - .catch(() => { - this.isDeleting = false; - }); + const msg = 'Are you sure you want to delete this list?'; + const isConfirmed = confirm(msg); // eslint-disable-line + + if (isConfirmed) { + this.isDeleting = true; + this.$store + .dispatch('deleteNote', this.note) + .then(() => { + this.isDeleting = false; + }) + .catch(() => { + this.isDeleting = false; + }); + } }, formUpdateHandler() { // console.log('update requested', data); @@ -66,10 +71,10 @@ export default { <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="author.name" - :img-size="40" /> + :linkHref="author.path" + :imgSrc="author.avatar_url" + :imgAlt="author.name" + :imgSize="40" /> </div> <div class="timeline-content"> <div class="note-header"> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 1f25421dd744..fb1c865d2397 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -97,6 +97,7 @@ export default { </li> <li> <a + v-if="canEdit" @click.prevent="deleteHandler" class="js-note-delete" href="#"> diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index c36b835c3830..01ad151c39c0 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -8,6 +8,6 @@ export default { return Vue.http.get(endpoint); }, deleteNote(endpoint) { - return Vue.http.get(endpoint); + return Vue.http.delete(endpoint); }, }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index eb387811443a..ac69ca93b1c3 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -53,10 +53,8 @@ const actions = { }); }, deleteNote(context, note) { - // FIXME: Implement request, remove fake delete timer... return service - .deleteNote(`${document.location.href}.json`) - .then(res => res.json) + .deleteNote(note.path) .then(() => { context.commit('deleteNote', note); }) -- GitLab From b3704dafacc0ff77523d091a2c74b4b8265f451e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 21 Jun 2017 00:45:42 +0300 Subject: [PATCH 023/243] IssueNotesRefactor: Use then instead of finally. --- app/assets/javascripts/notes/components/issue_notes.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 890252fc54c3..4bf15ebe8984 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -37,7 +37,7 @@ export default { mounted() { const path = this.$el.parentNode.dataset.discussionsPath; this.$store.dispatch('fetchNotes', path) - .finally(() => { + .then(() => { this.isLoading = false; }); }, -- GitLab From 905ad9cdd54464876a712e4fe8acbac1c54cb8c1 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 21 Jun 2017 00:45:59 +0300 Subject: [PATCH 024/243] IssueNotesRefactor: Implement show/hide of discussion reply form. --- .../notes/components/issue_discussion.vue | 22 +++++++++++++++++-- .../notes/components/issue_note_form.vue | 11 ++++++++-- app/assets/stylesheets/pages/issues.scss | 4 ++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index b362ef4b53ed..27e997e223b1 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -4,6 +4,7 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ import IssueNoteHeader from './issue_note_header.vue'; import IssueNoteActions from './issue_note_actions.vue'; import IssueNoteEditedText from './issue_note_edited_text.vue'; +import IssueNoteForm from './issue_note_form.vue'; export default { props: { @@ -16,6 +17,7 @@ export default { return { registerLink: '#', signInLink: '#', + isReplying: false, }; }, computed: { @@ -32,6 +34,7 @@ export default { IssueNoteHeader, IssueNoteActions, IssueNoteEditedText, + IssueNoteForm, }, mounted() { // We need to grab the register and sign in links from DOM for the time being. @@ -49,6 +52,15 @@ export default { discussionId: this.note.id, }); }, + showReplyForm() { + this.isReplying = true; + }, + cancelReplyForm() { + this.isReplying = false; + }, + saveReply() { + this.isReplying = false; + }, }, }; </script> @@ -95,10 +107,16 @@ export default { <div class="flash-container"></div> <div class="discussion-reply-holder"> <button - v-if="note.can_reply" + v-if="note.can_reply && !isReplying" + @click="showReplyForm" type="button" - class="btn btn-text-field js-discussion-reply-button" + class="btn btn-text-field" title="Add a reply">Reply...</button> + <issue-note-form + v-if="isReplying" + saveButtonTitle="Comment" + :updateHandler="saveReply" + :cancelHandler="cancelReplyForm" /> <div v-if="!note.can_reply" class="disabled-comment text-center"> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 86fd9c063e93..0b234b5192d5 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -5,7 +5,8 @@ export default { props: { noteBody: { type: String, - required: true, + required: false, + default: '', }, updateHandler: { type: Function, @@ -15,6 +16,11 @@ export default { type: Function, required: true, }, + saveButtonTitle: { + type: String, + required: false, + default: 'Save comment', + } }, data() { return { @@ -40,6 +46,7 @@ export default { this.markdownDocsUrl = markdownDocs; this.markdownPreviewUrl = markdownPreviewUrl; + this.$refs.textarea.focus(); }, }; </script> @@ -68,7 +75,7 @@ export default { @click="handleUpdate" type="button" class="btn btn-nr btn-save"> - Save comment + {{saveButtonTitle}} </button> <button @click="cancelHandler" diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 8cdb3f34ae55..e25694fd0cf0 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -250,6 +250,10 @@ ul.related-merge-requests > li { } } +.discussion-reply-holder .note-edit-form { + display: block; +} + @media (min-width: $screen-sm-min) { .emoji-block .row { display: flex; -- GitLab From ff6acdf1807d235d91479511f957e750bc7baca4 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 21 Jun 2017 03:40:36 +0300 Subject: [PATCH 025/243] IssueNotesRefactor: Implement discussion reply. --- .../notes/components/issue_discussion.vue | 21 +++++++++++++++++-- .../notes/services/issue_notes_service.js | 3 +++ .../notes/stores/issue_notes_store.js | 18 ++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 27e997e223b1..7c806eed4fcb 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -17,6 +17,7 @@ export default { return { registerLink: '#', signInLink: '#', + newNotePath: '', isReplying: false, }; }, @@ -45,6 +46,9 @@ export default { this.registerLink = registerLink.getAttribute('href'); this.signInLink = signInLink.getAttribute('href'); } + + const newNotePath = document.querySelector('.js-main-target-form').getAttribute('action'); + this.newNotePath = `${newNotePath}?full_data=1`; }, methods: { toggleDiscussion() { @@ -58,8 +62,21 @@ export default { cancelReplyForm() { this.isReplying = false; }, - saveReply() { - this.isReplying = false; + saveReply({ note }) { + const data = { + endpoint: this.newNotePath, + reply: { + in_reply_to_discussion_id: this.note.reply_id, + target_type: 'issue', + target_id: this.discussion.noteable_id, + note: { note }, + }, + }; + + this.$store.dispatch('replyToDiscussion', data) + .then(() => { + this.isReplying = false; + }); }, }, }; diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index 01ad151c39c0..629de6c0b76b 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -10,4 +10,7 @@ export default { deleteNote(endpoint) { return Vue.http.delete(endpoint); }, + replyToDiscussion(endpoint, data) { + return Vue.http.post(endpoint, data, { emulateJSON: true }); + }, }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index ac69ca93b1c3..0b2627a57a38 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -38,6 +38,11 @@ const mutations = { } } }, + addNewReplyToDiscussion(storeState, note) { + const noteObj = findNoteObjectById(storeState.notes, note.discussion_id); + + noteObj.notes.push(note); + }, }; const actions = { @@ -62,6 +67,19 @@ const actions = { new Flash('Something went wrong while deleting your note. Please try again.'); // eslint-disable-line }); }, + replyToDiscussion(context, data) { + const { endpoint, reply } = data; + + return service + .replyToDiscussion(endpoint, reply) + .then((res) => res.json()) + .then((res) => { + context.commit('addNewReplyToDiscussion', res); + }) + .catch(() => { + new Flash('Something went wrong while adding your reply. Please try again.'); // eslint-disable-line + }); + }, }; export default { -- GitLab From 092d4ca60c5f2640b154ad2a2abfd7efabc87091 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 21 Jun 2017 03:50:44 +0300 Subject: [PATCH 026/243] IssueNotesRefactor: Move catch statements into main file to make ESLint happy. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was happy before I don’t know what broke its heart. --- .../notes/components/issue_discussion.vue | 5 +++++ .../javascripts/notes/components/issue_note.vue | 3 +++ .../javascripts/notes/components/issue_note_form.vue | 2 +- .../javascripts/notes/components/issue_notes.vue | 5 +++++ .../javascripts/notes/stores/issue_notes_store.js | 12 +----------- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 7c806eed4fcb..6425d7fd698d 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,4 +1,6 @@ <script> +/* global Flash */ + import IssueNote from './issue_note.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import IssueNoteHeader from './issue_note_header.vue'; @@ -76,6 +78,9 @@ export default { this.$store.dispatch('replyToDiscussion', data) .then(() => { this.isReplying = false; + }) + .catch(() => { + new Flash('Something went wrong while adding your reply. Please try again.'); // eslint-disable-line }); }, }, diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 9ec31589b247..b91e6d05b8f5 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -1,4 +1,6 @@ <script> +/* global Flash */ + import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import IssueNoteHeader from './issue_note_header.vue'; import IssueNoteActions from './issue_note_actions.vue'; @@ -50,6 +52,7 @@ export default { this.isDeleting = false; }) .catch(() => { + new Flash('Something went wrong while deleting your note. Please try again.'); // eslint-disable-line this.isDeleting = false; }); } diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 0b234b5192d5..5470f94c5b8d 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -20,7 +20,7 @@ export default { type: String, required: false, default: 'Save comment', - } + }, }, data() { return { diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 4bf15ebe8984..c83a78c22df6 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -1,4 +1,6 @@ <script> +/* global Flash */ + import Vue from 'vue'; import Vuex from 'vuex'; import storeOptions from '../stores/issue_notes_store'; @@ -39,6 +41,9 @@ export default { this.$store.dispatch('fetchNotes', path) .then(() => { this.isLoading = false; + }) + .catch(() => { + new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line }); }, }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 0b2627a57a38..3f27381b208f 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -1,4 +1,3 @@ -/* global Flash */ /* eslint-disable no-param-reassign */ import service from '../services/issue_notes_service'; @@ -52,9 +51,6 @@ const actions = { .then(res => res.json()) .then((res) => { context.commit('setNotes', res); - }) - .catch(() => { - new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line }); }, deleteNote(context, note) { @@ -62,9 +58,6 @@ const actions = { .deleteNote(note.path) .then(() => { context.commit('deleteNote', note); - }) - .catch(() => { - new Flash('Something went wrong while deleting your note. Please try again.'); // eslint-disable-line }); }, replyToDiscussion(context, data) { @@ -72,12 +65,9 @@ const actions = { return service .replyToDiscussion(endpoint, reply) - .then((res) => res.json()) + .then(res => res.json()) .then((res) => { context.commit('addNewReplyToDiscussion', res); - }) - .catch(() => { - new Flash('Something went wrong while adding your reply. Please try again.'); // eslint-disable-line }); }, }; -- GitLab From 998299e22531afb3d18889f77aebce8185b69622 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 22 Jun 2017 02:28:37 +0300 Subject: [PATCH 027/243] IssueNotesRefactor: Implement note edit. --- .../notes/components/issue_note.vue | 19 ++++++++++++++++-- .../notes/components/issue_note_body.vue | 8 +++++++- .../notes/services/issue_notes_service.js | 3 +++ .../notes/stores/issue_notes_store.js | 20 +++++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index b91e6d05b8f5..eb39cbd5e7c6 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -57,8 +57,23 @@ export default { }); } }, - formUpdateHandler() { - // console.log('update requested', data); + formUpdateHandler(note) { + const data = { + endpoint: `${this.note.path}?full_data=1`, + note: { + target_type: 'issue', + target_id: this.note.noteable_id, + note, + }, + }; + + this.$store.dispatch('updateNote', data) + .then(() => { + this.isEditing = false; + }) + .catch(() => { + new Flash('Something went wrong while editing your comment. Please try again.'); // eslint-disable-line + }); }, formCancelHandler() { this.isEditing = false; diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 4ce7d61251c7..4f11683c7a94 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -32,6 +32,11 @@ export default { renderGFM() { $(this.$refs['note-body']).renderGFM(); }, + handleFormUpdate() { + this.formUpdateHandler({ + note: this.$refs.noteForm.note, + }); + }, }, mounted() { this.renderGFM(); @@ -48,7 +53,8 @@ export default { class="note-text md"></div> <issue-note-form v-if="isEditing" - :updateHandler="formUpdateHandler" + ref="noteForm" + :updateHandler="handleFormUpdate" :cancelHandler="formCancelHandler" :noteBody="note.note" /> <issue-note-edited-text diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index 629de6c0b76b..645e9772c685 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -13,4 +13,7 @@ export default { replyToDiscussion(endpoint, data) { return Vue.http.post(endpoint, data, { emulateJSON: true }); }, + updateNote(endpoint, data) { + return Vue.http.put(endpoint, data, { emulateJSON: true }); + }, }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 3f27381b208f..2900208f51c9 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -42,6 +42,16 @@ const mutations = { noteObj.notes.push(note); }, + updateNote(storeState, note) { + const noteObj = findNoteObjectById(storeState.notes, note.discussion_id); + + if (noteObj.individual_note) { + noteObj.notes.splice(0, 1, note); + } else { + const comment = findNoteObjectById(noteObj.notes, note.id); + noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); + } + }, }; const actions = { @@ -70,6 +80,16 @@ const actions = { context.commit('addNewReplyToDiscussion', res); }); }, + updateNote(context, data) { + const { endpoint, note } = data; + + return service + .updateNote(endpoint, note) + .then(res => res.json()) + .then((res) => { + context.commit('updateNote', res); + }); + }, }; export default { -- GitLab From e57093ff627a8fdf97fc5398808f1427e26463ad Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 23 Jun 2017 18:42:08 +0300 Subject: [PATCH 028/243] IssueNotesRefactor: Implement main note form. --- .../notes/components/issue_comment_form.vue | 191 ++++++++++++++++++ .../notes/components/issue_discussion.vue | 5 +- .../notes/components/issue_note_form.vue | 3 +- .../notes/components/issue_notes.vue | 12 +- .../notes/services/issue_notes_service.js | 3 + .../notes/stores/issue_notes_store.js | 13 ++ .../stylesheets/framework/dropdowns.scss | 4 + .../projects/issues/_discussion.html.haml | 4 +- 8 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_comment_form.vue diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue new file mode 100644 index 000000000000..b8df6359c1e3 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -0,0 +1,191 @@ +<script> +/* global Flash */ + +import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import MarkdownField from '../../vue_shared/components/markdown/field.vue'; + +export default { + props: {}, + data() { + return { + note: '', + markdownPreviewUrl: '', + markdownDocsUrl: '', + + // FIXME: @fatihacet - Fix the mock data below. + noteType: 'comment', + issueState: 'open', + endpoint: '/gitlab-org/gitlab-ce/notes', + author: { + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 1, + name: 'Administrator', + path: '/root', + state: 'active', + username: 'root', + }, + }; + }, + components: { + UserAvatarLink, + MarkdownField, + }, + computed: { + commentButtonTitle() { + return this.noteType === 'comment' ? 'Comment' : 'Start discussion'; + }, + issueActionButtonTitle() { + if (this.note.length) { + const actionText = this.issueState === 'open' ? 'close' : 'reopen'; + + return this.noteType === 'comment' ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`; + } + + return this.issueState === 'open' ? 'Close issue' : 'Reopen issue'; + }, + }, + methods: { + handleSave() { + const data = { + endpoint: `${this.endpoint}?full_data=1`, + noteData: { + target_type: 'issue', + target_id: '89', + note: { + noteable_type: 'Issue', + noteable_id: 89, + note: this.note, + } + }, + }; + + if (this.noteType === 'discussion') { + data.noteData.note.type = 'DiscussionNote'; + } + + this.$store.dispatch('createNewNote', data) + .then(() => { + this.discard(); + }) + .catch(() => { + new Flash('Something went wrong while adding your comment. Please try again.'); // eslint-disable-line + }); + }, + discard() { + this.note = ''; + this.$refs.textarea.focus(); + }, + setNoteType(type) { + this.noteType = type; + }, + }, + mounted() { + const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); + const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); + const { markdownDocs, markdownPreviewUrl } = issueData; + + this.markdownDocsUrl = markdownDocs; + this.markdownPreviewUrl = markdownPreviewUrl; + }, +}; +</script> + +<template> + <ul class="notes notes-form timeline new-note"> + <li class="timeline-entry"> + <div class="timeline-icon hidden-xs hidden-sm"> + <user-avatar-link + :linkHref="author.path" + :imgSrc="author.avatar_url" + :imgAlt="author.name" + :imgSize="40" /> + </div> + <div class="timeline-content timeline-content-form common-note-form"> + <markdown-field + :markdown-preview-url="markdownPreviewUrl" + :markdown-docs="markdownDocsUrl" + :addSpacingClasses="false"> + <textarea + id="note-body" + class="note-textarea js-gfm-input js-autosize markdown-area" + data-supports-slash-commands="true" + data-supports-quick-actions="true" + aria-label="Description" + v-model="note" + ref="textarea" + slot="textarea" + placeholder="Write a comment or drag your files here..." + @keydown.meta.enter="handleSave"> + </textarea> + </markdown-field> + <div class="note-form-actions clearfix"> + <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown"> + <input + @click="handleSave" + :disabled="!note.length" + :value="commentButtonTitle" + class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" + type="submit" /> + <button + :disabled="!note.length" + name="button" + type="button" + class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion" + data-toggle="dropdown" + aria-label="Open comment type dropdown"> + <i + aria-hidden="true" + class="fa fa-caret-down toggle-icon"></i> + </button> + <ul + class="dropdown-menu note-type-dropdown dropdown-open-top"> + <li + :class="{ 'item-selected': noteType === 'comment' }" + @click.prevent="setNoteType('comment')"> + <a href="#"> + <i + aria-hidden="true" + class="fa fa-check"></i> + <div class="description"> + <strong>Comment</strong> + <p> + Add a general comment to this issue. + </p> + </div> + </a> + </li> + <li class="divider"></li> + <li + :class="{ 'item-selected': noteType === 'discussion' }" + @click.prevent="setNoteType('discussion')"> + <a href="#"> + <i + aria-hidden="true" + class="fa fa-check"></i> + <div class="description"> + <strong>Start discussion</strong> + <p> + Discuss a specific suggestion or question. + </p> + </div> + </a> + </li> + </ul> + </div> + <a + :class="{'btn-reopen': issueState === 'closed', 'btn-close': issueState === 'open'}" + class="btn btn-nr btn-comment"> + {{issueActionButtonTitle}} + </a> + <a + v-if="note.length" + @click="discard" + class="btn btn-cancel js-note-discard" + role="button"> + Discard draft + </a> + </div> + </div> + </li> + </ul> +</template> diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 6425d7fd698d..f2ab8ba278e5 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -49,8 +49,9 @@ export default { this.signInLink = signInLink.getAttribute('href'); } - const newNotePath = document.querySelector('.js-main-target-form').getAttribute('action'); - this.newNotePath = `${newNotePath}?full_data=1`; + // TODO: @fatihacet - Reimplement this when we have data for it. + // const newNotePath = document.querySelector('.js-main-target-form').getAttribute('action'); + // this.newNotePath = `${newNotePath}?full_data=1`; }, methods: { toggleDiscussion() { diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 5470f94c5b8d..137e84fb361c 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -61,7 +61,8 @@ export default { <textarea id="note-body" class="note-textarea js-gfm-input js-autosize markdown-area" - data-supports-slash-commands="false" + data-supports-slash-commands="true" + data-supports-quick-actions="true" aria-label="Description" v-model="note" ref="textarea" diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index c83a78c22df6..f817be373a60 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -7,6 +7,7 @@ import storeOptions from '../stores/issue_notes_store'; import IssueNote from './issue_note.vue'; import IssueDiscussion from './issue_discussion.vue'; import IssueSystemNote from './issue_system_note.vue'; +import IssueCommentForm from './issue_comment_form.vue'; Vue.use(Vuex); const store = new Vuex.Store(storeOptions); @@ -23,6 +24,7 @@ export default { IssueNote, IssueDiscussion, IssueSystemNote, + IssueCommentForm, }, methods: { component(note) { @@ -55,17 +57,19 @@ export default { v-if="isLoading" class="loading"> <i - aria-hidden="true" - class="fa fa-spinner fa-spin"></i> + class="fa fa-spinner fa-spin" + aria-hidden="true"></i> </div> <ul - class="notes main-notes-list timeline" - id="notes-list"> + v-if="!isLoading" + id="notes-list" + class="notes main-notes-list timeline"> <component v-for="note in $store.getters.notes" :is="component(note)" :note="componentData(note)" :key="note.id" /> </ul> + <issue-comment-form v-if="!isLoading" /> </div> </template> diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index 645e9772c685..52ccaa82a998 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -16,4 +16,7 @@ export default { updateNote(endpoint, data) { return Vue.http.put(endpoint, data, { emulateJSON: true }); }, + createNewNote(endpoint, data) { + return Vue.http.post(endpoint, data, { emulateJSON: true }); + } }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 2900208f51c9..8881ba2aa9ac 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -52,6 +52,10 @@ const mutations = { noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); } }, + addNewNote(storeState, note) { + // TODO: @fatihacet - When we get the correct data from server update the store + // storeState.notes.push(note); + }, }; const actions = { @@ -90,6 +94,15 @@ const actions = { context.commit('updateNote', res); }); }, + createNewNote(context, data) { + const { endpoint, noteData } = data; + return service + .createNewNote(endpoint, noteData) + .then(res => res.json()) + .then((res) => { + context.commit('addNewNote', res); + }); + }, }; export default { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 5e410cbf5632..e93643de31d7 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -369,6 +369,10 @@ transform: translateY(0); } +.comment-type-dropdown.open .dropdown-menu { + display: block; +} + .filtered-search-box-input-container { .dropdown-menu, .dropdown-menu-nav { diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 6ce7014095b7..960366a48274 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -10,5 +10,5 @@ = webpack_bundle_tag 'notes' -#notes{style: "margin-top: 150px"} - = render 'shared/notes/notes_with_form', :autocomplete => true +/ #notes{style: "margin-top: 150px"} +/ = render 'shared/notes/notes_with_form', :autocomplete => true -- GitLab From ebf915511359c336b99c1127c5b902f8757ba5e0 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Fri, 23 Jun 2017 17:10:05 -0500 Subject: [PATCH 029/243] Add data required for note form --- app/controllers/projects/issues_controller.rb | 8 ++++++-- app/helpers/issuables_helper.rb | 2 +- app/serializers/issuable_entity.rb | 2 ++ app/serializers/issue_entity.rb | 20 +++++++++++++++++-- app/serializers/user_serializer.rb | 3 +++ .../projects/issues/_discussion.html.haml | 6 ++++++ 6 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 app/serializers/user_serializer.rb diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 153c490ce3f1..e238f6f69afc 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -92,7 +92,7 @@ def show respond_to do |format| format.html format.json do - render json: IssueSerializer.new.represent(@issue) + render json: serializer.represent(@issue) end end end @@ -152,7 +152,7 @@ def update format.json do if @issue.valid? - render json: IssueSerializer.new.represent(@issue) + render json: serializer.represent(@issue) else render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity end @@ -308,4 +308,8 @@ def authenticate_user! redirect_to new_user_session_path, notice: notice end + + def serializer + IssueSerializer.new(current_user: current_user, project: issue.project) + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 425af5473307..f85afb1894e0 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -35,7 +35,7 @@ def issuable_json_path(issuable) def serialize_issuable(issuable) case issuable when Issue - IssueSerializer.new.represent(issuable).to_json + IssueSerializer.new(current_user: current_user, project: issuable.project).represent(issuable).to_json when MergeRequest MergeRequestSerializer .new(current_user: current_user, project: issuable.project) diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb index bd5211b8e586..61c7a4287454 100644 --- a/app/serializers/issuable_entity.rb +++ b/app/serializers/issuable_entity.rb @@ -15,4 +15,6 @@ class IssuableEntity < Grape::Entity expose :total_time_spent expose :human_time_estimate expose :human_total_time_spent + expose :milestone, using: API::Entities::Milestone + expose :labels, using: LabelEntity end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index c189a4992da8..62db7f6aade6 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -7,10 +7,26 @@ class IssueEntity < IssuableEntity expose :due_date expose :moved_to_id expose :project_id - expose :milestone, using: API::Entities::Milestone - expose :labels, using: LabelEntity expose :web_url do |issue| project_issue_path(issue.project, issue) end + + expose :current_user do + expose :can_create_note do |issue| + can?(request.current_user, :create_note, issue.project) + end + + expose :can_update do |issue| + can?(request.current_user, :update_issue, issue) + end + end + + expose :create_note_path do |issue| + namespace_project_notes_path(issue.project.namespace, issue.project, noteable_type: 'Issue', noteable_id: issue.id, target_type: 'issue', target_id: issue.id) + end + + expose :preview_note_path do |issue| + preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.id) + end end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb new file mode 100644 index 000000000000..49a71ebac614 --- /dev/null +++ b/app/serializers/user_serializer.rb @@ -0,0 +1,3 @@ +class UserSerializer < BaseSerializer + entity UserEntity +end diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 960366a48274..04e3211fb873 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -12,3 +12,9 @@ / #notes{style: "margin-top: 150px"} / = render 'shared/notes/notes_with_form', :autocomplete => true + +:javascript + window.gl.issueData = #{serialize_issuable(@issue)}; + window.gl.currentUserData = #{UserSerializer.new.represent(current_user).to_json}; + +%section#note-form{ data: { new_session_path: new_session_path(:user, redirect_to_referer: 'yes') } } -- GitLab From cf926f6b7f412c194a47329f101dee1adc6d19dc Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Mon, 26 Jun 2017 19:39:40 -0500 Subject: [PATCH 030/243] Remove duplicate attributes from discussion entity and move note.can_edit into note.current_user --- app/serializers/discussion_entity.rb | 10 ---------- app/serializers/note_entity.rb | 6 ++++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb index cb6c3c238070..0a92e3f81672 100644 --- a/app/serializers/discussion_entity.rb +++ b/app/serializers/discussion_entity.rb @@ -3,18 +3,8 @@ class DiscussionEntity < Grape::Entity expose :id, :reply_id expose :expanded?, as: :expanded - expose :author, using: UserEntity - - expose :created_at - - expose :last_updated_at, if: -> (discussion, _) { discussion.updated? } - expose :last_updated_by, if: -> (discussion, _) { discussion.updated? }, using: UserEntity expose :notes, using: NoteEntity expose :individual_note?, as: :individual_note - - expose :can_reply do |discussion| - can?(request.current_user, :create_note, discussion.project) - end end diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index 7a49ec4ef553..53b3ed419401 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -17,8 +17,10 @@ class NoteEntity < API::Entities::Note expose :last_edited_at, if: -> (note, _) { note.is_edited? } expose :last_edited_by, using: UserEntity, if: -> (note, _) { note.is_edited? } - expose :can_edit do |note| - Ability.can_edit_note?(request.current_user, note) + expose :current_user do + expose :can_edit do |note| + Ability.can_edit_note?(request.current_user, note) + end end expose :system_note_icon_name, if: -> (note, _) { note.system? } do |note| -- GitLab From 73b813117a5297aba8bcf38a4a3f98e471dda82b Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Wed, 28 Jun 2017 12:48:41 -0500 Subject: [PATCH 031/243] Fix IssueEntity create_note_path --- app/serializers/issue_entity.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 62db7f6aade6..e0a8cf7570a5 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -23,7 +23,7 @@ class IssueEntity < IssuableEntity end expose :create_note_path do |issue| - namespace_project_notes_path(issue.project.namespace, issue.project, noteable_type: 'Issue', noteable_id: issue.id, target_type: 'issue', target_id: issue.id) + namespace_project_notes_path(issue.project.namespace, issue.project, target_type: 'issue', target_id: issue.id, note: { noteable_type: 'Issue', noteable_id: issue.id }) end expose :preview_note_path do |issue| -- GitLab From 01496486382b90dd6642057370a9c710c80d2b41 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Wed, 28 Jun 2017 13:45:27 -0500 Subject: [PATCH 032/243] Remove noteable ID and type from IssueEntity create_note_path --- app/serializers/issue_entity.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index e0a8cf7570a5..fbdd9f947634 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -23,7 +23,7 @@ class IssueEntity < IssuableEntity end expose :create_note_path do |issue| - namespace_project_notes_path(issue.project.namespace, issue.project, target_type: 'issue', target_id: issue.id, note: { noteable_type: 'Issue', noteable_id: issue.id }) + namespace_project_notes_path(issue.project.namespace, issue.project, target_type: 'issue', target_id: issue.id) end expose :preview_note_path do |issue| -- GitLab From 44ccf2b1148e53a3b9148ea90a2f45cc8c8a000e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 28 Jun 2017 16:21:10 +0300 Subject: [PATCH 033/243] IssueNotesRefactor: Change data attr path. --- app/views/projects/issues/_discussion.html.haml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 04e3211fb873..8e4b9f8910a0 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -3,7 +3,7 @@ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' -%section{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json) } } +%section{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), new_session_path: new_session_path(:user, redirect_to_referer: 'yes') } } #js-notes - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' @@ -16,5 +16,3 @@ :javascript window.gl.issueData = #{serialize_issuable(@issue)}; window.gl.currentUserData = #{UserSerializer.new.represent(current_user).to_json}; - -%section#note-form{ data: { new_session_path: new_session_path(:user, redirect_to_referer: 'yes') } } -- GitLab From d24e47a983fd074fade60f44889bb304bf274d0d Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 28 Jun 2017 23:53:46 +0300 Subject: [PATCH 034/243] IssueDiscussionsRefactor: Do changes after data format change and fix dummy data. --- .../notes/components/issue_comment_form.vue | 38 +++++++++---------- .../notes/components/issue_discussion.vue | 14 +++---- .../notes/components/issue_note.vue | 4 +- .../components/issue_note_awards_list.vue | 4 +- .../notes/stores/issue_notes_store.js | 17 +++++++-- 5 files changed, 43 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index b8df6359c1e3..ee873ba4b77f 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -7,23 +7,17 @@ import MarkdownField from '../../vue_shared/components/markdown/field.vue'; export default { props: {}, data() { + const { create_note_path, state } = window.gl.issueData; + const { currentUserData } = window.gl; + return { note: '', markdownPreviewUrl: '', markdownDocsUrl: '', - - // FIXME: @fatihacet - Fix the mock data below. noteType: 'comment', - issueState: 'open', - endpoint: '/gitlab-org/gitlab-ce/notes', - author: { - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - id: 1, - name: 'Administrator', - path: '/root', - state: 'active', - username: 'root', - }, + issueState: state, + endpoint: create_note_path, + author: currentUserData, }; }, components: { @@ -47,13 +41,12 @@ export default { methods: { handleSave() { const data = { - endpoint: `${this.endpoint}?full_data=1`, + endpoint: this.endpoint, noteData: { - target_type: 'issue', - target_id: '89', + full_data: true, note: { noteable_type: 'Issue', - noteable_id: 89, + noteable_id: window.gl.issueData.id, note: this.note, } }, @@ -64,12 +57,14 @@ export default { } this.$store.dispatch('createNewNote', data) - .then(() => { + .then((res) => { + if (res.errors) { + return this.handleError(); + } + this.discard(); }) - .catch(() => { - new Flash('Something went wrong while adding your comment. Please try again.'); // eslint-disable-line - }); + .catch(this.handleError); }, discard() { this.note = ''; @@ -78,6 +73,9 @@ export default { setNoteType(type) { this.noteType = type; }, + handleError() { + new Flash('Something went wrong while adding your comment. Please try again.'); // eslint-disable-line + }, }, mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index f2ab8ba278e5..cf4f63968ecf 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -19,7 +19,7 @@ export default { return { registerLink: '#', signInLink: '#', - newNotePath: '', + newNotePath: window.gl.issueData.create_note_path, isReplying: false, }; }, @@ -30,6 +30,9 @@ export default { author() { return this.discussion.author; }, + canReply() { + return window.gl.issueData.current_user.can_create_note; + }, }, components: { IssueNote, @@ -48,10 +51,6 @@ export default { this.registerLink = registerLink.getAttribute('href'); this.signInLink = signInLink.getAttribute('href'); } - - // TODO: @fatihacet - Reimplement this when we have data for it. - // const newNotePath = document.querySelector('.js-main-target-form').getAttribute('action'); - // this.newNotePath = `${newNotePath}?full_data=1`; }, methods: { toggleDiscussion() { @@ -73,6 +72,7 @@ export default { target_type: 'issue', target_id: this.discussion.noteable_id, note: { note }, + full_data: true, }, }; @@ -130,7 +130,7 @@ export default { <div class="flash-container"></div> <div class="discussion-reply-holder"> <button - v-if="note.can_reply && !isReplying" + v-if="canReply && !isReplying" @click="showReplyForm" type="button" class="btn btn-text-field" @@ -141,7 +141,7 @@ export default { :updateHandler="saveReply" :cancelHandler="cancelReplyForm" /> <div - v-if="!note.can_reply" + v-if="!canReply" class="disabled-comment text-center"> Please <a :href="registerLink">register</a> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index eb39cbd5e7c6..9a7ad1db7bbb 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -104,8 +104,8 @@ export default { <issue-note-actions :accessLevel="note.human_access" :canAward="note.emoji_awardable" - :canEdit="note.can_edit" - :canDelete="note.can_edit" + :canEdit="note.current_user.can_edit" + :canDelete="note.current_user.can_edit" :reportAbusePath="note.report_abuse_path" :editHandler="editHandler" :deleteHandler="deleteHandler" /> diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index f0e499aa4776..2fc50c05c7c6 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -1,5 +1,5 @@ <script> -import { glEmojiTag } from '~/behaviors/gl_emoji'; +import * as Emoji from '../../emoji'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; @@ -66,7 +66,7 @@ export default { }, methods: { getAwardHTML(name) { - return glEmojiTag(name); + return Emoji.glEmojiTag(name); }, getAwardClassBindings(awardList, awardName) { return { diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 8881ba2aa9ac..9914b001c250 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -53,8 +53,16 @@ const mutations = { } }, addNewNote(storeState, note) { - // TODO: @fatihacet - When we get the correct data from server update the store - // storeState.notes.push(note); + const { discussion_id, type } = note; + const noteData = { + expanded: true, + id: discussion_id, + individual_note: !(type === 'DiscussionNote'), + notes: [ note ], + reply_id: discussion_id, + }; + + storeState.notes.push(noteData); }, }; @@ -100,7 +108,10 @@ const actions = { .createNewNote(endpoint, noteData) .then(res => res.json()) .then((res) => { - context.commit('addNewNote', res); + if (!res.errors) { + context.commit('addNewNote', res); + } + return res; }); }, }; -- GitLab From 17d67a989bb1a87df17583e96b387aa3fa3a9f56 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 29 Jun 2017 14:47:59 +0300 Subject: [PATCH 035/243] IssueNotesRefactor: Implement close/reopen issue actions. --- .../notes/components/issue_comment_form.vue | 69 ++++++++++++------- .../notes/components/issue_note.vue | 3 +- .../components/issue_note_awards_list.vue | 2 +- .../notes/services/issue_notes_service.js | 2 +- .../notes/stores/issue_notes_store.js | 2 +- app/views/projects/issues/show.html.haml | 4 +- 6 files changed, 52 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index ee873ba4b77f..42bac8338ff2 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -28,43 +28,63 @@ export default { commentButtonTitle() { return this.noteType === 'comment' ? 'Comment' : 'Start discussion'; }, + isIssueOpen() { + return this.issueState === 'opened' || this.issueState === 'reopened'; + }, issueActionButtonTitle() { if (this.note.length) { - const actionText = this.issueState === 'open' ? 'close' : 'reopen'; + const actionText = this.isIssueOpen ? 'close' : 'reopen'; return this.noteType === 'comment' ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`; } - return this.issueState === 'open' ? 'Close issue' : 'Reopen issue'; + return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; }, }, methods: { - handleSave() { - const data = { - endpoint: this.endpoint, - noteData: { - full_data: true, - note: { - noteable_type: 'Issue', - noteable_id: window.gl.issueData.id, - note: this.note, - } - }, - }; + handleSave(withIssueAction) { + if (this.note.length) { + const data = { + endpoint: this.endpoint, + noteData: { + full_data: true, + note: { + noteable_type: 'Issue', + noteable_id: window.gl.issueData.id, + note: this.note, + }, + }, + }; - if (this.noteType === 'discussion') { - data.noteData.note.type = 'DiscussionNote'; + if (this.noteType === 'discussion') { + data.noteData.note.type = 'DiscussionNote'; + } + + this.$store.dispatch('createNewNote', data) + .then((res) => { + if (res.errors) { + this.handleError(); + } else { + this.discard(); + } + }) + .catch(this.handleError); } - this.$store.dispatch('createNewNote', data) - .then((res) => { - if (res.errors) { - return this.handleError(); - } + if (withIssueAction) { + if (this.isIssueOpen) { + gl.issueData.state = 'closed'; + this.issueState = 'closed'; + } else { + gl.issueData.state = 'reopened'; + this.issueState = 'reopened'; + } + this.isIssueOpen = !this.isIssueOpen; - this.discard(); - }) - .catch(this.handleError); + // This is out of scope for the Notes Vue component. + // It was the shortest path to update the issue state and relevant places. + $('.js-btn-issue-action:visible').trigger('click'); + } }, discard() { this.note = ''; @@ -171,6 +191,7 @@ export default { </ul> </div> <a + @click="handleSave(true)" :class="{'btn-reopen': issueState === 'closed', 'btn-close': issueState === 'open'}" class="btn btn-nr btn-comment"> {{issueActionButtonTitle}} diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 9a7ad1db7bbb..ac906e9440e2 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -59,8 +59,9 @@ export default { }, formUpdateHandler(note) { const data = { - endpoint: `${this.note.path}?full_data=1`, + endpoint: this.note.path, note: { + full_data: true, target_type: 'issue', target_id: this.note.noteable_id, note, diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 2fc50c05c7c6..a6e441ac90c8 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -1,8 +1,8 @@ <script> -import * as Emoji from '../../emoji'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; +import * as Emoji from '../../emoji'; export default { props: { diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index 52ccaa82a998..c8b108d8a906 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -18,5 +18,5 @@ export default { }, createNewNote(endpoint, data) { return Vue.http.post(endpoint, data, { emulateJSON: true }); - } + }, }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 9914b001c250..5a9c3aaad22c 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -58,7 +58,7 @@ const mutations = { expanded: true, id: discussion_id, individual_note: !(type === 'DiscussionNote'), - notes: [ note ], + notes: [note], reply_id: discussion_id, }; diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index a57844f974ee..8509e97fbc69 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -34,8 +34,8 @@ - unless current_user == @issue.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue - %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' + %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - if can_report_spam %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' - if can_update_issue || can_report_spam -- GitLab From 15f3362d343ab7ea51402b81c339bc1bd25fa9eb Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 29 Jun 2017 18:59:22 +0300 Subject: [PATCH 036/243] IssueDiscussionsRefactor: Implement polling mechanism. --- .../notes/components/issue_comment_form.vue | 4 +- .../notes/components/issue_notes.vue | 17 ++++++- .../notes/services/issue_notes_service.js | 9 ++++ .../notes/stores/issue_notes_store.js | 47 +++++++++++++++++-- .../projects/issues/_discussion.html.haml | 2 +- 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 42bac8338ff2..72d9bc7451ab 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -133,13 +133,13 @@ export default { ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." - @keydown.meta.enter="handleSave"> + @keydown.meta.enter="handleSave()"> </textarea> </markdown-field> <div class="note-form-actions clearfix"> <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown"> <input - @click="handleSave" + @click="handleSave()" :disabled="!note.length" :value="commentButtonTitle" class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index f817be373a60..72fbadbcd21c 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -39,14 +39,27 @@ export default { }, }, mounted() { - const path = this.$el.parentNode.dataset.discussionsPath; - this.$store.dispatch('fetchNotes', path) + const { discussionsPath, notesPath, lastFetchedAt } = this.$el.parentNode.dataset; + this.$store.dispatch('fetchNotes', discussionsPath) .then(() => { this.isLoading = false; }) .catch(() => { new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line }); + + const options = { + endpoint: `${notesPath}?full_data=1`, + lastFetchedAt, + }; + + // FIXME: @fatihacet Implement real polling mechanism + setInterval(() => { + this.$store.dispatch('poll', options) + .then((res) => { + options.lastFetchedAt = res.last_fetched_at; + }); + }, 6000); }, }; </script> diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index c8b108d8a906..6400e607d8ae 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -19,4 +19,13 @@ export default { createNewNote(endpoint, data) { return Vue.http.post(endpoint, data, { emulateJSON: true }); }, + poll(endpoint, lastFetchedAt) { + const options = { + headers: { + 'X-Last-Fetched-At': lastFetchedAt, + } + }; + + return Vue.http.get(endpoint, options); + }, }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 5a9c3aaad22c..0eec355975e8 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -15,7 +15,7 @@ const getters = { }; const mutations = { - setNotes(storeState, notes) { + setInitialNotes(storeState, notes) { storeState.notes = notes; }, toggleDiscussion(storeState, { discussionId }) { @@ -40,7 +40,9 @@ const mutations = { addNewReplyToDiscussion(storeState, note) { const noteObj = findNoteObjectById(storeState.notes, note.discussion_id); - noteObj.notes.push(note); + if (noteObj) { + noteObj.notes.push(note); + } }, updateNote(storeState, note) { const noteObj = findNoteObjectById(storeState.notes, note.discussion_id); @@ -72,7 +74,7 @@ const actions = { .fetchNotes(path) .then(res => res.json()) .then((res) => { - context.commit('setNotes', res); + context.commit('setInitialNotes', res); }); }, deleteNote(context, note) { @@ -114,6 +116,45 @@ const actions = { return res; }); }, + poll(context, data) { + const { endpoint, lastFetchedAt } = data; + + return service + .poll(endpoint, lastFetchedAt) + .then(res => res.json()) + .then((res) => { + if (res.notes.length) { + const notesById = {}; + + // Simple lookup object to check whether we have a discussion id already in our store + context.state.notes.forEach((note) => { + note.notes.forEach((n) => { + notesById[n.id] = true; + }); + }); + + res.notes.forEach((note) => { + if (notesById[note.id]) { + context.commit('updateNote', note); + } else { + if (note.type === 'DiscussionNote') { + const discussion = findNoteObjectById(context.state.notes, note.discussion_id); + + if (discussion) { + context.commit('addNewReplyToDiscussion', note); + } else { + context.commit('addNewNote', note); + } + } else { + context.commit('addNewNote', note); + } + } + }); + } + + return res; + }); + }, }; export default { diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 8e4b9f8910a0..ab0534f1fb04 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -3,7 +3,7 @@ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' -%section{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), new_session_path: new_session_path(:user, redirect_to_referer: 'yes') } } +%section{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), notes_path: notes_url, last_fetched_at: Time.now.to_i } } #js-notes - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' -- GitLab From 91f490690103fc20a772b39b31cddbb5927e08b9 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 4 Jul 2017 01:31:11 +0300 Subject: [PATCH 037/243] IssueNotesRefactor: Implement ESC to cancel note form. --- .../notes/components/issue_discussion.vue | 13 +++++++++++-- .../javascripts/notes/components/issue_note.vue | 13 +++++++++++-- .../notes/components/issue_note_form.vue | 11 +++++++++-- .../javascripts/notes/components/issue_notes.vue | 6 +++++- .../notes/services/issue_notes_service.js | 2 +- .../notes/stores/issue_notes_store.js | 16 +++++++--------- 6 files changed, 44 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index cf4f63968ecf..901462a40db1 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -61,7 +61,15 @@ export default { showReplyForm() { this.isReplying = true; }, - cancelReplyForm() { + cancelReplyForm(shouldConfirm) { + if (shouldConfirm && this.$refs.noteForm.isDirty) { + const msg = 'Are you sure you want to cancel creating this comment?'; + const isConfirmed = confirm(msg); // eslint-disable-line + if (!isConfirmed) { + return; + } + } + this.isReplying = false; }, saveReply({ note }) { @@ -139,7 +147,8 @@ export default { v-if="isReplying" saveButtonTitle="Comment" :updateHandler="saveReply" - :cancelHandler="cancelReplyForm" /> + :cancelHandler="cancelReplyForm" + ref="noteForm" /> <div v-if="!canReply" class="disabled-comment text-center"> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index ac906e9440e2..870a06b34cdf 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -76,7 +76,15 @@ export default { new Flash('Something went wrong while editing your comment. Please try again.'); // eslint-disable-line }); }, - formCancelHandler() { + formCancelHandler(shouldConfirm) { + if (shouldConfirm && this.$refs.noteBody.$refs.noteForm.isDirty) { + const msg = 'Are you sure you want to cancel editing this comment?'; + const isConfirmed = confirm(msg); // eslint-disable-line + if (!isConfirmed) { + return; + } + } + this.isEditing = false; }, }, @@ -115,7 +123,8 @@ export default { :note="note" :isEditing="isEditing" :formUpdateHandler="formUpdateHandler" - :formCancelHandler="formCancelHandler" /> + :formCancelHandler="formCancelHandler" + ref="noteBody" /> </div> </div> </li> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 137e84fb361c..b322f777968e 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -24,6 +24,7 @@ export default { }, data() { return { + initialNote: this.noteBody, note: this.noteBody, markdownPreviewUrl: '', markdownDocsUrl: '', @@ -39,6 +40,11 @@ export default { }); }, }, + computed: { + isDirty() { + return this.initialNote !== this.note; + }, + }, mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); @@ -68,7 +74,8 @@ export default { ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." - @keydown.meta.enter="handleUpdate"> + @keydown.meta.enter="handleUpdate" + @keydown.esc="cancelHandler(true)"> </textarea> </markdown-field> <div class="note-form-actions clearfix"> @@ -79,7 +86,7 @@ export default { {{saveButtonTitle}} </button> <button - @click="cancelHandler" + @click="cancelHandler()" class="btn btn-nr btn-cancel" type="button"> Cancel diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 72fbadbcd21c..37bdc9ed8d65 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -39,7 +39,8 @@ export default { }, }, mounted() { - const { discussionsPath, notesPath, lastFetchedAt } = this.$el.parentNode.dataset; + const { discussionsPath, notesPath, lastFetchedAt } = this.$el.parentNode.dataset; + this.$store.dispatch('fetchNotes', discussionsPath) .then(() => { this.isLoading = false; @@ -58,6 +59,9 @@ export default { this.$store.dispatch('poll', options) .then((res) => { options.lastFetchedAt = res.last_fetched_at; + }) + .catch(() => { + new Flash('Something went wrong while fetching latest comments.'); // eslint-disable-line }); }, 6000); }, diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index 6400e607d8ae..0a9df5562920 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -23,7 +23,7 @@ export default { const options = { headers: { 'X-Last-Fetched-At': lastFetchedAt, - } + }, }; return Vue.http.get(endpoint, options); diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 0eec355975e8..16824b72ebc8 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -136,18 +136,16 @@ const actions = { res.notes.forEach((note) => { if (notesById[note.id]) { context.commit('updateNote', note); - } else { - if (note.type === 'DiscussionNote') { - const discussion = findNoteObjectById(context.state.notes, note.discussion_id); - - if (discussion) { - context.commit('addNewReplyToDiscussion', note); - } else { - context.commit('addNewNote', note); - } + } else if (note.type === 'DiscussionNote') { + const discussion = findNoteObjectById(context.state.notes, note.discussion_id); + + if (discussion) { + context.commit('addNewReplyToDiscussion', note); } else { context.commit('addNewNote', note); } + } else { + context.commit('addNewNote', note); } }); } -- GitLab From d9bf04f69c81f21a4c5a0d20d00c57a517499c34 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 4 Jul 2017 01:37:36 +0300 Subject: [PATCH 038/243] IssueNotesRefactor: Flip toggle discussions chevron when discussion toggled. --- .../notes/components/issue_note_header.vue | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index bf944732ede8..1eca05652672 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -38,6 +38,22 @@ export default { components: { TimeAgoTooltip, }, + data() { + return { + isExpanded: true, + } + }, + computed: { + toggleChevronClass() { + return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; + }, + }, + methods: { + handleToggle() { + this.isExpanded = !this.isExpanded; + this.toggleHandler(); + }, + }, }; </script> @@ -71,12 +87,13 @@ export default { v-if="includeToggle" class="discussion-actions"> <button - @click="toggleHandler" + @click="handleToggle" class="note-action-button discussion-toggle-button js-toggle-button" type="button"> <i - aria-hidden="true" - class="fa fa-chevron-up"></i> + :class="toggleChevronClass" + class="fa" + aria-hidden="true"></i> Toggle discussion </button> </div> -- GitLab From 0c0b5fab13582da3240dbc1526c8758033646c70 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 4 Jul 2017 01:52:34 +0300 Subject: [PATCH 039/243] IssueNotesRefactor: Render GFM again when a comment is edited. --- app/assets/javascripts/notes/components/issue_note.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 870a06b34cdf..36561d06eaca 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -71,6 +71,7 @@ export default { this.$store.dispatch('updateNote', data) .then(() => { this.isEditing = false; + $(this.$refs['noteBody'].$el).renderGFM(); }) .catch(() => { new Flash('Something went wrong while editing your comment. Please try again.'); // eslint-disable-line -- GitLab From 7fcc1eef9e0c082baf0c97c287ea0627678867cd Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 4 Jul 2017 01:59:37 +0300 Subject: [PATCH 040/243] =?UTF-8?q?Show=20=E2=80=9DNothing=20to=20preview?= =?UTF-8?q?=E2=80=9D=20if=20the=20markdown=20area=20is=20empty.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../javascripts/vue_shared/components/markdown/field.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 547e459d8e92..865cd0244d5c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -54,6 +54,10 @@ this.markdownPreviewLoading = false; this.markdownPreview = data.body; + if (!this.markdownPreview) { + this.markdownPreview = 'Nothing to preview.'; + } + this.$nextTick(() => { $(this.$refs['markdown-preview']).renderGFM(); }); -- GitLab From 1f093bcda5cc848d5031f80ecc66f17da3a13aa9 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 4 Jul 2017 02:04:58 +0300 Subject: [PATCH 041/243] =?UTF-8?q?IssueNotesRefactor:=20Don=E2=80=99t=20s?= =?UTF-8?q?how=20report=20as=20abuse=20link=20for=20own=20notes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/javascripts/notes/components/issue_note.vue | 4 ++++ .../javascripts/notes/components/issue_note_actions.vue | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 36561d06eaca..8eadea95381c 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -35,6 +35,9 @@ export default { 'disabled-content': this.isDeleting, }; }, + canReportAsAbuse() { + return this.note.report_abuse_path && this.author.id !== window.gon.current_user_id; + }, }, methods: { editHandler() { @@ -116,6 +119,7 @@ export default { :canAward="note.emoji_awardable" :canEdit="note.current_user.can_edit" :canDelete="note.current_user.can_edit" + :canReportAsAbuse="canReportAsAbuse" :reportAbusePath="note.report_abuse_path" :editHandler="editHandler" :deleteHandler="deleteHandler" /> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index fb1c865d2397..f95686497f5a 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -22,6 +22,10 @@ export default { type: Boolean, required: true, }, + canReportAsAbuse: { + type: Boolean, + required: true, + }, editHandler: { type: Function, required: true, @@ -90,7 +94,7 @@ export default { </li> <li class="divider"></li> </template> - <li v-if="reportAbusePath"> + <li v-if="canReportAsAbuse"> <a :href="reportAbusePath"> Report as abuse </a> -- GitLab From d6dab4dbcca61684c15f46976f0cfd9443436637 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 4 Jul 2017 02:20:02 +0300 Subject: [PATCH 042/243] IssueNotesRefactor: Show note actions dropdown for logged in users only. --- .../javascripts/notes/components/issue_note_actions.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index f95686497f5a..b26861a13867 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -42,6 +42,11 @@ export default { emojiSmiley, }; }, + computed: { + shouldShowActionsDropdown() { + return window.gon.current_user_id && (this.canEdit || this.canReportAsAbuse); + }, + }, }; </script> @@ -71,7 +76,9 @@ export default { v-html="emojiSmile" class="link-highlight award-control-icon-super-positive"></span> </a> - <div class="dropdown more-actions"> + <div + v-if="shouldShowActionsDropdown" + class="dropdown more-actions"> <button type="button" title="More actions" -- GitLab From 0f098eb60a706fb69f1a822ee409826f663ee82f Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 5 Jul 2017 00:56:25 +0300 Subject: [PATCH 043/243] IssueNotesRefactor: Create signed out widget. --- .../notes/components/issue_comment_form.vue | 203 +++++++++--------- .../notes/components/issue_discussion.vue | 24 +-- .../issue_note_signed_out_widget.vue | 26 +++ .../projects/issues/_discussion.html.haml | 2 +- 4 files changed, 137 insertions(+), 118 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 72d9bc7451ab..e4f7a1dbb791 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -3,6 +3,7 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import MarkdownField from '../../vue_shared/components/markdown/field.vue'; +import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; export default { props: {}, @@ -23,8 +24,12 @@ export default { components: { UserAvatarLink, MarkdownField, + IssueNoteSignedOutWidget, }, computed: { + isLoggedIn() { + return window.gon.current_user_id; + }, commentButtonTitle() { return this.noteType === 'comment' ? 'Comment' : 'Start discussion'; }, @@ -109,102 +114,108 @@ export default { </script> <template> - <ul class="notes notes-form timeline new-note"> - <li class="timeline-entry"> - <div class="timeline-icon hidden-xs hidden-sm"> - <user-avatar-link - :linkHref="author.path" - :imgSrc="author.avatar_url" - :imgAlt="author.name" - :imgSize="40" /> - </div> - <div class="timeline-content timeline-content-form common-note-form"> - <markdown-field - :markdown-preview-url="markdownPreviewUrl" - :markdown-docs="markdownDocsUrl" - :addSpacingClasses="false"> - <textarea - id="note-body" - class="note-textarea js-gfm-input js-autosize markdown-area" - data-supports-slash-commands="true" - data-supports-quick-actions="true" - aria-label="Description" - v-model="note" - ref="textarea" - slot="textarea" - placeholder="Write a comment or drag your files here..." - @keydown.meta.enter="handleSave()"> - </textarea> - </markdown-field> - <div class="note-form-actions clearfix"> - <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown"> - <input - @click="handleSave()" - :disabled="!note.length" - :value="commentButtonTitle" - class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" - type="submit" /> - <button - :disabled="!note.length" - name="button" - type="button" - class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion" - data-toggle="dropdown" - aria-label="Open comment type dropdown"> - <i - aria-hidden="true" - class="fa fa-caret-down toggle-icon"></i> - </button> - <ul - class="dropdown-menu note-type-dropdown dropdown-open-top"> - <li - :class="{ 'item-selected': noteType === 'comment' }" - @click.prevent="setNoteType('comment')"> - <a href="#"> - <i - aria-hidden="true" - class="fa fa-check"></i> - <div class="description"> - <strong>Comment</strong> - <p> - Add a general comment to this issue. - </p> - </div> - </a> - </li> - <li class="divider"></li> - <li - :class="{ 'item-selected': noteType === 'discussion' }" - @click.prevent="setNoteType('discussion')"> - <a href="#"> - <i - aria-hidden="true" - class="fa fa-check"></i> - <div class="description"> - <strong>Start discussion</strong> - <p> - Discuss a specific suggestion or question. - </p> - </div> - </a> - </li> - </ul> + <div> + <issue-note-signed-out-widget v-if="!isLoggedIn" /> + <ul + v-if="isLoggedIn" + class="notes notes-form timeline new-note"> + <li class="timeline-entry"> + <div class="timeline-icon hidden-xs hidden-sm"> + <user-avatar-link + v-if="author" + :linkHref="author.path" + :imgSrc="author.avatar_url" + :imgAlt="author.name" + :imgSize="40" /> + </div> + <div class="timeline-content timeline-content-form common-note-form"> + <markdown-field + :markdown-preview-url="markdownPreviewUrl" + :markdown-docs="markdownDocsUrl" + :addSpacingClasses="false"> + <textarea + id="note-body" + class="note-textarea js-gfm-input js-autosize markdown-area" + data-supports-slash-commands="true" + data-supports-quick-actions="true" + aria-label="Description" + v-model="note" + ref="textarea" + slot="textarea" + placeholder="Write a comment or drag your files here..." + @keydown.meta.enter="handleSave()"> + </textarea> + </markdown-field> + <div class="note-form-actions clearfix"> + <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown"> + <input + @click="handleSave()" + :disabled="!note.length" + :value="commentButtonTitle" + class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" + type="submit" /> + <button + :disabled="!note.length" + name="button" + type="button" + class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion" + data-toggle="dropdown" + aria-label="Open comment type dropdown"> + <i + aria-hidden="true" + class="fa fa-caret-down toggle-icon"></i> + </button> + <ul + class="dropdown-menu note-type-dropdown dropdown-open-top"> + <li + :class="{ 'item-selected': noteType === 'comment' }" + @click.prevent="setNoteType('comment')"> + <a href="#"> + <i + aria-hidden="true" + class="fa fa-check"></i> + <div class="description"> + <strong>Comment</strong> + <p> + Add a general comment to this issue. + </p> + </div> + </a> + </li> + <li class="divider"></li> + <li + :class="{ 'item-selected': noteType === 'discussion' }" + @click.prevent="setNoteType('discussion')"> + <a href="#"> + <i + aria-hidden="true" + class="fa fa-check"></i> + <div class="description"> + <strong>Start discussion</strong> + <p> + Discuss a specific suggestion or question. + </p> + </div> + </a> + </li> + </ul> + </div> + <a + @click="handleSave(true)" + :class="{'btn-reopen': issueState === 'closed', 'btn-close': issueState === 'open'}" + class="btn btn-nr btn-comment"> + {{issueActionButtonTitle}} + </a> + <a + v-if="note.length" + @click="discard" + class="btn btn-cancel js-note-discard" + role="button"> + Discard draft + </a> </div> - <a - @click="handleSave(true)" - :class="{'btn-reopen': issueState === 'closed', 'btn-close': issueState === 'open'}" - class="btn btn-nr btn-comment"> - {{issueActionButtonTitle}} - </a> - <a - v-if="note.length" - @click="discard" - class="btn btn-cancel js-note-discard" - role="button"> - Discard draft - </a> </div> - </div> - </li> - </ul> + </li> + </ul> + </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 901462a40db1..5cebd6249350 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -5,6 +5,7 @@ import IssueNote from './issue_note.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import IssueNoteHeader from './issue_note_header.vue'; import IssueNoteActions from './issue_note_actions.vue'; +import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import IssueNoteEditedText from './issue_note_edited_text.vue'; import IssueNoteForm from './issue_note_form.vue'; @@ -17,8 +18,6 @@ export default { }, data() { return { - registerLink: '#', - signInLink: '#', newNotePath: window.gl.issueData.create_note_path, isReplying: false, }; @@ -40,18 +39,9 @@ export default { IssueNoteHeader, IssueNoteActions, IssueNoteEditedText, + IssueNoteSignedOutWidget, IssueNoteForm, }, - mounted() { - // We need to grab the register and sign in links from DOM for the time being. - const registerLink = document.querySelector('.js-disabled-comment .js-register-link'); - const signInLink = document.querySelector('.js-disabled-comment .js-sign-in-link'); - - if (registerLink && signInLink) { - this.registerLink = registerLink.getAttribute('href'); - this.signInLink = signInLink.getAttribute('href'); - } - }, methods: { toggleDiscussion() { this.$store.commit('toggleDiscussion', { @@ -149,15 +139,7 @@ export default { :updateHandler="saveReply" :cancelHandler="cancelReplyForm" ref="noteForm" /> - <div - v-if="!canReply" - class="disabled-comment text-center"> - Please - <a :href="registerLink">register</a> - or - <a :href="signInLink">sign in</a> - to reply - </div> + <issue-note-signed-out-widget v-if="!canReply" /> </div> </div> </div> diff --git a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue new file mode 100644 index 000000000000..1b819dfcb8be --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue @@ -0,0 +1,26 @@ +<script> +export default { + data() { + return { + signInLink: '#', + }; + }, + mounted() { + const wrapper = document.querySelector('.js-notes-wrapper'); + + if (wrapper) { + this.signInLink = wrapper.dataset.newSessionPath; + } + }, +}; +</script> + +<template> + <div class="disabled-comment text-center"> + Please + <a :href="signInLink">register</a> + or + <a :href="signInLink">sign in</a> + to reply + </div> +</template> diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index ab0534f1fb04..de92e55a5bcd 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -3,7 +3,7 @@ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' -%section{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), notes_path: notes_url, last_fetched_at: Time.now.to_i } } +%section.js-notes-wrapper{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), notes_path: notes_url, last_fetched_at: Time.now.to_i } } #js-notes - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' -- GitLab From 116474c50690f5b99ebcce46c6bc93f743ac256e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 5 Jul 2017 01:01:12 +0300 Subject: [PATCH 044/243] IssueNotesRefactor: Show award actions to logged in users. --- .../javascripts/notes/components/issue_note_actions.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index b26861a13867..bcaf5282a9e3 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -46,6 +46,9 @@ export default { shouldShowActionsDropdown() { return window.gon.current_user_id && (this.canEdit || this.canReportAsAbuse); }, + canAddAwardEmoji() { + return window.gon.current_user_id; + }, }, }; </script> @@ -58,6 +61,7 @@ export default { {{accessLevel}} </span> <a + v-if="canAddAwardEmoji" class="note-action-button note-emoji-button js-add-award js-note-emoji js-user-authored has-tooltip" data-position="right" href="#" -- GitLab From afece66004420bdb18edf0891fa9c6dfa86ce87c Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 5 Jul 2017 01:19:47 +0300 Subject: [PATCH 045/243] IssueNotesRefactor: Fix anchor of note timestamp. --- .../javascripts/notes/components/issue_discussion.vue | 2 +- app/assets/javascripts/notes/components/issue_note.vue | 2 +- .../javascripts/notes/components/issue_note_header.vue | 9 ++++++--- .../javascripts/notes/components/issue_system_note.vue | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 5cebd6249350..3ad48c78ab01 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -102,7 +102,7 @@ export default { <issue-note-header :author="author" :createdAt="discussion.created_at" - :notePath="discussion.path" + :noteId="discussion.id" :includeToggle="true" :toggleHandler="toggleDiscussion" actionText="started a discussion" /> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 8eadea95381c..f0b0750fdd90 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -112,7 +112,7 @@ export default { <issue-note-header :author="author" :createdAt="note.created_at" - :notePath="note.path" + :noteId="note.id" actionText="commented" /> <issue-note-actions :accessLevel="note.human_access" diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 1eca05652672..106c50d1fb17 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -21,8 +21,8 @@ export default { required: false, default: '', }, - notePath: { - type: String, + noteId: { + type: Number, required: true, }, includeToggle: { @@ -47,6 +47,9 @@ export default { toggleChevronClass() { return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; }, + noteTimestampLink() { + return `#note_${this.noteId}`; + }, }, methods: { handleToggle() { @@ -76,7 +79,7 @@ export default { v-if="actionTextHtml" v-html="actionTextHtml" class="system-note-message"></span> - <a :href="notePath"> + <a :href="noteTimestampLink"> <time-ago-tooltip :time="createdAt" tooltipPlacement="bottom" /> diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index a2ca4c828c19..220b15675db0 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -31,7 +31,7 @@ export default { <issue-note-header :author="note.author" :createdAt="note.created_at" - :notePath="note.path" + :noteId="note.id" :actionTextHtml="note.note_html" /> </div> </div> -- GitLab From b72db79668ff5ede3124933a989a547cf17c4dee Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 7 Jul 2017 02:05:13 +0300 Subject: [PATCH 046/243] IssueNotesRefactor: Use map getters for notes. --- app/assets/javascripts/notes/components/issue_notes.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 37bdc9ed8d65..d22253d231d5 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -3,6 +3,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; +import { mapGetters } from 'vuex'; import storeOptions from '../stores/issue_notes_store'; import IssueNote from './issue_note.vue'; import IssueDiscussion from './issue_discussion.vue'; @@ -26,6 +27,11 @@ export default { IssueSystemNote, IssueCommentForm, }, + computed: { + ...mapGetters([ + 'notes', + ]) + }, methods: { component(note) { if (note.individual_note) { @@ -82,7 +88,7 @@ export default { id="notes-list" class="notes main-notes-list timeline"> <component - v-for="note in $store.getters.notes" + v-for="note in notes" :is="component(note)" :note="componentData(note)" :key="note.id" /> -- GitLab From 993936fbd0ea62acb37da865767a1e67bea5cc1f Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 7 Jul 2017 02:13:10 +0300 Subject: [PATCH 047/243] IssueNotesRefactor: Implement jumping to target note. --- .../javascripts/lib/utils/common_utils.js | 5 +++-- .../notes/components/issue_note.vue | 10 ++++++++++ .../notes/components/issue_note_header.vue | 7 ++++++- .../notes/components/issue_notes.vue | 18 ++++++++++++++++++ .../notes/components/issue_system_note.vue | 17 ++++++++++++++++- .../notes/stores/issue_notes_store.js | 7 +++++++ 6 files changed, 60 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 122ec138c59c..1580bb67d65f 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -162,10 +162,11 @@ gl.utils.scrollToElement = function($el) { var top = $el.offset().top; - gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); + var mrTabsHeight = $('.merge-request-tabs').height() || 0; + var headerHeight = $('.navbar-gitlab').height() || 0; return $('body, html').animate({ - scrollTop: top - (gl.mrTabsHeight) + scrollTop: top - mrTabsHeight - headerHeight }, 200); }; diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index f0b0750fdd90..a3203e529ed4 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -1,6 +1,7 @@ <script> /* global Flash */ +import { mapGetters } from 'vuex'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import IssueNoteHeader from './issue_note_header.vue'; import IssueNoteActions from './issue_note_actions.vue'; @@ -26,6 +27,9 @@ export default { IssueNoteBody, }, computed: { + ...mapGetters([ + 'targetNoteHash', + ]), author() { return this.note.author; }, @@ -33,11 +37,15 @@ export default { return { 'is-editing': this.isEditing, 'disabled-content': this.isDeleting, + target: this.targetNoteHash === this.noteAnchorId, }; }, canReportAsAbuse() { return this.note.report_abuse_path && this.author.id !== window.gon.current_user_id; }, + noteAnchorId() { + return `note_${this.note.id}`; + }, }, methods: { editHandler() { @@ -98,6 +106,7 @@ export default { <template> <li class="note timeline-entry" + :id="noteAnchorId" :class="classNameBindings"> <div class="timeline-entry-inner"> <div class="timeline-icon"> @@ -115,6 +124,7 @@ export default { :noteId="note.id" actionText="commented" /> <issue-note-actions + :authorId="author.id" :accessLevel="note.human_access" :canAward="note.emoji_awardable" :canEdit="note.current_user.can_edit" diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 106c50d1fb17..879b7c0716b1 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -56,6 +56,9 @@ export default { this.isExpanded = !this.isExpanded; this.toggleHandler(); }, + updateTargetNoteHash() { + this.$store.commit('setTargetNoteHash', this.noteTimestampLink); + }, }, }; </script> @@ -79,7 +82,9 @@ export default { v-if="actionTextHtml" v-html="actionTextHtml" class="system-note-message"></span> - <a :href="noteTimestampLink"> + <a + :href="noteTimestampLink" + @click="updateTargetNoteHash"> <time-ago-tooltip :time="createdAt" tooltipPlacement="bottom" /> diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index d22253d231d5..d7ca0e3f688c 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -43,6 +43,19 @@ export default { componentData(note) { return note.individual_note ? note.notes[0] : note; }, + checkLocationHash() { + const hash = gl.utils.getLocationHash(); + const $el = $(`#${hash}`); + + if (hash && $el) { + const isInViewport = gl.utils.isInViewport($el[0]); + this.$store.commit('setTargetNoteHash', hash); + + if (!isInViewport) { + gl.utils.scrollToElement($el); + } + } + }, }, mounted() { const { discussionsPath, notesPath, lastFetchedAt } = this.$el.parentNode.dataset; @@ -50,6 +63,11 @@ export default { this.$store.dispatch('fetchNotes', discussionsPath) .then(() => { this.isLoading = false; + + // Scroll to note if we have hash fragment in the page URL + Vue.nextTick(() => { + this.checkLocationHash(); + }); }) .catch(() => { new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index 220b15675db0..0763f44552ea 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -1,4 +1,5 @@ <script> +import { mapGetters } from 'vuex'; import iconsMap from './issue_note_icons'; import IssueNoteHeader from './issue_note_header.vue'; @@ -17,11 +18,25 @@ export default { components: { IssueNoteHeader, }, + computed: { + ...mapGetters([ + 'targetNoteHash', + ]), + noteAnchorId() { + return `note_${this.note.id}`; + }, + isTargetNote() { + return this.targetNoteHash === this.noteAnchorId; + }, + }, }; </script> <template> - <li class="note system-note timeline-entry"> + <li + :id="noteAnchorId" + :class="{ target: isTargetNote }" + class="note system-note timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <span v-html="svg"></span> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 16824b72ebc8..f6af52ec364d 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -6,18 +6,25 @@ const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; const state = { notes: [], + targetNoteHash: null, }; const getters = { notes(storeState) { return storeState.notes; }, + targetNoteHash(storeState) { + return storeState.targetNoteHash; + }, }; const mutations = { setInitialNotes(storeState, notes) { storeState.notes = notes; }, + setTargetNoteHash(storeState, hash) { + storeState.targetNoteHash = hash; + }, toggleDiscussion(storeState, { discussionId }) { const discussion = findNoteObjectById(storeState.notes, discussionId); -- GitLab From a2cba2b1ef7d408c4fd573f838adbc2b45fde4cc Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 7 Jul 2017 23:54:34 +0300 Subject: [PATCH 048/243] IssueNotesRefactor: Use notesById getter. --- .../notes/stores/issue_notes_store.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index f6af52ec364d..6f3dd24cad3c 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -16,6 +16,17 @@ const getters = { targetNoteHash(storeState) { return storeState.targetNoteHash; }, + notesById(storeState) { + const notesById = {}; + + storeState.notes.forEach((note) => { + note.notes.forEach((n) => { + notesById[n.id] = n; + }); + }); + + return notesById; + }, }; const mutations = { @@ -131,14 +142,7 @@ const actions = { .then(res => res.json()) .then((res) => { if (res.notes.length) { - const notesById = {}; - - // Simple lookup object to check whether we have a discussion id already in our store - context.state.notes.forEach((note) => { - note.notes.forEach((n) => { - notesById[n.id] = true; - }); - }); + const { notesById } = context.getters; res.notes.forEach((note) => { if (notesById[note.id]) { -- GitLab From deed4725e32361f84440c33c2b8d8006e05d158e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 7 Jul 2017 23:55:44 +0300 Subject: [PATCH 049/243] IssueNotesRefactor: Add needed js- class for the notes current user authored. --- .../javascripts/notes/components/issue_note.vue | 1 + .../notes/components/issue_note_actions.vue | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index a3203e529ed4..0c6d17e23025 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -125,6 +125,7 @@ export default { actionText="commented" /> <issue-note-actions :authorId="author.id" + :noteId="note.id" :accessLevel="note.human_access" :canAward="note.emoji_awardable" :canEdit="note.current_user.can_edit" diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index bcaf5282a9e3..60e0d10f8e7e 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -1,10 +1,19 @@ <script> +import Vue from 'vue'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; export default { props: { + authorId: { + type: Number, + required: true, + }, + noteId: { + type: Number, + required: true, + }, accessLevel: { type: String, required: false, @@ -49,6 +58,9 @@ export default { canAddAwardEmoji() { return window.gon.current_user_id; }, + isAuthoredByMe() { + return this.authorId === window.gon.current_user_id; + }, }, }; </script> @@ -62,7 +74,8 @@ export default { </span> <a v-if="canAddAwardEmoji" - class="note-action-button note-emoji-button js-add-award js-note-emoji js-user-authored has-tooltip" + :class="{ 'js-user-authored': isAuthoredByMe }" + class="note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip" data-position="right" href="#" title="Add reaction"> -- GitLab From 9869b6b38943cad9064d0c0906414686908ad733 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 8 Jul 2017 01:26:52 +0300 Subject: [PATCH 050/243] IssueNotesRefactor: Implement emoji actions from emoji list. --- .../notes/components/issue_note.vue | 2 +- .../notes/components/issue_note_actions.vue | 1 - .../components/issue_note_awards_list.vue | 29 ++++++++++++++- .../notes/components/issue_note_body.vue | 4 ++- .../notes/components/issue_note_header.vue | 2 +- .../notes/components/issue_notes.vue | 5 ++- .../notes/services/issue_notes_service.js | 3 ++ .../notes/stores/issue_notes_store.js | 35 +++++++++++++++++++ 8 files changed, 73 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 0c6d17e23025..1dd4786d6058 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -82,7 +82,7 @@ export default { this.$store.dispatch('updateNote', data) .then(() => { this.isEditing = false; - $(this.$refs['noteBody'].$el).renderGFM(); + $(this.$refs.noteBody.$el).renderGFM(); }) .catch(() => { new Flash('Something went wrong while editing your comment. Please try again.'); // eslint-disable-line diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 60e0d10f8e7e..88f0fdb9a256 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -1,5 +1,4 @@ <script> -import Vue from 'vue'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index a6e441ac90c8..3753eecbb997 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -1,4 +1,6 @@ <script> +/* global Flash */ + import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; @@ -10,10 +12,18 @@ export default { type: Array, required: true, }, + toggleAwardPath: { + type: String, + required: true, + }, noteAuthorId: { type: Number, required: true, }, + noteId: { + type: Number, + required: true, + }, }, data() { const userId = window.gon.current_user_id; @@ -129,6 +139,22 @@ export default { return title; }, + handleAward(awardName, isAwarded) { + const data = { + endpoint: this.toggleAwardPath, + action: isAwarded ? 'remove' : 'add', + noteId: this.noteId, + awardName, + }; + + this.$store.dispatch('toggleAward', data) + .then(() => { + $(this.$el).find('.award-control').tooltip('fixTitle'); + }) + .catch(() => { + new Flash('Something went wrong on our end.'); // eslint-disable-line + }); + }, }, }; </script> @@ -138,9 +164,10 @@ export default { <div class="awards js-awards-block"> <button v-for="(awardList, awardName) in groupedAwards" - class="btn award-control has-tooltip" :class="getAwardClassBindings(awardList, awardName)" :title="awardTitle(awardList)" + @click="handleAward(awardName, amIAwarded(awardList))" + class="btn award-control has-tooltip" data-placement="bottom" type="button"> <span v-html="getAwardHTML(awardName)"></span> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 4f11683c7a94..b5a8a624c858 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -64,7 +64,9 @@ export default { actionText="Edited" /> <issue-note-awards-list v-if="note.award_emoji.length" + :noteId="note.id" + :noteAuthorId="note.author.id" :awards="note.award_emoji" - :noteAuthorId="note.author.id" /> + :toggleAwardPath="note.toggle_award_path" /> </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 879b7c0716b1..cf2826e7b2e8 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -41,7 +41,7 @@ export default { data() { return { isExpanded: true, - } + }; }, computed: { toggleChevronClass() { diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index d7ca0e3f688c..4aa8bd1d1d87 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -3,7 +3,6 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import { mapGetters } from 'vuex'; import storeOptions from '../stores/issue_notes_store'; import IssueNote from './issue_note.vue'; import IssueDiscussion from './issue_discussion.vue'; @@ -28,9 +27,9 @@ export default { IssueCommentForm, }, computed: { - ...mapGetters([ + ...Vuex.mapGetters([ 'notes', - ]) + ]), }, methods: { component(note) { diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index 0a9df5562920..c80e23f02cb0 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -28,4 +28,7 @@ export default { return Vue.http.get(endpoint, options); }, + toggleAward(endpoint, data) { + return Vue.http.post(endpoint, data, { emulateJSON: true }); + }, }; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 6f3dd24cad3c..6a11c65fedca 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -84,6 +84,29 @@ const mutations = { storeState.notes.push(noteData); }, + toggleAward(storeState, data) { + const { awardName, note, action } = data; + const { id, name, username } = window.gl.currentUserData; + + if (action === 'add') { + note.award_emoji.push({ + name: awardName, + user: { id, name, username }, + }); + } else if (action === 'remove') { + let index = -1; + + note.award_emoji.forEach((a, i) => { + if (a.name === awardName && a.user.id === id) { + index = i; + } + }); + + if (index > -1) { + note.award_emoji.splice(index, 1); + } + } + }, }; const actions = { @@ -124,6 +147,7 @@ const actions = { }, createNewNote(context, data) { const { endpoint, noteData } = data; + return service .createNewNote(endpoint, noteData) .then(res => res.json()) @@ -164,6 +188,17 @@ const actions = { return res; }); }, + toggleAward(context, data) { + const { endpoint, awardName, action, noteId } = data; + const note = context.getters.notesById[noteId]; + + return service + .toggleAward(endpoint, { name: awardName }) + .then(res => res.json()) + .then(() => { + context.commit('toggleAward', { awardName, note, action }); + }); + }, }; export default { -- GitLab From 575544f6d5477504821a0f73b9ec3407a6242170 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 8 Jul 2017 01:32:30 +0300 Subject: [PATCH 051/243] IssueNotesRefactor: Slightly refactor toggleAward to remove action. --- .../components/issue_note_awards_list.vue | 5 ++-- .../notes/stores/issue_notes_store.js | 29 +++++++++---------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 3753eecbb997..3f861341f52d 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -139,10 +139,9 @@ export default { return title; }, - handleAward(awardName, isAwarded) { + handleAward(awardName) { const data = { endpoint: this.toggleAwardPath, - action: isAwarded ? 'remove' : 'add', noteId: this.noteId, awardName, }; @@ -166,7 +165,7 @@ export default { v-for="(awardList, awardName) in groupedAwards" :class="getAwardClassBindings(awardList, awardName)" :title="awardTitle(awardList)" - @click="handleAward(awardName, amIAwarded(awardList))" + @click="handleAward(awardName)" class="btn award-control has-tooltip" data-placement="bottom" type="button"> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 6a11c65fedca..bed516b072aa 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -85,26 +85,23 @@ const mutations = { storeState.notes.push(noteData); }, toggleAward(storeState, data) { - const { awardName, note, action } = data; + const { awardName, note } = data; const { id, name, username } = window.gl.currentUserData; + let index = -1; - if (action === 'add') { + note.award_emoji.forEach((a, i) => { + if (a.name === awardName && a.user.id === id) { + index = i; + } + }); + + if (index > -1) { // if I am awarded, remove my award + note.award_emoji.splice(index, 1); + } else { note.award_emoji.push({ name: awardName, user: { id, name, username }, }); - } else if (action === 'remove') { - let index = -1; - - note.award_emoji.forEach((a, i) => { - if (a.name === awardName && a.user.id === id) { - index = i; - } - }); - - if (index > -1) { - note.award_emoji.splice(index, 1); - } } }, }; @@ -189,14 +186,14 @@ const actions = { }); }, toggleAward(context, data) { - const { endpoint, awardName, action, noteId } = data; + const { endpoint, awardName, noteId } = data; const note = context.getters.notesById[noteId]; return service .toggleAward(endpoint, { name: awardName }) .then(res => res.json()) .then(() => { - context.commit('toggleAward', { awardName, note, action }); + context.commit('toggleAward', { awardName, note }); }); }, }; -- GitLab From 7edc1bc3a62769f5186d237ca716b321a4a88d56 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 8 Jul 2017 12:19:39 +0300 Subject: [PATCH 052/243] IssueNotesRefactor: Implement awarding emoji from emoji dropdown. --- app/assets/javascripts/awards_handler.js | 23 +++++++++++++++++++ .../components/issue_note_awards_list.vue | 8 +++++-- .../notes/components/issue_notes.vue | 9 ++++++++ app/assets/javascripts/notes/event_hub.js | 3 +++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/notes/event_hub.js diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 18cd04b176a4..177904543b64 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,6 +2,7 @@ /* global Flash */ import Cookies from 'js-cookie'; +import issueNotesEventHub from './notes/event_hub'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -234,12 +235,23 @@ class AwardsHandler { } addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { + if (this.isInIssuePage()) { + const id = votesBlock[0].id.replace('note_', ''); + + $('.emoji-menu').removeClass('is-visible'); + $('.js-add-award.is-active').removeClass('is-active'); + + return issueNotesEventHub.$emit('toggleAward', { awardName: emoji, noteId: id }); + } + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); + this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); return typeof callback === 'function' ? callback() : undefined; }); + $('.emoji-menu').removeClass('is-visible'); $('.js-add-award.is-active').removeClass('is-active'); } @@ -267,7 +279,18 @@ class AwardsHandler { } } + isInIssuePage() { + const page = gl.utils.getPagePath(1); + const action = gl.utils.getPagePath(2); + + return page === 'issues' && action === 'show'; + } + getVotesBlock() { + if (this.isInIssuePage()) { + return $('.js-add-award.is-active').closest('.note.timeline-entry'); + } + const currentBlock = $('.js-awards-block.current'); let resultantVotesBlock = currentBlock; if (currentBlock.length === 0) { diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 3f861341f52d..f84442f15c1f 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -73,6 +73,9 @@ export default { return orderedAwards; }, + isAuthoredByMe() { + return this.noteAuthorId === window.gon.current_user_id; + }, }, methods: { getAwardHTML(name) { @@ -178,10 +181,11 @@ export default { v-if="canAward" class="award-menu-holder"> <button + :class="{ 'js-user-authored': isAuthoredByMe }" + class="award-control btn has-tooltip js-add-award" + title="Add reaction" aria-label="Add reaction" - class="award-control btn has-tooltip" data-placement="bottom" - title="Add reaction" type="button"> <span v-html="emojiSmiling" diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 4aa8bd1d1d87..4dea00a7dd9a 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -4,6 +4,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import storeOptions from '../stores/issue_notes_store'; +import eventHub from '../event_hub'; import IssueNote from './issue_note.vue'; import IssueDiscussion from './issue_discussion.vue'; import IssueSystemNote from './issue_system_note.vue'; @@ -29,6 +30,7 @@ export default { computed: { ...Vuex.mapGetters([ 'notes', + 'notesById', ]), }, methods: { @@ -87,6 +89,13 @@ export default { new Flash('Something went wrong while fetching latest comments.'); // eslint-disable-line }); }, 6000); + + eventHub.$on('toggleAward', (data) => { + const { awardName, noteId } = data; + const endpoint = this.notesById[noteId].toggle_award_path; + + this.$store.dispatch('toggleAward', { endpoint, awardName, noteId }); + }); }, }; </script> diff --git a/app/assets/javascripts/notes/event_hub.js b/app/assets/javascripts/notes/event_hub.js new file mode 100644 index 000000000000..0948c2e53524 --- /dev/null +++ b/app/assets/javascripts/notes/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); -- GitLab From a32f291a9e8c9d868904ec56f8b1bbceb497f836 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 8 Jul 2017 12:24:13 +0300 Subject: [PATCH 053/243] IssueNotesRefactor: Separate mounted blocks to methods. --- app/assets/javascripts/awards_handler.js | 2 +- .../notes/components/issue_notes.vue | 82 ++++++++++--------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 177904543b64..d6e5a1d3b579 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -253,7 +253,7 @@ class AwardsHandler { }); $('.emoji-menu').removeClass('is-visible'); - $('.js-add-award.is-active').removeClass('is-active'); + return $('.js-add-award.is-active').removeClass('is-active'); } addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) { diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 4dea00a7dd9a..bf801f8879fc 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -44,6 +44,48 @@ export default { componentData(note) { return note.individual_note ? note.notes[0] : note; }, + fetchNotes() { + const { discussionsPath } = this.$el.parentNode.dataset; + + this.$store.dispatch('fetchNotes', discussionsPath) + .then(() => { + this.isLoading = false; + + // Scroll to note if we have hash fragment in the page URL + Vue.nextTick(() => { + this.checkLocationHash(); + }); + }) + .catch(() => { + new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line + }); + }, + initPolling() { + const { notesPath, lastFetchedAt } = this.$el.parentNode.dataset; + const options = { + endpoint: `${notesPath}?full_data=1`, + lastFetchedAt, + }; + + // FIXME: @fatihacet Implement real polling mechanism + setInterval(() => { + this.$store.dispatch('poll', options) + .then((res) => { + options.lastFetchedAt = res.last_fetched_at; + }) + .catch(() => { + new Flash('Something went wrong while fetching latest comments.'); // eslint-disable-line + }); + }, 15000); + }, + bindEventHubListeners() { + eventHub.$on('toggleAward', (data) => { + const { awardName, noteId } = data; + const endpoint = this.notesById[noteId].toggle_award_path; + + this.$store.dispatch('toggleAward', { endpoint, awardName, noteId }); + }); + }, checkLocationHash() { const hash = gl.utils.getLocationHash(); const $el = $(`#${hash}`); @@ -59,43 +101,9 @@ export default { }, }, mounted() { - const { discussionsPath, notesPath, lastFetchedAt } = this.$el.parentNode.dataset; - - this.$store.dispatch('fetchNotes', discussionsPath) - .then(() => { - this.isLoading = false; - - // Scroll to note if we have hash fragment in the page URL - Vue.nextTick(() => { - this.checkLocationHash(); - }); - }) - .catch(() => { - new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line - }); - - const options = { - endpoint: `${notesPath}?full_data=1`, - lastFetchedAt, - }; - - // FIXME: @fatihacet Implement real polling mechanism - setInterval(() => { - this.$store.dispatch('poll', options) - .then((res) => { - options.lastFetchedAt = res.last_fetched_at; - }) - .catch(() => { - new Flash('Something went wrong while fetching latest comments.'); // eslint-disable-line - }); - }, 6000); - - eventHub.$on('toggleAward', (data) => { - const { awardName, noteId } = data; - const endpoint = this.notesById[noteId].toggle_award_path; - - this.$store.dispatch('toggleAward', { endpoint, awardName, noteId }); - }); + this.fetchNotes(); + this.initPolling(); + this.bindEventHubListeners(); }, }; </script> -- GitLab From d9a9c33bcafc334226d279f3f202e503543967cd Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sun, 9 Jul 2017 16:43:40 +0300 Subject: [PATCH 054/243] IssueNotesRefactor: Add catch handler to toggleAward action. --- app/assets/javascripts/notes/components/issue_notes.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index bf801f8879fc..c9904e5e0ecb 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -83,7 +83,10 @@ export default { const { awardName, noteId } = data; const endpoint = this.notesById[noteId].toggle_award_path; - this.$store.dispatch('toggleAward', { endpoint, awardName, noteId }); + this.$store.dispatch('toggleAward', { endpoint, awardName, noteId }) + .catch(() => { + new Flash('Something went wrong on our end.'); // eslint-disable-line + }); }); }, checkLocationHash() { -- GitLab From 3d4d9c5ab9464954d80a7bad3cff70d49af9b74f Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sun, 9 Jul 2017 16:44:01 +0300 Subject: [PATCH 055/243] IssueNotesRefactor: Fix adding main note awards. --- app/assets/javascripts/awards_handler.js | 10 ++++++++-- app/views/projects/issues/show.html.haml | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index d6e5a1d3b579..0c884f409639 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -235,7 +235,9 @@ class AwardsHandler { } addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { - if (this.isInIssuePage()) { + const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length; + + if (this.isInIssuePage() && !isMainAwardsBlock) { const id = votesBlock[0].id.replace('note_', ''); $('.emoji-menu').removeClass('is-visible'); @@ -288,7 +290,11 @@ class AwardsHandler { getVotesBlock() { if (this.isInIssuePage()) { - return $('.js-add-award.is-active').closest('.note.timeline-entry'); + const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); + + if ($el.length) { + return $el; + } } const currentBlock = $('.js-awards-block.current'); diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 8509e97fbc69..f4e4b6cb8faf 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -72,7 +72,7 @@ .content-block.emoji-block .row - .col-sm-8 + .col-sm-8.js-issue-note-awards = render 'award_emoji/awards_block', awardable: @issue, inline: true .col-sm-4.new-branch-col = render 'new_branch' unless @issue.confidential? -- GitLab From 08d597c7ce779608a689a85b63387a1f9da3e898 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sun, 9 Jul 2017 17:16:18 +0300 Subject: [PATCH 056/243] IssueNotesRefactor: Implement :+1: :-1: mutality check. --- .../notes/stores/issue_notes_store.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index bed516b072aa..bc71d1062942 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -186,7 +186,7 @@ const actions = { }); }, toggleAward(context, data) { - const { endpoint, awardName, noteId } = data; + const { endpoint, awardName, noteId, skipMutalityCheck } = data; const note = context.getters.notesById[noteId]; return service @@ -194,6 +194,24 @@ const actions = { .then(res => res.json()) .then(() => { context.commit('toggleAward', { awardName, note }); + + if (!skipMutalityCheck && (awardName === 'thumbsup' || awardName === 'thumbsdown')) { + const counterAward = awardName === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + const note = context.getters.notesById[noteId]; + let amIAwarded = false; + + note.award_emoji.forEach((a) => { + if (a.name === counterAward && a.user.id === window.gon.current_user_id) { + amIAwarded = true; + } + }); + + if (amIAwarded) { + data.awardName = counterAward; + data.skipMutalityCheck = true; + context.dispatch('toggleAward', data); + } + } }); }, }; -- GitLab From d45a8e006a4718b30a2669f7b888447e8b6dcb4f Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 11 Jul 2017 00:53:04 +0300 Subject: [PATCH 057/243] IssueNotesRefactor: Make scroll into note an action. --- app/assets/javascripts/notes/components/issue_notes.vue | 6 +----- app/assets/javascripts/notes/stores/issue_notes_store.js | 7 +++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index c9904e5e0ecb..4bf04867dc0f 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -94,12 +94,8 @@ export default { const $el = $(`#${hash}`); if (hash && $el) { - const isInViewport = gl.utils.isInViewport($el[0]); this.$store.commit('setTargetNoteHash', hash); - - if (!isInViewport) { - gl.utils.scrollToElement($el); - } + this.$store.dispatch('scrollToNoteIfNeeded', $el); } }, }, diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index bc71d1062942..3e6b883bd4b0 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -214,6 +214,13 @@ const actions = { } }); }, + scrollToNoteIfNeeded(context, el) { + const isInViewport = gl.utils.isInViewport(el[0]); + + if (!isInViewport) { + gl.utils.scrollToElement(el); + } + }, }; export default { -- GitLab From 717c30221cbaa348ce0f448e57bd3e2620c9cbcf Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 11 Jul 2017 00:53:52 +0300 Subject: [PATCH 058/243] IssueNotesRefactor: Implement up arrow to edit last note. --- .../notes/components/issue_comment_form.vue | 13 +++++++++++++ .../javascripts/notes/components/issue_note.vue | 10 ++++++++++ .../notes/components/issue_note_form.vue | 14 ++++++++++++++ .../javascripts/notes/stores/issue_notes_store.js | 4 ++-- .../shared/issuable/_close_reopen_button.html.haml | 4 ++-- 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index e4f7a1dbb791..e070ee0ff3a0 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -4,6 +4,7 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import MarkdownField from '../../vue_shared/components/markdown/field.vue'; import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; +import eventHub from '../event_hub'; export default { props: {}, @@ -101,6 +102,17 @@ export default { handleError() { new Flash('Something went wrong while adding your comment. Please try again.'); // eslint-disable-line }, + editMyLastNote() { + if (this.note === '') { + const myLastNoteId = $('.js-my-note').last().attr('id'); + + if (myLastNoteId) { + eventHub.$emit('EnterEditMode', { + noteId: parseInt(myLastNoteId.replace('note_', ''), 10), + }); + } + } + }, }, mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); @@ -143,6 +155,7 @@ export default { ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." + @keydown.up="editMyLastNote" @keydown.meta.enter="handleSave()"> </textarea> </markdown-field> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 1dd4786d6058..e2fe638bef8f 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -6,6 +6,7 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ import IssueNoteHeader from './issue_note_header.vue'; import IssueNoteActions from './issue_note_actions.vue'; import IssueNoteBody from './issue_note_body.vue'; +import eventHub from '../event_hub'; export default { props: { @@ -37,6 +38,7 @@ export default { return { 'is-editing': this.isEditing, 'disabled-content': this.isDeleting, + 'js-my-note': this.author.id === window.gon.current_user_id, target: this.targetNoteHash === this.noteAnchorId, }; }, @@ -100,6 +102,14 @@ export default { this.isEditing = false; }, }, + created() { + eventHub.$on('EnterEditMode', ({ noteId }) => { + if (noteId === this.note.id) { + this.isEditing = true; + this.$store.dispatch('scrollToNoteIfNeeded', $(this.$el)); + } + }); + }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index b322f777968e..86ff8bd8c693 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -1,5 +1,6 @@ <script> import MarkdownField from '../../vue_shared/components/markdown/field.vue'; +import eventHub from '../event_hub'; export default { props: { @@ -39,6 +40,18 @@ export default { note: this.note, }); }, + editMyLastNote() { + if (this.note === '') { + const discussion = $(this.$el).closest('.discussion-notes'); + const myLastNoteId = discussion.find('.js-my-note').last().attr('id'); + + if (myLastNoteId) { + eventHub.$emit('EnterEditMode', { + noteId: parseInt(myLastNoteId.replace('note_', ''), 10), + }); + } + } + }, }, computed: { isDirty() { @@ -75,6 +88,7 @@ export default { slot="textarea" placeholder="Write a comment or drag your files here..." @keydown.meta.enter="handleUpdate" + @keydown.up="editMyLastNote" @keydown.esc="cancelHandler(true)"> </textarea> </markdown-field> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 3e6b883bd4b0..a5811515fc4f 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -197,10 +197,10 @@ const actions = { if (!skipMutalityCheck && (awardName === 'thumbsup' || awardName === 'thumbsdown')) { const counterAward = awardName === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; - const note = context.getters.notesById[noteId]; + const targetNote = context.getters.notesById[noteId]; let amIAwarded = false; - note.award_emoji.forEach((a) => { + targetNote.award_emoji.forEach((a) => { if (a.name === counterAward && a.user.id === window.gon.current_user_id) { amIAwarded = true; } diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index 8a1268a1c6de..f16bc8dd4307 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -4,9 +4,9 @@ - if can_update && is_current_user = link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method, - class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" + class: "hidden-xs hidden-sm btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" = link_to "Reopen #{display_issuable_type}", reopen_issuable_url(issuable), method: button_method, - class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" + class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - elsif can_update && !is_current_user = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable - else -- GitLab From ddb193d09d280495ba60dab2ee0c097a57ce4752 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 11 Jul 2017 18:35:28 +0300 Subject: [PATCH 059/243] IssueNotesRefactor: Add GfmAutoComplete references to page. --- app/views/projects/issues/_discussion.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index de92e55a5bcd..90b97d36d2cb 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -13,6 +13,8 @@ / #notes{style: "margin-top: 150px"} / = render 'shared/notes/notes_with_form', :autocomplete => true += render "layouts/init_auto_complete" + :javascript window.gl.issueData = #{serialize_issuable(@issue)}; window.gl.currentUserData = #{UserSerializer.new.represent(current_user).to_json}; -- GitLab From 4e86445b9da91762f278619bfe490f9c76f4531b Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 12 Jul 2017 01:35:07 +0300 Subject: [PATCH 060/243] IssueNotesRefactor: Fix issue reopen/close bug after merging from master. --- app/assets/javascripts/issue.js | 4 ++-- .../javascripts/notes/components/issue_comment_form.vue | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 2bee4fb045a6..733ae48e8829 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -42,7 +42,7 @@ class Issue { initIssueBtnEventListeners() { const issueFailMessage = 'Unable to update this issue at this time.'; - return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => { + return $(document).on('click', '.issuable-actions a.btn-close, .issuable-actions a.btn-reopen', (e) => { var $button, shouldSubmit, url; e.preventDefault(); e.stopImmediatePropagation(); @@ -121,7 +121,7 @@ class Issue { static submitNoteForm(form) { var noteText; noteText = form.find("textarea.js-note-text").val(); - if (noteText.trim().length > 0) { + if (noteText && noteText.trim().length > 0) { return form.submit(); } } diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index e070ee0ff3a0..9eb54bc21ed9 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -89,7 +89,8 @@ export default { // This is out of scope for the Notes Vue component. // It was the shortest path to update the issue state and relevant places. - $('.js-btn-issue-action:visible').trigger('click'); + const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close'; + $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); } }, discard() { @@ -215,7 +216,7 @@ export default { </div> <a @click="handleSave(true)" - :class="{'btn-reopen': issueState === 'closed', 'btn-close': issueState === 'open'}" + :class="{'btn-reopen': !isIssueOpen, 'btn-close': isIssueOpen}" class="btn btn-nr btn-comment"> {{issueActionButtonTitle}} </a> -- GitLab From 77f6f0b88b1367c72c4c28a254e7c874f64b4b67 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 12 Jul 2017 01:57:28 +0300 Subject: [PATCH 061/243] IssueNotesRefactor: Listen main issue action and update note form buttons. --- app/assets/javascripts/issue.js | 3 +-- .../javascripts/notes/components/issue_comment_form.vue | 4 ++++ app/assets/javascripts/notes/components/issue_notes.vue | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 733ae48e8829..343e932ba843 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -66,12 +66,11 @@ class Issue { const projectIssuesCounter = $('.issue_counter'); if ('id' in data) { - $(document).trigger('issuable:change'); - const isClosed = $button.hasClass('btn-close'); isClosedBadge.toggleClass('hidden', !isClosed); isOpenBadge.toggleClass('hidden', isClosed); + $(document).trigger('issuable:change', isClosed); this.toggleCloseReopenButton(isClosed); let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, '')); diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 9eb54bc21ed9..be81a5885719 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -122,6 +122,10 @@ export default { this.markdownDocsUrl = markdownDocs; this.markdownPreviewUrl = markdownPreviewUrl; + + eventHub.$on('IssueStateChanged', (isClosed) => { + this.issueState = isClosed ? 'closed' : 'reopened'; + }); }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 4bf04867dc0f..a0c951df8226 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -88,6 +88,10 @@ export default { new Flash('Something went wrong on our end.'); // eslint-disable-line }); }); + + $(document).on('issuable:change', (e, isClosed) => { + eventHub.$emit('IssueStateChanged', isClosed); + }); }, checkLocationHash() { const hash = gl.utils.getLocationHash(); -- GitLab From a45157783320422a500b2b62a318558199ca5081 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 12 Jul 2017 23:53:48 +0300 Subject: [PATCH 062/243] IssueNotesRefactor: Fix ToDo list editability. --- .../notes/components/issue_note.vue | 1 + .../notes/components/issue_note_body.vue | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index e2fe638bef8f..39f153a3045c 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -147,6 +147,7 @@ export default { </div> <issue-note-body :note="note" + :canEdit="note.current_user.can_edit" :isEditing="isEditing" :formUpdateHandler="formUpdateHandler" :formCancelHandler="formCancelHandler" diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index b5a8a624c858..73af85b3b171 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -2,6 +2,7 @@ import IssueNoteEditedText from './issue_note_edited_text.vue'; import IssueNoteAwardsList from './issue_note_awards_list.vue'; import IssueNoteForm from './issue_note_form.vue'; +import TaskList from '../../task_list'; export default { props: { @@ -9,6 +10,10 @@ export default { type: Object, required: true, }, + canEdit: { + type: Boolean, + required: true, + }, isEditing: { type: Boolean, required: false, @@ -32,6 +37,15 @@ export default { renderGFM() { $(this.$refs['note-body']).renderGFM(); }, + initTaskList() { + if (this.canEdit) { + new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes' + }); + } + }, handleFormUpdate() { this.formUpdateHandler({ note: this.$refs.noteForm.note, @@ -40,12 +54,17 @@ export default { }, mounted() { this.renderGFM(); + this.initTaskList(); + }, + updated() { + this.initTaskList(); }, }; </script> <template> <div + :class="{ 'js-task-list-container': canEdit }" ref="note-body" class="note-body"> <div @@ -57,6 +76,11 @@ export default { :updateHandler="handleFormUpdate" :cancelHandler="formCancelHandler" :noteBody="note.note" /> + <textarea + v-if="canEdit" + v-model="note.note" + :data-update-url="note.path" + class="hidden js-task-list-field"></textarea> <issue-note-edited-text v-if="note.last_edited_by" :editedAt="note.last_edited_at" -- GitLab From f15be51b6c30b967a2adcf16d5a0eb227e2f951e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 13 Jul 2017 00:50:25 +0300 Subject: [PATCH 063/243] IssueNotesRefactor: Wrap comment form with needed element. --- .../notes/components/issue_comment_form.vue | 188 +++++++++--------- 1 file changed, 95 insertions(+), 93 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index be81a5885719..6e219f1b5882 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -137,100 +137,102 @@ export default { v-if="isLoggedIn" class="notes notes-form timeline new-note"> <li class="timeline-entry"> - <div class="timeline-icon hidden-xs hidden-sm"> - <user-avatar-link - v-if="author" - :linkHref="author.path" - :imgSrc="author.avatar_url" - :imgAlt="author.name" - :imgSize="40" /> - </div> - <div class="timeline-content timeline-content-form common-note-form"> - <markdown-field - :markdown-preview-url="markdownPreviewUrl" - :markdown-docs="markdownDocsUrl" - :addSpacingClasses="false"> - <textarea - id="note-body" - class="note-textarea js-gfm-input js-autosize markdown-area" - data-supports-slash-commands="true" - data-supports-quick-actions="true" - aria-label="Description" - v-model="note" - ref="textarea" - slot="textarea" - placeholder="Write a comment or drag your files here..." - @keydown.up="editMyLastNote" - @keydown.meta.enter="handleSave()"> - </textarea> - </markdown-field> - <div class="note-form-actions clearfix"> - <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown"> - <input - @click="handleSave()" - :disabled="!note.length" - :value="commentButtonTitle" - class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" - type="submit" /> - <button - :disabled="!note.length" - name="button" - type="button" - class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion" - data-toggle="dropdown" - aria-label="Open comment type dropdown"> - <i - aria-hidden="true" - class="fa fa-caret-down toggle-icon"></i> - </button> - <ul - class="dropdown-menu note-type-dropdown dropdown-open-top"> - <li - :class="{ 'item-selected': noteType === 'comment' }" - @click.prevent="setNoteType('comment')"> - <a href="#"> - <i - aria-hidden="true" - class="fa fa-check"></i> - <div class="description"> - <strong>Comment</strong> - <p> - Add a general comment to this issue. - </p> - </div> - </a> - </li> - <li class="divider"></li> - <li - :class="{ 'item-selected': noteType === 'discussion' }" - @click.prevent="setNoteType('discussion')"> - <a href="#"> - <i - aria-hidden="true" - class="fa fa-check"></i> - <div class="description"> - <strong>Start discussion</strong> - <p> - Discuss a specific suggestion or question. - </p> - </div> - </a> - </li> - </ul> + <div class="timeline-entry-inner"> + <div class="timeline-icon hidden-xs hidden-sm"> + <user-avatar-link + v-if="author" + :linkHref="author.path" + :imgSrc="author.avatar_url" + :imgAlt="author.name" + :imgSize="40" /> + </div> + <div class="timeline-content timeline-content-form common-note-form"> + <markdown-field + :markdown-preview-url="markdownPreviewUrl" + :markdown-docs="markdownDocsUrl" + :addSpacingClasses="false"> + <textarea + id="note-body" + class="note-textarea js-gfm-input js-autosize markdown-area" + data-supports-slash-commands="true" + data-supports-quick-actions="true" + aria-label="Description" + v-model="note" + ref="textarea" + slot="textarea" + placeholder="Write a comment or drag your files here..." + @keydown.up="editMyLastNote" + @keydown.meta.enter="handleSave()"> + </textarea> + </markdown-field> + <div class="note-form-actions clearfix"> + <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown"> + <input + @click="handleSave()" + :disabled="!note.length" + :value="commentButtonTitle" + class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" + type="submit" /> + <button + :disabled="!note.length" + name="button" + type="button" + class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion" + data-toggle="dropdown" + aria-label="Open comment type dropdown"> + <i + aria-hidden="true" + class="fa fa-caret-down toggle-icon"></i> + </button> + <ul + class="dropdown-menu note-type-dropdown dropdown-open-top"> + <li + :class="{ 'item-selected': noteType === 'comment' }" + @click.prevent="setNoteType('comment')"> + <a href="#"> + <i + aria-hidden="true" + class="fa fa-check"></i> + <div class="description"> + <strong>Comment</strong> + <p> + Add a general comment to this issue. + </p> + </div> + </a> + </li> + <li class="divider"></li> + <li + :class="{ 'item-selected': noteType === 'discussion' }" + @click.prevent="setNoteType('discussion')"> + <a href="#"> + <i + aria-hidden="true" + class="fa fa-check"></i> + <div class="description"> + <strong>Start discussion</strong> + <p> + Discuss a specific suggestion or question. + </p> + </div> + </a> + </li> + </ul> + </div> + <a + @click="handleSave(true)" + :class="{'btn-reopen': !isIssueOpen, 'btn-close': isIssueOpen}" + class="btn btn-nr btn-comment"> + {{issueActionButtonTitle}} + </a> + <a + v-if="note.length" + @click="discard" + class="btn btn-cancel js-note-discard" + role="button"> + Discard draft + </a> </div> - <a - @click="handleSave(true)" - :class="{'btn-reopen': !isIssueOpen, 'btn-close': isIssueOpen}" - class="btn btn-nr btn-comment"> - {{issueActionButtonTitle}} - </a> - <a - v-if="note.length" - @click="discard" - class="btn btn-cancel js-note-discard" - role="button"> - Discard draft - </a> </div> </div> </li> -- GitLab From 820c0d6dd120f1bf137a985bba8abf5a8ca97ac0 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 13 Jul 2017 14:31:43 +0300 Subject: [PATCH 064/243] IssueNotesRefactor: Clear AC cache. --- .../javascripts/notes/components/issue_comment_form.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 6e219f1b5882..08b67ee90766 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -94,8 +94,11 @@ export default { } }, discard() { - this.note = ''; + // `blur` is needed to clear slash commands autocomplete cache if event fired. + // `focus` is needed to remain cursor in the textarea. + this.$refs.textarea.blur(); this.$refs.textarea.focus(); + this.note = ''; }, setNoteType(type) { this.noteType = type; -- GitLab From d9928d1a7c8d9ea08759b26b49a874d4ad0138f1 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 13 Jul 2017 14:32:09 +0300 Subject: [PATCH 065/243] IssueNotesRefactor: Decouple poll from main component to increase reusability. --- .../javascripts/notes/components/issue_notes.vue | 11 ++++------- .../javascripts/notes/stores/issue_notes_store.js | 8 ++++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index a0c951df8226..95ab4a5e2548 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -61,17 +61,14 @@ export default { }); }, initPolling() { - const { notesPath, lastFetchedAt } = this.$el.parentNode.dataset; - const options = { - endpoint: `${notesPath}?full_data=1`, - lastFetchedAt, - }; + const { lastFetchedAt } = $('.js-notes-wrapper')[0].dataset; + this.$store.commit('setLastFetchedAt', lastFetchedAt); // FIXME: @fatihacet Implement real polling mechanism setInterval(() => { - this.$store.dispatch('poll', options) + this.$store.dispatch('poll') .then((res) => { - options.lastFetchedAt = res.last_fetched_at; + this.$store.commit('setLastFetchedAt', res.lastFetchedAt); }) .catch(() => { new Flash('Something went wrong while fetching latest comments.'); // eslint-disable-line diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index a5811515fc4f..ef5bb8dce3d4 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -7,6 +7,7 @@ const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; const state = { notes: [], targetNoteHash: null, + lastFetchedAt: null, }; const getters = { @@ -104,6 +105,9 @@ const mutations = { }); } }, + setLastFetchedAt(storeState, fetchedAt) { + storeState.lastFetchedAt = fetchedAt; + }, }; const actions = { @@ -156,10 +160,10 @@ const actions = { }); }, poll(context, data) { - const { endpoint, lastFetchedAt } = data; + const { notesPath } = $('.js-notes-wrapper')[0].dataset; return service - .poll(endpoint, lastFetchedAt) + .poll(`${notesPath}?full_data=1`, context.state.lastFetchedAt) .then(res => res.json()) .then((res) => { if (res.notes.length) { -- GitLab From bf88af0e24ab79f998f311db8d7dc7fca675318e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 13 Jul 2017 16:31:48 +0300 Subject: [PATCH 066/243] IssueNotesRefactor: Implement quick actions with system note placeholders. --- .../notes/components/issue_comment_form.vue | 62 ++++++++++++++++--- .../notes/components/issue_notes.vue | 9 ++- .../issue_placeholder_system_note.vue | 20 ++++++ .../notes/stores/issue_notes_store.js | 26 ++++++++ 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_placeholder_system_note.vue diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 08b67ee90766..7e2cf8fc0e95 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,13 +1,14 @@ <script> /* global Flash */ +import AjaxCache from '~/lib/utils/ajax_cache'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import MarkdownField from '../../vue_shared/components/markdown/field.vue'; import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import eventHub from '../event_hub'; +const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; export default { - props: {}, data() { const { create_note_path, state } = window.gl.issueData; const { currentUserData } = window.gl; @@ -67,14 +68,14 @@ export default { } this.$store.dispatch('createNewNote', data) - .then((res) => { - if (res.errors) { - this.handleError(); - } else { - this.discard(); - } - }) + .then(this.handleNewNoteCreated) .catch(this.handleError); + + if (this.hasQuickActions()) { + this.$store.commit('showPlaceholderSystemNote', { + noteBody: this.getQuickActionText(), + }); + } } if (withIssueAction) { @@ -93,6 +94,26 @@ export default { $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); } }, + handleNewNoteCreated(res) { + const { commands_changes, errors, valid } = res; + + if (!valid && errors) { + const { commands_only } = errors; + + if (commands_only) { + new Flash(commands_only, 'notice', $(this.$el)); // eslint-disable-line + $(this.$refs.textarea).trigger('clear-commands-cache.atwho'); + this.$store.dispatch('poll'); + this.discard(); + } else { + this.handleError(); + } + } else { + this.discard(); + } + + this.$store.commit('removePlaceholderSystemNote'); + }, discard() { // `blur` is needed to clear slash commands autocomplete cache if event fired. // `focus` is needed to remain cursor in the textarea. @@ -117,6 +138,30 @@ export default { } } }, + getQuickActionText() { + let text = 'Applying command'; + const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands); + const { note } = this; + + const executedCommands = quickActions.filter((command, index) => { + const commandRegex = new RegExp(`/${command.name}`); + return commandRegex.test(note); + }); + + if (executedCommands && executedCommands.length) { + if (executedCommands.length > 1) { + text = 'Applying multiple commands'; + } else { + const commandDescription = executedCommands[0].description.toLowerCase(); + text = `Applying command to ${commandDescription}`; + } + } + + return text; + }, + hasQuickActions() { + return REGEX_QUICK_ACTIONS.test(this.note); + }, }, mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); @@ -141,6 +186,7 @@ export default { class="notes notes-form timeline new-note"> <li class="timeline-entry"> <div class="timeline-entry-inner"> + <div class="flash-container timeline-content"></div> <div class="timeline-icon hidden-xs hidden-sm"> <user-avatar-link v-if="author" diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 95ab4a5e2548..f9de277e4657 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -9,6 +9,7 @@ import IssueNote from './issue_note.vue'; import IssueDiscussion from './issue_discussion.vue'; import IssueSystemNote from './issue_system_note.vue'; import IssueCommentForm from './issue_comment_form.vue'; +import PlaceholderSystemNote from './issue_placeholder_system_note.vue'; Vue.use(Vuex); const store = new Vuex.Store(storeOptions); @@ -26,6 +27,7 @@ export default { IssueDiscussion, IssueSystemNote, IssueCommentForm, + PlaceholderSystemNote, }, computed: { ...Vuex.mapGetters([ @@ -35,7 +37,12 @@ export default { }, methods: { component(note) { - if (note.individual_note) { + if (note.placeholderNote) { + if (note.placeholderType === 'systemNote') { + return PlaceholderSystemNote; + } + } + else if (note.individual_note) { return note.notes[0].system ? IssueSystemNote : IssueNote; } diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue new file mode 100644 index 000000000000..d84e236c92c4 --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue @@ -0,0 +1,20 @@ +<script> +export default { + props: { + note: { + type: Object, + required: true, + } + }, +}; +</script> + +<template> + <li class="note system-note timeline-entry being-posted fade-in-half"> + <div class="timeline-entry-inner"> + <div class="timeline-content"> + <i>{{note.body}}</i> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index ef5bb8dce3d4..5da416b9320d 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -108,6 +108,32 @@ const mutations = { setLastFetchedAt(storeState, fetchedAt) { storeState.lastFetchedAt = fetchedAt; }, + showPlaceholderSystemNote(storeState, data) { + storeState.notes.push({ + placeholderNote: true, + individual_note: true, + placeholderType: 'systemNote', + notes: [ + { + id: 'placeholderSystemNote', + body: data.noteBody, + }, + ], + }); + }, + removePlaceholderSystemNote(storeState) { + let index = -1; + + storeState.notes.forEach((n, i) => { + if (n.placeholderNote && n.placeholderType === 'systemNote') { + index = i; + } + }); + + if (index > -1) { + storeState.notes.splice(index, 1); + } + }, }; const actions = { -- GitLab From b3da0ab5e827af4b3ba99c488c9c9c9ef5940450 Mon Sep 17 00:00:00 2001 From: Simon Knox <psimyn@gmail.com> Date: Fri, 14 Jul 2017 07:09:21 +1000 Subject: [PATCH 067/243] js-main-target-form class needed for tests to init textarea --- app/assets/javascripts/notes/components/issue_comment_form.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 7e2cf8fc0e95..2673b3a32027 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -195,7 +195,7 @@ export default { :imgAlt="author.name" :imgSize="40" /> </div> - <div class="timeline-content timeline-content-form common-note-form"> + <div class="js-main-target-form timeline-content timeline-content-form common-note-form"> <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" -- GitLab From 4dce23c00b88a1010877464c7f5e6413889de813 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 14 Jul 2017 00:21:53 +0300 Subject: [PATCH 068/243] IssueNotesRefactor: Implement showing placeholder note while creating note. --- .../notes/components/issue_comment_form.vue | 72 ++++++++++++------- .../notes/components/issue_note_body.vue | 4 +- .../notes/components/issue_notes.vue | 8 ++- .../components/issue_placeholder_note.vue | 46 ++++++++++++ .../issue_placeholder_system_note.vue | 2 +- .../notes/stores/issue_notes_store.js | 23 +++--- 6 files changed, 108 insertions(+), 47 deletions(-) create mode 100644 app/assets/javascripts/notes/components/issue_placeholder_note.vue diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 2673b3a32027..fbe339bd2739 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -6,8 +6,8 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ import MarkdownField from '../../vue_shared/components/markdown/field.vue'; import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import eventHub from '../event_hub'; -const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; +const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; export default { data() { const { create_note_path, state } = window.gl.issueData; @@ -67,15 +67,50 @@ export default { data.noteData.note.type = 'DiscussionNote'; } - this.$store.dispatch('createNewNote', data) - .then(this.handleNewNoteCreated) - .catch(this.handleError); + let placeholderText = this.note; + const hasQuickActions = this.hasQuickActions(); + + if (hasQuickActions) { + placeholderText = this.stripQuickActions(); + } - if (this.hasQuickActions()) { - this.$store.commit('showPlaceholderSystemNote', { + if (placeholderText.length) { + this.$store.commit('showPlaceholderNote', { + noteBody: placeholderText, + }); + } + + if (hasQuickActions) { + this.$store.commit('showPlaceholderNote', { + isSystemNote: true, noteBody: this.getQuickActionText(), }); } + + this.$store.dispatch('createNewNote', data) + .then((res) => { + const { errors } = res; + + if (hasQuickActions) { + this.$store.dispatch('poll'); + $(this.$refs.textarea).trigger('clear-commands-cache.atwho'); + new Flash('Commands applied', 'notice', $(this.$el)); // eslint-disable-line + } + + if (errors) { + if (errors.commands_only) { + new Flash(errors.commands_only, 'notice', $(this.$el)); // eslint-disable-line + this.discard(); + } else { + this.handleError(); + } + } else { + this.discard(); + } + + this.$store.commit('removePlaceholderNotes'); + }) + .catch(this.handleError); } if (withIssueAction) { @@ -94,26 +129,6 @@ export default { $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); } }, - handleNewNoteCreated(res) { - const { commands_changes, errors, valid } = res; - - if (!valid && errors) { - const { commands_only } = errors; - - if (commands_only) { - new Flash(commands_only, 'notice', $(this.$el)); // eslint-disable-line - $(this.$refs.textarea).trigger('clear-commands-cache.atwho'); - this.$store.dispatch('poll'); - this.discard(); - } else { - this.handleError(); - } - } else { - this.discard(); - } - - this.$store.commit('removePlaceholderSystemNote'); - }, discard() { // `blur` is needed to clear slash commands autocomplete cache if event fired. // `focus` is needed to remain cursor in the textarea. @@ -143,7 +158,7 @@ export default { const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands); const { note } = this; - const executedCommands = quickActions.filter((command, index) => { + const executedCommands = quickActions.filter((command) => { const commandRegex = new RegExp(`/${command.name}`); return commandRegex.test(note); }); @@ -162,6 +177,9 @@ export default { hasQuickActions() { return REGEX_QUICK_ACTIONS.test(this.note); }, + stripQuickActions() { + return this.note.replace(REGEX_QUICK_ACTIONS, '').trim(); + }, }, mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 73af85b3b171..ee9318cbd6a0 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -39,10 +39,10 @@ export default { }, initTaskList() { if (this.canEdit) { - new TaskList({ + this.taskList = new TaskList({ dataType: 'note', fieldName: 'note', - selector: '.notes' + selector: '.notes', }); } }, diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index f9de277e4657..18e181d1b805 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -9,6 +9,7 @@ import IssueNote from './issue_note.vue'; import IssueDiscussion from './issue_discussion.vue'; import IssueSystemNote from './issue_system_note.vue'; import IssueCommentForm from './issue_comment_form.vue'; +import PlaceholderNote from './issue_placeholder_note.vue'; import PlaceholderSystemNote from './issue_placeholder_system_note.vue'; Vue.use(Vuex); @@ -27,6 +28,7 @@ export default { IssueDiscussion, IssueSystemNote, IssueCommentForm, + PlaceholderNote, PlaceholderSystemNote, }, computed: { @@ -37,12 +39,12 @@ export default { }, methods: { component(note) { - if (note.placeholderNote) { + if (note.isPlaceholderNote) { if (note.placeholderType === 'systemNote') { return PlaceholderSystemNote; } - } - else if (note.individual_note) { + return PlaceholderNote; + } else if (note.individual_note) { return note.notes[0].system ? IssueSystemNote : IssueNote; } diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_note.vue new file mode 100644 index 000000000000..af249c56a3df --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_placeholder_note.vue @@ -0,0 +1,46 @@ +<script> +export default { + props: { + note: { + type: Object, + required: true, + }, + }, + data() { + return { + currentUser: window.gl.currentUserData, + }; + }, +}; +</script> + +<template> + <li class="note being-posted fade-in-half timeline-entry"> + <div class="timeline-entry-inner"> + <div class="timeline-icon"> + <a :href="currentUser.path"> + <img + :src="currentUser.avatar_url" + class="avatar s40" /> + </a> + </div> + <div + :class="{ discussion: !note.individual_note }" + class="timeline-content"> + <div class="note-header"> + <div class="note-header-info"> + <a :href="currentUser.path"> + <span class="hidden-xs">{{currentUser.name}}</span> + <span class="note-headline-light">@{{currentUser.username}}</span> + </a> + </div> + </div> + <div class="note-body"> + <div class="note-text"> + <p>{{note.body}}</p> + </div> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue index d84e236c92c4..6738d82e7e7c 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue @@ -4,7 +4,7 @@ export default { note: { type: Object, required: true, - } + }, }, }; </script> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 5da416b9320d..469b850c3fba 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -108,30 +108,25 @@ const mutations = { setLastFetchedAt(storeState, fetchedAt) { storeState.lastFetchedAt = fetchedAt; }, - showPlaceholderSystemNote(storeState, data) { + showPlaceholderNote(storeState, data) { storeState.notes.push({ - placeholderNote: true, individual_note: true, - placeholderType: 'systemNote', + isPlaceholderNote: true, + placeholderType: data.isSystemNote ? 'systemNote' : 'note', notes: [ { - id: 'placeholderSystemNote', body: data.noteBody, }, ], }); }, - removePlaceholderSystemNote(storeState) { - let index = -1; + removePlaceholderNotes(storeState) { + const { notes } = storeState; - storeState.notes.forEach((n, i) => { - if (n.placeholderNote && n.placeholderType === 'systemNote') { - index = i; + for (let i = notes.length - 1; i >= 0; i -= 1) { + if (notes[i].isPlaceholderNote) { + notes.splice(i, 1); } - }); - - if (index > -1) { - storeState.notes.splice(index, 1); } }, }; @@ -185,7 +180,7 @@ const actions = { return res; }); }, - poll(context, data) { + poll(context) { const { notesPath } = $('.js-notes-wrapper')[0].dataset; return service -- GitLab From eae72cc71436d80395a3dcb4316b48c768a17d19 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 14 Jul 2017 17:09:06 +0300 Subject: [PATCH 069/243] IssueNotesRefactor: Move quick actions logic to Store. --- .../notes/components/issue_comment_form.vue | 73 ++----------------- .../notes/components/issue_discussion.vue | 6 +- .../notes/stores/issue_notes_store.js | 66 ++++++++++++++--- .../notes/stores/issue_notes_utils.js | 32 ++++++++ 4 files changed, 97 insertions(+), 80 deletions(-) create mode 100644 app/assets/javascripts/notes/stores/issue_notes_utils.js diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index fbe339bd2739..e19bd5b71612 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,13 +1,11 @@ <script> /* global Flash */ -import AjaxCache from '~/lib/utils/ajax_cache'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import MarkdownField from '../../vue_shared/components/markdown/field.vue'; import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import eventHub from '../event_hub'; -const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; export default { data() { const { create_note_path, state } = window.gl.issueData; @@ -51,9 +49,10 @@ export default { methods: { handleSave(withIssueAction) { if (this.note.length) { - const data = { + const noteData = { endpoint: this.endpoint, - noteData: { + flashContainer: this.$el, + data: { full_data: true, note: { noteable_type: 'Issue', @@ -64,42 +63,13 @@ export default { }; if (this.noteType === 'discussion') { - data.noteData.note.type = 'DiscussionNote'; + noteData.data.note.type = 'DiscussionNote'; } - let placeholderText = this.note; - const hasQuickActions = this.hasQuickActions(); - - if (hasQuickActions) { - placeholderText = this.stripQuickActions(); - } - - if (placeholderText.length) { - this.$store.commit('showPlaceholderNote', { - noteBody: placeholderText, - }); - } - - if (hasQuickActions) { - this.$store.commit('showPlaceholderNote', { - isSystemNote: true, - noteBody: this.getQuickActionText(), - }); - } - - this.$store.dispatch('createNewNote', data) + this.$store.dispatch('saveNote', noteData) .then((res) => { - const { errors } = res; - - if (hasQuickActions) { - this.$store.dispatch('poll'); - $(this.$refs.textarea).trigger('clear-commands-cache.atwho'); - new Flash('Commands applied', 'notice', $(this.$el)); // eslint-disable-line - } - - if (errors) { - if (errors.commands_only) { - new Flash(errors.commands_only, 'notice', $(this.$el)); // eslint-disable-line + if (res.errors) { + if (res.errors.commands_only) { this.discard(); } else { this.handleError(); @@ -107,8 +77,6 @@ export default { } else { this.discard(); } - - this.$store.commit('removePlaceholderNotes'); }) .catch(this.handleError); } @@ -153,33 +121,6 @@ export default { } } }, - getQuickActionText() { - let text = 'Applying command'; - const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands); - const { note } = this; - - const executedCommands = quickActions.filter((command) => { - const commandRegex = new RegExp(`/${command.name}`); - return commandRegex.test(note); - }); - - if (executedCommands && executedCommands.length) { - if (executedCommands.length > 1) { - text = 'Applying multiple commands'; - } else { - const commandDescription = executedCommands[0].description.toLowerCase(); - text = `Applying command to ${commandDescription}`; - } - } - - return text; - }, - hasQuickActions() { - return REGEX_QUICK_ACTIONS.test(this.note); - }, - stripQuickActions() { - return this.note.replace(REGEX_QUICK_ACTIONS, '').trim(); - }, }, mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 3ad48c78ab01..0361c31223e7 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -63,9 +63,9 @@ export default { this.isReplying = false; }, saveReply({ note }) { - const data = { + const replyData = { endpoint: this.newNotePath, - reply: { + data: { in_reply_to_discussion_id: this.note.reply_id, target_type: 'issue', target_id: this.discussion.noteable_id, @@ -74,7 +74,7 @@ export default { }, }; - this.$store.dispatch('replyToDiscussion', data) + this.$store.dispatch('saveNote', replyData) .then(() => { this.isReplying = false; }) diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 469b850c3fba..6942e26e1389 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ import service from '../services/issue_notes_service'; +import utils from './issue_notes_utils'; const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; @@ -147,31 +148,31 @@ const actions = { context.commit('deleteNote', note); }); }, - replyToDiscussion(context, data) { - const { endpoint, reply } = data; + updateNote(context, data) { + const { endpoint, note } = data; return service - .replyToDiscussion(endpoint, reply) + .updateNote(endpoint, note) .then(res => res.json()) .then((res) => { - context.commit('addNewReplyToDiscussion', res); + context.commit('updateNote', res); }); }, - updateNote(context, data) { - const { endpoint, note } = data; + replyToDiscussion(context, noteData) { + const { endpoint, data } = noteData; return service - .updateNote(endpoint, note) + .replyToDiscussion(endpoint, data) .then(res => res.json()) .then((res) => { - context.commit('updateNote', res); + context.commit('addNewReplyToDiscussion', res); }); }, - createNewNote(context, data) { - const { endpoint, noteData } = data; + createNewNote(context, noteData) { + const { endpoint, data } = noteData; return service - .createNewNote(endpoint, noteData) + .createNewNote(endpoint, data) .then(res => res.json()) .then((res) => { if (!res.errors) { @@ -180,6 +181,49 @@ const actions = { return res; }); }, + saveNote(context, noteData) { + const { note } = noteData.data.note; + let placeholderText = note; + const hasQuickActions = utils.hasQuickActions(placeholderText); + + if (hasQuickActions) { + placeholderText = utils.stripQuickActions(placeholderText); + } + + if (placeholderText.length) { + context.commit('showPlaceholderNote', { + noteBody: placeholderText, + }); + } + + if (hasQuickActions) { + context.commit('showPlaceholderNote', { + isSystemNote: true, + noteBody: utils.getQuickActionText(note), + }); + } + + const hasReplyId = noteData.data.in_reply_to_discussion_id; + const methodToDispatch = hasReplyId ? 'replyToDiscussion' : 'createNewNote'; + + return context.dispatch(methodToDispatch, noteData) + .then((res) => { + const { errors } = res; + + if (hasQuickActions) { + context.dispatch('poll'); + $('.js-gfm-input').trigger('clear-commands-cache.atwho'); + new Flash('Commands applied', 'notice', $(noteData.flashContainer)); // eslint-disable-line + } + + if (errors && errors.commands_only) { + new Flash(errors.commands_only, 'notice', $(noteData.flashContainer)); // eslint-disable-line + } + context.commit('removePlaceholderNotes'); + + return res; + }); + }, poll(context) { const { notesPath } = $('.js-notes-wrapper')[0].dataset; diff --git a/app/assets/javascripts/notes/stores/issue_notes_utils.js b/app/assets/javascripts/notes/stores/issue_notes_utils.js new file mode 100644 index 000000000000..6d08ba0ac7d7 --- /dev/null +++ b/app/assets/javascripts/notes/stores/issue_notes_utils.js @@ -0,0 +1,32 @@ +import AjaxCache from '~/lib/utils/ajax_cache'; + +const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; + +export default { + getQuickActionText(note) { + let text = 'Applying command'; + const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands); + + const executedCommands = quickActions.filter((command) => { + const commandRegex = new RegExp(`/${command.name}`); + return commandRegex.test(note); + }); + + if (executedCommands && executedCommands.length) { + if (executedCommands.length > 1) { + text = 'Applying multiple commands'; + } else { + const commandDescription = executedCommands[0].description.toLowerCase(); + text = `Applying command to ${commandDescription}`; + } + } + + return text; + }, + hasQuickActions(note) { + return REGEX_QUICK_ACTIONS.test(note); + }, + stripQuickActions(note) { + return note.replace(REGEX_QUICK_ACTIONS, '').trim(); + }, +} -- GitLab From aed5632ca4d848ebc1e0cfed3465807cf36afe9e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 14 Jul 2017 18:11:12 +0300 Subject: [PATCH 070/243] IssueNotesRefactor: Fix showing placeholders in discussion wrapper. --- .../notes/components/issue_discussion.vue | 25 ++++++++++++++--- .../notes/stores/issue_notes_store.js | 27 +++++++++++++++---- .../notes/stores/issue_notes_utils.js | 2 +- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 0361c31223e7..1a881335082a 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -8,6 +8,8 @@ import IssueNoteActions from './issue_note_actions.vue'; import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import IssueNoteEditedText from './issue_note_edited_text.vue'; import IssueNoteForm from './issue_note_form.vue'; +import PlaceholderNote from './issue_placeholder_note.vue'; +import PlaceholderSystemNote from './issue_placeholder_system_note.vue'; export default { props: { @@ -41,8 +43,23 @@ export default { IssueNoteEditedText, IssueNoteSignedOutWidget, IssueNoteForm, + PlaceholderNote, + PlaceholderSystemNote, }, methods: { + component(note) { + if (note.isPlaceholderNote) { + if (note.placeholderType === 'systemNote') { + return PlaceholderSystemNote; + } + return PlaceholderNote; + } + + return IssueNote; + }, + componentData(note) { + return note.isPlaceholderNote ? note.notes[0] : note; + }, toggleDiscussion() { this.$store.commit('toggleDiscussion', { discussionId: this.note.id, @@ -65,6 +82,7 @@ export default { saveReply({ note }) { const replyData = { endpoint: this.newNotePath, + flashContainer: this.$el, data: { in_reply_to_discussion_id: this.note.reply_id, target_type: 'issue', @@ -120,10 +138,11 @@ export default { <div class="panel panel-default"> <div class="discussion-notes"> <ul class="notes"> - <issue-note + <component v-for="note in note.notes" - key="note.id" - :note="note" /> + :is="component(note)" + :note="componentData(note)" + key="note.id" /> </ul> <div class="flash-container"></div> <div class="discussion-reply-holder"> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 6942e26e1389..9f570c0e5b17 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -110,7 +110,12 @@ const mutations = { storeState.lastFetchedAt = fetchedAt; }, showPlaceholderNote(storeState, data) { - storeState.notes.push({ + let notesArr = storeState.notes; + if (data.replyId) { + notesArr = findNoteObjectById(notesArr, data.replyId).notes; + } + + notesArr.push({ individual_note: true, isPlaceholderNote: true, placeholderType: data.isSystemNote ? 'systemNote' : 'note', @@ -125,7 +130,16 @@ const mutations = { const { notes } = storeState; for (let i = notes.length - 1; i >= 0; i -= 1) { - if (notes[i].isPlaceholderNote) { + const note = notes[i]; + const children = note.notes; + + if (children.length && !note.individual_note) { // remove placeholder from discussions + for (let j = children.length - 1; j >= 0; j -= 1) { + if (children[j].isPlaceholderNote) { + children.splice(j, 1); + } + } + } else if (note.isPlaceholderNote) { // remove placeholders from state root notes.splice(i, 1); } } @@ -166,6 +180,8 @@ const actions = { .then(res => res.json()) .then((res) => { context.commit('addNewReplyToDiscussion', res); + + return res; }); }, createNewNote(context, noteData) { @@ -185,6 +201,8 @@ const actions = { const { note } = noteData.data.note; let placeholderText = note; const hasQuickActions = utils.hasQuickActions(placeholderText); + const replyId = noteData.data.in_reply_to_discussion_id; + const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; if (hasQuickActions) { placeholderText = utils.stripQuickActions(placeholderText); @@ -193,6 +211,7 @@ const actions = { if (placeholderText.length) { context.commit('showPlaceholderNote', { noteBody: placeholderText, + replyId, }); } @@ -200,12 +219,10 @@ const actions = { context.commit('showPlaceholderNote', { isSystemNote: true, noteBody: utils.getQuickActionText(note), + replyId, }); } - const hasReplyId = noteData.data.in_reply_to_discussion_id; - const methodToDispatch = hasReplyId ? 'replyToDiscussion' : 'createNewNote'; - return context.dispatch(methodToDispatch, noteData) .then((res) => { const { errors } = res; diff --git a/app/assets/javascripts/notes/stores/issue_notes_utils.js b/app/assets/javascripts/notes/stores/issue_notes_utils.js index 6d08ba0ac7d7..5833630211ba 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_utils.js +++ b/app/assets/javascripts/notes/stores/issue_notes_utils.js @@ -29,4 +29,4 @@ export default { stripQuickActions(note) { return note.replace(REGEX_QUICK_ACTIONS, '').trim(); }, -} +}; -- GitLab From db0b7fb39e921728385b3287d206aabbeb88690e Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 17 Jul 2017 19:36:29 +0100 Subject: [PATCH 071/243] Expire ETag cache on note when award emoji changes --- app/models/award_emoji.rb | 9 +++++++++ app/models/note.rb | 22 ++++++++++---------- spec/models/award_emoji_spec.rb | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 91b62dabbcd9..1f07caf33662 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -17,6 +17,9 @@ class AwardEmoji < ActiveRecord::Base scope :downvotes, -> { where(name: DOWNVOTE_NAME) } scope :upvotes, -> { where(name: UPVOTE_NAME) } + after_save :expire_etag_cache + after_destroy :expire_etag_cache + class << self def votes_for_collection(ids, type) select('name', 'awardable_id', 'COUNT(*) as count') @@ -32,4 +35,10 @@ def downvote? def upvote? self.name == UPVOTE_NAME end + + def expire_etag_cache + return unless awardable.is_a?(Note) + + awardable.expire_etag_cache + end end diff --git a/app/models/note.rb b/app/models/note.rb index d0e3bc0bfed1..0e120e7de169 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -299,6 +299,17 @@ def in_reply_to?(other) end end + def expire_etag_cache + return unless for_issue? + + key = Gitlab::Routing.url_helpers.project_noteable_notes_path( + noteable.project, + target_type: noteable_type.underscore, + target_id: noteable.id + ) + Gitlab::EtagCaching::Store.new.touch(key) + end + private def keep_around_commit @@ -326,15 +337,4 @@ def ensure_discussion_id def set_discussion_id self.discussion_id ||= discussion_class.discussion_id(self) end - - def expire_etag_cache - return unless for_issue? - - key = Gitlab::Routing.url_helpers.project_noteable_notes_path( - noteable.project, - target_type: noteable_type.underscore, - target_id: noteable.id - ) - Gitlab::EtagCaching::Store.new.touch(key) - end end diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index 2a9a27752c12..2925fc1271c4 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -41,4 +41,40 @@ end end end + + describe 'expiring ETag cache' do + context 'on a note' do + let(:note) { create(:note_on_issue) } + let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: note) } + + it 'calls expire_etag_cache on the note when saved' do + expect(note).to receive(:expire_etag_cache) + + award_emoji.save! + end + + it 'calls expire_etag_cache on the note when destroyed' do + expect(note).to receive(:expire_etag_cache) + + award_emoji.destroy! + end + end + + context 'on another awardable' do + let(:issue) { create(:issue) } + let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: issue) } + + it 'does not call expire_etag_cache on the issue when saved' do + expect(issue).not_to receive(:expire_etag_cache) + + award_emoji.save! + end + + it 'does not call expire_etag_cache on the issue when destroyed' do + expect(issue).not_to receive(:expire_etag_cache) + + award_emoji.destroy! + end + end + end end -- GitLab From b26637c5b127caddc72484ae7d7adbf702dfc967 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 14 Jul 2017 20:15:43 +0300 Subject: [PATCH 072/243] IssueNotesRefactor: Move inline fn to utils file. --- .../notes/stores/issue_notes_store.js | 18 ++++++++---------- .../notes/stores/issue_notes_utils.js | 3 +++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 9f570c0e5b17..b78c6184bd71 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -3,8 +3,6 @@ import service from '../services/issue_notes_service'; import utils from './issue_notes_utils'; -const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; - const state = { notes: [], targetNoteHash: null, @@ -39,17 +37,17 @@ const mutations = { storeState.targetNoteHash = hash; }, toggleDiscussion(storeState, { discussionId }) { - const discussion = findNoteObjectById(storeState.notes, discussionId); + const discussion = utils.findNoteObjectById(storeState.notes, discussionId); discussion.expanded = !discussion.expanded; }, deleteNote(storeState, note) { - const noteObj = findNoteObjectById(storeState.notes, note.discussion_id); + const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id); if (noteObj.individual_note) { storeState.notes.splice(storeState.notes.indexOf(noteObj), 1); } else { - const comment = findNoteObjectById(noteObj.notes, note.id); + const comment = utils.findNoteObjectById(noteObj.notes, note.id); noteObj.notes.splice(noteObj.notes.indexOf(comment), 1); if (!noteObj.notes.length) { @@ -58,19 +56,19 @@ const mutations = { } }, addNewReplyToDiscussion(storeState, note) { - const noteObj = findNoteObjectById(storeState.notes, note.discussion_id); + const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id); if (noteObj) { noteObj.notes.push(note); } }, updateNote(storeState, note) { - const noteObj = findNoteObjectById(storeState.notes, note.discussion_id); + const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id); if (noteObj.individual_note) { noteObj.notes.splice(0, 1, note); } else { - const comment = findNoteObjectById(noteObj.notes, note.id); + const comment = utils.findNoteObjectById(noteObj.notes, note.id); noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); } }, @@ -112,7 +110,7 @@ const mutations = { showPlaceholderNote(storeState, data) { let notesArr = storeState.notes; if (data.replyId) { - notesArr = findNoteObjectById(notesArr, data.replyId).notes; + notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes; } notesArr.push({ @@ -255,7 +253,7 @@ const actions = { if (notesById[note.id]) { context.commit('updateNote', note); } else if (note.type === 'DiscussionNote') { - const discussion = findNoteObjectById(context.state.notes, note.discussion_id); + const discussion = utils.findNoteObjectById(context.state.notes, note.discussion_id); if (discussion) { context.commit('addNewReplyToDiscussion', note); diff --git a/app/assets/javascripts/notes/stores/issue_notes_utils.js b/app/assets/javascripts/notes/stores/issue_notes_utils.js index 5833630211ba..49e19ede572a 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_utils.js +++ b/app/assets/javascripts/notes/stores/issue_notes_utils.js @@ -3,6 +3,9 @@ import AjaxCache from '~/lib/utils/ajax_cache'; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; export default { + findNoteObjectById(notes, id) { + return notes.filter(n => n.id === id)[0]; + }, getQuickActionText(note) { let text = 'Applying command'; const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands); -- GitLab From 3c2d98f4c2bad4cccc981ff2e4bc2b5212eba9d6 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 14 Jul 2017 20:16:56 +0300 Subject: [PATCH 073/243] IssueNotesRefactor: Address MR comments. --- app/assets/javascripts/issue.js | 2 +- app/assets/javascripts/lib/utils/common_utils.js | 8 ++++---- app/views/projects/issues/show.html.haml | 2 +- app/views/projects/merge_requests/_mr_title.html.haml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 343e932ba843..7c4f4da6127f 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -42,7 +42,7 @@ class Issue { initIssueBtnEventListeners() { const issueFailMessage = 'Unable to update this issue at this time.'; - return $(document).on('click', '.issuable-actions a.btn-close, .issuable-actions a.btn-reopen', (e) => { + return $(document).on('click', '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen', (e) => { var $button, shouldSubmit, url; e.preventDefault(); e.stopImmediatePropagation(); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 1580bb67d65f..92ff9a1ed8cc 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -161,12 +161,12 @@ }; gl.utils.scrollToElement = function($el) { - var top = $el.offset().top; - var mrTabsHeight = $('.merge-request-tabs').height() || 0; - var headerHeight = $('.navbar-gitlab').height() || 0; + const top = $el.offset().top; + const mrTabsHeight = $('.merge-request-tabs').height() || 0; + const headerHeight = $('.navbar-gitlab').height() || 0; return $('body, html').animate({ - scrollTop: top - mrTabsHeight - headerHeight + scrollTop: top - mrTabsHeight - headerHeight, }, 200); }; diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f4e4b6cb8faf..043f862f5525 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -22,7 +22,7 @@ = confidential_icon(@issue) = issuable_meta(@issue, @project, "Issue") - .issuable-actions + .issuable-actions.js-issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } Options diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index a2e819fb3a7f..f3c44c94a5cd 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -17,7 +17,7 @@ .issuable-meta = issuable_meta(@merge_request, @project, "Merge request") - .issuable-actions + .issuable-actions.js-issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } Options -- GitLab From 72dfc763b47dc63bd43170c0439869d97635fe7b Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 17 Jul 2017 20:12:43 +0300 Subject: [PATCH 074/243] IssueNotesRefactor: MR comment fixes. --- .../notes/components/issue_comment_form.vue | 14 ++++++++------ .../notes/components/issue_discussion.vue | 16 ++++++++-------- .../javascripts/notes/components/issue_notes.vue | 4 ++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index e19bd5b71612..05f88d32234a 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -175,17 +175,18 @@ export default { </markdown-field> <div class="note-form-actions clearfix"> <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown"> - <input + <button @click="handleSave()" :disabled="!note.length" - :value="commentButtonTitle" class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" - type="submit" /> + type="button"> + {{commentButtonTitle}} + </button> <button :disabled="!note.length" name="button" type="button" - class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion" + class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" data-toggle="dropdown" aria-label="Open comment type dropdown"> <i @@ -193,7 +194,7 @@ export default { class="fa fa-caret-down toggle-icon"></i> </button> <ul - class="dropdown-menu note-type-dropdown dropdown-open-top"> + class="note-type-dropdown dropdown-open-top dropdown-menu"> <li :class="{ 'item-selected': noteType === 'comment' }" @click.prevent="setNoteType('comment')"> @@ -230,7 +231,8 @@ export default { <a @click="handleSave(true)" :class="{'btn-reopen': !isIssueOpen, 'btn-close': isIssueOpen}" - class="btn btn-nr btn-comment"> + class="btn btn-nr btn-comment btn-comment-and-close" + role="button"> {{issueActionButtonTitle}} </a> <a diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 1a881335082a..db20309ce2f4 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -47,7 +47,7 @@ export default { PlaceholderSystemNote, }, methods: { - component(note) { + componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === 'systemNote') { return PlaceholderSystemNote; @@ -140,7 +140,7 @@ export default { <ul class="notes"> <component v-for="note in note.notes" - :is="component(note)" + :is="componentName(note)" :note="componentData(note)" key="note.id" /> </ul> @@ -152,12 +152,12 @@ export default { type="button" class="btn btn-text-field" title="Add a reply">Reply...</button> - <issue-note-form - v-if="isReplying" - saveButtonTitle="Comment" - :updateHandler="saveReply" - :cancelHandler="cancelReplyForm" - ref="noteForm" /> + <issue-note-form + v-if="isReplying" + saveButtonTitle="Comment" + :updateHandler="saveReply" + :cancelHandler="cancelReplyForm" + ref="noteForm" /> <issue-note-signed-out-widget v-if="!canReply" /> </div> </div> diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 18e181d1b805..6b6796771715 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -38,7 +38,7 @@ export default { ]), }, methods: { - component(note) { + componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === 'systemNote') { return PlaceholderSystemNote; @@ -132,7 +132,7 @@ export default { class="notes main-notes-list timeline"> <component v-for="note in notes" - :is="component(note)" + :is="componentName(note)" :note="componentData(note)" :key="note.id" /> </ul> -- GitLab From 3c946b932b60ffa58245a091b58108396e5b6546 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 17 Jul 2017 20:12:55 +0300 Subject: [PATCH 075/243] IssueNotesRefactor: Fix Rspec tests. --- .../notes/components/issue_comment_form.vue | 32 ++++++++++++------- .../notes/components/issue_notes.vue | 2 ++ app/assets/javascripts/notes/index.js | 23 +++++++++---- .../_close_reopen_report_toggle.html.haml | 2 +- spec/features/issues/note_polling_spec.rb | 16 +++------- .../discussion_comments_shared_example.rb | 26 +++++++-------- 6 files changed, 56 insertions(+), 45 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 05f88d32234a..b53343b636ea 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -45,6 +45,14 @@ export default { return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; }, + actionButtonClassNames() { + return { + 'btn-reopen': !this.isIssueOpen, + 'btn-close': this.isIssueOpen, + 'js-note-target-close': this.isIssueOpen, + 'js-note-target-reopen': !this.isIssueOpen, + } + }, }, methods: { handleSave(withIssueAction) { @@ -173,8 +181,8 @@ export default { @keydown.meta.enter="handleSave()"> </textarea> </markdown-field> - <div class="note-form-actions clearfix"> - <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown"> + <div class="note-form-actions"> + <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button @click="handleSave()" :disabled="!note.length" @@ -196,41 +204,41 @@ export default { <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> <li - :class="{ 'item-selected': noteType === 'comment' }" + :class="{ 'droplab-item-selected': noteType === 'comment' }" @click.prevent="setNoteType('comment')"> - <a href="#"> + <button class="btn btn-transparent"> <i aria-hidden="true" - class="fa fa-check"></i> + class="fa fa-check icon"></i> <div class="description"> <strong>Comment</strong> <p> Add a general comment to this issue. </p> </div> - </a> + </button> </li> - <li class="divider"></li> + <li class="divider droplab-item-ignore"></li> <li - :class="{ 'item-selected': noteType === 'discussion' }" + :class="{ 'droplab-item-selected': noteType === 'discussion' }" @click.prevent="setNoteType('discussion')"> - <a href="#"> + <button class="btn btn-transparent"> <i aria-hidden="true" - class="fa fa-check"></i> + class="fa fa-check icon"></i> <div class="description"> <strong>Start discussion</strong> <p> Discuss a specific suggestion or question. </p> </div> - </a> + </button> </li> </ul> </div> <a @click="handleSave(true)" - :class="{'btn-reopen': !isIssueOpen, 'btn-close': isIssueOpen}" + :class="actionButtonClassNames" class="btn btn-nr btn-comment btn-comment-and-close" role="button"> {{issueActionButtonTitle}} diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 6b6796771715..d21a0a337908 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -3,6 +3,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; +import VueResource from 'vue-resource'; import storeOptions from '../stores/issue_notes_store'; import eventHub from '../event_hub'; import IssueNote from './issue_note.vue'; @@ -13,6 +14,7 @@ import PlaceholderNote from './issue_placeholder_note.vue'; import PlaceholderSystemNote from './issue_placeholder_system_note.vue'; Vue.use(Vuex); +Vue.use(VueResource); const store = new Vuex.Store(storeOptions); export default { diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index f0f94e2f5006..1271773658e8 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,10 +1,19 @@ import Vue from 'vue'; import IssueNotes from './components/issue_notes.vue'; +import '../vue_shared/vue_resource_interceptor'; -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#js-notes', - components: { IssueNotes }, - template: ` - <issue-notes /> - `, -})); +document.addEventListener('DOMContentLoaded', () => { + const vm = new Vue({ + el: '#js-notes', + components: { IssueNotes }, + template: ` + <issue-notes ref="notes" /> + `, + }); + + window.issueNotes = { + refresh() { + vm.$refs.notes.$store.dispatch('poll'); + }, + }; +}); diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml index 6756a7f17fd5..a38cd319e3ca 100644 --- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml +++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml @@ -2,7 +2,7 @@ - button_action = issuable.closed? ? 'reopen' : 'close' - display_button_action = button_action.capitalize - button_responsive_class = 'hidden-xs hidden-sm' -- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button issuable-close-button" +- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button js-btn-issue-action issuable-close-button" - toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle" - button_method = issuable_close_reopen_button_method(issuable) diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 184cde5b9c55..ebc1e1d0bbe1 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -13,7 +13,8 @@ it 'displays the new comment' do note = create(:note, noteable: issue, project: project, note: 'Looks good!') - page.execute_script('notes.refresh();') + page.execute_script('issueNotes.refresh();') + wait_for_requests expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') end @@ -31,16 +32,6 @@ visit project_issue_path(project, issue) end - it 'has .original-note-content to compare against' do - expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) - expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) - - update_note(existing_note, updated_text) - - expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) - expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) - end - it 'displays the updated content' do expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) @@ -127,7 +118,8 @@ def update_note(note, new_text) note.update(note: new_text) - page.execute_script('notes.refresh();') + page.execute_script('issueNotes.refresh();') + wait_for_requests end def click_edit_action(note) diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index bb4542b16835..1508783a1660 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -11,9 +11,10 @@ expect(page).to have_selector toggle_selector find("#{form_selector} .note-textarea").send_keys('a') - find(submit_selector).click + wait_for_requests + find(comments_selector, match: :first) new_comment = all(comments_selector).last @@ -26,6 +27,7 @@ find("#{form_selector} .note-textarea").send_keys('a') find(close_selector).click + wait_for_requests find(comments_selector, match: :first) find("#{comments_selector}.system-note") @@ -73,17 +75,17 @@ expect(page).not_to have_selector menu_selector end - it 'clicking the ul padding or divider should not change the text' do - find(menu_selector).trigger 'click' + # it 'clicking the ul padding or divider should not change the text' do + # find(menu_selector).trigger 'click' - expect(page).to have_selector menu_selector - expect(find(dropdown_selector)).to have_content 'Comment' + # expect(page).to have_selector menu_selector + # expect(find(dropdown_selector)).to have_content 'Comment' - find("#{menu_selector} .divider").trigger 'click' + # find("#{menu_selector} .divider").trigger 'click' - expect(page).to have_selector menu_selector - expect(find(dropdown_selector)).to have_content 'Comment' - end + # expect(page).to have_selector menu_selector + # expect(find(dropdown_selector)).to have_content 'Comment' + # end describe 'when selecting "Start discussion"' do before do @@ -91,9 +93,8 @@ all("#{menu_selector} li").last.click end - it 'updates the submit button text, note_type input and closes the dropdown' do + it 'updates the submit button text and closes the dropdown' do expect(find(dropdown_selector)).to have_content 'Start discussion' - expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote') expect(page).not_to have_selector menu_selector end @@ -157,9 +158,8 @@ find("#{menu_selector} li", match: :first).click end - it 'updates the submit button text, clears the note_type input and closes the dropdown' do + it 'updates the submit button text and closes the dropdown' do expect(find(dropdown_selector)).to have_content 'Comment' - expect(find("#{form_selector} #note_type", visible: false).value).to eq('') expect(page).not_to have_selector menu_selector end -- GitLab From 397686df515eac5123c9fdc1abfdcce6b27601d1 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 18 Jul 2017 01:07:42 +0300 Subject: [PATCH 076/243] IssueNotesRefactor: Implement note edit conflict warning. --- .../notes/components/issue_note_actions.vue | 4 +-- .../notes/components/issue_note_body.vue | 8 ++++- .../notes/components/issue_note_form.vue | 31 +++++++++++++++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 88f0fdb9a256..c733f2c8dfa2 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -111,7 +111,7 @@ export default { <button @click="editHandler" type="button" - class="btn btn-transparent"> + class="btn btn-transparent js-note-edit"> Edit comment </button> </li> @@ -126,7 +126,7 @@ export default { <a v-if="canEdit" @click.prevent="deleteHandler" - class="js-note-delete" + class="js-note-delete js-note-delete" href="#"> <span class="text-danger"> Delete comment diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index ee9318cbd6a0..fd4040452bf6 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -28,6 +28,11 @@ export default { required: true, }, }, + computed: { + noteBody() { + return this.note.note; + }, + }, components: { IssueNoteEditedText, IssueNoteAwardsList, @@ -75,7 +80,8 @@ export default { ref="noteForm" :updateHandler="handleFormUpdate" :cancelHandler="formCancelHandler" - :noteBody="note.note" /> + :noteBody="noteBody" + :noteId="note.id" /> <textarea v-if="canEdit" v-model="note.note" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 86ff8bd8c693..b60141eacf6c 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -9,6 +9,10 @@ export default { required: false, default: '', }, + noteId: { + type: Number, + required: false, + }, updateHandler: { type: Function, required: true, @@ -29,8 +33,18 @@ export default { note: this.noteBody, markdownPreviewUrl: '', markdownDocsUrl: '', + conflictWhileEditing: false, }; }, + watch: { + noteBody() { + if (this.note === this.initialNote) { + this.note = this.noteBody; + } else { + this.conflictWhileEditing = true; + } + }, + }, components: { MarkdownField, }, @@ -57,6 +71,9 @@ export default { isDirty() { return this.initialNote !== this.note; }, + noteHash() { + return `#note_${this.noteId}`; + }, }, mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); @@ -72,6 +89,16 @@ export default { <template> <div class="note-edit-form"> + <div + v-if="conflictWhileEditing" + class="js-conflict-edit-warning alert alert-danger"> + This comment has changed since you started editing, please review the + <a + :href="noteHash" + target="_blank" + rel="noopener noreferrer">updated comment</a> + to ensure information is not lost. + </div> <form class="edit-note common-note-form"> <markdown-field :markdown-preview-url="markdownPreviewUrl" @@ -79,7 +106,7 @@ export default { :addSpacingClasses="false"> <textarea id="note-body" - class="note-textarea js-gfm-input js-autosize markdown-area" + class="note-textarea js-gfm-input js-autosize markdown-area js-note-text" data-supports-slash-commands="true" data-supports-quick-actions="true" aria-label="Description" @@ -101,7 +128,7 @@ export default { </button> <button @click="cancelHandler()" - class="btn btn-nr btn-cancel" + class="btn btn-nr btn-cancel note-edit-cancel" type="button"> Cancel </button> -- GitLab From 44306f2662cdf8dcd43b123fb02cd5b099d29acf Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 18 Jul 2017 01:08:00 +0300 Subject: [PATCH 077/243] IssueNotesRefactor: Fix note polling specs. --- spec/features/issues/note_polling_spec.rb | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index ebc1e1d0bbe1..3471c49b3b9e 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -43,17 +43,17 @@ it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do click_edit_action(existing_note) - expect(page).to have_field("note[note]", with: note_text) + expect(page).to have_field("note-body", with: note_text) update_note(existing_note, updated_text) - expect(page).to have_field("note[note]", with: updated_text) + expect(page).to have_field("note-body", with: updated_text) end it 'when editing but you changed some things, and an update comes in, show a warning' do click_edit_action(existing_note) - expect(page).to have_field("note[note]", with: note_text) + expect(page).to have_field("note-body", with: note_text) find("#note_#{existing_note.id} .js-note-text").set('something random') update_note(existing_note, updated_text) @@ -64,7 +64,7 @@ it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do click_edit_action(existing_note) - expect(page).to have_field("note[note]", with: note_text) + expect(page).to have_field("note-body", with: note_text) find("#note_#{existing_note.id} .js-note-text").set('something random') @@ -88,14 +88,12 @@ visit project_issue_path(project, issue) end - it 'has .original-note-content to compare against' do + it 'displays the updated content' do expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) - expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) update_note(existing_note, updated_text) expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) - expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) end end @@ -109,9 +107,8 @@ visit project_issue_path(project, issue) end - it 'has .original-note-content to compare against' do + it 'shows the system note' do expect(page).to have_selector("#note_#{system_note.id}", text: note_text) - expect(page).to have_selector("#note_#{system_note.id} .original-note-content", count: 1, visible: false) end end end -- GitLab From 47bd607e83d72fdc15c8efa1b6dda2c772cbac4c Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 18 Jul 2017 10:25:20 +0300 Subject: [PATCH 078/243] IssueNotesRefactor: Fix specs. --- app/assets/javascripts/notes/components/issue_comment_form.vue | 1 + app/assets/javascripts/notes/components/issue_note_form.vue | 1 + app/assets/javascripts/notes/stores/issue_notes_utils.js | 2 +- spec/support/quick_actions_helpers.rb | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index b53343b636ea..6eb39d0e64d1 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -169,6 +169,7 @@ export default { :addSpacingClasses="false"> <textarea id="note-body" + name="note[note]" class="note-textarea js-gfm-input js-autosize markdown-area" data-supports-slash-commands="true" data-supports-quick-actions="true" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index b60141eacf6c..78ed88a579a5 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -106,6 +106,7 @@ export default { :addSpacingClasses="false"> <textarea id="note-body" + name="note[note]" class="note-textarea js-gfm-input js-autosize markdown-area js-note-text" data-supports-slash-commands="true" data-supports-quick-actions="true" diff --git a/app/assets/javascripts/notes/stores/issue_notes_utils.js b/app/assets/javascripts/notes/stores/issue_notes_utils.js index 49e19ede572a..eac80c9f9c2d 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_utils.js +++ b/app/assets/javascripts/notes/stores/issue_notes_utils.js @@ -8,7 +8,7 @@ export default { }, getQuickActionText(note) { let text = 'Applying command'; - const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands); + const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; const executedCommands = quickActions.filter((command) => { const commandRegex = new RegExp(`/${command.name}`); diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb index d2aaae7518f8..fc8243429f5f 100644 --- a/spec/support/quick_actions_helpers.rb +++ b/spec/support/quick_actions_helpers.rb @@ -2,7 +2,7 @@ module QuickActionsHelpers def write_note(text) Sidekiq::Testing.fake! do page.within('.js-main-target-form') do - fill_in 'note[note]', with: text + fill_in 'note-body', with: text find('.js-comment-submit-button').trigger('click') end end -- GitLab From 8a130d850487a27535dce420eab7230748a3cf0a Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 18 Jul 2017 23:08:26 +0300 Subject: [PATCH 079/243] IssueNotesRefactor: Only show close/reopen button if user can update issue. --- .../javascripts/notes/components/issue_comment_form.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 6eb39d0e64d1..8c47aeb2e757 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -53,6 +53,10 @@ export default { 'js-note-target-reopen': !this.isIssueOpen, } }, + canUpdateIssue() { + const { issueData } = window.gl; + return issueData && issueData.current_user.can_update; + }, }, methods: { handleSave(withIssueAction) { @@ -239,6 +243,7 @@ export default { </div> <a @click="handleSave(true)" + v-if="canUpdateIssue" :class="actionButtonClassNames" class="btn btn-nr btn-comment btn-comment-and-close" role="button"> -- GitLab From 0555d6918ccb92faf7a62927820efdac3576f1f3 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 19 Jul 2017 00:34:14 +0300 Subject: [PATCH 080/243] IssueNotesRefactor: Handle /award quick action. --- .../notes/components/issue_note_actions.vue | 4 ---- .../notes/stores/issue_notes_store.js | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index c733f2c8dfa2..0851dfb96741 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -78,10 +78,6 @@ export default { data-position="right" href="#" title="Add reaction"> - <i - aria-hidden="true" - data-hidden="true" - class="fa fa-spinner fa-spin"></i> <span v-html="emojiSmiling" class="link-highlight award-control-icon-neutral"></span> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index b78c6184bd71..9632a3534356 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -2,6 +2,7 @@ import service from '../services/issue_notes_service'; import utils from './issue_notes_utils'; +import loadAwardsHandler from '../../awards_handler'; const state = { notes: [], @@ -225,12 +226,25 @@ const actions = { .then((res) => { const { errors } = res; - if (hasQuickActions) { + if (hasQuickActions && Object.keys(errors).length) { context.dispatch('poll'); $('.js-gfm-input').trigger('clear-commands-cache.atwho'); new Flash('Commands applied', 'notice', $(noteData.flashContainer)); // eslint-disable-line } + + if (res.commands_changes.emoji_award) { + const votesBlock = $('.js-awards-block').eq(0); + + loadAwardsHandler().then((awardsHandler) => { + awardsHandler.addAwardToEmojiBar(votesBlock, res.commands_changes.emoji_award); + awardsHandler.scrollToAwards(); + }).catch(() => { + const msg = 'Something went wrong while adding your award. Please try again.'; + new Flash(msg, $(noteData.flashContainer)); // eslint-disable-line + }); + } + if (errors && errors.commands_only) { new Flash(errors.commands_only, 'notice', $(noteData.flashContainer)); // eslint-disable-line } -- GitLab From a22665931d85f3980f9b0f0e5301142a0803087d Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 20 Jul 2017 17:42:37 +0300 Subject: [PATCH 081/243] IssueNotesRefactor: Fix award emoji specs. --- .../javascripts/notes/components/issue_comment_form.vue | 2 +- app/assets/javascripts/notes/components/issue_note.vue | 1 - .../javascripts/notes/components/issue_note_actions.vue | 3 +++ .../javascripts/notes/components/issue_note_awards_list.vue | 2 +- app/assets/javascripts/notes/stores/issue_notes_store.js | 6 +++--- spec/features/issues/award_emoji_spec.rb | 4 ++-- spec/support/quick_actions_helpers.rb | 1 + 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 8c47aeb2e757..df03bee400a5 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -51,7 +51,7 @@ export default { 'btn-close': this.isIssueOpen, 'js-note-target-close': this.isIssueOpen, 'js-note-target-reopen': !this.isIssueOpen, - } + }; }, canUpdateIssue() { const { issueData } = window.gl; diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 39f153a3045c..a18e51eb48d7 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -137,7 +137,6 @@ export default { :authorId="author.id" :noteId="note.id" :accessLevel="note.human_access" - :canAward="note.emoji_awardable" :canEdit="note.current_user.can_edit" :canDelete="note.current_user.can_edit" :canReportAsAbuse="canReportAsAbuse" diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 0851dfb96741..f0fe13f3f370 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -78,6 +78,9 @@ export default { data-position="right" href="#" title="Add reaction"> + <i + aria-hidden="true" + class="fa fa-spinner fa-spin"></i> <span v-html="emojiSmiling" class="link-highlight award-control-icon-neutral"></span> diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index f84442f15c1f..01805ec11f39 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -173,7 +173,7 @@ export default { data-placement="bottom" type="button"> <span v-html="getAwardHTML(awardName)"></span> - <span class="award-control-text"> + <span class="award-control-text js-counter"> {{awardList.length}} </span> </button> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 9632a3534356..3c6b9b2f79d5 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -225,6 +225,7 @@ const actions = { return context.dispatch(methodToDispatch, noteData) .then((res) => { const { errors } = res; + const commandsChanges = res.commands_changes; if (hasQuickActions && Object.keys(errors).length) { context.dispatch('poll'); @@ -232,12 +233,11 @@ const actions = { new Flash('Commands applied', 'notice', $(noteData.flashContainer)); // eslint-disable-line } - - if (res.commands_changes.emoji_award) { + if (commandsChanges && commandsChanges.emoji_award) { const votesBlock = $('.js-awards-block').eq(0); loadAwardsHandler().then((awardsHandler) => { - awardsHandler.addAwardToEmojiBar(votesBlock, res.commands_changes.emoji_award); + awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); awardsHandler.scrollToAwards(); }).catch(() => { const msg = 'Something went wrong while adding your award. Please try again.'; diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 823c779e0d96..f6368167665b 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -70,13 +70,13 @@ it 'toggles the smiley emoji on a note', js: true do toggle_smiley_emoji(true) - within('.note-awards') do + within('.note-body') do expect(find(emoji_counter)).to have_text("1") end toggle_smiley_emoji(false) - within('.note-awards') do + within('.note-body') do expect(page).not_to have_selector(emoji_counter) end end diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb index fc8243429f5f..87d1e705fc84 100644 --- a/spec/support/quick_actions_helpers.rb +++ b/spec/support/quick_actions_helpers.rb @@ -4,6 +4,7 @@ def write_note(text) page.within('.js-main-target-form') do fill_in 'note-body', with: text find('.js-comment-submit-button').trigger('click') + wait_for_requests end end end -- GitLab From 665471e5f8aa7b547ac3bb0838d66402e2af0646 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 20 Jul 2017 19:12:10 +0300 Subject: [PATCH 082/243] IssueNotesRefactor: Fix task list specs. --- spec/features/task_lists_spec.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index dfc362321aaa..158e4f80a867 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Task Lists', feature: true do +feature 'Task Lists', feature: true, js: true do include Warden::Test::Helpers let(:project) { create(:empty_project) } @@ -194,7 +194,6 @@ def 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 @@ -264,7 +263,6 @@ def visit_merge_request(project, merge) 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-issuable-update') expect(page).to have_selector('a.btn-close') end -- GitLab From 4ab0aaeba33bec17e0828ea948cd98cff21f90cc Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 20 Jul 2017 20:16:13 +0300 Subject: [PATCH 083/243] IssueNotesRefactor: Fix markdown toolbar specs. --- spec/features/issues/markdown_toolbar_spec.rb | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb index affba35f61c7..816481be114f 100644 --- a/spec/features/issues/markdown_toolbar_spec.rb +++ b/spec/features/issues/markdown_toolbar_spec.rb @@ -12,26 +12,26 @@ end it "doesn't include first new line when adding bold" do - find('#note_note').native.send_keys('test') - find('#note_note').native.send_key(:enter) - find('#note_note').native.send_keys('bold') + find('#note-body').native.send_keys('test') + find('#note-body').native.send_key(:enter) + find('#note-body').native.send_keys('bold') - page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 9)') + page.evaluate_script('document.querySelectorAll(".js-main-target-form #note-body")[0].setSelectionRange(4, 9)') first('.toolbar-btn').click - expect(find('#note_note')[:value]).to eq("test\n**bold**\n") + expect(find('#note-body')[:value]).to eq("test\n**bold**\n") end it "doesn't include first new line when adding underline" do - find('#note_note').native.send_keys('test') - find('#note_note').native.send_key(:enter) - find('#note_note').native.send_keys('underline') + find('#note-body').native.send_keys('test') + find('#note-body').native.send_key(:enter) + find('#note-body').native.send_keys('underline') - page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 50)') + page.evaluate_script('document.querySelectorAll(".js-main-target-form #note-body")[0].setSelectionRange(4, 50)') find('.toolbar-btn:nth-child(2)').click - expect(find('#note_note')[:value]).to eq("test\n*underline*\n") + expect(find('#note-body')[:value]).to eq("test\n*underline*\n") end end -- GitLab From d493c16fb1f88d6c89228c1008dade426ede35fb Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 20 Jul 2017 22:12:07 +0300 Subject: [PATCH 084/243] IssueNotesRefactor: Fix reportable spec. --- .../notes/components/issue_note_actions.vue | 13 ++++++------- .../features/reportable_note_shared_examples.rb | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index f0fe13f3f370..fe274a4bc31b 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -121,16 +121,15 @@ export default { Report as abuse </a> </li> - <li> - <a - v-if="canEdit" - @click.prevent="deleteHandler" - class="js-note-delete js-note-delete" - href="#"> + <li v-if="canEdit"> + <button + @click="deleteHandler" + class="btn btn-transparent js-note-delete js-note-delete" + type="button"> <span class="text-danger"> Delete comment </span> - </a> + </button> </li> </ul> </div> diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 27e079c01dd1..c3a0623409b9 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -16,8 +16,8 @@ open_dropdown(dropdown) expect(dropdown).to have_button('Edit comment') + expect(dropdown).to have_button('Delete 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 -- GitLab From cbdbd24617b906f5aeb8c2441f686440b1f35056 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Thu, 20 Jul 2017 23:43:54 +0300 Subject: [PATCH 085/243] IssueNotesRefactor: Fixes autocomplete specs. --- spec/features/issues/gfm_autocomplete_spec.rb | 44 +++++++++---------- .../participants_autocomplete_spec.rb | 6 ++- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 9b4cc653af51..2bf38a98a852 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -28,8 +28,8 @@ it 'opens autocomplete menu when field starts with text' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys('@') + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys('@') end expect(page).to have_selector('.atwho-container') @@ -37,8 +37,8 @@ it 'doesnt open autocomplete menu character is prefixed with text' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('testing') - find('#note_note').native.send_keys('@') + find('#note-body').native.send_keys('testing') + find('#note-body').native.send_keys('@') end expect(page).not_to have_selector('.atwho-view') @@ -46,8 +46,8 @@ it 'doesnt select the first item for non-assignee dropdowns' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys(':') + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys(':') end expect(page).to have_selector('.atwho-container') @@ -58,7 +58,7 @@ end it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do - note = find('#note_note') + note = find('#note-body') # Number. page.within '.timeline-content-form' do @@ -86,8 +86,8 @@ it 'selects the first item for assignee dropdowns' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys('@') + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys('@') end expect(page).to have_selector('.atwho-container') @@ -99,8 +99,8 @@ it 'includes items for assignee dropdowns with non-ASCII characters in name' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys("@#{user.name[0...8]}") + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys("@#{user.name[0...8]}") end expect(page).to have_selector('.atwho-container') @@ -112,8 +112,8 @@ it 'selects the first item for non-assignee dropdowns if a query is entered' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys(':1') + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys(':1') end expect(page).to have_selector('.atwho-container') @@ -125,7 +125,7 @@ context 'if a selected value has special characters' do it 'wraps the result in double quotes' do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("~#{label.title[0]}") @@ -138,7 +138,7 @@ end it "shows dropdown after a new line" do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('test') note.native.send_keys(:enter) @@ -150,7 +150,7 @@ end it "does not show dropdown when preceded with a special character" do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("@") @@ -168,7 +168,7 @@ end it "does not throw an error if no labels exist" do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys('~') @@ -179,7 +179,7 @@ end it 'doesn\'t wrap for assignee values' do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("@#{user.username[0]}") @@ -192,7 +192,7 @@ end it 'doesn\'t wrap for emoji values' do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys(":cartwheel") @@ -206,7 +206,7 @@ it 'doesn\'t open autocomplete after non-word character' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys("@#{user.username[0..2]}!") + find('#note-body').native.send_keys("@#{user.username[0..2]}!") end expect(page).not_to have_selector('.atwho-view') @@ -214,14 +214,14 @@ it 'doesn\'t open autocomplete if there is no space before' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys("hello:#{user.username[0..2]}") + find('#note-body').native.send_keys("hello:#{user.username[0..2]}") end expect(page).not_to have_selector('.atwho-view') end it 'triggers autocomplete after selecting a quick action' do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys('/as') diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index 81b0a2f541b4..180447c2a0e4 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -14,7 +14,11 @@ shared_examples "open suggestions when typing @" do before do page.within('.new-note') do - find('#note_note').send_keys('@') + if note.noteable_type === 'Issue' + find('#note-body').send_keys('@') + else + find('#note_note').send_keys('@') + end end end -- GitLab From 6b80d13e402f6a96c87b9ac762968ea0645c4e39 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 00:27:20 +0300 Subject: [PATCH 086/243] IssueNotesRefactor: Add dropzone required elements and fix dropdown selector. --- app/assets/javascripts/dropzone_input.js | 4 +- .../components/markdown/toolbar.vue | 57 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 9ebbb22e8073..3ec4d9ba3187 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -129,7 +129,7 @@ window.DropzoneInput = (function() { // removeAllFiles(true) stops uploading files (if any) // and remove them from dropzone files queue. $cancelButton.on('click', (e) => { - const target = e.target.closest('form').querySelector('.div-dropzone'); + const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone'); e.preventDefault(); e.stopPropagation(); @@ -141,7 +141,7 @@ window.DropzoneInput = (function() { // and add that files to the dropzone files queue again. // addFile() adds file to dropzone files queue and upload it. $retryLink.on('click', (e) => { - const dropzoneInstance = Dropzone.forElement(e.target.closest('form').querySelector('.div-dropzone')); + const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone')); const failedFiles = dropzoneInstance.files; e.preventDefault(); diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 93252293ba68..0f3f6c6bb93f 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -19,15 +19,52 @@ Markdown is supported </a> </div> - <button - class="toolbar-button markdown-selector" - type="button" - tabindex="-1"> - <i - class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"> - </i> - Attach a file - </button> + <span class="uploading-container"> + <span class="uploading-progress-container hide"> + <i + class="fa fa-file-image-o toolbar-button-icon" + aria-hidden="true"></i> + <span class="attaching-file-message"></span> + <span class="uploading-progress">0%</span> + <span class="uploading-spinner"> + <i + class="fa fa-spinner fa-spin toolbar-button-icon" + aria-hidden="true"></i> + </span> + </span> + <span class="uploading-error-container hide"> + <span class="uploading-error-icon"> + <i + class="fa fa-file-image-o toolbar-button-icon" + aria-hidden="true"></i> + </span> + <span class="uploading-error-message"></span> + <button + class="retry-uploading-link" + type="button"> + Try again + </button> + or + <button + class="attach-new-file markdown-selector" + type="button"> + attach a new file + </button> + </span> + <button + class="markdown-selector button-attach-file" + tabindex="-1" + type="button"> + <i + class="fa fa-file-image-o toolbar-button-icon" + aria-hidden="true"></i> + Attach a file + </button> + <button + class="btn btn-default btn-xs hide button-cancel-uploading-files" + type="button"> + Cancel + </button> + </span> </div> </template> -- GitLab From 0b4a3d848323a0f8bab12df0f4ea8bc9c24660c9 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 00:27:45 +0300 Subject: [PATCH 087/243] IssueNotesRefactor: Fix upload specs. --- spec/features/uploads/user_uploads_file_to_note_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb index 01f10ca0933d..492fd8f909ef 100644 --- a/spec/features/uploads/user_uploads_file_to_note_spec.rb +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -10,6 +10,7 @@ before do sign_in(user) visit project_issue_path(project, issue) + wait_for_requests end context 'before uploading' do -- GitLab From a59f23695b9d05b0d9b6d7afae13a5fcd232a5e5 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 00:45:48 +0300 Subject: [PATCH 088/243] IssueNotesRefactor: camelCase component names. --- .../notes/components/issue_comment_form.vue | 12 +++--- .../notes/components/issue_discussion.vue | 42 +++++++++---------- .../notes/components/issue_note.vue | 16 +++---- .../notes/components/issue_note_body.vue | 12 +++--- .../components/issue_note_edited_text.vue | 4 +- .../notes/components/issue_note_form.vue | 4 +- .../notes/components/issue_note_header.vue | 4 +- .../notes/components/issue_notes.vue | 32 +++++++------- .../notes/components/issue_system_note.vue | 4 +- app/assets/javascripts/notes/index.js | 6 ++- 10 files changed, 69 insertions(+), 67 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index df03bee400a5..ca51783db610 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,9 +1,9 @@ <script> /* global Flash */ -import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import MarkdownField from '../../vue_shared/components/markdown/field.vue'; -import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import markdownField from '../../vue_shared/components/markdown/field.vue'; +import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import eventHub from '../event_hub'; export default { @@ -22,9 +22,9 @@ export default { }; }, components: { - UserAvatarLink, - MarkdownField, - IssueNoteSignedOutWidget, + userAvatarLink, + markdownField, + issueNoteSignedOutWidget, }, computed: { isLoggedIn() { diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index db20309ce2f4..12cbc79a75e0 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,15 +1,15 @@ <script> /* global Flash */ -import IssueNote from './issue_note.vue'; -import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import IssueNoteHeader from './issue_note_header.vue'; -import IssueNoteActions from './issue_note_actions.vue'; -import IssueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; -import IssueNoteEditedText from './issue_note_edited_text.vue'; -import IssueNoteForm from './issue_note_form.vue'; -import PlaceholderNote from './issue_placeholder_note.vue'; -import PlaceholderSystemNote from './issue_placeholder_system_note.vue'; +import issueNote from './issue_note.vue'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import issueNoteHeader from './issue_note_header.vue'; +import issueNoteActions from './issue_note_actions.vue'; +import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; +import issueNoteEditedText from './issue_note_edited_text.vue'; +import issueNoteForm from './issue_note_form.vue'; +import placeholderNote from './issue_placeholder_note.vue'; +import placeholderSystemNote from './issue_placeholder_system_note.vue'; export default { props: { @@ -36,26 +36,26 @@ export default { }, }, components: { - IssueNote, - UserAvatarLink, - IssueNoteHeader, - IssueNoteActions, - IssueNoteEditedText, - IssueNoteSignedOutWidget, - IssueNoteForm, - PlaceholderNote, - PlaceholderSystemNote, + issueNote, + userAvatarLink, + issueNoteHeader, + issueNoteActions, + issueNoteSignedOutWidget, + issueNoteEditedText, + issueNoteForm, + placeholderNote, + placeholderSystemNote, }, methods: { componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === 'systemNote') { - return PlaceholderSystemNote; + return placeholderSystemNote; } - return PlaceholderNote; + return placeholderNote; } - return IssueNote; + return issueNote; }, componentData(note) { return note.isPlaceholderNote ? note.notes[0] : note; diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index a18e51eb48d7..ffcd50321fbd 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -2,10 +2,10 @@ /* global Flash */ import { mapGetters } from 'vuex'; -import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import IssueNoteHeader from './issue_note_header.vue'; -import IssueNoteActions from './issue_note_actions.vue'; -import IssueNoteBody from './issue_note_body.vue'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import issueNoteHeader from './issue_note_header.vue'; +import issueNoteActions from './issue_note_actions.vue'; +import issueNoteBody from './issue_note_body.vue'; import eventHub from '../event_hub'; export default { @@ -22,10 +22,10 @@ export default { }; }, components: { - UserAvatarLink, - IssueNoteHeader, - IssueNoteActions, - IssueNoteBody, + userAvatarLink, + issueNoteHeader, + issueNoteActions, + issueNoteBody, }, computed: { ...mapGetters([ diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index fd4040452bf6..0c41cfa83198 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -1,7 +1,7 @@ <script> -import IssueNoteEditedText from './issue_note_edited_text.vue'; -import IssueNoteAwardsList from './issue_note_awards_list.vue'; -import IssueNoteForm from './issue_note_form.vue'; +import issueNoteEditedText from './issue_note_edited_text.vue'; +import issueNoteAwardsList from './issue_note_awards_list.vue'; +import issueNoteForm from './issue_note_form.vue'; import TaskList from '../../task_list'; export default { @@ -34,9 +34,9 @@ export default { }, }, components: { - IssueNoteEditedText, - IssueNoteAwardsList, - IssueNoteForm, + issueNoteEditedText, + issueNoteAwardsList, + issueNoteForm, }, methods: { renderGFM() { diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue index 8ed35bdb2a0c..e71d6aa7b589 100644 --- a/app/assets/javascripts/notes/components/issue_note_edited_text.vue +++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue @@ -1,5 +1,5 @@ <script> -import TimeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { props: { @@ -22,7 +22,7 @@ export default { }, }, components: { - TimeAgoTooltip, + timeAgoTooltip, }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 78ed88a579a5..ee63f0771b81 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -1,5 +1,5 @@ <script> -import MarkdownField from '../../vue_shared/components/markdown/field.vue'; +import markdownField from '../../vue_shared/components/markdown/field.vue'; import eventHub from '../event_hub'; export default { @@ -46,7 +46,7 @@ export default { }, }, components: { - MarkdownField, + markdownField, }, methods: { handleUpdate() { diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index cf2826e7b2e8..50eef36a7615 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -1,5 +1,5 @@ <script> -import TimeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { props: { @@ -36,7 +36,7 @@ export default { }, }, components: { - TimeAgoTooltip, + timeAgoTooltip, }, data() { return { diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index d21a0a337908..f78a8aa1aff1 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -6,12 +6,12 @@ import Vuex from 'vuex'; import VueResource from 'vue-resource'; import storeOptions from '../stores/issue_notes_store'; import eventHub from '../event_hub'; -import IssueNote from './issue_note.vue'; -import IssueDiscussion from './issue_discussion.vue'; -import IssueSystemNote from './issue_system_note.vue'; -import IssueCommentForm from './issue_comment_form.vue'; -import PlaceholderNote from './issue_placeholder_note.vue'; -import PlaceholderSystemNote from './issue_placeholder_system_note.vue'; +import issueNote from './issue_note.vue'; +import issueDiscussion from './issue_discussion.vue'; +import issueSystemNote from './issue_system_note.vue'; +import issueCommentForm from './issue_comment_form.vue'; +import placeholderNote from './issue_placeholder_note.vue'; +import placeholderSystemNote from './issue_placeholder_system_note.vue'; Vue.use(Vuex); Vue.use(VueResource); @@ -26,12 +26,12 @@ export default { }; }, components: { - IssueNote, - IssueDiscussion, - IssueSystemNote, - IssueCommentForm, - PlaceholderNote, - PlaceholderSystemNote, + issueNote, + issueDiscussion, + issueSystemNote, + issueCommentForm, + placeholderNote, + placeholderSystemNote, }, computed: { ...Vuex.mapGetters([ @@ -43,14 +43,14 @@ export default { componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === 'systemNote') { - return PlaceholderSystemNote; + return placeholderSystemNote; } - return PlaceholderNote; + return placeholderNote; } else if (note.individual_note) { - return note.notes[0].system ? IssueSystemNote : IssueNote; + return note.notes[0].system ? issueSystemNote : issueNote; } - return IssueDiscussion; + return issueDiscussion; }, componentData(note) { return note.individual_note ? note.notes[0] : note; diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index 0763f44552ea..f5713f0a5a64 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -1,7 +1,7 @@ <script> import { mapGetters } from 'vuex'; import iconsMap from './issue_note_icons'; -import IssueNoteHeader from './issue_note_header.vue'; +import issueNoteHeader from './issue_note_header.vue'; export default { props: { @@ -16,7 +16,7 @@ export default { }; }, components: { - IssueNoteHeader, + issueNoteHeader, }, computed: { ...mapGetters([ diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 1271773658e8..4c42d5ff4a79 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,11 +1,13 @@ import Vue from 'vue'; -import IssueNotes from './components/issue_notes.vue'; +import issueNotes from './components/issue_notes.vue'; import '../vue_shared/vue_resource_interceptor'; document.addEventListener('DOMContentLoaded', () => { const vm = new Vue({ el: '#js-notes', - components: { IssueNotes }, + components: { + issueNotes + }, template: ` <issue-notes ref="notes" /> `, -- GitLab From cd5599a59c38b4882697fc84072e97839f809d12 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 00:55:29 +0300 Subject: [PATCH 089/243] IssueNotesRefactor: camelCase event names. --- .../javascripts/notes/components/issue_comment_form.vue | 4 ++-- app/assets/javascripts/notes/components/issue_note.vue | 2 +- app/assets/javascripts/notes/components/issue_note_form.vue | 2 +- app/assets/javascripts/notes/components/issue_notes.vue | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index ca51783db610..c4285a276a19 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -127,7 +127,7 @@ export default { const myLastNoteId = $('.js-my-note').last().attr('id'); if (myLastNoteId) { - eventHub.$emit('EnterEditMode', { + eventHub.$emit('enterEditMode', { noteId: parseInt(myLastNoteId.replace('note_', ''), 10), }); } @@ -142,7 +142,7 @@ export default { this.markdownDocsUrl = markdownDocs; this.markdownPreviewUrl = markdownPreviewUrl; - eventHub.$on('IssueStateChanged', (isClosed) => { + eventHub.$on('issueStateChanged', (isClosed) => { this.issueState = isClosed ? 'closed' : 'reopened'; }); }, diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index ffcd50321fbd..08bf0418fbca 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -103,7 +103,7 @@ export default { }, }, created() { - eventHub.$on('EnterEditMode', ({ noteId }) => { + eventHub.$on('enterEditMode', ({ noteId }) => { if (noteId === this.note.id) { this.isEditing = true; this.$store.dispatch('scrollToNoteIfNeeded', $(this.$el)); diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index ee63f0771b81..c75756f61941 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -60,7 +60,7 @@ export default { const myLastNoteId = discussion.find('.js-my-note').last().attr('id'); if (myLastNoteId) { - eventHub.$emit('EnterEditMode', { + eventHub.$emit('enterEditMode', { noteId: parseInt(myLastNoteId.replace('note_', ''), 10), }); } diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index f78a8aa1aff1..1a6cc6fb6516 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -98,7 +98,7 @@ export default { }); $(document).on('issuable:change', (e, isClosed) => { - eventHub.$emit('IssueStateChanged', isClosed); + eventHub.$emit('issueStateChanged', isClosed); }); }, checkLocationHash() { -- GitLab From 787616d418460509e5468e8207dacfaf8a96b959 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 00:56:14 +0300 Subject: [PATCH 090/243] IssueNotesRefactor: Update order of Vue properties to match docs. --- .../notes/components/issue_discussion.vue | 22 ++++++------ .../notes/components/issue_note_body.vue | 10 +++--- .../notes/components/issue_note_form.vue | 34 +++++++++---------- .../notes/components/issue_note_header.vue | 6 ++-- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 12cbc79a75e0..cc1fd5a1b19d 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -24,17 +24,6 @@ export default { isReplying: false, }; }, - computed: { - discussion() { - return this.note.notes[0]; - }, - author() { - return this.discussion.author; - }, - canReply() { - return window.gl.issueData.current_user.can_create_note; - }, - }, components: { issueNote, userAvatarLink, @@ -46,6 +35,17 @@ export default { placeholderNote, placeholderSystemNote, }, + computed: { + discussion() { + return this.note.notes[0]; + }, + author() { + return this.discussion.author; + }, + canReply() { + return window.gl.issueData.current_user.can_create_note; + }, + }, methods: { componentName(note) { if (note.isPlaceholderNote) { diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 0c41cfa83198..b49910f3ef97 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -28,16 +28,16 @@ export default { required: true, }, }, - computed: { - noteBody() { - return this.note.note; - }, - }, components: { issueNoteEditedText, issueNoteAwardsList, issueNoteForm, }, + computed: { + noteBody() { + return this.note.note; + }, + }, methods: { renderGFM() { $(this.$refs['note-body']).renderGFM(); diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index c75756f61941..f70e074b9e4d 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -36,18 +36,17 @@ export default { conflictWhileEditing: false, }; }, - watch: { - noteBody() { - if (this.note === this.initialNote) { - this.note = this.noteBody; - } else { - this.conflictWhileEditing = true; - } - }, - }, components: { markdownField, }, + computed: { + isDirty() { + return this.initialNote !== this.note; + }, + noteHash() { + return `#note_${this.noteId}`; + }, + }, methods: { handleUpdate() { this.updateHandler({ @@ -67,14 +66,6 @@ export default { } }, }, - computed: { - isDirty() { - return this.initialNote !== this.note; - }, - noteHash() { - return `#note_${this.noteId}`; - }, - }, mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); @@ -84,6 +75,15 @@ export default { this.markdownPreviewUrl = markdownPreviewUrl; this.$refs.textarea.focus(); }, + watch: { + noteBody() { + if (this.note === this.initialNote) { + this.note = this.noteBody; + } else { + this.conflictWhileEditing = true; + } + }, + }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 50eef36a7615..d5249b1a72f4 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -35,14 +35,14 @@ export default { required: false, }, }, - components: { - timeAgoTooltip, - }, data() { return { isExpanded: true, }; }, + components: { + timeAgoTooltip, + }, computed: { toggleChevronClass() { return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; -- GitLab From a2326c8a12a84028ea05cc7803788a34c12d109c Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 01:12:44 +0300 Subject: [PATCH 091/243] IssueNotesRefactor: kebab-case all Vue data bindings. --- .../notes/components/issue_comment_form.vue | 8 ++-- .../notes/components/issue_discussion.vue | 24 ++++++------ .../notes/components/issue_note.vue | 38 +++++++++---------- .../notes/components/issue_note_body.vue | 18 ++++----- .../components/issue_note_edited_text.vue | 2 +- .../notes/components/issue_system_note.vue | 6 +-- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index c4285a276a19..fc740d9d165b 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -161,10 +161,10 @@ export default { <div class="timeline-icon hidden-xs hidden-sm"> <user-avatar-link v-if="author" - :linkHref="author.path" - :imgSrc="author.avatar_url" - :imgAlt="author.name" - :imgSize="40" /> + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" /> </div> <div class="js-main-target-form timeline-content timeline-content-form common-note-form"> <markdown-field diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index cc1fd5a1b19d..1d41232e8051 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -109,25 +109,25 @@ export default { <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link - :linkHref="author.path" - :imgSrc="author.avatar_url" - :imgAlt="author.name" - :imgSize="40" /> + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" /> </div> <div class="timeline-content"> <div class="discussion"> <div class="discussion-header"> <issue-note-header :author="author" - :createdAt="discussion.created_at" - :noteId="discussion.id" - :includeToggle="true" - :toggleHandler="toggleDiscussion" + :created-at="discussion.created_at" + :note-id="discussion.id" + :include-toggle="true" + :toggle-handler="toggleDiscussion" actionText="started a discussion" /> <issue-note-edited-text v-if="note.last_updated_by" - :editedAt="note.last_updated_at" - :editedBy="note.last_updated_by" + :edited-at="note.last_updated_at" + :edited-by="note.last_updated_by" actionText="Last updated" className="discussion-headline-light js-discussion-headline" /> </div> @@ -155,8 +155,8 @@ export default { <issue-note-form v-if="isReplying" saveButtonTitle="Comment" - :updateHandler="saveReply" - :cancelHandler="cancelReplyForm" + :update-handler="saveReply" + :cancel-handler="cancelReplyForm" ref="noteForm" /> <issue-note-signed-out-widget v-if="!canReply" /> </div> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 08bf0418fbca..712064895e4e 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -121,35 +121,35 @@ export default { <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link - :linkHref="author.path" - :imgSrc="author.avatar_url" - :imgAlt="author.name" - :imgSize="40" /> + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" /> </div> <div class="timeline-content"> <div class="note-header"> <issue-note-header :author="author" - :createdAt="note.created_at" - :noteId="note.id" + :created-at="note.created_at" + :note-id="note.id" actionText="commented" /> <issue-note-actions - :authorId="author.id" - :noteId="note.id" - :accessLevel="note.human_access" - :canEdit="note.current_user.can_edit" - :canDelete="note.current_user.can_edit" - :canReportAsAbuse="canReportAsAbuse" - :reportAbusePath="note.report_abuse_path" - :editHandler="editHandler" - :deleteHandler="deleteHandler" /> + :author-id="author.id" + :note-id="note.id" + :access-level="note.human_access" + :can-edit="note.current_user.can_edit" + :can-delete="note.current_user.can_edit" + :can-report-as-abuse="canReportAsAbuse" + :report-abuse-path="note.report_abuse_path" + :edit-handler="editHandler" + :delete-handler="deleteHandler" /> </div> <issue-note-body :note="note" - :canEdit="note.current_user.can_edit" - :isEditing="isEditing" - :formUpdateHandler="formUpdateHandler" - :formCancelHandler="formCancelHandler" + :can-edit="note.current_user.can_edit" + :is-editing="isEditing" + :form-update-handler="formUpdateHandler" + :form-cancel-handler="formCancelHandler" ref="noteBody" /> </div> </div> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index b49910f3ef97..30c8db6f041a 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -78,10 +78,10 @@ export default { <issue-note-form v-if="isEditing" ref="noteForm" - :updateHandler="handleFormUpdate" - :cancelHandler="formCancelHandler" - :noteBody="noteBody" - :noteId="note.id" /> + :update-handler="handleFormUpdate" + :cancel-handler="formCancelHandler" + :note-body="noteBody" + :note-id="note.id" /> <textarea v-if="canEdit" v-model="note.note" @@ -89,14 +89,14 @@ export default { class="hidden js-task-list-field"></textarea> <issue-note-edited-text v-if="note.last_edited_by" - :editedAt="note.last_edited_at" - :editedBy="note.last_edited_by" + :edited-at="note.last_edited_at" + :edited-by="note.last_edited_by" actionText="Edited" /> <issue-note-awards-list v-if="note.award_emoji.length" - :noteId="note.id" - :noteAuthorId="note.author.id" + :note-id="note.id" + :note-author-id="note.author.id" :awards="note.award_emoji" - :toggleAwardPath="note.toggle_award_path" /> + :toggle-award-path="note.toggle_award_path" /> </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue index e71d6aa7b589..aed82fd4a828 100644 --- a/app/assets/javascripts/notes/components/issue_note_edited_text.vue +++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue @@ -38,6 +38,6 @@ export default { </a> <time-ago-tooltip :time="editedAt" - tooltipPlacement="bottom" /> + tooltip-placement="bottom" /> </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index f5713f0a5a64..65ddaccbceac 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -45,9 +45,9 @@ export default { <div class="note-header"> <issue-note-header :author="note.author" - :createdAt="note.created_at" - :noteId="note.id" - :actionTextHtml="note.note_html" /> + :created-at="note.created_at" + :note-id="note.id" + :action-text-html="note.note_html" /> </div> </div> </div> -- GitLab From 6c3723d2d34cc3803ed65562b1471ea4ebd82123 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 11:38:34 +0300 Subject: [PATCH 092/243] IssueNotesRefactor: Fix shared spec. --- .../javascripts/notes/components/issue_note_actions.vue | 7 ++++--- .../javascripts/notes/components/issue_note_form.vue | 2 +- spec/support/features/reportable_note_shared_examples.rb | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index fe274a4bc31b..48b1e1d513f0 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -122,14 +122,15 @@ export default { </a> </li> <li v-if="canEdit"> - <button - @click="deleteHandler" + <a + @click.prevent="deleteHandler" class="btn btn-transparent js-note-delete js-note-delete" + href="#" type="button"> <span class="text-danger"> Delete comment </span> - </button> + </a> </li> </ul> </div> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index f70e074b9e4d..91b39a06d772 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -88,7 +88,7 @@ export default { </script> <template> - <div class="note-edit-form"> + <div class="note-edit-form current-note-edit-form"> <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index c3a0623409b9..dbe1c9b8aec2 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -16,7 +16,7 @@ open_dropdown(dropdown) expect(dropdown).to have_button('Edit comment') - expect(dropdown).to have_button('Delete comment') + expect(dropdown).to have_link('Delete comment') expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) end -- GitLab From c09b5852a98dad9ccd1d66993021ec548695871c Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 12:44:46 +0300 Subject: [PATCH 093/243] IssueNotesRefactor: Remove new keyword from Flash calls. --- .../javascripts/notes/components/issue_comment_form.vue | 2 +- .../javascripts/notes/components/issue_discussion.vue | 2 +- app/assets/javascripts/notes/components/issue_note.vue | 2 +- .../javascripts/notes/components/issue_note_awards_list.vue | 2 +- app/assets/javascripts/notes/components/issue_notes.vue | 6 +++--- app/assets/javascripts/notes/stores/issue_notes_store.js | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index fc740d9d165b..5d1996054b2b 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -120,7 +120,7 @@ export default { this.noteType = type; }, handleError() { - new Flash('Something went wrong while adding your comment. Please try again.'); // eslint-disable-line + Flash('Something went wrong while adding your comment. Please try again.'); }, editMyLastNote() { if (this.note === '') { diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 1d41232e8051..371ceffbf20f 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -97,7 +97,7 @@ export default { this.isReplying = false; }) .catch(() => { - new Flash('Something went wrong while adding your reply. Please try again.'); // eslint-disable-line + Flash('Something went wrong while adding your reply. Please try again.'); }); }, }, diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 712064895e4e..6514ec3b7aba 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -87,7 +87,7 @@ export default { $(this.$refs.noteBody.$el).renderGFM(); }) .catch(() => { - new Flash('Something went wrong while editing your comment. Please try again.'); // eslint-disable-line + Flash('Something went wrong while editing your comment. Please try again.'); }); }, formCancelHandler(shouldConfirm) { diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 01805ec11f39..c41c608979a6 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -154,7 +154,7 @@ export default { $(this.$el).find('.award-control').tooltip('fixTitle'); }) .catch(() => { - new Flash('Something went wrong on our end.'); // eslint-disable-line + Flash('Something went wrong on our end.'); }); }, }, diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 1a6cc6fb6516..94372cd9a774 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -68,7 +68,7 @@ export default { }); }) .catch(() => { - new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line + Flash('Something went wrong while fetching issue comments. Please try again.'); }); }, initPolling() { @@ -82,7 +82,7 @@ export default { this.$store.commit('setLastFetchedAt', res.lastFetchedAt); }) .catch(() => { - new Flash('Something went wrong while fetching latest comments.'); // eslint-disable-line + Flash('Something went wrong while fetching latest comments.'); }); }, 15000); }, @@ -93,7 +93,7 @@ export default { this.$store.dispatch('toggleAward', { endpoint, awardName, noteId }) .catch(() => { - new Flash('Something went wrong on our end.'); // eslint-disable-line + Flash('Something went wrong on our end.'); }); }); diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 3c6b9b2f79d5..12ba1b06dfc9 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -230,7 +230,7 @@ const actions = { if (hasQuickActions && Object.keys(errors).length) { context.dispatch('poll'); $('.js-gfm-input').trigger('clear-commands-cache.atwho'); - new Flash('Commands applied', 'notice', $(noteData.flashContainer)); // eslint-disable-line + Flash('Commands applied', 'notice', $(noteData.flashContainer)); } if (commandsChanges && commandsChanges.emoji_award) { @@ -241,12 +241,12 @@ const actions = { awardsHandler.scrollToAwards(); }).catch(() => { const msg = 'Something went wrong while adding your award. Please try again.'; - new Flash(msg, $(noteData.flashContainer)); // eslint-disable-line + Flash(msg, $(noteData.flashContainer)); }); } if (errors && errors.commands_only) { - new Flash(errors.commands_only, 'notice', $(noteData.flashContainer)); // eslint-disable-line + Flash(errors.commands_only, 'notice', $(noteData.flashContainer)); } context.commit('removePlaceholderNotes'); -- GitLab From a2b2f1ead7f247e37f8e07feb7e6d1ab9997bc00 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 12:45:09 +0300 Subject: [PATCH 094/243] IssueNotesRefactor: Fix issue spinach tests. --- .../notes/components/issue_comment_form.vue | 11 ++++++++--- .../javascripts/notes/stores/issue_notes_store.js | 7 ++++++- features/project/issues/issues.feature | 1 - 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 5d1996054b2b..d428bce33e2e 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -90,7 +90,9 @@ export default { this.discard(); } }) - .catch(this.handleError); + .catch(() => { + this.discard(false); + }); } if (withIssueAction) { @@ -109,12 +111,15 @@ export default { $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); } }, - discard() { + discard(shouldClear = true) { // `blur` is needed to clear slash commands autocomplete cache if event fired. // `focus` is needed to remain cursor in the textarea. this.$refs.textarea.blur(); this.$refs.textarea.focus(); - this.note = ''; + + if (shouldClear) { + this.note = ''; + } }, setNoteType(type) { this.noteType = type; diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 12ba1b06dfc9..17b8da380a54 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -251,7 +251,12 @@ const actions = { context.commit('removePlaceholderNotes'); return res; - }); + }) + .catch(() => { + const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + Flash(msg, 'alert', $(noteData.flashContainer)); + context.commit('removePlaceholderNotes'); + }) }, poll(context) { const { notesPath } = $('.js-notes-wrapper')[0].dataset; diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 4f905674d8cc..abc23257de56 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -46,7 +46,6 @@ Feature: Project Issues Given I visit issue page "Release 0.4" And I leave a comment like "XML attached" Then I should see comment "XML attached" - And I should see an error alert section within the comment form @javascript Scenario: Visiting Issues after being sorted the list -- GitLab From c00ff16ffe7559c46e53f8788836ac4e98b8f589 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 21 Jul 2017 19:54:25 +0300 Subject: [PATCH 095/243] IssueNotesRefactor: Revert note selector. --- spec/support/quick_actions_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb index 87d1e705fc84..f84938df3f07 100644 --- a/spec/support/quick_actions_helpers.rb +++ b/spec/support/quick_actions_helpers.rb @@ -2,7 +2,7 @@ module QuickActionsHelpers def write_note(text) Sidekiq::Testing.fake! do page.within('.js-main-target-form') do - fill_in 'note-body', with: text + fill_in 'note[note]', with: text find('.js-comment-submit-button').trigger('click') wait_for_requests end -- GitLab From 8fa1f65e71d5462fe08dce06df4f8bd77be375e4 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 22 Jul 2017 00:10:56 +0300 Subject: [PATCH 096/243] IssueNotesRefactor: Fix invalid tests. --- .../merge_requests/user_uses_slash_commands_spec.rb | 4 ++-- .../features/issuable_slash_commands_shared_examples.rb | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index b2187e01bdbe..3ad7e68f2ed0 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -67,7 +67,7 @@ it 'does not change the WIP prefix' do write_note("/wip") - expect(page).not_to have_content '/wip' + expect(page).to have_content '/wip' expect(page).not_to have_content 'Commands applied' expect(merge_request.reload.work_in_progress?).to eq false @@ -197,7 +197,7 @@ it 'does not change target branch' do write_note('/target_branch merge-test') - expect(page).not_to have_content '/target_branch merge-test' + expect(page).to have_content '/target_branch merge-test' expect(merge_request.target_branch).to eq 'feature' end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 033e338fe614..9597493aa39d 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -119,7 +119,7 @@ it "does not close the #{issuable_type}" do write_note("/close") - expect(page).not_to have_content '/close' + expect(page).to have_content '/close' expect(page).not_to have_content 'Commands applied' expect(issuable).to be_open @@ -154,7 +154,7 @@ it "does not reopen the #{issuable_type}" do write_note("/reopen") - expect(page).not_to have_content '/reopen' + expect(page).to have_content '/reopen' expect(page).not_to have_content 'Commands applied' expect(issuable).to be_closed @@ -184,7 +184,7 @@ it "does not reopen the #{issuable_type}" do write_note("/title Awesome new title") - expect(page).not_to have_content '/title' + expect(page).to have_content '/title' expect(page).not_to have_content 'Commands applied' expect(issuable.reload.title).not_to eq 'Awesome new title' -- GitLab From 14eb2abaa373cfd060e943e60f2b4d75fe45ab67 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 22 Jul 2017 00:55:55 +0300 Subject: [PATCH 097/243] IssueNotesRefactor: Implement time tracking sidebar integration with slash commands. --- .../notes/stores/issue_notes_store.js | 25 ++++++++++++------- .../time_tracking/sidebar_time_tracking.js | 4 +++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 17b8da380a54..25ffbdd9e164 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -3,6 +3,7 @@ import service from '../services/issue_notes_service'; import utils from './issue_notes_utils'; import loadAwardsHandler from '../../awards_handler'; +import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; const state = { notes: [], @@ -233,16 +234,22 @@ const actions = { Flash('Commands applied', 'notice', $(noteData.flashContainer)); } - if (commandsChanges && commandsChanges.emoji_award) { - const votesBlock = $('.js-awards-block').eq(0); + if (commandsChanges) { + if (commandsChanges.emoji_award) { + const votesBlock = $('.js-awards-block').eq(0); + + loadAwardsHandler().then((awardsHandler) => { + awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); + awardsHandler.scrollToAwards(); + }).catch(() => { + const msg = 'Something went wrong while adding your award. Please try again.'; + Flash(msg, $(noteData.flashContainer)); + }); + } - loadAwardsHandler().then((awardsHandler) => { - awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); - awardsHandler.scrollToAwards(); - }).catch(() => { - const msg = 'Something went wrong while adding your award. Please try again.'; - Flash(msg, $(noteData.flashContainer)); - }); + if (commandsChanges.spend_time || commandsChanges.time_estimate) { + sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); + } } if (errors && errors.commands_only) { diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js index 650e935b1165..efedea32d1e9 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js @@ -4,6 +4,7 @@ import timeTracker from './time_tracker'; import Store from '../../stores/sidebar_store'; import Mediator from '../../sidebar_mediator'; +import eventHub from '../../event_hub'; export default { data() { @@ -18,6 +19,9 @@ export default { methods: { listenForQuickActions() { $(document).on('ajax:success', '.gfm-form', this.quickActionListened); + eventHub.$on('timeTrackingUpdated', (data) => { + this.quickActionListened(null, data); + }); }, quickActionListened(e, data) { const subscribedCommands = ['spend_time', 'time_estimate']; -- GitLab From 562ccdae3efad9ae883867bd21bf7b274a9163f8 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 22 Jul 2017 01:42:31 +0300 Subject: [PATCH 098/243] IssueNotesRefactor: Add referenced users and commands to markdown field. --- .../notes/components/issue_comment_form.vue | 6 ++-- .../notes/components/issue_note_form.vue | 6 ++-- app/assets/javascripts/notes/index.js | 2 +- .../notes/stores/issue_notes_store.js | 3 +- .../vue_shared/components/markdown/field.vue | 33 +++++++++++++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index d428bce33e2e..7896f872a7d9 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -13,8 +13,8 @@ export default { return { note: '', - markdownPreviewUrl: '', markdownDocsUrl: '', + markdownPreviewUrl: gl.issueData.preview_note_path, noteType: 'comment', issueState: state, endpoint: create_note_path, @@ -142,10 +142,8 @@ export default { mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); - const { markdownDocs, markdownPreviewUrl } = issueData; - this.markdownDocsUrl = markdownDocs; - this.markdownPreviewUrl = markdownPreviewUrl; + this.markdownDocsUrl = issueData.markdownDocs; eventHub.$on('issueStateChanged', (isClosed) => { this.issueState = isClosed ? 'closed' : 'reopened'; diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 91b39a06d772..46ea030ce879 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -31,7 +31,7 @@ export default { return { initialNote: this.noteBody, note: this.noteBody, - markdownPreviewUrl: '', + markdownPreviewUrl: gl.issueData.preview_note_path, markdownDocsUrl: '', conflictWhileEditing: false, }; @@ -69,10 +69,8 @@ export default { mounted() { const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); - const { markdownDocs, markdownPreviewUrl } = issueData; - this.markdownDocsUrl = markdownDocs; - this.markdownPreviewUrl = markdownPreviewUrl; + this.markdownDocsUrl = issueData.markdownDocs; this.$refs.textarea.focus(); }, watch: { diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 4c42d5ff4a79..d48a9111ffe4 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -6,7 +6,7 @@ document.addEventListener('DOMContentLoaded', () => { const vm = new Vue({ el: '#js-notes', components: { - issueNotes + issueNotes, }, template: ` <issue-notes ref="notes" /> diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index 25ffbdd9e164..fb89b1743d1c 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign */ +/* global Flash */ import service from '../services/issue_notes_service'; import utils from './issue_notes_utils'; @@ -263,7 +264,7 @@ const actions = { const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; Flash(msg, 'alert', $(noteData.flashContainer)); context.commit('removePlaceholderNotes'); - }) + }); }, poll(context) { const { notesPath } = $('.js-notes-wrapper')[0].dataset; diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 865cd0244d5c..f1c7264ec4f8 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -3,6 +3,8 @@ import markdownHeader from './header.vue'; import markdownToolbar from './toolbar.vue'; + const REFERENCED_USERS_THRESHOLD = 10; + export default { props: { markdownPreviewUrl: { @@ -23,6 +25,8 @@ data() { return { markdownPreview: '', + referencedCommands: '', + referencedUsers: '', markdownPreviewLoading: false, previewMarkdown: false, }; @@ -31,6 +35,11 @@ markdownHeader, markdownToolbar, }, + computed: { + shouldShowReferencedUsers() { + return this.referencedUsers.length >= REFERENCED_USERS_THRESHOLD; + }, + }, methods: { toggleMarkdownPreview() { this.previewMarkdown = !this.previewMarkdown; @@ -53,6 +62,8 @@ .then((data) => { this.markdownPreviewLoading = false; this.markdownPreview = data.body; + this.referencedCommands = data.references.commands; + this.referencedUsers = data.references.users; if (!this.markdownPreview) { this.markdownPreview = 'Nothing to preview.'; @@ -118,5 +129,27 @@ Loading... </span> </div> + <template v-if="previewMarkdown && !markdownPreviewLoading"> + <div + v-if="referencedCommands" + v-html="referencedCommands" + class="referenced-commands"></div> + <div + v-if="shouldShowReferencedUsers" + class="referenced-users"> + <span> + <i + class="fa fa-exclamation-triangle" + aria-hidden="true"> + </i> + You are about to add + <strong> + <span class="js-referenced-users-count"> + {{referencedUsers.length}} + </span> + </strong> people to the discussion. Proceed with caution. + </span> + </div> + </template> </div> </template> -- GitLab From 99b82a0716b53b6edf5057fa05736f6c5d00a5b8 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 22 Jul 2017 02:59:25 +0300 Subject: [PATCH 099/243] IssueNotesRefactor: Use `gitlab_` prefixed auth helpers. --- .../issuable_slash_commands_shared_examples.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 9597493aa39d..a99ebb582ee3 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -18,7 +18,7 @@ project.team << [assignee, :developer] project.team << [guest, :guest] - sign_in(master) + gitlab_sign_in(master) end after do @@ -111,8 +111,8 @@ context "when current user cannot close #{issuable_type}" do before do - sign_out(:user) - sign_in(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -146,8 +146,8 @@ context "when current user cannot reopen #{issuable_type}" do before do - sign_out(:user) - sign_in(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -176,8 +176,8 @@ context "when current user cannot change title of #{issuable_type}" do before do - sign_out(:user) - sign_in(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -265,7 +265,7 @@ end end - describe "preview of note on #{issuable_type}" do + describe "preview of note on #{issuable_type}", js: true do it 'removes quick actions from note and explains them' do visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) -- GitLab From 33c20468cc2456c03431a1e32d84cd1fb31ea3c5 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 22 Jul 2017 03:17:21 +0300 Subject: [PATCH 100/243] IssueNotesRefactor: Check existence of command changes for time tracking. --- app/assets/javascripts/notes/stores/issue_notes_store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js index fb89b1743d1c..abe5edee03b9 100644 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ b/app/assets/javascripts/notes/stores/issue_notes_store.js @@ -248,7 +248,7 @@ const actions = { }); } - if (commandsChanges.spend_time || commandsChanges.time_estimate) { + if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) { sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); } } -- GitLab From fbdc02adc1682fe40c68e850496d94b89491a6f9 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 22 Jul 2017 23:46:50 +0300 Subject: [PATCH 101/243] IssueNotesRefactor: Temp workaround for a failing test. --- .../merge_requests/user_uses_slash_commands_spec.rb | 2 +- spec/support/quick_actions_helpers.rb | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 3ad7e68f2ed0..ce403de05264 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -78,7 +78,7 @@ describe 'merging the MR from the note' do context 'when the current user can merge the MR' do it 'merges the MR' do - write_note("/merge") + write_note("/merge", false) expect(page).to have_content 'Commands applied' diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb index f84938df3f07..e1f4a8f0a573 100644 --- a/spec/support/quick_actions_helpers.rb +++ b/spec/support/quick_actions_helpers.rb @@ -1,10 +1,13 @@ module QuickActionsHelpers - def write_note(text) + def write_note(text, wait = true) Sidekiq::Testing.fake! do page.within('.js-main-target-form') do fill_in 'note[note]', with: text find('.js-comment-submit-button').trigger('click') - wait_for_requests + + if wait + wait_for_requests + end end end end -- GitLab From cf5cc6a9cc0ac1a8fe2ae9dff260fd66e316e483 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 25 Jul 2017 20:01:53 +0100 Subject: [PATCH 102/243] Follow vuex docs to divide store into: actions, getters and mutations Use constants for mutation types as per vuex docs --- .../notes/components/issue_comment_form.vue | 30 +- .../notes/components/issue_discussion.vue | 15 +- .../notes/components/issue_note_actions.vue | 10 +- .../notes/components/issue_notes.vue | 5 +- app/assets/javascripts/notes/index.js | 10 +- .../javascripts/notes/stores/actions.js | 205 +++++++++++ .../javascripts/notes/stores/getters.js | 15 + app/assets/javascripts/notes/stores/index.js | 18 + .../notes/stores/issue_notes_store.js | 342 ------------------ .../notes/stores/issue_notes_utils.js | 35 -- .../notes/stores/mutation_types.js | 11 + .../javascripts/notes/stores/mutations.js | 127 +++++++ app/assets/javascripts/notes/stores/utils.js | 31 ++ 13 files changed, 446 insertions(+), 408 deletions(-) create mode 100644 app/assets/javascripts/notes/stores/actions.js create mode 100644 app/assets/javascripts/notes/stores/getters.js create mode 100644 app/assets/javascripts/notes/stores/index.js delete mode 100644 app/assets/javascripts/notes/stores/issue_notes_store.js delete mode 100644 app/assets/javascripts/notes/stores/issue_notes_utils.js create mode 100644 app/assets/javascripts/notes/stores/mutation_types.js create mode 100644 app/assets/javascripts/notes/stores/mutations.js create mode 100644 app/assets/javascripts/notes/stores/utils.js diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 7896f872a7d9..6519624f7e0f 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -209,12 +209,13 @@ export default { aria-hidden="true" class="fa fa-caret-down toggle-icon"></i> </button> - <ul - class="note-type-dropdown dropdown-open-top dropdown-menu"> + <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> <li :class="{ 'droplab-item-selected': noteType === 'comment' }" @click.prevent="setNoteType('comment')"> - <button class="btn btn-transparent"> + <button + type="button" + class="btn btn-transparent"> <i aria-hidden="true" class="fa fa-check icon"></i> @@ -230,10 +231,13 @@ export default { <li :class="{ 'droplab-item-selected': noteType === 'discussion' }" @click.prevent="setNoteType('discussion')"> - <button class="btn btn-transparent"> + <button + type="button" + class="btn btn-transparent"> <i aria-hidden="true" - class="fa fa-check icon"></i> + class="fa fa-check icon"> + </i> <div class="description"> <strong>Start discussion</strong> <p> @@ -244,21 +248,21 @@ export default { </li> </ul> </div> - <a + <button + type="button" @click="handleSave(true)" v-if="canUpdateIssue" :class="actionButtonClassNames" - class="btn btn-nr btn-comment btn-comment-and-close" - role="button"> + class="btn btn-nr btn-comment btn-comment-and-close"> {{issueActionButtonTitle}} - </a> - <a + </button> + <button + type="button" v-if="note.length" @click="discard" - class="btn btn-cancel js-note-discard" - role="button"> + class="btn btn-cancel js-note-discard"> Discard draft - </a> + </button> </div> </div> </div> diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 371ceffbf20f..7fc4a41fa8ac 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -71,7 +71,8 @@ export default { cancelReplyForm(shouldConfirm) { if (shouldConfirm && this.$refs.noteForm.isDirty) { const msg = 'Are you sure you want to cancel creating this comment?'; - const isConfirmed = confirm(msg); // eslint-disable-line + // eslint-disable-next-line no-alert + const isConfirmed = confirm(msg); if (!isConfirmed) { return; } @@ -112,7 +113,8 @@ export default { :link-href="author.path" :img-src="author.avatar_url" :img-alt="author.name" - :img-size="40" /> + :img-size="40" + /> </div> <div class="timeline-content"> <div class="discussion"> @@ -123,13 +125,15 @@ export default { :note-id="discussion.id" :include-toggle="true" :toggle-handler="toggleDiscussion" - actionText="started a discussion" /> + actionText="started a discussion" + /> <issue-note-edited-text v-if="note.last_updated_by" :edited-at="note.last_updated_at" :edited-by="note.last_updated_by" actionText="Last updated" - className="discussion-headline-light js-discussion-headline" /> + className="discussion-headline-light js-discussion-headline" + /> </div> </div> <div @@ -142,7 +146,8 @@ export default { v-for="note in note.notes" :is="componentName(note)" :note="componentData(note)" - key="note.id" /> + key="note.id" + /> </ul> <div class="flash-container"></div> <div class="discussion-reply-holder"> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 48b1e1d513f0..df5a060d894c 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -2,6 +2,7 @@ import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; +import loadingIcon from '../../vue_shared/components/loadingIcon.vue'; export default { props: { @@ -78,9 +79,7 @@ export default { data-position="right" href="#" title="Add reaction"> - <i - aria-hidden="true" - class="fa fa-spinner fa-spin"></i> + <loading-icon /> <span v-html="emojiSmiling" class="link-highlight award-control-icon-neutral"></span> @@ -122,15 +121,14 @@ export default { </a> </li> <li v-if="canEdit"> - <a + <button @click.prevent="deleteHandler" class="btn btn-transparent js-note-delete js-note-delete" - href="#" type="button"> <span class="text-danger"> Delete comment </span> - </a> + </button> </li> </ul> </div> diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 94372cd9a774..2fe071cc9905 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -12,10 +12,7 @@ import issueSystemNote from './issue_system_note.vue'; import issueCommentForm from './issue_comment_form.vue'; import placeholderNote from './issue_placeholder_note.vue'; import placeholderSystemNote from './issue_placeholder_system_note.vue'; - -Vue.use(Vuex); -Vue.use(VueResource); -const store = new Vuex.Store(storeOptions); +import store from './store'; export default { name: 'IssueNotes', diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index d48a9111ffe4..914e08ff1125 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -8,9 +8,13 @@ document.addEventListener('DOMContentLoaded', () => { components: { issueNotes, }, - template: ` - <issue-notes ref="notes" /> - `, + render(createElement) { + return createElement('issue-notes', { + attrs: { + ref: 'notes', + }, + }); + }, }); window.issueNotes = { diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js new file mode 100644 index 000000000000..3cfdcd68e0dd --- /dev/null +++ b/app/assets/javascripts/notes/stores/actions.js @@ -0,0 +1,205 @@ +/* global Flash */ + +import * as types from './mutation_types'; +import * as utils from './issue_notes_utils'; +import service from '../services/issue_notes_service'; +import loadAwardsHandler from '../../awards_handler'; +import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; + +export const fetchNotes = ({ commit }, path) => service + .fetchNotes(path) + .then(res => res.json()) + .then((res) => { + commit(types.SET_INITAL_NOTES, res); + }); + +export const deleteNote = ({ commit }, note) => service + .deleteNote(note.path) + .then(() => { + commit(types.DELETE_NOTE, note); + }); + +export const updateNote = ({ commit }, data) => { + const { endpoint, note } = data; + + return service + .updateNote(endpoint, note) + .then(res => res.json()) + .then((res) => { + commit(types.UPDATE_NOTE, res); + }); +}; + +export const replyToDiscussion = ({ commit }, note) => { + const { endpoint, data } = note; + + return service + .replyToDiscussion(endpoint, data) + .then(res => res.json()) + .then((res) => { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); + + return res; + }); +}; + +export const createNewNote = ({ commit }, note) => { + const { endpoint, data } = note; + + return service + .createNewNote(endpoint, data) + .then(res => res.json()) + .then((res) => { + if (!res.errors) { + commit(types.ADD_NEW_NOTE, res); + } + return res; + }); +}; + +export const saveNote = ({ commit, dispatch }, noteData) => { + const { note } = noteData.data.note; + let placeholderText = note; + const hasQuickActions = utils.hasQuickActions(placeholderText); + const replyId = noteData.data.in_reply_to_discussion_id; + const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; + + if (hasQuickActions) { + placeholderText = utils.stripQuickActions(placeholderText); + } + + if (placeholderText.length) { + commit(types.SHOW_PLACEHOLDER_NOTE, { + noteBody: placeholderText, + replyId, + }); + } + + if (hasQuickActions) { + commit(types.SHOW_PLACEHOLDER_NOTE, { + isSystemNote: true, + noteBody: utils.getQuickActionText(note), + replyId, + }); + } + + return dispatch(methodToDispatch, noteData) + .then((res) => { + const { errors } = res; + const commandsChanges = res.commands_changes; + + if (hasQuickActions && Object.keys(errors).length) { + dispatch('poll'); + $('.js-gfm-input').trigger('clear-commands-cache.atwho'); + Flash('Commands applied', 'notice', $(noteData.flashContainer)); + } + + if (commandsChanges) { + if (commandsChanges.emoji_award) { + const votesBlock = $('.js-awards-block').eq(0); + + loadAwardsHandler() + .then((awardsHandler) => { + awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); + awardsHandler.scrollToAwards(); + }) + .catch(() => { + Flash( + 'Something went wrong while adding your award. Please try again.', + null, + $(noteData.flashContainer), + ); + }); + } + + if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) { + sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); + } + } + + if (errors && errors.commands_only) { + Flash(errors.commands_only, 'notice', $(noteData.flashContainer)); + } + commit(types.REMOVE_PLACEHOLDER_NOTES); + + return res; + }) + .catch(() => { + Flash( + 'Your comment could not be submitted! Please check your network connection and try again.', + 'alert', + $(noteData.flashContainer), + ); + commit(types.REMOVE_PLACEHOLDER_NOTES); + }); +}; + +export const poll = ({ commit, state, getters }) => { + const { notesPath } = $('.js-notes-wrapper')[0].dataset; + + return service + .poll(`${notesPath}?full_data=1`, state.lastFetchedAt) + .then(res => res.json()) + .then((res) => { + if (res.notes.length) { + const { notesById } = getters; + + res.notes.forEach((note) => { + if (notesById[note.id]) { + commit(types.UPDATE_NOTE, note); + } else if (note.type === 'DiscussionNote') { + const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); + + if (discussion) { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); + } else { + commit(types.ADD_NEW_NOTE, note); + } + } else { + commit(types.ADD_NEW_NOTE, note); + } + }); + } + + return res; + }); +}; + +export const toggleAward = ({ commit, getters, dispatch }, data) => { + const { endpoint, awardName, noteId, skipMutalityCheck } = data; + const note = getters.notesById[noteId]; + + return service + .toggleAward(endpoint, { name: awardName }) + .then(res => res.json()) + .then(() => { + commit(types.TOGGLE_AWARD, { awardName, note }); + + if (!skipMutalityCheck && (awardName === 'thumbsup' || awardName === 'thumbsdown')) { + const counterAward = awardName === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + const targetNote = getters.notesById[noteId]; + let amIAwarded = false; + + targetNote.award_emoji.forEach((a) => { + if (a.name === counterAward && a.user.id === window.gon.current_user_id) { + amIAwarded = true; + } + }); + + if (amIAwarded) { + Object.assign(data, { awardName: counterAward }); + Object.assign(data, { skipMutalityCheck: true }); + + dispatch(types.TOGGLE_AWARD, data); + } + } + }); +}; + +export const scrollToNoteIfNeeded = (context, el) => { + const isInViewport = gl.utils.isInViewport(el[0]); + + if (!isInViewport) { + gl.utils.scrollToElement(el); + } +}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js new file mode 100644 index 000000000000..c3a9f0a5e89a --- /dev/null +++ b/app/assets/javascripts/notes/stores/getters.js @@ -0,0 +1,15 @@ +export const notes = state => state.notes; + +export const targetNoteHash = state => state.targetNoteHash; + +export const notesById = (state) => { + const notesByIdObject = {}; + + state.notes.forEach((note) => { + note.notes.forEach((n) => { + notesByIdObject[n.id] = n; + }); + }); + + return notesByIdObject; +}; diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js new file mode 100644 index 000000000000..edca63fae673 --- /dev/null +++ b/app/assets/javascripts/notes/stores/index.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: { + notes: [], + targetNoteHash: null, + lastFetchedAt: null, + }, + actions, + getters, + mutations, +}); diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js deleted file mode 100644 index abe5edee03b9..000000000000 --- a/app/assets/javascripts/notes/stores/issue_notes_store.js +++ /dev/null @@ -1,342 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Flash */ - -import service from '../services/issue_notes_service'; -import utils from './issue_notes_utils'; -import loadAwardsHandler from '../../awards_handler'; -import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; - -const state = { - notes: [], - targetNoteHash: null, - lastFetchedAt: null, -}; - -const getters = { - notes(storeState) { - return storeState.notes; - }, - targetNoteHash(storeState) { - return storeState.targetNoteHash; - }, - notesById(storeState) { - const notesById = {}; - - storeState.notes.forEach((note) => { - note.notes.forEach((n) => { - notesById[n.id] = n; - }); - }); - - return notesById; - }, -}; - -const mutations = { - setInitialNotes(storeState, notes) { - storeState.notes = notes; - }, - setTargetNoteHash(storeState, hash) { - storeState.targetNoteHash = hash; - }, - toggleDiscussion(storeState, { discussionId }) { - const discussion = utils.findNoteObjectById(storeState.notes, discussionId); - - discussion.expanded = !discussion.expanded; - }, - deleteNote(storeState, note) { - const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id); - - if (noteObj.individual_note) { - storeState.notes.splice(storeState.notes.indexOf(noteObj), 1); - } else { - const comment = utils.findNoteObjectById(noteObj.notes, note.id); - noteObj.notes.splice(noteObj.notes.indexOf(comment), 1); - - if (!noteObj.notes.length) { - storeState.notes.splice(storeState.notes.indexOf(noteObj), 1); - } - } - }, - addNewReplyToDiscussion(storeState, note) { - const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id); - - if (noteObj) { - noteObj.notes.push(note); - } - }, - updateNote(storeState, note) { - const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id); - - if (noteObj.individual_note) { - noteObj.notes.splice(0, 1, note); - } else { - const comment = utils.findNoteObjectById(noteObj.notes, note.id); - noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); - } - }, - addNewNote(storeState, note) { - const { discussion_id, type } = note; - const noteData = { - expanded: true, - id: discussion_id, - individual_note: !(type === 'DiscussionNote'), - notes: [note], - reply_id: discussion_id, - }; - - storeState.notes.push(noteData); - }, - toggleAward(storeState, data) { - const { awardName, note } = data; - const { id, name, username } = window.gl.currentUserData; - let index = -1; - - note.award_emoji.forEach((a, i) => { - if (a.name === awardName && a.user.id === id) { - index = i; - } - }); - - if (index > -1) { // if I am awarded, remove my award - note.award_emoji.splice(index, 1); - } else { - note.award_emoji.push({ - name: awardName, - user: { id, name, username }, - }); - } - }, - setLastFetchedAt(storeState, fetchedAt) { - storeState.lastFetchedAt = fetchedAt; - }, - showPlaceholderNote(storeState, data) { - let notesArr = storeState.notes; - if (data.replyId) { - notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes; - } - - notesArr.push({ - individual_note: true, - isPlaceholderNote: true, - placeholderType: data.isSystemNote ? 'systemNote' : 'note', - notes: [ - { - body: data.noteBody, - }, - ], - }); - }, - removePlaceholderNotes(storeState) { - const { notes } = storeState; - - for (let i = notes.length - 1; i >= 0; i -= 1) { - const note = notes[i]; - const children = note.notes; - - if (children.length && !note.individual_note) { // remove placeholder from discussions - for (let j = children.length - 1; j >= 0; j -= 1) { - if (children[j].isPlaceholderNote) { - children.splice(j, 1); - } - } - } else if (note.isPlaceholderNote) { // remove placeholders from state root - notes.splice(i, 1); - } - } - }, -}; - -const actions = { - fetchNotes(context, path) { - return service - .fetchNotes(path) - .then(res => res.json()) - .then((res) => { - context.commit('setInitialNotes', res); - }); - }, - deleteNote(context, note) { - return service - .deleteNote(note.path) - .then(() => { - context.commit('deleteNote', note); - }); - }, - updateNote(context, data) { - const { endpoint, note } = data; - - return service - .updateNote(endpoint, note) - .then(res => res.json()) - .then((res) => { - context.commit('updateNote', res); - }); - }, - replyToDiscussion(context, noteData) { - const { endpoint, data } = noteData; - - return service - .replyToDiscussion(endpoint, data) - .then(res => res.json()) - .then((res) => { - context.commit('addNewReplyToDiscussion', res); - - return res; - }); - }, - createNewNote(context, noteData) { - const { endpoint, data } = noteData; - - return service - .createNewNote(endpoint, data) - .then(res => res.json()) - .then((res) => { - if (!res.errors) { - context.commit('addNewNote', res); - } - return res; - }); - }, - saveNote(context, noteData) { - const { note } = noteData.data.note; - let placeholderText = note; - const hasQuickActions = utils.hasQuickActions(placeholderText); - const replyId = noteData.data.in_reply_to_discussion_id; - const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; - - if (hasQuickActions) { - placeholderText = utils.stripQuickActions(placeholderText); - } - - if (placeholderText.length) { - context.commit('showPlaceholderNote', { - noteBody: placeholderText, - replyId, - }); - } - - if (hasQuickActions) { - context.commit('showPlaceholderNote', { - isSystemNote: true, - noteBody: utils.getQuickActionText(note), - replyId, - }); - } - - return context.dispatch(methodToDispatch, noteData) - .then((res) => { - const { errors } = res; - const commandsChanges = res.commands_changes; - - if (hasQuickActions && Object.keys(errors).length) { - context.dispatch('poll'); - $('.js-gfm-input').trigger('clear-commands-cache.atwho'); - Flash('Commands applied', 'notice', $(noteData.flashContainer)); - } - - if (commandsChanges) { - if (commandsChanges.emoji_award) { - const votesBlock = $('.js-awards-block').eq(0); - - loadAwardsHandler().then((awardsHandler) => { - awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); - awardsHandler.scrollToAwards(); - }).catch(() => { - const msg = 'Something went wrong while adding your award. Please try again.'; - Flash(msg, $(noteData.flashContainer)); - }); - } - - if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) { - sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); - } - } - - if (errors && errors.commands_only) { - Flash(errors.commands_only, 'notice', $(noteData.flashContainer)); - } - context.commit('removePlaceholderNotes'); - - return res; - }) - .catch(() => { - const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; - Flash(msg, 'alert', $(noteData.flashContainer)); - context.commit('removePlaceholderNotes'); - }); - }, - poll(context) { - const { notesPath } = $('.js-notes-wrapper')[0].dataset; - - return service - .poll(`${notesPath}?full_data=1`, context.state.lastFetchedAt) - .then(res => res.json()) - .then((res) => { - if (res.notes.length) { - const { notesById } = context.getters; - - res.notes.forEach((note) => { - if (notesById[note.id]) { - context.commit('updateNote', note); - } else if (note.type === 'DiscussionNote') { - const discussion = utils.findNoteObjectById(context.state.notes, note.discussion_id); - - if (discussion) { - context.commit('addNewReplyToDiscussion', note); - } else { - context.commit('addNewNote', note); - } - } else { - context.commit('addNewNote', note); - } - }); - } - - return res; - }); - }, - toggleAward(context, data) { - const { endpoint, awardName, noteId, skipMutalityCheck } = data; - const note = context.getters.notesById[noteId]; - - return service - .toggleAward(endpoint, { name: awardName }) - .then(res => res.json()) - .then(() => { - context.commit('toggleAward', { awardName, note }); - - if (!skipMutalityCheck && (awardName === 'thumbsup' || awardName === 'thumbsdown')) { - const counterAward = awardName === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; - const targetNote = context.getters.notesById[noteId]; - let amIAwarded = false; - - targetNote.award_emoji.forEach((a) => { - if (a.name === counterAward && a.user.id === window.gon.current_user_id) { - amIAwarded = true; - } - }); - - if (amIAwarded) { - data.awardName = counterAward; - data.skipMutalityCheck = true; - context.dispatch('toggleAward', data); - } - } - }); - }, - scrollToNoteIfNeeded(context, el) { - const isInViewport = gl.utils.isInViewport(el[0]); - - if (!isInViewport) { - gl.utils.scrollToElement(el); - } - }, -}; - -export default { - state, - getters, - mutations, - actions, -}; diff --git a/app/assets/javascripts/notes/stores/issue_notes_utils.js b/app/assets/javascripts/notes/stores/issue_notes_utils.js deleted file mode 100644 index eac80c9f9c2d..000000000000 --- a/app/assets/javascripts/notes/stores/issue_notes_utils.js +++ /dev/null @@ -1,35 +0,0 @@ -import AjaxCache from '~/lib/utils/ajax_cache'; - -const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; - -export default { - findNoteObjectById(notes, id) { - return notes.filter(n => n.id === id)[0]; - }, - getQuickActionText(note) { - let text = 'Applying command'; - const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; - - const executedCommands = quickActions.filter((command) => { - const commandRegex = new RegExp(`/${command.name}`); - return commandRegex.test(note); - }); - - if (executedCommands && executedCommands.length) { - if (executedCommands.length > 1) { - text = 'Applying multiple commands'; - } else { - const commandDescription = executedCommands[0].description.toLowerCase(); - text = `Applying command to ${commandDescription}`; - } - } - - return text; - }, - hasQuickActions(note) { - return REGEX_QUICK_ACTIONS.test(note); - }, - stripQuickActions(note) { - return note.replace(REGEX_QUICK_ACTIONS, '').trim(); - }, -}; diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js new file mode 100644 index 000000000000..f84f26684ca9 --- /dev/null +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -0,0 +1,11 @@ +export const ADD_NEW_NOTE = 'ADD_NEW_NOTE'; +export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION'; +export const DELETE_NOTE = 'DELETE_NOTE'; +export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; +export const SET_INITAL_NOTES = 'SET_INITIAL_NOTES'; +export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; +export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH'; +export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE'; +export const TOGGLE_AWARD = 'TOGGLE_AWARD'; +export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; +export const UPDATE_NOTE = 'UPDATE_NOTE'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js new file mode 100644 index 000000000000..61be4aa18645 --- /dev/null +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -0,0 +1,127 @@ +import * as utils from './utils'; +import * as types from './mutation_types'; + +export default { + [types.ADD_NEW_NOTE](state, note) { + const { discussion_id, type } = note; + const noteData = { + expanded: true, + id: discussion_id, + individual_note: !(type === 'DiscussionNote'), + notes: [note], + reply_id: discussion_id, + }; + + state.notes.push(noteData); + }, + + [types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) { + const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id); + + if (noteObj) { + noteObj.notes.push(note); + } + }, + + [types.DELETE_NOTE](state, note) { + const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id); + + if (noteObj.individual_note) { + state.notes.splice(state.notes.indexOf(noteObj), 1); + } else { + const comment = utils.findNoteObjectById(noteObj.notes, note.id); + noteObj.notes.splice(noteObj.notes.indexOf(comment), 1); + + if (!noteObj.notes.length) { + state.notes.splice(state.notes.indexOf(noteObj), 1); + } + } + }, + + [types.REMOVE_PLACEHOLDER_NOTES](state) { + const { notes } = state; + + for (let i = notes.length - 1; i >= 0; i -= 1) { + const note = notes[i]; + const children = note.notes; + + if (children.length && !note.individual_note) { // remove placeholder from discussions + for (let j = children.length - 1; j >= 0; j -= 1) { + if (children[j].isPlaceholderNote) { + children.splice(j, 1); + } + } + } else if (note.isPlaceholderNote) { // remove placeholders from state root + notes.splice(i, 1); + } + } + }, + + [types.SET_INITAL_NOTES](state, notes) { + state.notes = notes; + }, + + [types.SET_LAST_FETCHED_AT](state, fetchedAt) { + state.lastFetchedAt = fetchedAt; + }, + + [types.SET_TARGET_NOTE_HASH](state, hash) { + state.targetNoteHash = hash; + }, + + [types.SHOW_PLACEHOLDER_NOTE](state, data) { + let notesArr = state.notes; + if (data.replyId) { + notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes; + } + + notesArr.push({ + individual_note: true, + isPlaceholderNote: true, + placeholderType: data.isSystemNote ? 'systemNote' : 'note', + notes: [ + { + body: data.noteBody, + }, + ], + }); + }, + + [types.TOGGLE_AWARD](state, data) { + const { awardName, note } = data; + const { id, name, username } = window.gl.currentUserData; + let index = -1; + + note.award_emoji.forEach((a, i) => { + if (a.name === awardName && a.user.id === id) { + index = i; + } + }); + + if (index > -1) { // if I am awarded, remove my award + note.award_emoji.splice(index, 1); + } else { + note.award_emoji.push({ + name: awardName, + user: { id, name, username }, + }); + } + }, + + [types.TOGGLE_DISCUSSION](state, { discussionId }) { + const discussion = utils.findNoteObjectById(state.notes, discussionId); + + discussion.expanded = !discussion.expanded; + }, + + [types.UPDATE_NOTE](state, note) { + const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id); + + if (noteObj.individual_note) { + noteObj.notes.splice(0, 1, note); + } else { + const comment = utils.findNoteObjectById(noteObj.notes, note.id); + noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); + } + }, +}; diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js new file mode 100644 index 000000000000..6074115e8558 --- /dev/null +++ b/app/assets/javascripts/notes/stores/utils.js @@ -0,0 +1,31 @@ +import AjaxCache from '~/lib/utils/ajax_cache'; + +const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; + +export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; + +export const getQuickActionText = (note) => { + let text = 'Applying command'; + const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; + + const executedCommands = quickActions.filter((command) => { + const commandRegex = new RegExp(`/${command.name}`); + return commandRegex.test(note); + }); + + if (executedCommands && executedCommands.length) { + if (executedCommands.length > 1) { + text = 'Applying multiple commands'; + } else { + const commandDescription = executedCommands[0].description.toLowerCase(); + text = `Applying command to ${commandDescription}`; + } + } + + return text; +}; + +export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); + +export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); + -- GitLab From 4e81ad2ab9eb0190527514cd8b7cef44df4a9bfc Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 26 Jul 2017 09:01:12 +0100 Subject: [PATCH 103/243] [ci skip] Add constants --- .../notes/components/issue_comment_form.vue | 266 +++++++++--------- app/assets/javascripts/notes/constants.js | 8 + .../javascripts/notes/stores/actions.js | 5 +- .../javascripts/notes/stores/mutations.js | 5 +- 4 files changed, 150 insertions(+), 134 deletions(-) create mode 100644 app/assets/javascripts/notes/constants.js diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 6519624f7e0f..c913f3f5fad6 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,155 +1,160 @@ <script> /* global Flash */ -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import markdownField from '../../vue_shared/components/markdown/field.vue'; -import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; -import eventHub from '../event_hub'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import markdownField from '../../vue_shared/components/markdown/field.vue'; + import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; + import eventHub from '../event_hub'; + import * as constants from '../constants'; -export default { - data() { - const { create_note_path, state } = window.gl.issueData; - const { currentUserData } = window.gl; + export default { + data() { + const { create_note_path, state } = window.gl.issueData; + const { currentUserData } = window.gl; - return { - note: '', - markdownDocsUrl: '', - markdownPreviewUrl: gl.issueData.preview_note_path, - noteType: 'comment', - issueState: state, - endpoint: create_note_path, - author: currentUserData, - }; - }, - components: { - userAvatarLink, - markdownField, - issueNoteSignedOutWidget, - }, - computed: { - isLoggedIn() { - return window.gon.current_user_id; - }, - commentButtonTitle() { - return this.noteType === 'comment' ? 'Comment' : 'Start discussion'; + return { + note: '', + markdownDocsUrl: '', + markdownPreviewUrl: gl.issueData.preview_note_path, + noteType: constants.COMMENT, + issueState: state, + endpoint: create_note_path, + author: currentUserData, + }; }, - isIssueOpen() { - return this.issueState === 'opened' || this.issueState === 'reopened'; + components: { + userAvatarLink, + markdownField, + issueNoteSignedOutWidget, }, - issueActionButtonTitle() { - if (this.note.length) { - const actionText = this.isIssueOpen ? 'close' : 'reopen'; + computed: { + isLoggedIn() { + return window.gon.current_user_id; + }, + commentButtonTitle() { + return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; + }, + isIssueOpen() { + return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; + }, + issueActionButtonTitle() { + if (this.note.length) { + const actionText = this.isIssueOpen ? 'close' : 'reopen'; - return this.noteType === 'comment' ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`; - } + return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`; + } - return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; - }, - actionButtonClassNames() { - return { - 'btn-reopen': !this.isIssueOpen, - 'btn-close': this.isIssueOpen, - 'js-note-target-close': this.isIssueOpen, - 'js-note-target-reopen': !this.isIssueOpen, - }; - }, - canUpdateIssue() { - const { issueData } = window.gl; - return issueData && issueData.current_user.can_update; + return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; + }, + actionButtonClassNames() { + return { + 'btn-reopen': !this.isIssueOpen, + 'btn-close': this.isIssueOpen, + 'js-note-target-close': this.isIssueOpen, + 'js-note-target-reopen': !this.isIssueOpen, + }; + }, + canUpdateIssue() { + const { issueData } = window.gl; + return issueData && issueData.current_user.can_update; + }, }, - }, - methods: { - handleSave(withIssueAction) { - if (this.note.length) { - const noteData = { - endpoint: this.endpoint, - flashContainer: this.$el, - data: { - full_data: true, - note: { - noteable_type: 'Issue', - noteable_id: window.gl.issueData.id, - note: this.note, + methods: { + handleSave(withIssueAction) { + if (this.note.length) { + const noteData = { + endpoint: this.endpoint, + flashContainer: this.$el, + data: { + full_data: true, + note: { + noteable_type: 'Issue', + noteable_id: window.gl.issueData.id, + note: this.note, + }, }, - }, - }; + }; - if (this.noteType === 'discussion') { - noteData.data.note.type = 'DiscussionNote'; - } + if (this.noteType === constants.DISCUSSION) { + noteData.data.note.type = constants.DISCUSSION_NOTE; + } - this.$store.dispatch('saveNote', noteData) - .then((res) => { - if (res.errors) { - if (res.errors.commands_only) { - this.discard(); + this.$store.dispatch('saveNote', noteData) + .then((res) => { + if (res.errors) { + if (res.errors.commands_only) { + this.discard(); + } else { + this.handleError(); + } } else { - this.handleError(); + this.discard(); } - } else { - this.discard(); - } - }) - .catch(() => { - this.discard(false); - }); - } - - if (withIssueAction) { - if (this.isIssueOpen) { - gl.issueData.state = 'closed'; - this.issueState = 'closed'; - } else { - gl.issueData.state = 'reopened'; - this.issueState = 'reopened'; + }) + .catch(() => { + this.discard(false); + }); } - this.isIssueOpen = !this.isIssueOpen; - // This is out of scope for the Notes Vue component. - // It was the shortest path to update the issue state and relevant places. - const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close'; - $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); - } - }, - discard(shouldClear = true) { - // `blur` is needed to clear slash commands autocomplete cache if event fired. - // `focus` is needed to remain cursor in the textarea. - this.$refs.textarea.blur(); - this.$refs.textarea.focus(); + if (withIssueAction) { + if (this.isIssueOpen) { + gl.issueData.state = constants.CLOSED; + this.issueState = constants.CLOSED; + } else { + gl.issueData.state = constants.REOPENED; + this.issueState =constants.REOPENED; + } + this.isIssueOpen = !this.isIssueOpen; - if (shouldClear) { - this.note = ''; - } - }, - setNoteType(type) { - this.noteType = type; - }, - handleError() { - Flash('Something went wrong while adding your comment. Please try again.'); - }, - editMyLastNote() { - if (this.note === '') { - const myLastNoteId = $('.js-my-note').last().attr('id'); + // This is out of scope for the Notes Vue component. + // It was the shortest path to update the issue state and relevant places. + const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close'; + $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); + } + }, + discard(shouldClear = true) { + // `blur` is needed to clear slash commands autocomplete cache if event fired. + // `focus` is needed to remain cursor in the textarea. + this.$refs.textarea.blur(); + this.$refs.textarea.focus(); + + if (shouldClear) { + this.note = ''; + } + }, + setNoteType(type) { + this.noteType = type; + }, + handleError() { + Flash('Something went wrong while adding your comment. Please try again.'); + }, + editMyLastNote() { + if (this.note === '') { + const myLastNoteId = $('.js-my-note').last().attr('id'); - if (myLastNoteId) { - eventHub.$emit('enterEditMode', { - noteId: parseInt(myLastNoteId.replace('note_', ''), 10), - }); + if (myLastNoteId) { + eventHub.$emit('enterEditMode', { + noteId: parseInt(myLastNoteId.replace('note_', ''), 10), + }); + } } - } + }, }, - }, - mounted() { - const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); - const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); + mounted() { + const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); + const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); - this.markdownDocsUrl = issueData.markdownDocs; + this.markdownDocsUrl = issueData.markdownDocs; + + eventHub.$on('issueStateChanged', (isClosed) => { + this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; + }); + }, - eventHub.$on('issueStateChanged', (isClosed) => { - this.issueState = isClosed ? 'closed' : 'reopened'; - }); - }, -}; + destroyed() { + eventHub.$off('issueStateChanged'); + } + }; </script> <template> @@ -167,7 +172,8 @@ export default { :link-href="author.path" :img-src="author.avatar_url" :img-alt="author.name" - :img-size="40" /> + :img-size="40" + /> </div> <div class="js-main-target-form timeline-content timeline-content-form common-note-form"> <markdown-field diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js new file mode 100644 index 000000000000..663434587a25 --- /dev/null +++ b/app/assets/javascripts/notes/constants.js @@ -0,0 +1,8 @@ +export const DISCUSSION_NOTE = 'DiscussionNote'; +export const DISCUSSION = 'discussion'; +export const NOTE = 'note'; +export const SYSTEM_NOTE = 'systemNote'; +export const COMMENT = 'comment'; +export const OPENED = 'opened'; +export const REOPENED = 'reopened'; +export const CLOSED = 'closed'; \ No newline at end of file diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 3cfdcd68e0dd..0f81e08a2c8d 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -1,7 +1,8 @@ /* global Flash */ import * as types from './mutation_types'; -import * as utils from './issue_notes_utils'; +import * as utils from './utils'; +import * as constants from '../constants'; import service from '../services/issue_notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; @@ -147,7 +148,7 @@ export const poll = ({ commit, state, getters }) => { res.notes.forEach((note) => { if (notesById[note.id]) { commit(types.UPDATE_NOTE, note); - } else if (note.type === 'DiscussionNote') { + } else if (note.type === constants.DISCUSSION_NOTE) { const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); if (discussion) { diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 61be4aa18645..bb2ed91e570d 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -1,5 +1,6 @@ import * as utils from './utils'; import * as types from './mutation_types'; +import * as constants from '../constants'; export default { [types.ADD_NEW_NOTE](state, note) { @@ -7,7 +8,7 @@ export default { const noteData = { expanded: true, id: discussion_id, - individual_note: !(type === 'DiscussionNote'), + individual_note: !(type === constants.DISCUSSION_NOTE), notes: [note], reply_id: discussion_id, }; @@ -78,7 +79,7 @@ export default { notesArr.push({ individual_note: true, isPlaceholderNote: true, - placeholderType: data.isSystemNote ? 'systemNote' : 'note', + placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE, notes: [ { body: data.noteBody, -- GitLab From ffef16690c61581e3254dd074e55b58dc4d2b7bb Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 26 Jul 2017 12:02:01 +0100 Subject: [PATCH 104/243] Use mapActions, mapGetters and mapMutations for components --- .../notes/components/issue_comment_form.vue | 6 +- .../notes/components/issue_discussion.vue | 196 +++++------ .../notes/components/issue_note.vue | 208 ++++++------ .../notes/components/issue_note_actions.vue | 133 ++++---- .../components/issue_note_awards_list.vue | 315 +++++++++--------- .../notes/components/issue_note_body.vue | 118 +++---- .../components/issue_note_edited_text.vue | 49 +-- .../notes/components/issue_note_form.vue | 148 ++++---- .../notes/components/issue_note_header.vue | 126 +++---- .../issue_note_signed_out_widget.vue | 26 +- .../notes/components/issue_notes.vue | 225 +++++++------ .../components/issue_placeholder_note.vue | 27 +- .../issue_placeholder_system_note.vue | 14 +- .../notes/components/issue_system_note.vue | 54 +-- app/assets/javascripts/notes/constants.js | 2 +- .../javascripts/notes/stores/actions.js | 7 +- 16 files changed, 850 insertions(+), 804 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index c913f3f5fad6..bd6caddb40f1 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,6 +1,7 @@ <script> /* global Flash */ + import { mapActions } from 'vuex'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; @@ -60,6 +61,9 @@ }, }, methods: { + ...mapActions([ + 'saveNote' + ]), handleSave(withIssueAction) { if (this.note.length) { const noteData = { @@ -79,7 +83,7 @@ noteData.data.note.type = constants.DISCUSSION_NOTE; } - this.$store.dispatch('saveNote', noteData) + this.saveNote(noteData) .then((res) => { if (res.errors) { if (res.errors.commands_only) { diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 7fc4a41fa8ac..21b5b2881216 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,108 +1,112 @@ <script> -/* global Flash */ + /* global Flash */ + import { mapActions } from 'vuex'; + import { TOGGLE_DISCUSSION } from '../stores/mutation_types'; + import { SYSTEM_NOTE } from '../constants'; + import issueNote from './issue_note.vue'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import issueNoteHeader from './issue_note_header.vue'; + import issueNoteActions from './issue_note_actions.vue'; + import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; + import issueNoteEditedText from './issue_note_edited_text.vue'; + import issueNoteForm from './issue_note_form.vue'; + import placeholderNote from './issue_placeholder_note.vue'; + import placeholderSystemNote from './issue_placeholder_system_note.vue'; -import issueNote from './issue_note.vue'; -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import issueNoteHeader from './issue_note_header.vue'; -import issueNoteActions from './issue_note_actions.vue'; -import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; -import issueNoteEditedText from './issue_note_edited_text.vue'; -import issueNoteForm from './issue_note_form.vue'; -import placeholderNote from './issue_placeholder_note.vue'; -import placeholderSystemNote from './issue_placeholder_system_note.vue'; - -export default { - props: { - note: { - type: Object, - required: true, + export default { + props: { + note: { + type: Object, + required: true, + }, }, - }, - data() { - return { - newNotePath: window.gl.issueData.create_note_path, - isReplying: false, - }; - }, - components: { - issueNote, - userAvatarLink, - issueNoteHeader, - issueNoteActions, - issueNoteSignedOutWidget, - issueNoteEditedText, - issueNoteForm, - placeholderNote, - placeholderSystemNote, - }, - computed: { - discussion() { - return this.note.notes[0]; + data() { + return { + newNotePath: window.gl.issueData.create_note_path, + isReplying: false, + }; }, - author() { - return this.discussion.author; + components: { + issueNote, + userAvatarLink, + issueNoteHeader, + issueNoteActions, + issueNoteSignedOutWidget, + issueNoteEditedText, + issueNoteForm, + placeholderNote, + placeholderSystemNote, }, - canReply() { - return window.gl.issueData.current_user.can_create_note; + computed: { + discussion() { + return this.note.notes[0]; + }, + author() { + return this.discussion.author; + }, + canReply() { + return window.gl.issueData.current_user.can_create_note; + }, }, - }, - methods: { - componentName(note) { - if (note.isPlaceholderNote) { - if (note.placeholderType === 'systemNote') { - return placeholderSystemNote; + methods: { + ...mapActions([ + 'saveNote', + ]), + ...mapMutations({ + toggleDiscussion: TOGGLE_DISCUSSION, + }), + componentName(note) { + if (note.isPlaceholderNote) { + if (note.placeholderType === SYSTEM_NOTE) { + return placeholderSystemNote; + } + return placeholderNote; } - return placeholderNote; - } - return issueNote; - }, - componentData(note) { - return note.isPlaceholderNote ? note.notes[0] : note; - }, - toggleDiscussion() { - this.$store.commit('toggleDiscussion', { - discussionId: this.note.id, - }); - }, - showReplyForm() { - this.isReplying = true; - }, - cancelReplyForm(shouldConfirm) { - if (shouldConfirm && this.$refs.noteForm.isDirty) { - const msg = 'Are you sure you want to cancel creating this comment?'; - // eslint-disable-next-line no-alert - const isConfirmed = confirm(msg); - if (!isConfirmed) { - return; + return issueNote; + }, + componentData(note) { + return note.isPlaceholderNote ? note.notes[0] : note; + }, + toggleDiscussion() { + this.toggleDiscussion({ discussionId: this.note.id }); + }, + showReplyForm() { + this.isReplying = true; + }, + cancelReplyForm(shouldConfirm) { + if (shouldConfirm && this.$refs.noteForm.isDirty) { + const msg = 'Are you sure you want to cancel creating this comment?'; + // eslint-disable-next-line no-alert + const isConfirmed = confirm(msg); + if (!isConfirmed) { + return; + } } - } - this.isReplying = false; - }, - saveReply({ note }) { - const replyData = { - endpoint: this.newNotePath, - flashContainer: this.$el, - data: { - in_reply_to_discussion_id: this.note.reply_id, - target_type: 'issue', - target_id: this.discussion.noteable_id, - note: { note }, - full_data: true, - }, - }; + this.isReplying = false; + }, + saveReply({ note }) { + const replyData = { + endpoint: this.newNotePath, + flashContainer: this.$el, + data: { + in_reply_to_discussion_id: this.note.reply_id, + target_type: 'issue', + target_id: this.discussion.noteable_id, + note: { note }, + full_data: true, + }, + }; - this.$store.dispatch('saveNote', replyData) - .then(() => { - this.isReplying = false; - }) - .catch(() => { - Flash('Something went wrong while adding your reply. Please try again.'); - }); + this.saveNote(replyData) + .then(() => { + this.isReplying = false; + }) + .catch(() => Flash('Something went wrong while adding your reply. Please try again.')); + }, }, - }, -}; + }; </script> <template> @@ -132,8 +136,7 @@ export default { :edited-at="note.last_updated_at" :edited-by="note.last_updated_by" actionText="Last updated" - className="discussion-headline-light js-discussion-headline" - /> + className="discussion-headline-light js-discussion-headline" /> </div> </div> <div @@ -162,7 +165,8 @@ export default { saveButtonTitle="Comment" :update-handler="saveReply" :cancel-handler="cancelReplyForm" - ref="noteForm" /> + ref="noteForm" + /> <issue-note-signed-out-widget v-if="!canReply" /> </div> </div> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 6514ec3b7aba..d490578ce6f1 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -1,116 +1,118 @@ <script> -/* global Flash */ + /* global Flash */ -import { mapGetters } from 'vuex'; -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import issueNoteHeader from './issue_note_header.vue'; -import issueNoteActions from './issue_note_actions.vue'; -import issueNoteBody from './issue_note_body.vue'; -import eventHub from '../event_hub'; + import { mapGetters, mapActions } from 'vuex'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import issueNoteHeader from './issue_note_header.vue'; + import issueNoteActions from './issue_note_actions.vue'; + import issueNoteBody from './issue_note_body.vue'; + import eventHub from '../event_hub'; -export default { - props: { - note: { - type: Object, - required: true, + export default { + props: { + note: { + type: Object, + required: true, + }, }, - }, - data() { - return { - isEditing: false, - isDeleting: false, - }; - }, - components: { - userAvatarLink, - issueNoteHeader, - issueNoteActions, - issueNoteBody, - }, - computed: { - ...mapGetters([ - 'targetNoteHash', - ]), - author() { - return this.note.author; - }, - classNameBindings() { + data() { return { - 'is-editing': this.isEditing, - 'disabled-content': this.isDeleting, - 'js-my-note': this.author.id === window.gon.current_user_id, - target: this.targetNoteHash === this.noteAnchorId, + isEditing: false, + isDeleting: false, }; }, - canReportAsAbuse() { - return this.note.report_abuse_path && this.author.id !== window.gon.current_user_id; - }, - noteAnchorId() { - return `note_${this.note.id}`; + components: { + userAvatarLink, + issueNoteHeader, + issueNoteActions, + issueNoteBody, }, - }, - methods: { - editHandler() { - this.isEditing = true; + computed: { + ...mapGetters([ + 'targetNoteHash', + ]), + author() { + return this.note.author; + }, + classNameBindings() { + return { + 'is-editing': this.isEditing, + 'disabled-content': this.isDeleting, + 'js-my-note': this.author.id === window.gon.current_user_id, + target: this.targetNoteHash === this.noteAnchorId, + }; + }, + canReportAsAbuse() { + return this.note.report_abuse_path && this.author.id !== window.gon.current_user_id; + }, + noteAnchorId() { + return `note_${this.note.id}`; + }, }, - deleteHandler() { - const msg = 'Are you sure you want to delete this list?'; - const isConfirmed = confirm(msg); // eslint-disable-line + methods: { + ...mapActions([ + 'deleteNote', + 'updateNote', + 'scrollToNoteIfNeeded', + ]), + editHandler() { + this.isEditing = true; + }, + deleteHandler() { + const msg = 'Are you sure you want to delete this list?'; + const isConfirmed = confirm(msg); // eslint-disable-line + + if (isConfirmed) { + this.isDeleting = true; + this.deleteNote(this.note) + .then(() => { + this.isDeleting = false; + }) + .catch(() => { + Flash('Something went wrong while deleting your note. Please try again.'); + this.isDeleting = false; + }); + } + }, + formUpdateHandler(note) { + const data = { + endpoint: this.note.path, + note: { + full_data: true, + target_type: 'issue', + target_id: this.note.noteable_id, + note, + }, + }; - if (isConfirmed) { - this.isDeleting = true; - this.$store - .dispatch('deleteNote', this.note) + this.updateNote(data) .then(() => { - this.isDeleting = false; + this.isEditing = false; + $(this.$refs.noteBody.$el).renderGFM(); }) - .catch(() => { - new Flash('Something went wrong while deleting your note. Please try again.'); // eslint-disable-line - this.isDeleting = false; - }); - } - }, - formUpdateHandler(note) { - const data = { - endpoint: this.note.path, - note: { - full_data: true, - target_type: 'issue', - target_id: this.note.noteable_id, - note, - }, - }; + .catch(() => Flash('Something went wrong while editing your comment. Please try again.')); + }, + formCancelHandler(shouldConfirm) { + if (shouldConfirm && this.$refs.noteBody.$refs.noteForm.isDirty) { + const msg = 'Are you sure you want to cancel editing this comment?'; + const isConfirmed = confirm(msg); // eslint-disable-line + if (!isConfirmed) { + return; + } + } - this.$store.dispatch('updateNote', data) - .then(() => { - this.isEditing = false; - $(this.$refs.noteBody.$el).renderGFM(); - }) - .catch(() => { - Flash('Something went wrong while editing your comment. Please try again.'); - }); + this.isEditing = false; + }, }, - formCancelHandler(shouldConfirm) { - if (shouldConfirm && this.$refs.noteBody.$refs.noteForm.isDirty) { - const msg = 'Are you sure you want to cancel editing this comment?'; - const isConfirmed = confirm(msg); // eslint-disable-line - if (!isConfirmed) { - return; + created() { + eventHub.$on('enterEditMode', ({ noteId }) => { + if (noteId === this.note.id) { + this.isEditing = true; + this.scrollToNoteIfNeeded($(this.$el)); } - } - - this.isEditing = false; + }); }, - }, - created() { - eventHub.$on('enterEditMode', ({ noteId }) => { - if (noteId === this.note.id) { - this.isEditing = true; - this.$store.dispatch('scrollToNoteIfNeeded', $(this.$el)); - } - }); - }, -}; + }; </script> <template> @@ -124,7 +126,8 @@ export default { :link-href="author.path" :img-src="author.avatar_url" :img-alt="author.name" - :img-size="40" /> + :img-size="40" + /> </div> <div class="timeline-content"> <div class="note-header"> @@ -132,7 +135,8 @@ export default { :author="author" :created-at="note.created_at" :note-id="note.id" - actionText="commented" /> + actionText="commented" + /> <issue-note-actions :author-id="author.id" :note-id="note.id" @@ -142,7 +146,8 @@ export default { :can-report-as-abuse="canReportAsAbuse" :report-abuse-path="note.report_abuse_path" :edit-handler="editHandler" - :delete-handler="deleteHandler" /> + :delete-handler="deleteHandler" + /> </div> <issue-note-body :note="note" @@ -150,7 +155,8 @@ export default { :is-editing="isEditing" :form-update-handler="formUpdateHandler" :form-cancel-handler="formCancelHandler" - ref="noteBody" /> + ref="noteBody" + /> </div> </div> </li> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index df5a060d894c..6c63cae17ccd 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -1,68 +1,71 @@ <script> -import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; -import emojiSmile from 'icons/_emoji_smile.svg'; -import emojiSmiley from 'icons/_emoji_smiley.svg'; -import loadingIcon from '../../vue_shared/components/loadingIcon.vue'; + import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; + import emojiSmile from 'icons/_emoji_smile.svg'; + import emojiSmiley from 'icons/_emoji_smiley.svg'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -export default { - props: { - authorId: { - type: Number, - required: true, + export default { + props: { + authorId: { + type: Number, + required: true, + }, + noteId: { + type: Number, + required: true, + }, + accessLevel: { + type: String, + required: false, + default: '', + }, + reportAbusePath: { + type: String, + required: true, + }, + canEdit: { + type: Boolean, + required: true, + }, + canDelete: { + type: Boolean, + required: true, + }, + canReportAsAbuse: { + type: Boolean, + required: true, + }, + editHandler: { + type: Function, + required: true, + }, + deleteHandler: { + type: Function, + required: true, + }, }, - noteId: { - type: Number, - required: true, + data() { + return { + emojiSmiling, + emojiSmile, + emojiSmiley, + }; }, - accessLevel: { - type: String, - required: false, - default: '', + components: { + loadingIcon, }, - reportAbusePath: { - type: String, - required: true, + computed: { + shouldShowActionsDropdown() { + return window.gon.current_user_id && (this.canEdit || this.canReportAsAbuse); + }, + canAddAwardEmoji() { + return window.gon.current_user_id; + }, + isAuthoredByMe() { + return this.authorId === window.gon.current_user_id; + }, }, - canEdit: { - type: Boolean, - required: true, - }, - canDelete: { - type: Boolean, - required: true, - }, - canReportAsAbuse: { - type: Boolean, - required: true, - }, - editHandler: { - type: Function, - required: true, - }, - deleteHandler: { - type: Function, - required: true, - }, - }, - data() { - return { - emojiSmiling, - emojiSmile, - emojiSmiley, - }; - }, - computed: { - shouldShowActionsDropdown() { - return window.gon.current_user_id && (this.canEdit || this.canReportAsAbuse); - }, - canAddAwardEmoji() { - return window.gon.current_user_id; - }, - isAuthoredByMe() { - return this.authorId === window.gon.current_user_id; - }, - }, -}; + }; </script> <template> @@ -82,13 +85,16 @@ export default { <loading-icon /> <span v-html="emojiSmiling" - class="link-highlight award-control-icon-neutral"></span> + class="link-highlight award-control-icon-neutral"> + </span> <span v-html="emojiSmiley" - class="link-highlight award-control-icon-positive"></span> + class="link-highlight award-control-icon-positive"> + </span> <span v-html="emojiSmile" - class="link-highlight award-control-icon-super-positive"></span> + class="link-highlight award-control-icon-super-positive"> + </span> </a> <div v-if="shouldShowActionsDropdown" @@ -101,7 +107,8 @@ export default { data-container="body"> <i aria-hidden="true" - class="fa fa-ellipsis-v icon"></i> + class="fa fa-ellipsis-v icon"> + </i> </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <template v-if="canEdit"> diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index c41c608979a6..02c0aa0b9c4b 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -1,164 +1,166 @@ <script> -/* global Flash */ - -import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; -import emojiSmile from 'icons/_emoji_smile.svg'; -import emojiSmiley from 'icons/_emoji_smiley.svg'; -import * as Emoji from '../../emoji'; - -export default { - props: { - awards: { - type: Array, - required: true, + /* global Flash */ + + import { mapActions } from 'vuex'; + import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; + import emojiSmile from 'icons/_emoji_smile.svg'; + import emojiSmiley from 'icons/_emoji_smiley.svg'; + import * as Emoji from '../../emoji'; + + export default { + props: { + awards: { + type: Array, + required: true, + }, + toggleAwardPath: { + type: String, + required: true, + }, + noteAuthorId: { + type: Number, + required: true, + }, + noteId: { + type: Number, + required: true, + }, }, - toggleAwardPath: { - type: String, - required: true, - }, - noteAuthorId: { - type: Number, - required: true, - }, - noteId: { - type: Number, - required: true, - }, - }, - data() { - const userId = window.gon.current_user_id; - - return { - emojiSmiling, - emojiSmile, - emojiSmiley, - canAward: !!userId, - myUserId: userId, - }; - }, - computed: { - // `this.awards` is an array with emojis but they are not grouped by emoji name. See below. - // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ] - // This method will group emojis by their name as an Object. See below. - // { - // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ], - // bar: [ { name: bar, user: user1 } ] - // } - // We need to do this otherwise we will render the same emoji over and over again. - groupedAwards() { - const awards = {}; - const orderedAwards = {}; - - this.awards.forEach((award) => { - awards[award.name] = awards[award.name] || []; - awards[award.name].push(award); - }); - - // Always show thumbsup and thumbsdown first - const { thumbsup, thumbsdown } = awards; - if (thumbsup) { - orderedAwards.thumbsup = thumbsup; - delete awards.thumbsup; - } - if (thumbsdown) { - orderedAwards.thumbsdown = thumbsdown; - delete awards.thumbsdown; - } - - // Because for-in forbidden - const keys = Object.keys(awards); - keys.forEach((key) => { - orderedAwards[key] = awards[key]; - }); - - return orderedAwards; - }, - isAuthoredByMe() { - return this.noteAuthorId === window.gon.current_user_id; - }, - }, - methods: { - getAwardHTML(name) { - return Emoji.glEmojiTag(name); - }, - getAwardClassBindings(awardList, awardName) { + data() { + const userId = window.gon.current_user_id; + return { - active: this.amIAwarded(awardList), - disabled: !this.canInteractWithEmoji(awardList, awardName), + emojiSmiling, + emojiSmile, + emojiSmiley, + canAward: !!userId, + myUserId: userId, }; }, - canInteractWithEmoji(awardList, awardName) { - let isAllowed = true; - const restrictedEmojis = ['thumbsup', 'thumbsdown']; - const { myUserId, noteAuthorId } = this; - - // Users can not add :+1: and :-1: to their notes - if (myUserId === noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) { - isAllowed = false; - } + computed: { + // `this.awards` is an array with emojis but they are not grouped by emoji name. See below. + // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ] + // This method will group emojis by their name as an Object. See below. + // { + // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ], + // bar: [ { name: bar, user: user1 } ] + // } + // We need to do this otherwise we will render the same emoji over and over again. + groupedAwards() { + const awards = {}; + const orderedAwards = {}; + + this.awards.forEach((award) => { + awards[award.name] = awards[award.name] || []; + awards[award.name].push(award); + }); - return this.canAward && isAllowed; - }, - amIAwarded(awardList) { - const isAwarded = awardList.filter(award => award.user.id === this.myUserId); + // Always show thumbsup and thumbsdown first + const { thumbsup, thumbsdown } = awards; + if (thumbsup) { + orderedAwards.thumbsup = thumbsup; + delete awards.thumbsup; + } + if (thumbsdown) { + orderedAwards.thumbsdown = thumbsdown; + delete awards.thumbsdown; + } + + // Because for-in forbidden + const keys = Object.keys(awards); + keys.forEach((key) => { + orderedAwards[key] = awards[key]; + }); - return isAwarded.length; - }, - awardTitle(awardsList) { - const amIAwarded = this.amIAwarded(awardsList); - const TOOLTIP_NAME_COUNT = amIAwarded ? 9 : 10; - let awardList = awardsList; - - // Filter myself from list if I am awarded. - if (amIAwarded) { - awardList = awardList.filter(award => award.user.id !== this.myUserId); - } - - // Get only 9-10 usernames to show in tooltip text. - const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); - - // Get the remaining list to use in `and x more` text. - const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); - - // Add myself to the begining of the list so title will start with You. - if (amIAwarded) { - namesToShow.unshift('You'); - } - - let title = ''; - - // We have 10+ awarded user, join them with comma and add `and x more`. - if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; - } else if (namesToShow.length > 1) { - // Join all names with comma but not the last one, it will be added with and text. - title = namesToShow.slice(0, namesToShow.length - 1).join(', '); - // If we have more than 2 users we need an extra comma before and text. - title += namesToShow.length > 2 ? ',' : ''; - title += ` and ${namesToShow.slice(-1)}`; // Append and text - } else { // We have only 2 users so join them with and. - title = namesToShow.join(' and '); - } - - return title; + return orderedAwards; + }, + isAuthoredByMe() { + return this.noteAuthorId === window.gon.current_user_id; + }, }, - handleAward(awardName) { - const data = { - endpoint: this.toggleAwardPath, - noteId: this.noteId, - awardName, - }; - - this.$store.dispatch('toggleAward', data) - .then(() => { - $(this.$el).find('.award-control').tooltip('fixTitle'); - }) - .catch(() => { - Flash('Something went wrong on our end.'); - }); + methods: { + ...mapActions([ + 'toggleAward', + ]), + getAwardHTML(name) { + return Emoji.glEmojiTag(name); + }, + getAwardClassBindings(awardList, awardName) { + return { + active: this.amIAwarded(awardList), + disabled: !this.canInteractWithEmoji(awardList, awardName), + }; + }, + canInteractWithEmoji(awardList, awardName) { + let isAllowed = true; + const restrictedEmojis = ['thumbsup', 'thumbsdown']; + const { myUserId, noteAuthorId } = this; + + // Users can not add :+1: and :-1: to their own notes + if (myUserId === noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) { + isAllowed = false; + } + + return this.canAward && isAllowed; + }, + amIAwarded(awardList) { + const isAwarded = awardList.filter(award => award.user.id === this.myUserId); + + return isAwarded.length; + }, + awardTitle(awardsList) { + const amIAwarded = this.amIAwarded(awardsList); + const TOOLTIP_NAME_COUNT = amIAwarded ? 9 : 10; + let awardList = awardsList; + + // Filter myself from list if I am awarded. + if (amIAwarded) { + awardList = awardList.filter(award => award.user.id !== this.myUserId); + } + + // Get only 9-10 usernames to show in tooltip text. + const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); + + // Get the remaining list to use in `and x more` text. + const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); + + // Add myself to the begining of the list so title will start with You. + if (amIAwarded) { + namesToShow.unshift('You'); + } + + let title = ''; + + // We have 10+ awarded user, join them with comma and add `and x more`. + if (remainingAwardList.length) { + title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; + } else if (namesToShow.length > 1) { + // Join all names with comma but not the last one, it will be added with and text. + title = namesToShow.slice(0, namesToShow.length - 1).join(', '); + // If we have more than 2 users we need an extra comma before and text. + title += namesToShow.length > 2 ? ',' : ''; + title += ` and ${namesToShow.slice(-1)}`; // Append and text + } else { // We have only 2 users so join them with and. + title = namesToShow.join(' and '); + } + + return title; + }, + handleAward(awardName) { + const data = { + endpoint: this.toggleAwardPath, + noteId: this.noteId, + awardName, + }; + + this.toggleAward(data) + .then(() => { + $(this.$el).find('.award-control').tooltip('fixTitle'); + }) + .catch(() => Flash('Something went wrong on our end.')); + }, }, - }, -}; + }; </script> <template> @@ -189,13 +191,16 @@ export default { type="button"> <span v-html="emojiSmiling" - class="award-control-icon award-control-icon-neutral"></span> + class="award-control-icon award-control-icon-neutral"> + </span> <span v-html="emojiSmiley" - class="award-control-icon award-control-icon-positive"></span> + class="award-control-icon award-control-icon-positive"> + </span> <span v-html="emojiSmile" - class="award-control-icon award-control-icon-super-positive"></span> + class="award-control-icon award-control-icon-super-positive"> + </span> <i aria-hidden="true" class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 30c8db6f041a..dee8bb0c7f9d 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -1,70 +1,70 @@ <script> -import issueNoteEditedText from './issue_note_edited_text.vue'; -import issueNoteAwardsList from './issue_note_awards_list.vue'; -import issueNoteForm from './issue_note_form.vue'; -import TaskList from '../../task_list'; + import issueNoteEditedText from './issue_note_edited_text.vue'; + import issueNoteAwardsList from './issue_note_awards_list.vue'; + import issueNoteForm from './issue_note_form.vue'; + import TaskList from '../../task_list'; -export default { - props: { - note: { - type: Object, - required: true, + export default { + props: { + note: { + type: Object, + required: true, + }, + canEdit: { + type: Boolean, + required: true, + }, + isEditing: { + type: Boolean, + required: false, + default: false, + }, + formUpdateHandler: { + type: Function, + required: true, + }, + formCancelHandler: { + type: Function, + required: true, + }, }, - canEdit: { - type: Boolean, - required: true, + components: { + issueNoteEditedText, + issueNoteAwardsList, + issueNoteForm, }, - isEditing: { - type: Boolean, - required: false, - default: false, + computed: { + noteBody() { + return this.note.note; + }, }, - formUpdateHandler: { - type: Function, - required: true, - }, - formCancelHandler: { - type: Function, - required: true, - }, - }, - components: { - issueNoteEditedText, - issueNoteAwardsList, - issueNoteForm, - }, - computed: { - noteBody() { - return this.note.note; - }, - }, - methods: { - renderGFM() { - $(this.$refs['note-body']).renderGFM(); - }, - initTaskList() { - if (this.canEdit) { - this.taskList = new TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes', + methods: { + renderGFM() { + $(this.$refs['note-body']).renderGFM(); + }, + initTaskList() { + if (this.canEdit) { + this.taskList = new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes', + }); + } + }, + handleFormUpdate() { + this.formUpdateHandler({ + note: this.$refs.noteForm.note, }); - } + }, + }, + mounted() { + this.renderGFM(); + this.initTaskList(); }, - handleFormUpdate() { - this.formUpdateHandler({ - note: this.$refs.noteForm.note, - }); + updated() { + this.initTaskList(); }, - }, - mounted() { - this.renderGFM(); - this.initTaskList(); - }, - updated() { - this.initTaskList(); - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue index aed82fd4a828..5e4f1c828226 100644 --- a/app/assets/javascripts/notes/components/issue_note_edited_text.vue +++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue @@ -1,30 +1,30 @@ <script> -import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; -export default { - props: { - actionText: { - type: String, - required: true, + export default { + props: { + actionText: { + type: String, + required: true, + }, + editedAt: { + type: String, + required: true, + }, + editedBy: { + type: Object, + required: true, + }, + className: { + type: String, + required: false, + default: 'edited-text', + }, }, - editedAt: { - type: String, - required: true, + components: { + timeAgoTooltip, }, - editedBy: { - type: Object, - required: true, - }, - className: { - type: String, - required: false, - default: 'edited-text', - }, - }, - components: { - timeAgoTooltip, - }, -}; + }; </script> <template> @@ -38,6 +38,7 @@ export default { </a> <time-ago-tooltip :time="editedAt" - tooltip-placement="bottom" /> + tooltip-placement="bottom" + /> </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 46ea030ce879..e5d8ef475f9a 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -1,88 +1,88 @@ <script> -import markdownField from '../../vue_shared/components/markdown/field.vue'; -import eventHub from '../event_hub'; + import markdownField from '../../vue_shared/components/markdown/field.vue'; + import eventHub from '../event_hub'; -export default { - props: { - noteBody: { - type: String, - required: false, - default: '', + export default { + props: { + noteBody: { + type: String, + required: false, + default: '', + }, + noteId: { + type: Number, + required: false, + }, + updateHandler: { + type: Function, + required: true, + }, + cancelHandler: { + type: Function, + required: true, + }, + saveButtonTitle: { + type: String, + required: false, + default: 'Save comment', + }, }, - noteId: { - type: Number, - required: false, + data() { + return { + initialNote: this.noteBody, + note: this.noteBody, + markdownPreviewUrl: gl.issueData.preview_note_path, + markdownDocsUrl: '', + conflictWhileEditing: false, + }; }, - updateHandler: { - type: Function, - required: true, + components: { + markdownField, }, - cancelHandler: { - type: Function, - required: true, + computed: { + isDirty() { + return this.initialNote !== this.note; + }, + noteHash() { + return `#note_${this.noteId}`; + }, }, - saveButtonTitle: { - type: String, - required: false, - default: 'Save comment', - }, - }, - data() { - return { - initialNote: this.noteBody, - note: this.noteBody, - markdownPreviewUrl: gl.issueData.preview_note_path, - markdownDocsUrl: '', - conflictWhileEditing: false, - }; - }, - components: { - markdownField, - }, - computed: { - isDirty() { - return this.initialNote !== this.note; - }, - noteHash() { - return `#note_${this.noteId}`; - }, - }, - methods: { - handleUpdate() { - this.updateHandler({ - note: this.note, - }); - }, - editMyLastNote() { - if (this.note === '') { - const discussion = $(this.$el).closest('.discussion-notes'); - const myLastNoteId = discussion.find('.js-my-note').last().attr('id'); + methods: { + handleUpdate() { + this.updateHandler({ + note: this.note, + }); + }, + editMyLastNote() { + if (this.note === '') { + const discussion = $(this.$el).closest('.discussion-notes'); + const myLastNoteId = discussion.find('.js-my-note').last().attr('id'); - if (myLastNoteId) { - eventHub.$emit('enterEditMode', { - noteId: parseInt(myLastNoteId.replace('note_', ''), 10), - }); + if (myLastNoteId) { + eventHub.$emit('enterEditMode', { + noteId: parseInt(myLastNoteId.replace('note_', ''), 10), + }); + } } - } + }, }, - }, - mounted() { - const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); - const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); + mounted() { + const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); + const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); - this.markdownDocsUrl = issueData.markdownDocs; - this.$refs.textarea.focus(); - }, - watch: { - noteBody() { - if (this.note === this.initialNote) { - this.note = this.noteBody; - } else { - this.conflictWhileEditing = true; - } + this.markdownDocsUrl = issueData.markdownDocs; + this.$refs.textarea.focus(); + }, + watch: { + noteBody() { + if (this.note === this.initialNote) { + this.note = this.noteBody; + } else { + this.conflictWhileEditing = true; + } + }, }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index d5249b1a72f4..20f23bf828a7 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -1,66 +1,71 @@ <script> -import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + import { mapMutations } from 'vuex'; + import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + import * as types from '../stores/mutation_types'; -export default { - props: { - author: { - type: Object, - required: true, + export default { + props: { + author: { + type: Object, + required: true, + }, + createdAt: { + type: String, + required: true, + }, + actionText: { + type: String, + required: false, + default: '', + }, + actionTextHtml: { + type: String, + required: false, + default: '', + }, + noteId: { + type: Number, + required: true, + }, + includeToggle: { + type: Boolean, + required: false, + default: false, + }, + toggleHandler: { + type: Function, + required: false, + }, }, - createdAt: { - type: String, - required: true, + data() { + return { + isExpanded: true, + }; }, - actionText: { - type: String, - required: false, - default: '', + components: { + timeAgoTooltip, }, - actionTextHtml: { - type: String, - required: false, - default: '', + computed: { + toggleChevronClass() { + return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; + }, + noteTimestampLink() { + return `#note_${this.noteId}`; + }, }, - noteId: { - type: Number, - required: true, + methods: { + ...mapMutations({ + setTargetNoteHash: types.SET_TARGET_NOTE_HASH, + }), + handleToggle() { + this.isExpanded = !this.isExpanded; + this.toggleHandler(); + }, + updateTargetNoteHash() { + this.setTargetNoteHash(this.noteTimestampLink); + }, }, - includeToggle: { - type: Boolean, - required: false, - default: false, - }, - toggleHandler: { - type: Function, - required: false, - }, - }, - data() { - return { - isExpanded: true, - }; - }, - components: { - timeAgoTooltip, - }, - computed: { - toggleChevronClass() { - return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; - }, - noteTimestampLink() { - return `#note_${this.noteId}`; - }, - }, - methods: { - handleToggle() { - this.isExpanded = !this.isExpanded; - this.toggleHandler(); - }, - updateTargetNoteHash() { - this.$store.commit('setTargetNoteHash', this.noteTimestampLink); - }, - }, -}; + }; </script> <template> @@ -81,13 +86,15 @@ export default { <span v-if="actionTextHtml" v-html="actionTextHtml" - class="system-note-message"></span> + class="system-note-message"> + </span> <a :href="noteTimestampLink" @click="updateTargetNoteHash"> <time-ago-tooltip :time="createdAt" - tooltipPlacement="bottom" /> + tooltipPlacement="bottom" + /> </a> </span> </span> @@ -101,7 +108,8 @@ export default { <i :class="toggleChevronClass" class="fa" - aria-hidden="true"></i> + aria-hidden="true"> + </i> Toggle discussion </button> </div> diff --git a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue index 1b819dfcb8be..59d052b35fbb 100644 --- a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue @@ -1,18 +1,18 @@ <script> -export default { - data() { - return { - signInLink: '#', - }; - }, - mounted() { - const wrapper = document.querySelector('.js-notes-wrapper'); + export default { + data() { + return { + signInLink: '#', + }; + }, + mounted() { + const wrapper = document.querySelector('.js-notes-wrapper'); - if (wrapper) { - this.signInLink = wrapper.dataset.newSessionPath; - } - }, -}; + if (wrapper) { + this.signInLink = wrapper.dataset.newSessionPath; + } + }, + }; </script> <template> diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index 2fe071cc9905..a3b3bd54cd44 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -1,119 +1,131 @@ <script> -/* global Flash */ + /* global Flash */ -import Vue from 'vue'; -import Vuex from 'vuex'; -import VueResource from 'vue-resource'; -import storeOptions from '../stores/issue_notes_store'; -import eventHub from '../event_hub'; -import issueNote from './issue_note.vue'; -import issueDiscussion from './issue_discussion.vue'; -import issueSystemNote from './issue_system_note.vue'; -import issueCommentForm from './issue_comment_form.vue'; -import placeholderNote from './issue_placeholder_note.vue'; -import placeholderSystemNote from './issue_placeholder_system_note.vue'; -import store from './store'; + import Vue from 'vue'; + import { mapGetters, mapActions, mapMutations } from 'vuex'; + import store from '../stores/'; + import * as constants from '../constants' + import * as types from '../stores/mutation_types'; + import eventHub from '../event_hub'; + import issueNote from './issue_note.vue'; + import issueDiscussion from './issue_discussion.vue'; + import issueSystemNote from './issue_system_note.vue'; + import issueCommentForm from './issue_comment_form.vue'; + import placeholderNote from './issue_placeholder_note.vue'; + import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -export default { - name: 'IssueNotes', - store, - data() { - return { - isLoading: true, - }; - }, - components: { - issueNote, - issueDiscussion, - issueSystemNote, - issueCommentForm, - placeholderNote, - placeholderSystemNote, - }, - computed: { - ...Vuex.mapGetters([ - 'notes', - 'notesById', - ]), - }, - methods: { - componentName(note) { - if (note.isPlaceholderNote) { - if (note.placeholderType === 'systemNote') { - return placeholderSystemNote; - } - return placeholderNote; - } else if (note.individual_note) { - return note.notes[0].system ? issueSystemNote : issueNote; - } - - return issueDiscussion; + export default { + name: 'IssueNotes', + store, + data() { + return { + isLoading: true, + }; }, - componentData(note) { - return note.individual_note ? note.notes[0] : note; + components: { + issueNote, + issueDiscussion, + issueSystemNote, + issueCommentForm, + loadingIcon, + placeholderNote, + placeholderSystemNote, }, - fetchNotes() { - const { discussionsPath } = this.$el.parentNode.dataset; + computed: { + ...mapGetters([ + 'notes', + 'notesById', + ]), + }, + methods: { + ...mapActions({ + actionFetchNotes: 'fetchNotes', + }), + ...mapActions([ + 'poll', + 'toggleAward', + 'scrollToNoteIfNeeded', + ]), + ...mapMutations({ + setLastFetchedAt: types.SET_LAST_FETCHED_AT, + setTargetNoteHash: types.SET_TARGET_NOTE_HASH, + }), + getComponentName(note) { + if (note.isPlaceholderNote) { + if (note.placeholderType === constants.SYSTEM_NOTE) { + return placeholderSystemNote; + } + return placeholderNote; + } else if (note.individual_note) { + return note.notes[0].system ? issueSystemNote : issueNote; + } - this.$store.dispatch('fetchNotes', discussionsPath) - .then(() => { - this.isLoading = false; + return issueDiscussion; + }, + getComponentData(note) { + return note.individual_note ? note.notes[0] : note; + }, + fetchNotes() { + const { discussionsPath } = this.$el.parentNode.dataset; - // Scroll to note if we have hash fragment in the page URL - Vue.nextTick(() => { - this.checkLocationHash(); - }); - }) - .catch(() => { - Flash('Something went wrong while fetching issue comments. Please try again.'); - }); - }, - initPolling() { - const { lastFetchedAt } = $('.js-notes-wrapper')[0].dataset; - this.$store.commit('setLastFetchedAt', lastFetchedAt); + this.actionFetchNotes(discussionsPath) + .then(() => { + this.isLoading = false; - // FIXME: @fatihacet Implement real polling mechanism - setInterval(() => { - this.$store.dispatch('poll') - .then((res) => { - this.$store.commit('setLastFetchedAt', res.lastFetchedAt); + // Scroll to note if we have hash fragment in the page URL + Vue.nextTick(() => { + this.checkLocationHash(); + }); }) .catch(() => { - Flash('Something went wrong while fetching latest comments.'); + Flash('Something went wrong while fetching issue comments. Please try again.'); }); - }, 15000); - }, - bindEventHubListeners() { - eventHub.$on('toggleAward', (data) => { - const { awardName, noteId } = data; - const endpoint = this.notesById[noteId].toggle_award_path; + }, + initPolling() { + const { lastFetchedAt } = $('.js-notes-wrapper')[0].dataset; + this.setLastFetchedAt(lastFetchedAt); - this.$store.dispatch('toggleAward', { endpoint, awardName, noteId }) - .catch(() => { - Flash('Something went wrong on our end.'); - }); - }); + // FIXME: @fatihacet Implement real polling mechanism + setInterval(() => { + this.poll() + .then((res) => { + this.setLastFetchedAt(res.lastFetchedAt); + }) + .catch(() => { + Flash('Something went wrong while fetching latest comments.'); + }); + }, 15000); + }, + bindEventHubListeners() { + eventHub.$on('toggleAward', (data) => { + const { awardName, noteId } = data; + const endpoint = this.notesById[noteId].toggle_award_path; - $(document).on('issuable:change', (e, isClosed) => { - eventHub.$emit('issueStateChanged', isClosed); - }); - }, - checkLocationHash() { - const hash = gl.utils.getLocationHash(); - const $el = $(`#${hash}`); + this.toggleAward({ endpoint, awardName, noteId }) + .catch(() => {new Flash('Something went wrong on our end.')}); + }); - if (hash && $el) { - this.$store.commit('setTargetNoteHash', hash); - this.$store.dispatch('scrollToNoteIfNeeded', $el); - } + $(document).on('issuable:change', (e, isClosed) => { + eventHub.$emit('issueStateChanged', isClosed); + }); + }, + checkLocationHash() { + const hash = gl.utils.getLocationHash(); + const $el = $(`#${hash}`); + + if (hash && $el) { + this.setTargetNoteHash(hash); + this.scrollToNoteIfNeeded($el); + } + }, + }, + mounted() { + this.fetchNotes(); + this.initPolling(); + this.bindEventHubListeners(); }, - }, - mounted() { - this.fetchNotes(); - this.initPolling(); - this.bindEventHubListeners(); - }, -}; + }; </script> <template> @@ -121,9 +133,7 @@ export default { <div v-if="isLoading" class="loading"> - <i - class="fa fa-spinner fa-spin" - aria-hidden="true"></i> + <loading-icon /> </div> <ul v-if="!isLoading" @@ -131,9 +141,10 @@ export default { class="notes main-notes-list timeline"> <component v-for="note in notes" - :is="componentName(note)" - :note="componentData(note)" - :key="note.id" /> + :is="getComponentName(note)" + :note="getComponentData(note)" + :key="note.id" + /> </ul> <issue-comment-form v-if="!isLoading" /> </div> diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_note.vue index af249c56a3df..fec60d9667bd 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue +++ b/app/assets/javascripts/notes/components/issue_placeholder_note.vue @@ -1,17 +1,17 @@ <script> -export default { - props: { - note: { - type: Object, - required: true, + export default { + props: { + note: { + type: Object, + required: true, + }, }, - }, - data() { - return { - currentUser: window.gl.currentUserData, - }; - }, -}; + data() { + return { + currentUser: window.gl.currentUserData, + }; + }, + }; </script> <template> @@ -21,7 +21,8 @@ export default { <a :href="currentUser.path"> <img :src="currentUser.avatar_url" - class="avatar s40" /> + class="avatar s40" + /> </a> </div> <div diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue index 6738d82e7e7c..b0d442c1a87f 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue @@ -1,12 +1,12 @@ <script> -export default { - props: { - note: { - type: Object, - required: true, + export default { + props: { + note: { + type: Object, + required: true, + }, }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index 65ddaccbceac..91e1c62bba0b 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -1,35 +1,35 @@ <script> -import { mapGetters } from 'vuex'; -import iconsMap from './issue_note_icons'; -import issueNoteHeader from './issue_note_header.vue'; + import { mapGetters } from 'vuex'; + import iconsMap from './issue_note_icons'; + import issueNoteHeader from './issue_note_header.vue'; -export default { - props: { - note: { - type: Object, - required: true, + export default { + props: { + note: { + type: Object, + required: true, + }, }, - }, - data() { - return { - svg: iconsMap[this.note.system_note_icon_name], - }; - }, - components: { - issueNoteHeader, - }, - computed: { - ...mapGetters([ - 'targetNoteHash', - ]), - noteAnchorId() { - return `note_${this.note.id}`; + data() { + return { + svg: iconsMap[this.note.system_note_icon_name], + }; }, - isTargetNote() { - return this.targetNoteHash === this.noteAnchorId; + components: { + issueNoteHeader, }, - }, -}; + computed: { + ...mapGetters([ + 'targetNoteHash', + ]), + noteAnchorId() { + return `note_${this.note.id}`; + }, + isTargetNote() { + return this.targetNoteHash === this.noteAnchorId; + }, + }, + }; </script> <template> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 663434587a25..53fb03bab41a 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -5,4 +5,4 @@ export const SYSTEM_NOTE = 'systemNote'; export const COMMENT = 'comment'; export const OPENED = 'opened'; export const REOPENED = 'reopened'; -export const CLOSED = 'closed'; \ No newline at end of file +export const CLOSED = 'closed'; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 0f81e08a2c8d..092049cb377f 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -138,8 +138,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { export const poll = ({ commit, state, getters }) => { const { notesPath } = $('.js-notes-wrapper')[0].dataset; - return service - .poll(`${notesPath}?full_data=1`, state.lastFetchedAt) + return service.poll(`${notesPath}?full_data=1`, state.lastFetchedAt) .then(res => res.json()) .then((res) => { if (res.notes.length) { @@ -188,8 +187,8 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => { }); if (amIAwarded) { - Object.assign(data, { awardName: counterAward }); - Object.assign(data, { skipMutalityCheck: true }); + data.awardName = counterAward; + data.skipMutalityCheck = true; dispatch(types.TOGGLE_AWARD, data); } -- GitLab From d2cfd4060ff2516f6613a29facd3db7b31613a50 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 26 Jul 2017 16:26:03 +0100 Subject: [PATCH 105/243] Fix edit text --- .../notes/components/issue_comment_form.vue | 2 +- .../notes/components/issue_discussion.vue | 6 +++--- .../notes/components/issue_note_edited_text.vue | 14 +++++++------- .../javascripts/notes/components/issue_notes.vue | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index bd6caddb40f1..afd51f8fa211 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -187,7 +187,7 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area" + class="note-textarea js-gfm-input js-autosize markdown-area js-note-text" data-supports-slash-commands="true" data-supports-quick-actions="true" aria-label="Description" diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 21b5b2881216..514b6cf34ca4 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,6 +1,6 @@ <script> /* global Flash */ - import { mapActions } from 'vuex'; + import { mapActions, mapMutations } from 'vuex'; import { TOGGLE_DISCUSSION } from '../stores/mutation_types'; import { SYSTEM_NOTE } from '../constants'; import issueNote from './issue_note.vue'; @@ -129,13 +129,13 @@ :note-id="discussion.id" :include-toggle="true" :toggle-handler="toggleDiscussion" - actionText="started a discussion" + action-text="started a discussion" /> <issue-note-edited-text v-if="note.last_updated_by" :edited-at="note.last_updated_at" :edited-by="note.last_updated_by" - actionText="Last updated" + action-text="Last updated" className="discussion-headline-light js-discussion-headline" /> </div> </div> diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue index 5e4f1c828226..bea013419eca 100644 --- a/app/assets/javascripts/notes/components/issue_note_edited_text.vue +++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue @@ -29,16 +29,16 @@ <template> <div :class="className"> - <span>{{actionText}} </span> - <span> by </span> - <a - :href="editedBy.path" - class="author_link"> - <span>{{editedBy.name}}</span> - </a> + {{actionText}} <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" /> + by + <a + :href="editedBy.path" + class="author_link"> + {{editedBy.name}} + </a> </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue index a3b3bd54cd44..b1c8f495eb90 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes.vue @@ -103,7 +103,7 @@ const endpoint = this.notesById[noteId].toggle_award_path; this.toggleAward({ endpoint, awardName, noteId }) - .catch(() => {new Flash('Something went wrong on our end.')}); + .catch(() => Flash('Something went wrong on our end.')); }); $(document).on('issuable:change', (e, isClosed) => { -- GitLab From 0663eb4cc2d9df6db375a233cbffd75fdec9bb62 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 26 Jul 2017 17:06:09 +0100 Subject: [PATCH 106/243] [ci skip] Adds quick actions links to the toolbar --- .../notes/components/issue_comment_form.vue | 8 +++-- .../vue_shared/components/markdown/field.vue | 8 ++++- .../components/markdown/toolbar.vue | 34 +++++++++++++++---- app/helpers/issuables_helper.rb | 3 +- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index afd51f8fa211..a8d8a22be763 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -16,6 +16,7 @@ return { note: '', markdownDocsUrl: '', + quickActionsDocsUrl: null, markdownPreviewUrl: gl.issueData.preview_note_path, noteType: constants.COMMENT, issueState: state, @@ -149,6 +150,7 @@ const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); this.markdownDocsUrl = issueData.markdownDocs; + this.quickActionsDocsUrl = issueData.quickActionsDocs; eventHub.$on('issueStateChanged', (isClosed) => { this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; @@ -183,7 +185,8 @@ <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" - :addSpacingClasses="false"> + :quick-actions-docs="quickActionsDocsUrl" + :add-spacing-classes="false"> <textarea id="note-body" name="note[note]" @@ -217,7 +220,8 @@ aria-label="Open comment type dropdown"> <i aria-hidden="true" - class="fa fa-caret-down toggle-icon"></i> + class="fa fa-caret-down toggle-icon"> + </i> </button> <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> <li diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index f1c7264ec4f8..af7ea7480771 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -21,6 +21,10 @@ required: false, default: true, }, + quickActionsDocs: { + type: String, + required: false, + }, }, data() { return { @@ -115,7 +119,9 @@ </i> </a> <markdown-toolbar - :markdown-docs="markdownDocs" /> + :markdown-docs="markdownDocs" + :quick-actions-docs="quickActionsDocs" + /> </div> </div> <div diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 0f3f6c6bb93f..13402f34c5b6 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -5,6 +5,10 @@ type: String, required: true, }, + quickActionsDocs: { + type: String, + required: false, + }, }, }; </script> @@ -12,12 +16,30 @@ <template> <div class="comment-toolbar clearfix"> <div class="toolbar-text"> - <a - :href="markdownDocs" - target="_blank" - tabindex="-1"> - Markdown is supported - </a> + <template v-if="!quickActionsDocs && markdownDocs"> + <a + :href="markdownDocs" + target="_blank" + tabindex="-1"> + Markdown is supported + </a> + </template> + <template v-if="quickActionsDocs && markdownDocs"> + <a + :href="markdownDocs" + target="_blank" + tabindex="-1"> + Markdown + </a> + and + <a + :href="quickActionsDocs" + target="_blank" + tabindex="-1"> + quick actions + </a> + are supported + </template> </div> <span class="uploading-container"> <span class="uploading-progress-container hide"> diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index f85afb1894e0..38ffc62fbdfb 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -213,7 +213,8 @@ def issuable_initial_data(issuable) initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionText: issuable.description, - initialTaskStatus: issuable.task_status + initialTaskStatus: issuable.task_status, + quickActionsDocs: help_page_path('user/project/quick_actions'), } data.merge!(updated_at_by(issuable)) -- GitLab From 57b1b8156ab5be99e926d5d4bfcfc256482c3d4d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 26 Jul 2017 17:26:07 +0100 Subject: [PATCH 107/243] [ci skip] use tooltip directive --- .../notes/components/issue_discussion.vue | 5 +++-- .../notes/components/issue_note_actions.vue | 10 ++++++++-- .../notes/components/issue_note_awards_list.vue | 13 ++++++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 514b6cf34ca4..9d0625d30dee 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -136,7 +136,8 @@ :edited-at="note.last_updated_at" :edited-by="note.last_updated_by" action-text="Last updated" - className="discussion-headline-light js-discussion-headline" /> + class-name="discussion-headline-light js-discussion-headline" + /> </div> </div> <div @@ -149,7 +150,7 @@ v-for="note in note.notes" :is="componentName(note)" :note="componentData(note)" - key="note.id" + :key="note.id" /> </ul> <div class="flash-container"></div> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 6c63cae17ccd..f08081e35775 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -3,6 +3,7 @@ import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -44,6 +45,9 @@ required: true, }, }, + directives: { + tooltip, + }, data() { return { emojiSmiling, @@ -76,9 +80,10 @@ {{accessLevel}} </span> <a + v-tooltip v-if="canAddAwardEmoji" :class="{ 'js-user-authored': isAuthoredByMe }" - class="note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip" + class="note-action-button note-emoji-button js-add-award js-note-emoji" data-position="right" href="#" title="Add reaction"> @@ -100,9 +105,10 @@ v-if="shouldShowActionsDropdown" class="dropdown more-actions"> <button + v-tooltip type="button" title="More actions" - class="note-action-button more-actions-toggle has-tooltip btn btn-transparent" + class="note-action-button more-actions-toggle btn btn-transparent" data-toggle="dropdown" data-container="body"> <i diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 02c0aa0b9c4b..7785ef017507 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -6,6 +6,7 @@ import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; import * as Emoji from '../../emoji'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -26,6 +27,9 @@ required: true, }, }, + directives: { + tooltip, + }, data() { const userId = window.gon.current_user_id; @@ -154,9 +158,6 @@ }; this.toggleAward(data) - .then(() => { - $(this.$el).find('.award-control').tooltip('fixTitle'); - }) .catch(() => Flash('Something went wrong on our end.')); }, }, @@ -167,11 +168,12 @@ <div class="note-awards"> <div class="awards js-awards-block"> <button + v-tooltip v-for="(awardList, awardName) in groupedAwards" :class="getAwardClassBindings(awardList, awardName)" :title="awardTitle(awardList)" @click="handleAward(awardName)" - class="btn award-control has-tooltip" + class="btn award-control" data-placement="bottom" type="button"> <span v-html="getAwardHTML(awardName)"></span> @@ -183,8 +185,9 @@ v-if="canAward" class="award-menu-holder"> <button + v-tooltip :class="{ 'js-user-authored': isAuthoredByMe }" - class="award-control btn has-tooltip js-add-award" + class="award-control btn js-add-award" title="Add reaction" aria-label="Add reaction" data-placement="bottom" -- GitLab From 487ed06f444892abce47a6e21aaea79c9ca9c800 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 26 Jul 2017 17:28:46 +0100 Subject: [PATCH 108/243] [ci skip] Adds :key property to awards v-for --- .../javascripts/notes/components/issue_note_awards_list.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 7785ef017507..9770b57fc039 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -169,7 +169,8 @@ <div class="awards js-awards-block"> <button v-tooltip - v-for="(awardList, awardName) in groupedAwards" + v-for="(awardList, awardName, index) in groupedAwards" + :key="index" :class="getAwardClassBindings(awardList, awardName)" :title="awardTitle(awardList)" @click="handleAward(awardName)" -- GitLab From d34c620fae23597e0130474fe20883f0718ded58 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 27 Jul 2017 21:24:05 +0100 Subject: [PATCH 109/243] [ci skip] Add issue data and notes data provided through haml to the store to stop querying the DOM everywhere --- .../notes/components/issue_comment_form.vue | 16 ++-- .../{issue_notes.vue => issue_notes_app.vue} | 54 ++++++----- .../notes/components/issue_system_note.vue | 5 +- app/assets/javascripts/notes/constants.js | 2 + app/assets/javascripts/notes/index.js | 58 +++++++----- .../javascripts/notes/stores/actions.js | 91 +++++++++---------- .../javascripts/notes/stores/getters.js | 6 +- app/assets/javascripts/notes/stores/index.js | 5 + .../notes/stores/mutation_types.js | 3 + .../javascripts/notes/stores/mutations.js | 12 +++ .../projects/issues/_discussion.html.haml | 12 +-- 11 files changed, 154 insertions(+), 110 deletions(-) rename app/assets/javascripts/notes/components/{issue_notes.vue => issue_notes_app.vue} (77%) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index a8d8a22be763..b39dd2febe90 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,7 +1,7 @@ <script> /* global Flash */ - import { mapActions } from 'vuex'; + import { mapActions, mapGetters } from 'vuex'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; @@ -30,6 +30,10 @@ issueNoteSignedOutWidget, }, computed: { + ...mapGetters([ + 'getNotesDataByProp', + 'getIssueDataByProp', + ]), isLoggedIn() { return window.gon.current_user_id; }, @@ -57,8 +61,7 @@ }; }, canUpdateIssue() { - const { issueData } = window.gl; - return issueData && issueData.current_user.can_update; + return this.getIssueDataByProp(current_user).can_update; }, }, methods: { @@ -146,11 +149,8 @@ }, }, mounted() { - const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); - const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); - - this.markdownDocsUrl = issueData.markdownDocs; - this.quickActionsDocsUrl = issueData.quickActionsDocs; + this.markdownDocsUrl = this.getIssueDataByProp(markdownDocs); + this.quickActionsDocsUrl = this.getIssueDataByProp(quickActionsDocs); eventHub.$on('issueStateChanged', (isClosed) => { this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue similarity index 77% rename from app/assets/javascripts/notes/components/issue_notes.vue rename to app/assets/javascripts/notes/components/issue_notes_app.vue index b1c8f495eb90..a3b6d928a51a 100644 --- a/app/assets/javascripts/notes/components/issue_notes.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -5,7 +5,6 @@ import { mapGetters, mapActions, mapMutations } from 'vuex'; import store from '../stores/'; import * as constants from '../constants' - import * as types from '../stores/mutation_types'; import eventHub from '../event_hub'; import issueNote from './issue_note.vue'; import issueDiscussion from './issue_discussion.vue'; @@ -17,6 +16,16 @@ export default { name: 'IssueNotes', + props: { + issueData: { + type: Object, + required: true, + }, + notesData: { + type: Object, + required: true, + }, + }, store, data() { return { @@ -36,20 +45,19 @@ ...mapGetters([ 'notes', 'notesById', + 'getNotesData', + 'getNotesDataByProp', + 'setLastFetchedAt', + 'setTargetNoteHash', ]), }, methods: { ...mapActions({ actionFetchNotes: 'fetchNotes', - }), - ...mapActions([ - 'poll', - 'toggleAward', - 'scrollToNoteIfNeeded', - ]), - ...mapMutations({ - setLastFetchedAt: types.SET_LAST_FETCHED_AT, - setTargetNoteHash: types.SET_TARGET_NOTE_HASH, + poll: 'poll', + toggleAward: 'toggleAward', + scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', + setNotesData: 'setNotesData' }), getComponentName(note) { if (note.isPlaceholderNote) { @@ -67,9 +75,7 @@ return note.individual_note ? note.notes[0] : note; }, fetchNotes() { - const { discussionsPath } = this.$el.parentNode.dataset; - - this.actionFetchNotes(discussionsPath) + this.actionFetchNotes(his.getNotesDataByProp('discussionsPath')) .then(() => { this.isLoading = false; @@ -78,23 +84,19 @@ this.checkLocationHash(); }); }) - .catch(() => { - Flash('Something went wrong while fetching issue comments. Please try again.'); - }); + .catch(() => Flash('Something went wrong while fetching issue comments. Please try again.')); }, initPolling() { - const { lastFetchedAt } = $('.js-notes-wrapper')[0].dataset; - this.setLastFetchedAt(lastFetchedAt); + this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); // FIXME: @fatihacet Implement real polling mechanism + // TODO: FILIPA: DEAL WITH THIS setInterval(() => { this.poll() .then((res) => { this.setLastFetchedAt(res.lastFetchedAt); }) - .catch(() => { - Flash('Something went wrong while fetching latest comments.'); - }); + .catch(() => Flash('Something went wrong while fetching latest comments.')); }, 15000); }, bindEventHubListeners() { @@ -106,6 +108,7 @@ .catch(() => Flash('Something went wrong on our end.')); }); + //TODO: FILIPA: REMOVE JQUERY $(document).on('issuable:change', (e, isClosed) => { eventHub.$emit('issueStateChanged', isClosed); }); @@ -120,6 +123,10 @@ } }, }, + created() { + this.setNotesData(this.notesData); + this.setIssueData(this.issueData); + }, mounted() { this.fetchNotes(); this.initPolling(); @@ -135,10 +142,12 @@ class="loading"> <loading-icon /> </div> + <ul v-if="!isLoading" id="notes-list" class="notes main-notes-list timeline"> + <component v-for="note in notes" :is="getComponentName(note)" @@ -146,6 +155,7 @@ :key="note.id" /> </ul> - <issue-comment-form v-if="!isLoading" /> + + <issue-comment-form /> </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index 91e1c62bba0b..492ba58c6a6f 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -38,8 +38,9 @@ :class="{ target: isTargetNote }" class="note system-note timeline-entry"> <div class="timeline-entry-inner"> - <div class="timeline-icon"> - <span v-html="svg"></span> + <div + class="timeline-icon" + v-html="svg"> </div> <div class="timeline-content"> <div class="note-header"> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 53fb03bab41a..0ebde2dccb82 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -6,3 +6,5 @@ export const COMMENT = 'comment'; export const OPENED = 'opened'; export const REOPENED = 'reopened'; export const CLOSED = 'closed'; +export const EMOJI_THUMBSUP = 'thumbsup'; +export const EMOJI_THUMBSDOWN = 'thumbsdown'; diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 914e08ff1125..0bf52b551f42 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,25 +1,39 @@ import Vue from 'vue'; -import issueNotes from './components/issue_notes.vue'; -import '../vue_shared/vue_resource_interceptor'; +import issueNotesApp from './components/issue_notes_app.vue'; -document.addEventListener('DOMContentLoaded', () => { - const vm = new Vue({ - el: '#js-notes', - components: { - issueNotes, - }, - render(createElement) { - return createElement('issue-notes', { - attrs: { - ref: 'notes', - }, - }); - }, - }); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#js-vue-notes', + components: { + issueNotesApp, + }, + data() { + const notesDataset = document.getElementById('js-vue-notes').dataset; - window.issueNotes = { - refresh() { - vm.$refs.notes.$store.dispatch('poll'); - }, - }; -}); + return { + issueData: JSON.parse(notesDataset.issueData), + currentUserData: JSON.parse(notesDataset.currentUserData), + notesData: { + lastFetchedAt: notesDataset.lastFetchedAt, + discussionsPath: notesDataset.discussionsPath, + }, + }; + }, + render(createElement) { + return createElement('issue-notes-app', { + attrs: { + ref: 'notes', + }, + props: { + issueData: this.issueData, + notesData: this.notesData, + }, + }); + }, +})); + + // // TODO: FILIPA: FIX THIS + // window.issueNotes = { + // refresh() { + // vm.$refs.notes.$store.dispatch('poll'); + // }, + // }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 092049cb377f..7f806fcd6757 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -7,6 +7,13 @@ import service from '../services/issue_notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; +export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); +export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data); +export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); +export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); +export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITAL_NOTES, data); +export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); + export const fetchNotes = ({ commit }, path) => service .fetchNotes(path) .then(res => res.json()) @@ -20,43 +27,31 @@ export const deleteNote = ({ commit }, note) => service commit(types.DELETE_NOTE, note); }); -export const updateNote = ({ commit }, data) => { - const { endpoint, note } = data; - - return service - .updateNote(endpoint, note) - .then(res => res.json()) - .then((res) => { - commit(types.UPDATE_NOTE, res); - }); -}; - -export const replyToDiscussion = ({ commit }, note) => { - const { endpoint, data } = note; - - return service - .replyToDiscussion(endpoint, data) - .then(res => res.json()) - .then((res) => { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); +export const updateNote = ({ commit }, { endpoint, note }) => service + .updateNote(endpoint, note) + .then(res => res.json()) + .then((res) => { + commit(types.UPDATE_NOTE, res); + }); - return res; - }); -}; +export const replyToDiscussion = ({ commit }, { endpoint, data }) => service + .replyToDiscussion(endpoint, data) + .then(res => res.json()) + .then((res) => { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); -export const createNewNote = ({ commit }, note) => { - const { endpoint, data } = note; + return res; + }); - return service - .createNewNote(endpoint, data) - .then(res => res.json()) - .then((res) => { - if (!res.errors) { - commit(types.ADD_NEW_NOTE, res); - } - return res; - }); -}; +export const createNewNote = ({ commit }, { endpoint, data }) => service + .createNewNote(endpoint, data) + .then(res => res.json()) + .then((res) => { + if (!res.errors) { + commit(types.ADD_NEW_NOTE, res); + } + return res; + }); export const saveNote = ({ commit, dispatch }, noteData) => { const { note } = noteData.data.note; @@ -91,6 +86,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { if (hasQuickActions && Object.keys(errors).length) { dispatch('poll'); + $('.js-gfm-input').trigger('clear-commands-cache.atwho'); Flash('Commands applied', 'notice', $(noteData.flashContainer)); } @@ -136,9 +132,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }; export const poll = ({ commit, state, getters }) => { - const { notesPath } = $('.js-notes-wrapper')[0].dataset; - - return service.poll(`${notesPath}?full_data=1`, state.lastFetchedAt) + return service.poll(state.notesData.notesPath, state.lastFetchedAt) .then(res => res.json()) .then((res) => { if (res.notes.length) { @@ -160,7 +154,6 @@ export const poll = ({ commit, state, getters }) => { } }); } - return res; }); }; @@ -175,20 +168,24 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => { .then(() => { commit(types.TOGGLE_AWARD, { awardName, note }); - if (!skipMutalityCheck && (awardName === 'thumbsup' || awardName === 'thumbsdown')) { - const counterAward = awardName === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + if (!skipMutalityCheck && + (awardName === constants.EMOJI_THUMBSUP || awardName === constants.EMOJI_THUMBSDOWN)) { + const counterAward = awardName === constants.EMOJI_THUMBSUP ? + constants.EMOJI_THUMBSDOWN : + constants.EMOJI_THUMBSUP; + const targetNote = getters.notesById[noteId]; - let amIAwarded = false; + let noteHasAward = false; targetNote.award_emoji.forEach((a) => { if (a.name === counterAward && a.user.id === window.gon.current_user_id) { - amIAwarded = true; + noteHasAward = true; } }); - if (amIAwarded) { - data.awardName = counterAward; - data.skipMutalityCheck = true; + if (noteHasAward) { + Object.assign(data, { awardName: counterAward }); + Object.assign(data, { kipMutalityCheck: true }); dispatch(types.TOGGLE_AWARD, data); } @@ -197,9 +194,7 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => { }; export const scrollToNoteIfNeeded = (context, el) => { - const isInViewport = gl.utils.isInViewport(el[0]); - - if (!isInViewport) { + if (!gl.utils.isInViewport(el[0])) { gl.utils.scrollToElement(el); } }; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index c3a9f0a5e89a..2db3c3f02afc 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,10 +1,12 @@ export const notes = state => state.notes; - export const targetNoteHash = state => state.targetNoteHash; +export const getNotesDataByProp = state => prop => state.notesData[prop]; +export const getIssueDataByProp = state => prop => state.notesData[prop]; +export const getUserDataByProp = state => prop => state.notesData[prop]; export const notesById = (state) => { const notesByIdObject = {}; - + // TODO: FILIPA: TRANSFORM INTO A REDUCE state.notes.forEach((note) => { note.notes.forEach((n) => { notesByIdObject[n.id] = n; diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index edca63fae673..8e0c8531bbcb 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -11,6 +11,11 @@ export default new Vuex.Store({ notes: [], targetNoteHash: null, lastFetchedAt: null, + + // holds endpoints and permissions provided through haml + notesData: {}, + userData: {}, + issueData: {}, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index f84f26684ca9..4eccc2af56eb 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -2,6 +2,9 @@ export const ADD_NEW_NOTE = 'ADD_NEW_NOTE'; export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION'; export const DELETE_NOTE = 'DELETE_NOTE'; export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; +export const SET_NOTES_DATA = 'SET_NOTES_DATA'; +export const SET_ISSUE_DATA = 'SET_ISSUE_DATA'; +export const SET_USER_DATA = 'SET_USER_DATA'; export const SET_INITAL_NOTES = 'SET_INITIAL_NOTES'; export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index bb2ed91e570d..4af8b0e6b9d7 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -58,6 +58,18 @@ export default { } }, + [types.SET_NOTES_DATA](state, data) { + state.notesData = data; + }, + + [types.SET_ISSUE_DATA](state, data) { + state.issueData = data; + }, + + [types.SET_USER_DATA](state, data) { + state.userData = data; + }, + [types.SET_INITAL_NOTES](state, notes) { state.notes = notes; }, diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 90b97d36d2cb..2b4fb6be3274 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -3,16 +3,16 @@ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' -%section.js-notes-wrapper{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), notes_path: notes_url, last_fetched_at: Time.now.to_i } } - #js-notes +%section + #js-vue-notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), + new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), + notes_path: '#{notes_url}?full_data=1', last_fetched_at: Time.now.to_i, + issue_data: serialize_issuable(@issue), + current_user_data: UserSerializer.new.represent(current_user).to_json }} - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' - -/ #notes{style: "margin-top: 150px"} -/ = render 'shared/notes/notes_with_form', :autocomplete => true - = render "layouts/init_auto_complete" :javascript -- GitLab From 3343bf30f737b25fe5337c1e6c21180649f8dc3d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 10:46:46 +0100 Subject: [PATCH 110/243] [ci skip] Remove DOM querying for paths, save them in the store --- .../notes/components/issue_comment_form.vue | 36 +++++++------------ .../issue_note_signed_out_widget.vue | 14 +++----- .../notes/components/issue_notes_app.vue | 19 ++++++---- app/assets/javascripts/notes/index.js | 4 +++ .../javascripts/notes/stores/getters.js | 8 ++++- app/assets/javascripts/notes/stores/index.js | 1 + .../javascripts/notes/stores/mutations.js | 1 - .../projects/issues/_discussion.html.haml | 4 ++- 8 files changed, 45 insertions(+), 42 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index b39dd2febe90..90fa893253e9 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,7 +1,7 @@ <script> -/* global Flash */ + /* global Flash */ - import { mapActions, mapGetters } from 'vuex'; + import { mapActions } from 'vuex'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; @@ -10,18 +10,18 @@ export default { data() { - const { create_note_path, state } = window.gl.issueData; - const { currentUserData } = window.gl; + const { getUserData, getIssueData } = this.$store.getters; return { note: '', - markdownDocsUrl: '', - quickActionsDocsUrl: null, - markdownPreviewUrl: gl.issueData.preview_note_path, + markdownDocsUrl: getIssueData.markdownDocs, + quickActionsDocsUrl: getIssueData.quickActionsDocs, + markdownPreviewUrl: getIssueData.preview_note_path, noteType: constants.COMMENT, - issueState: state, - endpoint: create_note_path, - author: currentUserData, + issueState: getIssueData.state, + endpoint: getIssueData.create_note_path, + author: getUserData, + canUpdateIssue: getIssueData.current_user.can_update, }; }, components: { @@ -30,10 +30,6 @@ issueNoteSignedOutWidget, }, computed: { - ...mapGetters([ - 'getNotesDataByProp', - 'getIssueDataByProp', - ]), isLoggedIn() { return window.gon.current_user_id; }, @@ -60,9 +56,6 @@ 'js-note-target-reopen': !this.isIssueOpen, }; }, - canUpdateIssue() { - return this.getIssueDataByProp(current_user).can_update; - }, }, methods: { ...mapActions([ @@ -106,12 +99,12 @@ if (withIssueAction) { if (this.isIssueOpen) { - gl.issueData.state = constants.CLOSED; this.issueState = constants.CLOSED; } else { - gl.issueData.state = constants.REOPENED; this.issueState =constants.REOPENED; } + + gl.issueData.state = this.issueState; this.isIssueOpen = !this.isIssueOpen; // This is out of scope for the Notes Vue component. @@ -139,7 +132,7 @@ editMyLastNote() { if (this.note === '') { const myLastNoteId = $('.js-my-note').last().attr('id'); - + debugger; if (myLastNoteId) { eventHub.$emit('enterEditMode', { noteId: parseInt(myLastNoteId.replace('note_', ''), 10), @@ -149,9 +142,6 @@ }, }, mounted() { - this.markdownDocsUrl = this.getIssueDataByProp(markdownDocs); - this.quickActionsDocsUrl = this.getIssueDataByProp(quickActionsDocs); - eventHub.$on('issueStateChanged', (isClosed) => { this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; }); diff --git a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue index 59d052b35fbb..7cb7d4d25e5b 100644 --- a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue @@ -1,17 +1,13 @@ <script> export default { data() { + const { newSessionPath, registerPath } = this.$store.getters.notesData; + return { - signInLink: '#', + signInLink: newSessionPath, + registerLink: registerPath, }; }, - mounted() { - const wrapper = document.querySelector('.js-notes-wrapper'); - - if (wrapper) { - this.signInLink = wrapper.dataset.newSessionPath; - } - }, }; </script> @@ -20,7 +16,7 @@ Please <a :href="signInLink">register</a> or - <a :href="signInLink">sign in</a> + <a :href="registerLink">sign in</a> to reply </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index a3b6d928a51a..4da82434ed12 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -25,6 +25,10 @@ type: Object, required: true, }, + userData: { + type: Object, + required: true, + }, }, store, data() { @@ -43,21 +47,21 @@ }, computed: { ...mapGetters([ - 'notes', - 'notesById', - 'getNotesData', 'getNotesDataByProp', - 'setLastFetchedAt', - 'setTargetNoteHash', ]), }, methods: { + ...mapGetters([ + 'getNotesDataByProp', + ]), ...mapActions({ actionFetchNotes: 'fetchNotes', poll: 'poll', toggleAward: 'toggleAward', scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', - setNotesData: 'setNotesData' + setNotesData: 'setNotesData', + setIssueData: 'setIssueData', + setUserData: 'setUserData', }), getComponentName(note) { if (note.isPlaceholderNote) { @@ -75,7 +79,7 @@ return note.individual_note ? note.notes[0] : note; }, fetchNotes() { - this.actionFetchNotes(his.getNotesDataByProp('discussionsPath')) + this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) .then(() => { this.isLoading = false; @@ -126,6 +130,7 @@ created() { this.setNotesData(this.notesData); this.setIssueData(this.issueData); + this.setUserData(this.userData) }, mounted() { this.fetchNotes(); diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 0bf52b551f42..4c5b06f4791a 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -15,6 +15,9 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ notesData: { lastFetchedAt: notesDataset.lastFetchedAt, discussionsPath: notesDataset.discussionsPath, + newSessionPath: notesDataset.newSessionPath, + registerPath: notesDataset.registerPath, + notesPath: notesDataset.notesPath, }, }; }, @@ -26,6 +29,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ props: { issueData: this.issueData, notesData: this.notesData, + userData: this.currentUserData, }, }); }, diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 2db3c3f02afc..83f18886b24b 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,7 +1,13 @@ export const notes = state => state.notes; export const targetNoteHash = state => state.targetNoteHash; + +export const getNotesData = state => state.notesData; export const getNotesDataByProp = state => prop => state.notesData[prop]; -export const getIssueDataByProp = state => prop => state.notesData[prop]; + +export const getIssueData = state => state.issueData; +export const getIssueDataByProp = state => prop => state.issueData[prop]; + +export const getUserData = state => state.userData; export const getUserDataByProp = state => prop => state.notesData[prop]; export const notesById = (state) => { diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index 8e0c8531bbcb..be4f509932ff 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -16,6 +16,7 @@ export default new Vuex.Store({ notesData: {}, userData: {}, issueData: {}, + paths: {}, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 4af8b0e6b9d7..15707643091a 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -69,7 +69,6 @@ export default { [types.SET_USER_DATA](state, data) { state.userData = data; }, - [types.SET_INITAL_NOTES](state, notes) { state.notes = notes; }, diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 2b4fb6be3274..76150264e31d 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -5,8 +5,10 @@ %section #js-vue-notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), + register_path: "#{new_session_path(:user, redirect_to_referer: 'yes')}#register-pane", new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), - notes_path: '#{notes_url}?full_data=1', last_fetched_at: Time.now.to_i, + notes_path: '#{notes_url}?full_data=1', + last_fetched_at: Time.now.to_i, issue_data: serialize_issuable(@issue), current_user_data: UserSerializer.new.represent(current_user).to_json }} - content_for :page_specific_javascripts do -- GitLab From ed05a62c4ab3407c21bdda2625c2286a704bf353 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 10:50:32 +0100 Subject: [PATCH 111/243] [ci skip] Adds test cases for sign in component --- .../notes/components/issue_note_signed_out_widget.vue | 4 ++-- .../components/issue_note_signed_out_widget_spec.js | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js diff --git a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue index 7cb7d4d25e5b..9527d0fd80a1 100644 --- a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue @@ -14,9 +14,9 @@ <template> <div class="disabled-comment text-center"> Please - <a :href="signInLink">register</a> + <a :href="registerLink">register</a> or - <a :href="registerLink">sign in</a> + <a :href="signInLink">sign in</a> to reply </div> </template> diff --git a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js new file mode 100644 index 000000000000..1ba202090d99 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js @@ -0,0 +1,9 @@ +describe('issue note signed out widget component', () => { + it('should render sign in link provided in the store', () => { + + }); + + it('should render register link provided in the store', () => { + + }); +}); -- GitLab From 6530035fb031ab00a8d3fd6726b6d71296b462e8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 10:55:41 +0100 Subject: [PATCH 112/243] [ci skip] Adds tests cases for system note --- .../components/issue_note_signed_out_widget.vue | 1 + .../notes/components/issue_system_note.vue | 1 + .../notes/components/issue_system_note_spec.js | 17 +++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 spec/javascripts/notes/components/issue_system_note_spec.js diff --git a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue index 9527d0fd80a1..6d551225b29b 100644 --- a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue @@ -1,5 +1,6 @@ <script> export default { + name: 'singInLinksNotes', data() { const { newSessionPath, registerPath } = this.$store.getters.notesData; diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index 492ba58c6a6f..d7af04581980 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -4,6 +4,7 @@ import issueNoteHeader from './issue_note_header.vue'; export default { + name: 'systemNote', props: { note: { type: Object, diff --git a/spec/javascripts/notes/components/issue_system_note_spec.js b/spec/javascripts/notes/components/issue_system_note_spec.js new file mode 100644 index 000000000000..779de4ab6575 --- /dev/null +++ b/spec/javascripts/notes/components/issue_system_note_spec.js @@ -0,0 +1,17 @@ +describe('issue system note', () => { + it('should render a list item with correct id', () => { + + }); + + it('should render target class is note is target note', () => { + + }); + + it('should render svg icon', () => { + + }); + + it('should render note header component', () => { + + }); +}); -- GitLab From 7639fb189c2fb5664cc8a89c03680d5de4b22db3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 11:20:17 +0100 Subject: [PATCH 113/243] [ci skip] Adds tests for placeholder system note --- .../components/issue_placeholder_note.vue | 19 +++++--- .../issue_placeholder_system_note.vue | 1 + .../components/issue_placeholder_note_spec.js | 43 +++++++++++++++++++ .../issue_placeholder_system_note_spec.js | 25 +++++++++++ 4 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 spec/javascripts/notes/components/issue_placeholder_note_spec.js create mode 100644 spec/javascripts/notes/components/issue_placeholder_system_note_spec.js diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_note.vue index fec60d9667bd..4c089d755c85 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue +++ b/app/assets/javascripts/notes/components/issue_placeholder_note.vue @@ -1,14 +1,20 @@ <script> + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + export default { + name: 'issuePlaceholderNote', props: { note: { type: Object, required: true, }, }, + components: { + userAvatarLink, + }, data() { return { - currentUser: window.gl.currentUserData, + currentUser: this.$store.getters.getUserData, }; }, }; @@ -18,12 +24,11 @@ <li class="note being-posted fade-in-half timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> - <a :href="currentUser.path"> - <img - :src="currentUser.avatar_url" - class="avatar s40" - /> - </a> + <user-avatar-link + :link-href="currentUser.path" + :img-src="currentUser.avatar_url" + :size="40" + /> </div> <div :class="{ discussion: !note.individual_note }" diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue index b0d442c1a87f..9c041728047a 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue @@ -1,5 +1,6 @@ <script> export default { + name: 'placeholderSystemNote', props: { note: { type: Object, diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js new file mode 100644 index 000000000000..f4d8e01bfe60 --- /dev/null +++ b/spec/javascripts/notes/components/issue_placeholder_note_spec.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +import placeholderNote from '~/notes/components/issue_placeholder_note.vue'; + +describe('issue placeholder system note component', () => { + let mountComponent; + beforeEach(() => { + const PlaceholderNote = Vue.extend(placeholderNote); + + mountComponent = props => new PlaceholderNote({ + propsData: { + note: props, + }, + }).$mount(); + }); + + describe('user information', () => { + it('should render user avatar with link', () => { + + }); + }); + + describe('note content', () => { + it('should render note header information', () => { + + }); + + it('should render note body', () => { + + }); + + it('should render system note placeholder with markdown', () => { + + }); + + it('should render emojis', () => { + + }); + + it('should render slash commands', () => { + + }); + }); +}); diff --git a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js new file mode 100644 index 000000000000..fd28b33d60b3 --- /dev/null +++ b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import placeholderSystemNote from '~/notes/components/issue_placeholder_system_note.vue'; + +describe('issue placeholder system note component', () => { + let mountComponent; + beforeEach(() => { + const PlaceholderSystemNote = Vue.extend(placeholderSystemNote); + + mountComponent = props => new PlaceholderSystemNote({ + propsData: { + note: { + body: props, + }, + }, + }).$mount(); + }); + + it('should render system note placeholder with plain text', () => { + const vm = mountComponent('This is a placeholder'); + + expect(vm.$el.tagName).toEqua('LI'); + + expect(vm.$el.querySelector('.timeline-content i').textContent.trim()).toEqua('This is a placeholder'); + }); +}); -- GitLab From b27fb2337ef43bd7060ef1d3cdf46cbb6d794d8e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 11:32:08 +0100 Subject: [PATCH 114/243] [ci skip] Remove mutations from components, always mutate through actions --- .../javascripts/notes/components/issue_note_header.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 20f23bf828a7..49f7980b272d 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -1,5 +1,5 @@ <script> - import { mapMutations } from 'vuex'; + import { mapActions } from 'vuex'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import * as types from '../stores/mutation_types'; @@ -54,9 +54,9 @@ }, }, methods: { - ...mapMutations({ - setTargetNoteHash: types.SET_TARGET_NOTE_HASH, - }), + ...mapActions([ + 'setTargetNoteHash' + ]), handleToggle() { this.isExpanded = !this.isExpanded; this.toggleHandler(); -- GitLab From c433f5203fa12619dce53c2fc4c1c69d698f2172 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 11:36:21 +0100 Subject: [PATCH 115/243] [ci skip] Remove usage of global scope and remove dom querying from issue note form --- .../javascripts/notes/components/issue_note_form.vue | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index e5d8ef475f9a..3a8e7239a01a 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -28,11 +28,13 @@ }, }, data() { + const { getIssueData, getNotesData } = this.$store.getters; + return { initialNote: this.noteBody, note: this.noteBody, - markdownPreviewUrl: gl.issueData.preview_note_path, - markdownDocsUrl: '', + markdownPreviewUrl: getIssueData.preview_note_path, + markdownDocsUrl: getNotesData.markdownDocs, conflictWhileEditing: false, }; }, @@ -67,10 +69,6 @@ }, }, mounted() { - const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); - const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/"/g, '"')); - - this.markdownDocsUrl = issueData.markdownDocs; this.$refs.textarea.focus(); }, watch: { @@ -126,7 +124,7 @@ {{saveButtonTitle}} </button> <button - @click="cancelHandler()" + @click="cancelHandler" class="btn btn-nr btn-cancel note-edit-cancel" type="button"> Cancel -- GitLab From b45b604d9410e19b7a6c0783343626f1ff163708 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 11:40:31 +0100 Subject: [PATCH 116/243] [ci skip] Move click handler to button instead of `li`` --- .../notes/components/issue_comment_form.vue | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 90fa893253e9..6761f15becce 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -195,7 +195,7 @@ <div class="note-form-actions"> <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button - @click="handleSave()" + @click="handleSave" :disabled="!note.length" class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" type="button"> @@ -213,16 +213,17 @@ class="fa fa-caret-down toggle-icon"> </i> </button> + <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> - <li - :class="{ 'droplab-item-selected': noteType === 'comment' }" - @click.prevent="setNoteType('comment')"> + <li :class="{ 'droplab-item-selected': noteType === 'comment' }"> <button type="button" - class="btn btn-transparent"> + class="btn btn-transparent" + @click.prevent="setNoteType('comment')"> <i aria-hidden="true" - class="fa fa-check icon"></i> + class="fa fa-check icon"> + </i> <div class="description"> <strong>Comment</strong> <p> @@ -232,12 +233,11 @@ </button> </li> <li class="divider droplab-item-ignore"></li> - <li - :class="{ 'droplab-item-selected': noteType === 'discussion' }" - @click.prevent="setNoteType('discussion')"> + <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> <button type="button" - class="btn btn-transparent"> + class="btn btn-transparent" + @click.prevent="setNoteType('discussion')"> <i aria-hidden="true" class="fa fa-check icon"> -- GitLab From 7a251207e1b6f2b4c709d319d870694ace620e0c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 12:53:51 +0100 Subject: [PATCH 117/243] [ci skip] Emit events up to prevent accessing refs of refs --- .../notes/components/issue_comment_form.vue | 11 +++----- .../notes/components/issue_note.vue | 27 +++++++++---------- .../notes/components/issue_note_actions.vue | 11 ++++---- .../components/issue_note_awards_list.vue | 16 +++++------ .../notes/components/issue_note_body.vue | 22 ++++++++------- .../notes/components/issue_note_form.vue | 26 +++++++----------- .../notes/components/issue_note_header.vue | 2 +- .../notes/components/issue_notes_app.vue | 4 +-- .../issue_placeholder_system_note.vue | 2 +- app/assets/javascripts/notes/index.js | 2 ++ .../javascripts/notes/stores/actions.js | 8 +++--- app/helpers/issuables_helper.rb | 1 - .../projects/issues/_discussion.html.haml | 2 ++ 13 files changed, 63 insertions(+), 71 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 6761f15becce..5b8b7a67fa8f 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -10,12 +10,12 @@ export default { data() { - const { getUserData, getIssueData } = this.$store.getters; + const { getUserData, getIssueData, getNotesData } = this.$store.getters; return { note: '', - markdownDocsUrl: getIssueData.markdownDocs, - quickActionsDocsUrl: getIssueData.quickActionsDocs, + markdownDocsUrl: getNotesData.markdownDocs, + quickActionsDocsUrl: getNotesData.quickActionsDocs, markdownPreviewUrl: getIssueData.preview_note_path, noteType: constants.COMMENT, issueState: getIssueData.state, @@ -89,7 +89,7 @@ this.handleError(); } } else { - this.discard(); + return Flash('Something went wrong while adding your comment. Please try again.'); } }) .catch(() => { @@ -126,9 +126,6 @@ setNoteType(type) { this.noteType = type; }, - handleError() { - Flash('Something went wrong while adding your comment. Please try again.'); - }, editMyLastNote() { if (this.note === '') { const myLastNoteId = $('.js-my-note').last().attr('id'); diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index d490578ce6f1..49edac407dd1 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -19,6 +19,7 @@ return { isEditing: false, isDeleting: false, + currentUserId: window.gon.current_user_id, }; }, components: { @@ -38,12 +39,12 @@ return { 'is-editing': this.isEditing, 'disabled-content': this.isDeleting, - 'js-my-note': this.author.id === window.gon.current_user_id, + 'js-my-note': this.author.id === this.currentUserId, target: this.targetNoteHash === this.noteAnchorId, }; }, canReportAsAbuse() { - return this.note.report_abuse_path && this.author.id !== window.gon.current_user_id; + return this.note.report_abuse_path && this.author.id !== this.currentUserId; }, noteAnchorId() { return `note_${this.note.id}`; @@ -59,8 +60,8 @@ this.isEditing = true; }, deleteHandler() { - const msg = 'Are you sure you want to delete this list?'; - const isConfirmed = confirm(msg); // eslint-disable-line + // eslint-disable-next-line no-alert + const isConfirmed = confirm('Are you sure you want to delete this list?'); if (isConfirmed) { this.isDeleting = true; @@ -88,17 +89,15 @@ this.updateNote(data) .then(() => { this.isEditing = false; + // TODO: this could be moved down, by setting a prop $(this.$refs.noteBody.$el).renderGFM(); }) .catch(() => Flash('Something went wrong while editing your comment. Please try again.')); }, - formCancelHandler(shouldConfirm) { - if (shouldConfirm && this.$refs.noteBody.$refs.noteForm.isDirty) { - const msg = 'Are you sure you want to cancel editing this comment?'; - const isConfirmed = confirm(msg); // eslint-disable-line - if (!isConfirmed) { - return; - } + formCancelHandler(shouldConfirm, isDirty) { + if (shouldConfirm && isDirty) { + // eslint-disable-next-line no-alert + if (!confirm('Are you sure you want to cancel editing this comment?')) return; } this.isEditing = false; @@ -135,7 +134,7 @@ :author="author" :created-at="note.created_at" :note-id="note.id" - actionText="commented" + action-text="commented" /> <issue-note-actions :author-id="author.id" @@ -153,8 +152,8 @@ :note="note" :can-edit="note.current_user.can_edit" :is-editing="isEditing" - :form-update-handler="formUpdateHandler" - :form-cancel-handler="formCancelHandler" + @handleFormUpdate="formUpdateHandler" + @cancelFormEdition="formCancelHandler" ref="noteBody" /> </div> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index f08081e35775..dd2c55073b43 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -53,6 +53,7 @@ emojiSmiling, emojiSmile, emojiSmiley, + currentUserId: window.gon.current_user_id, }; }, components: { @@ -60,13 +61,13 @@ }, computed: { shouldShowActionsDropdown() { - return window.gon.current_user_id && (this.canEdit || this.canReportAsAbuse); + return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, canAddAwardEmoji() { - return window.gon.current_user_id; + return this.currentUserId; }, - isAuthoredByMe() { - return this.authorId === window.gon.current_user_id; + isAuthoredByCurrentUser() { + return this.authorId === this.currentUserId }, }, }; @@ -82,7 +83,7 @@ <a v-tooltip v-if="canAddAwardEmoji" - :class="{ 'js-user-authored': isAuthoredByMe }" + :class="{ 'js-user-authored': isAuthoredByCurrentUser }" class="note-action-button note-emoji-button js-add-award js-note-emoji" data-position="right" href="#" diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 9770b57fc039..479e3c8762aa 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -91,7 +91,7 @@ }, getAwardClassBindings(awardList, awardName) { return { - active: this.amIAwarded(awardList), + active: this.hasReactionByCurrentUser(awardList), disabled: !this.canInteractWithEmoji(awardList, awardName), }; }, @@ -107,18 +107,16 @@ return this.canAward && isAllowed; }, - amIAwarded(awardList) { - const isAwarded = awardList.filter(award => award.user.id === this.myUserId); - - return isAwarded.length; + hasReactionByCurrentUser(awardList) { + return awardList.filter(award => award.user.id === this.myUserId).length; }, awardTitle(awardsList) { - const amIAwarded = this.amIAwarded(awardsList); - const TOOLTIP_NAME_COUNT = amIAwarded ? 9 : 10; + const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); + const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; let awardList = awardsList; // Filter myself from list if I am awarded. - if (amIAwarded) { + if (hasReactionByCurrentUser) { awardList = awardList.filter(award => award.user.id !== this.myUserId); } @@ -129,7 +127,7 @@ const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); // Add myself to the begining of the list so title will start with You. - if (amIAwarded) { + if (hasReactionByCurrentUser) { namesToShow.unshift('You'); } diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index dee8bb0c7f9d..c7dc146985bd 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -51,11 +51,12 @@ }); } }, - handleFormUpdate() { - this.formUpdateHandler({ - note: this.$refs.noteForm.note, - }); + handleFormUpdate(note) { + this.$emit('handleFormUpdate', note); }, + formCancelHandler(shouldConfirm, isDirty) { + this.$emit('cancelFormEdition', shouldConfirm, isDirty); + } }, mounted() { this.renderGFM(); @@ -78,10 +79,11 @@ <issue-note-form v-if="isEditing" ref="noteForm" - :update-handler="handleFormUpdate" - :cancel-handler="formCancelHandler" + @handleFormUpdate="handleFormUpdate" + @cancelFormEdition="formCancelHandler" :note-body="noteBody" - :note-id="note.id" /> + :note-id="note.id" + /> <textarea v-if="canEdit" v-model="note.note" @@ -91,12 +93,14 @@ v-if="note.last_edited_by" :edited-at="note.last_edited_at" :edited-by="note.last_edited_by" - actionText="Edited" /> + actionText="Edited" + /> <issue-note-awards-list v-if="note.award_emoji.length" :note-id="note.id" :note-author-id="note.author.id" :awards="note.award_emoji" - :toggle-award-path="note.toggle_award_path" /> + :toggle-award-path="note.toggle_award_path" + /> </div> </template> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 3a8e7239a01a..75aca2d5cc93 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -13,14 +13,6 @@ type: Number, required: false, }, - updateHandler: { - type: Function, - required: true, - }, - cancelHandler: { - type: Function, - required: true, - }, saveButtonTitle: { type: String, required: false, @@ -42,18 +34,13 @@ markdownField, }, computed: { - isDirty() { - return this.initialNote !== this.note; - }, noteHash() { return `#note_${this.noteId}`; }, }, methods: { handleUpdate() { - this.updateHandler({ - note: this.note, - }); + this.$emit('handleFormUpdate', note); }, editMyLastNote() { if (this.note === '') { @@ -67,6 +54,10 @@ } } }, + cancelHandler(shouldConfirm = false) { + // Sends information about confirm message and if the textarea has changed + this.$emit('cancelFormEdition', shouldConfirm, this.initialNote !== this.note); + } }, mounted() { this.$refs.textarea.focus(); @@ -95,7 +86,9 @@ rel="noopener noreferrer">updated comment</a> to ensure information is not lost. </div> - <form class="edit-note common-note-form"> + <form + @submit="handleUpdate" + class="edit-note common-note-form"> <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" @@ -118,8 +111,7 @@ </markdown-field> <div class="note-form-actions clearfix"> <button - @click="handleUpdate" - type="button" + type="submit" class="btn btn-nr btn-save"> {{saveButtonTitle}} </button> diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 49f7980b272d..17f3fe3b0003 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -93,7 +93,7 @@ @click="updateTargetNoteHash"> <time-ago-tooltip :time="createdAt" - tooltipPlacement="bottom" + tooltip-placement="bottom" /> </a> </span> diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 4da82434ed12..21f9e7089bb3 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -47,13 +47,11 @@ }, computed: { ...mapGetters([ + 'notes', 'getNotesDataByProp', ]), }, methods: { - ...mapGetters([ - 'getNotesDataByProp', - ]), ...mapActions({ actionFetchNotes: 'fetchNotes', poll: 'poll', diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue index 9c041728047a..80a8ef56a831 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue @@ -14,7 +14,7 @@ <li class="note system-note timeline-entry being-posted fade-in-half"> <div class="timeline-entry-inner"> <div class="timeline-content"> - <i>{{note.body}}</i> + <em>{{note.body}}</em> </div> </div> </li> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 4c5b06f4791a..5a234564f189 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -18,6 +18,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ newSessionPath: notesDataset.newSessionPath, registerPath: notesDataset.registerPath, notesPath: notesDataset.notesPath, + markdownDocs: notesDataset.markdownDocs, + quickActionsDocs: notesDataset.quickActionsDocs, }, }; }, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 7f806fcd6757..e1e0aec8a831 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -175,17 +175,17 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => { constants.EMOJI_THUMBSUP; const targetNote = getters.notesById[noteId]; - let noteHasAward = false; + let noteHasAwardByCurrentUser = false; targetNote.award_emoji.forEach((a) => { if (a.name === counterAward && a.user.id === window.gon.current_user_id) { - noteHasAward = true; + noteHasAwardByCurrentUser = true; } }); - if (noteHasAward) { + if (noteHasAwardByCurrentUser) { Object.assign(data, { awardName: counterAward }); - Object.assign(data, { kipMutalityCheck: true }); + Object.assign(data, { skipMutalityCheck: true }); dispatch(types.TOGGLE_AWARD, data); } diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 38ffc62fbdfb..5f1f5918f06d 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -214,7 +214,6 @@ def issuable_initial_data(issuable) initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionText: issuable.description, initialTaskStatus: issuable.task_status, - quickActionsDocs: help_page_path('user/project/quick_actions'), } data.merge!(updated_at_by(issuable)) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 76150264e31d..987cde6c38be 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -7,6 +7,8 @@ #js-vue-notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), register_path: "#{new_session_path(:user, redirect_to_referer: 'yes')}#register-pane", new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), + markdown_docs: help_page_path('user/markdown'), + quick_actions_docs: help_page_path('user/project/quick_actions'), notes_path: '#{notes_url}?full_data=1', last_fetched_at: Time.now.to_i, issue_data: serialize_issuable(@issue), -- GitLab From 2845dad2afcfd1f6e4d48d96d957ac7b33051524 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 18:43:19 +0100 Subject: [PATCH 118/243] Find last note created by current user through vue instead of querying the DOM --- .../notes/components/issue_comment_form.vue | 17 ++++++++++------- .../notes/components/issue_discussion.vue | 5 +++-- .../javascripts/notes/components/issue_note.vue | 3 ++- .../notes/components/issue_note_body.vue | 10 +--------- .../notes/components/issue_note_form.vue | 11 ++++++++++- .../notes/components/issue_notes_app.vue | 11 +++++++++-- app/assets/javascripts/notes/stores/getters.js | 13 +++++++++++++ 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 5b8b7a67fa8f..ff15b3549174 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,7 +1,7 @@ <script> /* global Flash */ - import { mapActions } from 'vuex'; + import { mapActions, mapGetters } from 'vuex'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; @@ -30,6 +30,9 @@ issueNoteSignedOutWidget, }, computed: { + ...mapGetters([ + 'getCurrentUserLastNote', + ]), isLoggedIn() { return window.gon.current_user_id; }, @@ -126,13 +129,13 @@ setNoteType(type) { this.noteType = type; }, - editMyLastNote() { + editCurrentUserLastNote() { if (this.note === '') { - const myLastNoteId = $('.js-my-note').last().attr('id'); - debugger; - if (myLastNoteId) { + const lastNote = this.getCurrentUserLastNote(window.gon.current_user_id); + console.log(lastNote) + if (lastNote) { eventHub.$emit('enterEditMode', { - noteId: parseInt(myLastNoteId.replace('note_', ''), 10), + noteId: lastNote.id, }); } } @@ -185,7 +188,7 @@ ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." - @keydown.up="editMyLastNote" + @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()"> </textarea> </markdown-field> diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 9d0625d30dee..087d80e3f5f5 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -164,8 +164,9 @@ <issue-note-form v-if="isReplying" saveButtonTitle="Comment" - :update-handler="saveReply" - :cancel-handler="cancelReplyForm" + :discussion="note" + @handleFormUpdate="saveReply" + @cancelFormEdition="cancelReplyForm" ref="noteForm" /> <issue-note-signed-out-widget v-if="!canReply" /> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 49edac407dd1..efe2bf73c40e 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -118,7 +118,8 @@ <li class="note timeline-entry" :id="noteAnchorId" - :class="classNameBindings"> + :class="classNameBindings" + :note-id="note.id"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index c7dc146985bd..fb663b59a8d1 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -19,14 +19,6 @@ required: false, default: false, }, - formUpdateHandler: { - type: Function, - required: true, - }, - formCancelHandler: { - type: Function, - required: true, - }, }, components: { issueNoteEditedText, @@ -93,7 +85,7 @@ v-if="note.last_edited_by" :edited-at="note.last_edited_at" :edited-by="note.last_edited_by" - actionText="Edited" + action-text="Edited" /> <issue-note-awards-list v-if="note.award_emoji.length" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 75aca2d5cc93..739a7df64a81 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -18,6 +18,10 @@ required: false, default: 'Save comment', }, + discussion: { + type: Array, + required: false, + } }, data() { const { getIssueData, getNotesData } = this.$store.getters; @@ -44,9 +48,14 @@ }, editMyLastNote() { if (this.note === '') { + // TODO: HANDLE THIS WITHOUTH JQUERY OR QUERYING THE DOM + // FIND the discussion we are in and the last comment on that discussion const discussion = $(this.$el).closest('.discussion-notes'); const myLastNoteId = discussion.find('.js-my-note').last().attr('id'); + debugger; + const lastNoteInDiscussion = this.$store.getters.getDiscussionLastNote(this.discussion); + if (myLastNoteId) { eventHub.$emit('enterEditMode', { noteId: parseInt(myLastNoteId.replace('note_', ''), 10), @@ -116,7 +125,7 @@ {{saveButtonTitle}} </button> <button - @click="cancelHandler" + @click="cancelHandler()" class="btn btn-nr btn-cancel note-edit-cancel" type="button"> Cancel diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 21f9e7089bb3..964ad5312b93 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -60,6 +60,7 @@ setNotesData: 'setNotesData', setIssueData: 'setIssueData', setUserData: 'setUserData', + setLastFetchedAt: 'setLastFetchedAt' }), getComponentName(note) { if (note.isPlaceholderNote) { @@ -86,7 +87,10 @@ this.checkLocationHash(); }); }) - .catch(() => Flash('Something went wrong while fetching issue comments. Please try again.')); + .catch((error) => { + console.log(error) + Flash('Something went wrong while fetching issue comments. Please try again.') + }); }, initPolling() { this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); @@ -98,7 +102,10 @@ .then((res) => { this.setLastFetchedAt(res.lastFetchedAt); }) - .catch(() => Flash('Something went wrong while fetching latest comments.')); + .catch((error) =>{ + console.log(error) + Flash('Something went wrong while fetching latest comments.') + } ); }, 15000); }, bindEventHubListeners() { diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 83f18886b24b..c454b834649f 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -21,3 +21,16 @@ export const notesById = (state) => { return notesByIdObject; }; + +const reverseNotes = array => array.slice(0).reverse(); +const isLastNote = (note, userId) => !note.system && note.author.id === userId; + +export const getCurrentUserLastNote = state => userId => reverseNotes(state.notes) + .reduce((acc, note) => { + acc.push(reverseNotes(note.notes).find(el => isLastNote(el, userId))); + return acc; + }, []).filter(el => el !== undefined)[0]; + +export const getDiscussionLastNote = state => (discussion, userId) => reverseNotes(discussion[0].notes) + .find(el => isLastNote(el, userId)); + -- GitLab From 9b87e680ca9653f40897ab8fa916d44fcfd1f4d5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 18:48:13 +0100 Subject: [PATCH 119/243] [ci skip] Find last note in discussion through vue instead of through the DOM --- .../notes/components/issue_note_form.vue | 18 ++++++++---------- app/assets/javascripts/notes/stores/getters.js | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 739a7df64a81..cb37afd410a0 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -1,4 +1,5 @@ <script> + import { mapGetters } from 'vuex'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import eventHub from '../event_hub'; @@ -19,7 +20,7 @@ default: 'Save comment', }, discussion: { - type: Array, + type: Object, required: false, } }, @@ -38,6 +39,9 @@ markdownField, }, computed: { + ...mapGetters([ + 'getDiscussionLastNote', + ]), noteHash() { return `#note_${this.noteId}`; }, @@ -48,17 +52,11 @@ }, editMyLastNote() { if (this.note === '') { - // TODO: HANDLE THIS WITHOUTH JQUERY OR QUERYING THE DOM - // FIND the discussion we are in and the last comment on that discussion - const discussion = $(this.$el).closest('.discussion-notes'); - const myLastNoteId = discussion.find('.js-my-note').last().attr('id'); + const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion, window.gon.current_user_id); - debugger; - const lastNoteInDiscussion = this.$store.getters.getDiscussionLastNote(this.discussion); - - if (myLastNoteId) { + if (lastNoteInDiscussion) { eventHub.$emit('enterEditMode', { - noteId: parseInt(myLastNoteId.replace('note_', ''), 10), + noteId: lastNoteInDiscussion.id, }); } } diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index c454b834649f..8dc24cd745e8 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -31,6 +31,6 @@ export const getCurrentUserLastNote = state => userId => reverseNotes(state.note return acc; }, []).filter(el => el !== undefined)[0]; -export const getDiscussionLastNote = state => (discussion, userId) => reverseNotes(discussion[0].notes) +export const getDiscussionLastNote = state => (discussion, userId) => reverseNotes(discussion.notes) .find(el => isLastNote(el, userId)); -- GitLab From f5a21c596b79c5f86c6b8034e1d0bf1d7078c593 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 19:59:31 +0100 Subject: [PATCH 120/243] [ci skip] Fix shortcuts for preview --- .../notes/components/issue_comment_form.vue | 188 +++++++++--------- .../notes/components/issue_note_form.vue | 6 +- .../notes/components/issue_notes_app.vue | 9 +- app/assets/javascripts/notes/index.js | 83 ++++---- .../javascripts/notes/stores/actions.js | 45 ++--- .../javascripts/notes/stores/getters.js | 16 +- app/assets/javascripts/notes/stores/index.js | 1 - .../projects/issues/_discussion.html.haml | 2 +- 8 files changed, 174 insertions(+), 176 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index ff15b3549174..bfe81d4dd421 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -89,10 +89,10 @@ if (res.errors.commands_only) { this.discard(); } else { - this.handleError(); + return Flash('Something went wrong while adding your comment. Please try again.'); } } else { - return Flash('Something went wrong while adding your comment. Please try again.'); + this.discard(); } }) .catch(() => { @@ -132,7 +132,7 @@ editCurrentUserLastNote() { if (this.note === '') { const lastNote = this.getCurrentUserLastNote(window.gon.current_user_id); - console.log(lastNote) + if (lastNote) { eventHub.$emit('enterEditMode', { noteId: lastNote.id, @@ -172,102 +172,104 @@ /> </div> <div class="js-main-target-form timeline-content timeline-content-form common-note-form"> - <markdown-field - :markdown-preview-url="markdownPreviewUrl" - :markdown-docs="markdownDocsUrl" - :quick-actions-docs="quickActionsDocsUrl" - :add-spacing-classes="false"> - <textarea - id="note-body" - name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area js-note-text" - data-supports-slash-commands="true" - data-supports-quick-actions="true" - aria-label="Description" - v-model="note" - ref="textarea" - slot="textarea" - placeholder="Write a comment or drag your files here..." - @keydown.up="editCurrentUserLastNote()" - @keydown.meta.enter="handleSave()"> - </textarea> - </markdown-field> - <div class="note-form-actions"> - <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> + <form> + <markdown-field + :markdown-preview-url="markdownPreviewUrl" + :markdown-docs="markdownDocsUrl" + :quick-actions-docs="quickActionsDocsUrl" + :add-spacing-classes="false"> + <textarea + id="note-body" + name="note[note]" + class="note-textarea js-gfm-input js-autosize markdown-area js-note-text" + data-supports-slash-commands="true" + data-supports-quick-actions="true" + aria-label="Description" + v-model="note" + ref="textarea" + slot="textarea" + placeholder="Write a comment or drag your files here..." + @keydown.up="editCurrentUserLastNote()" + @keydown.meta.enter="handleSave()"> + </textarea> + </markdown-field> + <div class="note-form-actions"> + <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> + <button + @click="handleSave()" + :disabled="!note.length" + class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" + type="button"> + {{commentButtonTitle}} + </button> + <button + :disabled="!note.length" + name="button" + type="button" + class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" + data-toggle="dropdown" + aria-label="Open comment type dropdown"> + <i + aria-hidden="true" + class="fa fa-caret-down toggle-icon"> + </i> + </button> + + <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> + <li :class="{ 'droplab-item-selected': noteType === 'comment' }"> + <button + type="button" + class="btn btn-transparent" + @click.prevent="setNoteType('comment')"> + <i + aria-hidden="true" + class="fa fa-check icon"> + </i> + <div class="description"> + <strong>Comment</strong> + <p> + Add a general comment to this issue. + </p> + </div> + </button> + </li> + <li class="divider droplab-item-ignore"></li> + <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> + <button + type="button" + class="btn btn-transparent" + @click.prevent="setNoteType('discussion')"> + <i + aria-hidden="true" + class="fa fa-check icon"> + </i> + <div class="description"> + <strong>Start discussion</strong> + <p> + Discuss a specific suggestion or question. + </p> + </div> + </button> + </li> + </ul> + </div> <button - @click="handleSave" - :disabled="!note.length" - class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" - type="button"> - {{commentButtonTitle}} + type="button" + @click="handleSave(true)" + v-if="canUpdateIssue" + :class="actionButtonClassNames" + class="btn btn-nr btn-comment btn-comment-and-close"> + {{issueActionButtonTitle}} </button> <button - :disabled="!note.length" - name="button" type="button" - class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" - data-toggle="dropdown" - aria-label="Open comment type dropdown"> - <i - aria-hidden="true" - class="fa fa-caret-down toggle-icon"> - </i> + v-if="note.length" + @click="discard" + class="btn btn-cancel js-note-discard"> + Discard draft </button> - - <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> - <li :class="{ 'droplab-item-selected': noteType === 'comment' }"> - <button - type="button" - class="btn btn-transparent" - @click.prevent="setNoteType('comment')"> - <i - aria-hidden="true" - class="fa fa-check icon"> - </i> - <div class="description"> - <strong>Comment</strong> - <p> - Add a general comment to this issue. - </p> - </div> - </button> - </li> - <li class="divider droplab-item-ignore"></li> - <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> - <button - type="button" - class="btn btn-transparent" - @click.prevent="setNoteType('discussion')"> - <i - aria-hidden="true" - class="fa fa-check icon"> - </i> - <div class="description"> - <strong>Start discussion</strong> - <p> - Discuss a specific suggestion or question. - </p> - </div> - </button> - </li> - </ul> </div> - <button - type="button" - @click="handleSave(true)" - v-if="canUpdateIssue" - :class="actionButtonClassNames" - class="btn btn-nr btn-comment btn-comment-and-close"> - {{issueActionButtonTitle}} - </button> - <button - type="button" - v-if="note.length" - @click="discard" - class="btn btn-cancel js-note-discard"> - Discard draft - </button> - </div> + </form> </div> </div> </li> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index cb37afd410a0..2953ea888a53 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -94,7 +94,6 @@ to ensure information is not lost. </div> <form - @submit="handleUpdate" class="edit-note common-note-form"> <markdown-field :markdown-preview-url="markdownPreviewUrl" @@ -103,7 +102,7 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area js-note-text" + class="note-textarea js-gfm-input js-autosize markdown-area" data-supports-slash-commands="true" data-supports-quick-actions="true" aria-label="Description" @@ -118,7 +117,8 @@ </markdown-field> <div class="note-form-actions clearfix"> <button - type="submit" + type="button" + @click="handleUpdate" class="btn btn-nr btn-save"> {{saveButtonTitle}} </button> diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 964ad5312b93..22653e561c32 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -48,6 +48,7 @@ computed: { ...mapGetters([ 'notes', + 'notesById', 'getNotesDataByProp', ]), }, @@ -55,7 +56,7 @@ ...mapActions({ actionFetchNotes: 'fetchNotes', poll: 'poll', - toggleAward: 'toggleAward', + actionToggleAward: 'toggleAward', scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', setNotesData: 'setNotesData', setIssueData: 'setIssueData', @@ -113,11 +114,11 @@ const { awardName, noteId } = data; const endpoint = this.notesById[noteId].toggle_award_path; - this.toggleAward({ endpoint, awardName, noteId }) - .catch(() => Flash('Something went wrong on our end.')); + this.actionToggleAward({ endpoint, awardName, noteId }) + .catch((error) => Flash('Something went wrong on our end.')); }); - //TODO: FILIPA: REMOVE JQUERY + // JQuery is needed here because it is a custom event being dispatched with jQuery. $(document).on('issuable:change', (e, isClosed) => { eventHub.$emit('issueStateChanged', isClosed); }); diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 5a234564f189..6ae4a7cb45eb 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,45 +1,48 @@ import Vue from 'vue'; import issueNotesApp from './components/issue_notes_app.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#js-vue-notes', - components: { - issueNotesApp, - }, - data() { - const notesDataset = document.getElementById('js-vue-notes').dataset; +document.addEventListener('DOMContentLoaded', () => { + const vm = new Vue({ + el: '#js-vue-notes', + components: { + issueNotesApp, + }, + data() { + const notesDataset = document.getElementById('js-vue-notes').dataset; - return { - issueData: JSON.parse(notesDataset.issueData), - currentUserData: JSON.parse(notesDataset.currentUserData), - notesData: { - lastFetchedAt: notesDataset.lastFetchedAt, - discussionsPath: notesDataset.discussionsPath, - newSessionPath: notesDataset.newSessionPath, - registerPath: notesDataset.registerPath, - notesPath: notesDataset.notesPath, - markdownDocs: notesDataset.markdownDocs, - quickActionsDocs: notesDataset.quickActionsDocs, - }, - }; - }, - render(createElement) { - return createElement('issue-notes-app', { - attrs: { - ref: 'notes', - }, - props: { - issueData: this.issueData, - notesData: this.notesData, - userData: this.currentUserData, - }, - }); - }, -})); + return { + issueData: JSON.parse(notesDataset.issueData), + currentUserData: JSON.parse(notesDataset.currentUserData), + notesData: { + lastFetchedAt: notesDataset.lastFetchedAt, + discussionsPath: notesDataset.discussionsPath, + newSessionPath: notesDataset.newSessionPath, + registerPath: notesDataset.registerPath, + notesPath: notesDataset.notesPath, + markdownDocs: notesDataset.markdownDocs, + quickActionsDocs: notesDataset.quickActionsDocs, + }, + }; + }, + render(createElement) { + return createElement('issue-notes-app', { + attrs: { + ref: 'notes', + }, + props: { + issueData: this.issueData, + notesData: this.notesData, + userData: this.currentUserData, + }, + }); + }, + }); + + // This is used in note_polling_spec + window.issueNotes = { + refresh() { + vm.$refs.notes.$store.dispatch('poll'); + }, + }; +}); - // // TODO: FILIPA: FIX THIS - // window.issueNotes = { - // refresh() { - // vm.$refs.notes.$store.dispatch('poll'); - // }, - // }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index e1e0aec8a831..d7059f462a43 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -131,32 +131,31 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }); }; -export const poll = ({ commit, state, getters }) => { - return service.poll(state.notesData.notesPath, state.lastFetchedAt) - .then(res => res.json()) - .then((res) => { - if (res.notes.length) { - const { notesById } = getters; - - res.notes.forEach((note) => { - if (notesById[note.id]) { - commit(types.UPDATE_NOTE, note); - } else if (note.type === constants.DISCUSSION_NOTE) { - const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); - - if (discussion) { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); - } else { - commit(types.ADD_NEW_NOTE, note); - } +export const poll = ({ commit, state, getters }) => service + .poll(state.notesData.notesPath, state.lastFetchedAt) + .then(res => res.json()) + .then((res) => { + if (res.notes.length) { + const { notesById } = getters; + + res.notes.forEach((note) => { + if (notesById[note.id]) { + commit(types.UPDATE_NOTE, note); + } else if (note.type === constants.DISCUSSION_NOTE) { + const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); + + if (discussion) { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); } else { commit(types.ADD_NEW_NOTE, note); } - }); - } - return res; - }); -}; + } else { + commit(types.ADD_NEW_NOTE, note); + } + }); + } + return res; + }); export const toggleAward = ({ commit, getters, dispatch }, data) => { const { endpoint, awardName, noteId, skipMutalityCheck } = data; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 8dc24cd745e8..fab2252eeb86 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -10,17 +10,10 @@ export const getIssueDataByProp = state => prop => state.issueData[prop]; export const getUserData = state => state.userData; export const getUserDataByProp = state => prop => state.notesData[prop]; -export const notesById = (state) => { - const notesByIdObject = {}; - // TODO: FILIPA: TRANSFORM INTO A REDUCE - state.notes.forEach((note) => { - note.notes.forEach((n) => { - notesByIdObject[n.id] = n; - }); - }); - - return notesByIdObject; -}; +export const notesById = state => state.notes.reduce((acc, note) => { + note.notes.every(n => Object.assign(acc, { [n.id]: n })); + return acc; +}, {}); const reverseNotes = array => array.slice(0).reverse(); const isLastNote = (note, userId) => !note.system && note.author.id === userId; @@ -31,6 +24,7 @@ export const getCurrentUserLastNote = state => userId => reverseNotes(state.note return acc; }, []).filter(el => el !== undefined)[0]; +// eslint-disable-next-line no-unused-vars export const getDiscussionLastNote = state => (discussion, userId) => reverseNotes(discussion.notes) .find(el => isLastNote(el, userId)); diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index be4f509932ff..8e0c8531bbcb 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -16,7 +16,6 @@ export default new Vuex.Store({ notesData: {}, userData: {}, issueData: {}, - paths: {}, }, actions, getters, diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 987cde6c38be..d31ec19b97db 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -9,7 +9,7 @@ new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), markdown_docs: help_page_path('user/markdown'), quick_actions_docs: help_page_path('user/project/quick_actions'), - notes_path: '#{notes_url}?full_data=1', + notes_path: "#{notes_url}?full_data=1", last_fetched_at: Time.now.to_i, issue_data: serialize_issuable(@issue), current_user_data: UserSerializer.new.represent(current_user).to_json }} -- GitLab From 9daae8cc31700ebb23319aef775257dbf689488a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 28 Jul 2017 20:44:58 +0100 Subject: [PATCH 121/243] [ci skip] Fiz zen mode css problem --- app/assets/stylesheets/framework/zen.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 0c226ff75980..735be8b95ccc 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -19,7 +19,7 @@ display: block; outline: none; resize: none; - height: 100vh; + height: 100vh !important; // reason being: https://github.com/vuejs/vue/issues/6246 max-height: calc(100vh - 10px); max-width: 900px; margin: 0 auto 10px; -- GitLab From b8957b0d8a839742868af94fee00b4b933768f6a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 29 Jul 2017 19:26:29 +0100 Subject: [PATCH 122/243] [ci skip] Adds quick actions link to input in discussion --- .../javascripts/notes/components/issue_comment_form.vue | 2 +- app/assets/javascripts/notes/components/issue_note_form.vue | 4 +++- app/assets/stylesheets/framework/zen.scss | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index bfe81d4dd421..bb7867a800cf 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -181,7 +181,7 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area js-note-text" + class="note-textarea js-gfm-input markdown-area" data-supports-slash-commands="true" data-supports-quick-actions="true" aria-label="Description" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 2953ea888a53..9fe8f2c73eba 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -32,6 +32,7 @@ note: this.noteBody, markdownPreviewUrl: getIssueData.preview_note_path, markdownDocsUrl: getNotesData.markdownDocs, + quickActionsDocsUrl: getNotesData.quickActionsDocs, conflictWhileEditing: false, }; }, @@ -98,7 +99,8 @@ <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" - :addSpacingClasses="false"> + :quick-actions-docs="quickActionsDocsUrl" + :add-spacing-classes="false"> <textarea id="note-body" name="note[note]" diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 735be8b95ccc..0c226ff75980 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -19,7 +19,7 @@ display: block; outline: none; resize: none; - height: 100vh !important; // reason being: https://github.com/vuejs/vue/issues/6246 + height: 100vh; max-height: calc(100vh - 10px); max-width: 900px; margin: 0 auto 10px; -- GitLab From cefc0af14a31464547f15840329078398b67972d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 29 Jul 2017 22:56:12 +0100 Subject: [PATCH 123/243] [ci skip] Fix emoji 100 bug: Creating strucutre to a forEach and then using Object.keys was causing a type inconsistency --- .../notes/components/issue_note_actions.vue | 4 +-- .../components/issue_note_awards_list.vue | 29 +++++++------------ .../javascripts/notes/stores/mutations.js | 4 +-- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index dd2c55073b43..7ac193bf5476 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -77,9 +77,7 @@ <div class="note-actions"> <span v-if="accessLevel" - class="note-role"> - {{accessLevel}} - </span> + class="note-role">{{accessLevel}}</span> <a v-tooltip v-if="canAddAwardEmoji" diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 479e3c8762aa..eaaa6cd16b14 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -51,32 +51,22 @@ // } // We need to do this otherwise we will render the same emoji over and over again. groupedAwards() { - const awards = {}; - const orderedAwards = {}; - - this.awards.forEach((award) => { - awards[award.name] = awards[award.name] || []; - awards[award.name].push(award); - }); + const awards = this.awards.reduce((acc, award) => { + Object.assign(acc, {[award.name]: [award]}); + return acc; + }, {}); + const orderedAwards = {}; // Always show thumbsup and thumbsdown first - const { thumbsup, thumbsdown } = awards; - if (thumbsup) { + if (awards.thumbsup) { orderedAwards.thumbsup = thumbsup; delete awards.thumbsup; } - if (thumbsdown) { + if (awards.thumbsdown) { orderedAwards.thumbsdown = thumbsdown; delete awards.thumbsdown; } - - // Because for-in forbidden - const keys = Object.keys(awards); - keys.forEach((key) => { - orderedAwards[key] = awards[key]; - }); - - return orderedAwards; + return Object.assign({}, orderedAwards, awards); }, isAuthoredByMe() { return this.noteAuthorId === window.gon.current_user_id; @@ -152,7 +142,8 @@ const data = { endpoint: this.toggleAwardPath, noteId: this.noteId, - awardName, + // 100 emoji is a number. Callback for v-for click sends it as a string + awardName: awardName === "100" ? 100: awardName, }; this.toggleAward(data) diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 15707643091a..15179a22b83a 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -101,7 +101,7 @@ export default { [types.TOGGLE_AWARD](state, data) { const { awardName, note } = data; - const { id, name, username } = window.gl.currentUserData; + const { id, name, username } = state.userData; let index = -1; note.award_emoji.forEach((a, i) => { @@ -110,7 +110,7 @@ export default { } }); - if (index > -1) { // if I am awarded, remove my award + if (index > -1) { // If current user has awarded this emoji, remove it. note.award_emoji.splice(index, 1); } else { note.award_emoji.push({ -- GitLab From 9ff88aa2834374a2c5f217b355f314cdfda0819b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 29 Jul 2017 23:42:30 +0100 Subject: [PATCH 124/243] [ci skip] Uses jquery to get correct note in awards --- app/assets/javascripts/awards_handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 0c884f409639..394e2fae5679 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -238,7 +238,7 @@ class AwardsHandler { const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length; if (this.isInIssuePage() && !isMainAwardsBlock) { - const id = votesBlock[0].id.replace('note_', ''); + const id = votesBlock.attr('id').replace('note_', ''); $('.emoji-menu').removeClass('is-visible'); $('.js-add-award.is-active').removeClass('is-active'); -- GitLab From c92881c394a482bdb6d3f0f0a5f3cc95d5eed1fc Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 30 Jul 2017 00:55:41 +0100 Subject: [PATCH 125/243] [ci skip] Fix broken awards --- .../javascripts/notes/components/issue_note_awards_list.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index eaaa6cd16b14..1c4bd6f35605 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -57,12 +57,13 @@ }, {}); const orderedAwards = {}; + const { thumbsdown, thumbsup } = awards; // Always show thumbsup and thumbsdown first - if (awards.thumbsup) { + if (thumbsup) { orderedAwards.thumbsup = thumbsup; delete awards.thumbsup; } - if (awards.thumbsdown) { + if (thumbsdown) { orderedAwards.thumbsdown = thumbsdown; delete awards.thumbsdown; } -- GitLab From 7a98970bd84b8f42bc117040cc284a73cbf03a8a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 1 Aug 2017 18:23:42 +0100 Subject: [PATCH 126/243] [ci skip] Removes Vue from awards handler code --- app/assets/javascripts/awards_handler.js | 9 +++++++-- app/assets/javascripts/dispatcher.js | 1 - .../javascripts/notes/components/issue_notes_app.vue | 4 ++-- app/views/projects/issues/_discussion.html.haml | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 394e2fae5679..ae0a742340df 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,7 +2,6 @@ /* global Flash */ import Cookies from 'js-cookie'; -import issueNotesEventHub from './notes/event_hub'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -242,8 +241,14 @@ class AwardsHandler { $('.emoji-menu').removeClass('is-visible'); $('.js-add-award.is-active').removeClass('is-active'); + const toggleAwardEvent = new CustomEvent('toggleAward', { + detail: { + awardName: emoji, + noteId: id, + }, + }); - return issueNotesEventHub.$emit('toggleAward', { awardName: emoji, noteId: id }); + document.querySelector('.js-vue-notes-event').dispatchEvent(toggleAwardEvent); } const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index a2664c0301e5..3a328a62558c 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -170,7 +170,6 @@ import GpgBadges from './gpg_badges'; shortcut_handler = new ShortcutsIssuable(); new ZenMode(); initIssuableSidebar(); - initNotes(); break; case 'dashboard:milestones:index': new ProjectSelect(); diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 22653e561c32..d84976ad1739 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -110,8 +110,8 @@ }, 15000); }, bindEventHubListeners() { - eventHub.$on('toggleAward', (data) => { - const { awardName, noteId } = data; + this.$el.parentElement.addEventListener('toggleAward', (event) => { + const { awardName, noteId } = event.detail; const endpoint = this.notesById[noteId].toggle_award_path; this.actionToggleAward({ endpoint, awardName, noteId }) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index d31ec19b97db..12ac93addd7e 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -3,7 +3,7 @@ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' -%section +%section.js-vue-notes-event #js-vue-notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), register_path: "#{new_session_path(:user, redirect_to_referer: 'yes')}#register-pane", new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), -- GitLab From 6d50b7529bcb1720aad34f9de90c2140b2744204 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 11:34:56 +0100 Subject: [PATCH 127/243] [ci skip] Enable etag polling --- app/controllers/concerns/notes_actions.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 3f86a067229e..0f90137ad3d1 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -3,6 +3,7 @@ module NotesActions extend ActiveSupport::Concern included do + before_action :set_polling_interval_header, only: [:index] before_action :authorize_admin_note!, only: [:update, :destroy] before_action :note_project, only: [:create] end @@ -175,6 +176,12 @@ def note_params ) end + def set_polling_interval_header + return unless noteable.is_a?(Issue) + + Gitlab::PollingInterval.set_header(response, interval: 3_000) + end + def noteable @noteable ||= notes_finder.target end -- GitLab From 60f6b596da4373c253c8387d5ebb555ea56d1759 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 11:45:52 +0100 Subject: [PATCH 128/243] [ci skip] Use eTag polling with Poll utility to allow stoping polling when visibily changes --- .../notes/components/issue_notes_app.vue | 18 +---- .../notes/services/issue_notes_service.js | 3 +- .../javascripts/notes/stores/actions.js | 70 +++++++++++++------ 3 files changed, 53 insertions(+), 38 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index d84976ad1739..557f0536a91a 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -88,26 +88,12 @@ this.checkLocationHash(); }); }) - .catch((error) => { - console.log(error) - Flash('Something went wrong while fetching issue comments. Please try again.') - }); + .catch((error) => Flash('Something went wrong while fetching issue comments. Please try again.')); }, initPolling() { this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); - // FIXME: @fatihacet Implement real polling mechanism - // TODO: FILIPA: DEAL WITH THIS - setInterval(() => { - this.poll() - .then((res) => { - this.setLastFetchedAt(res.lastFetchedAt); - }) - .catch((error) =>{ - console.log(error) - Flash('Something went wrong while fetching latest comments.') - } ); - }, 15000); + this.poll(); }, bindEventHubListeners() { this.$el.parentElement.addEventListener('toggleAward', (event) => { diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js index c80e23f02cb0..b51b0cb20139 100644 --- a/app/assets/javascripts/notes/services/issue_notes_service.js +++ b/app/assets/javascripts/notes/services/issue_notes_service.js @@ -19,7 +19,8 @@ export default { createNewNote(endpoint, data) { return Vue.http.post(endpoint, data, { emulateJSON: true }); }, - poll(endpoint, lastFetchedAt) { + poll(data = {}) { + const { endpoint, lastFetchedAt } = data; const options = { headers: { 'X-Last-Fetched-At': lastFetchedAt, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index d7059f462a43..f0d176e858c6 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -1,5 +1,6 @@ /* global Flash */ - +import Visibility from 'visibilityjs'; +import Poll from '../../lib/utils/poll'; import * as types from './mutation_types'; import * as utils from './utils'; import * as constants from '../constants'; @@ -131,31 +132,58 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }); }; -export const poll = ({ commit, state, getters }) => service - .poll(state.notesData.notesPath, state.lastFetchedAt) - .then(res => res.json()) - .then((res) => { - if (res.notes.length) { - const { notesById } = getters; - - res.notes.forEach((note) => { - if (notesById[note.id]) { - commit(types.UPDATE_NOTE, note); - } else if (note.type === constants.DISCUSSION_NOTE) { - const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); - - if (discussion) { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); - } else { - commit(types.ADD_NEW_NOTE, note); - } +const pollSuccessCallBack = (resp, commit, state, getters) => { + if (resp.notes.length) { + const { notesById } = getters; + + resp.notes.forEach((note) => { + if (notesById[note.id]) { + commit(types.UPDATE_NOTE, note); + } else if (note.type === constants.DISCUSSION_NOTE) { + const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); + + if (discussion) { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); } else { commit(types.ADD_NEW_NOTE, note); } - }); + } else { + commit(types.ADD_NEW_NOTE, note); + } + }); + } + + commit(types.SET_LAST_FETCHED_AT, resp.lastFetchedAt); + + return resp; +}; + +export const poll = ({ commit, state, getters }) => { + const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; + + const eTagPoll = new Poll({ + resource: service, + method: 'poll', + data: requestData, + successCallback: resp => resp.json() + .then(data => pollSuccessCallBack(data, commit, state, getters)), + errorCallback: () => Flash('Something went wrong while fetching latest comments.'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } else { + this.service.poll(requestData); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + eTagPoll.restart(); + } else { + eTagPoll.stop(); } - return res; }); +}; export const toggleAward = ({ commit, getters, dispatch }, data) => { const { endpoint, awardName, noteId, skipMutalityCheck } = data; -- GitLab From 91481002ac3da567224f30116fe35d3d2e14df04 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 11:57:28 +0100 Subject: [PATCH 129/243] [ci skip] Fix broken discussion reply --- app/assets/javascripts/notes/components/issue_discussion.vue | 4 ++-- app/assets/javascripts/notes/components/issue_note_form.vue | 2 +- app/assets/javascripts/notes/components/issue_notes_app.vue | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 087d80e3f5f5..c02a217cc9f9 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -86,7 +86,7 @@ this.isReplying = false; }, - saveReply({ note }) { + saveReply(note) { const replyData = { endpoint: this.newNotePath, flashContainer: this.$el, @@ -94,7 +94,7 @@ in_reply_to_discussion_id: this.note.reply_id, target_type: 'issue', target_id: this.discussion.noteable_id, - note: { note }, + note: { note: note }, full_data: true, }, }; diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 9fe8f2c73eba..9f5c174daf67 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -49,7 +49,7 @@ }, methods: { handleUpdate() { - this.$emit('handleFormUpdate', note); + this.$emit('handleFormUpdate', this.note); }, editMyLastNote() { if (this.note === '') { diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 557f0536a91a..ccda0992f2b4 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -61,7 +61,8 @@ setNotesData: 'setNotesData', setIssueData: 'setIssueData', setUserData: 'setUserData', - setLastFetchedAt: 'setLastFetchedAt' + setLastFetchedAt: 'setLastFetchedAt', + setTargetNoteHash: 'setTargetNoteHash', }), getComponentName(note) { if (note.isPlaceholderNote) { @@ -84,7 +85,7 @@ this.isLoading = false; // Scroll to note if we have hash fragment in the page URL - Vue.nextTick(() => { + this.$nextTick(() => { this.checkLocationHash(); }); }) -- GitLab From d8ebcb747acdc80a6f6cffe5b28c58044f40341e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 12:25:14 +0100 Subject: [PATCH 130/243] [ci skip] Fix toggle discussion --- .../javascripts/notes/components/issue_discussion.vue | 11 ++++------- .../javascripts/notes/components/issue_note.vue | 2 +- app/assets/javascripts/notes/stores/actions.js | 1 + 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index c02a217cc9f9..7a3b136f5ba0 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,7 +1,6 @@ <script> /* global Flash */ - import { mapActions, mapMutations } from 'vuex'; - import { TOGGLE_DISCUSSION } from '../stores/mutation_types'; + import { mapActions } from 'vuex'; import { SYSTEM_NOTE } from '../constants'; import issueNote from './issue_note.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -51,10 +50,8 @@ methods: { ...mapActions([ 'saveNote', + 'toggleDiscussion' ]), - ...mapMutations({ - toggleDiscussion: TOGGLE_DISCUSSION, - }), componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === SYSTEM_NOTE) { @@ -68,7 +65,7 @@ componentData(note) { return note.isPlaceholderNote ? note.notes[0] : note; }, - toggleDiscussion() { + toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.note.id }); }, showReplyForm() { @@ -128,7 +125,7 @@ :created-at="discussion.created_at" :note-id="discussion.id" :include-toggle="true" - :toggle-handler="toggleDiscussion" + :toggle-handler="toggleDiscussionHandler" action-text="started a discussion" /> <issue-note-edited-text diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index efe2bf73c40e..36e5609ab0ce 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -82,7 +82,7 @@ full_data: true, target_type: 'issue', target_id: this.note.noteable_id, - note, + note: { note: note }, }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index f0d176e858c6..6d60c46d49bc 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -14,6 +14,7 @@ export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, dat export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITAL_NOTES, data); export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); +export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const fetchNotes = ({ commit }, path) => service .fetchNotes(path) -- GitLab From 7b093581826612f3627ff75b74056b7294988453 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 15:39:47 +0100 Subject: [PATCH 131/243] [ci skip] Fix emoji being posted twice originating an error --- .../notes/components/issue_note.vue | 1 + .../components/issue_note_awards_list.vue | 12 +++++-- .../notes/components/issue_notes_app.vue | 4 +-- .../javascripts/notes/stores/actions.js | 35 ++++--------------- .../javascripts/notes/stores/mutations.js | 13 +++---- 5 files changed, 23 insertions(+), 42 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 36e5609ab0ce..8b1f9dbca015 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -119,6 +119,7 @@ class="note timeline-entry" :id="noteAnchorId" :class="classNameBindings" + :data-award-url="note.toggle_award_path" :note-id="note.id"> <div class="timeline-entry-inner"> <div class="timeline-icon"> diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 1c4bd6f35605..2dd27d650965 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -52,7 +52,12 @@ // We need to do this otherwise we will render the same emoji over and over again. groupedAwards() { const awards = this.awards.reduce((acc, award) => { - Object.assign(acc, {[award.name]: [award]}); + if (acc.hasOwnProperty(award.name)) { + acc[award.name].push(award); + } else { + Object.assign(acc, {[award.name]: [award]}); + } + return acc; }, {}); @@ -67,6 +72,7 @@ orderedAwards.thumbsdown = thumbsdown; delete awards.thumbsdown; } + return Object.assign({}, orderedAwards, awards); }, isAuthoredByMe() { @@ -75,7 +81,7 @@ }, methods: { ...mapActions([ - 'toggleAward', + 'toggleAwardRequest', ]), getAwardHTML(name) { return Emoji.glEmojiTag(name); @@ -147,7 +153,7 @@ awardName: awardName === "100" ? 100: awardName, }; - this.toggleAward(data) + this.toggleAwardRequest(data) .catch(() => Flash('Something went wrong on our end.')); }, }, diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index ccda0992f2b4..734970d3bfc8 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -99,10 +99,8 @@ bindEventHubListeners() { this.$el.parentElement.addEventListener('toggleAward', (event) => { const { awardName, noteId } = event.detail; - const endpoint = this.notesById[noteId].toggle_award_path; + this.actionToggleAward({ awardName, noteId }) - this.actionToggleAward({ endpoint, awardName, noteId }) - .catch((error) => Flash('Something went wrong on our end.')); }); // JQuery is needed here because it is a custom event being dispatched with jQuery. diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 6d60c46d49bc..0c9eb0462986 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -174,7 +174,7 @@ export const poll = ({ commit, state, getters }) => { if (!Visibility.hidden()) { eTagPoll.makeRequest(); } else { - this.service.poll(requestData); + service.poll(requestData); } Visibility.change(() => { @@ -186,38 +186,17 @@ export const poll = ({ commit, state, getters }) => { }); }; -export const toggleAward = ({ commit, getters, dispatch }, data) => { - const { endpoint, awardName, noteId, skipMutalityCheck } = data; - const note = getters.notesById[noteId]; +export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => { + commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); +}; +export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { + const { endpoint, awardName } = data; return service .toggleAward(endpoint, { name: awardName }) .then(res => res.json()) .then(() => { - commit(types.TOGGLE_AWARD, { awardName, note }); - - if (!skipMutalityCheck && - (awardName === constants.EMOJI_THUMBSUP || awardName === constants.EMOJI_THUMBSDOWN)) { - const counterAward = awardName === constants.EMOJI_THUMBSUP ? - constants.EMOJI_THUMBSDOWN : - constants.EMOJI_THUMBSUP; - - const targetNote = getters.notesById[noteId]; - let noteHasAwardByCurrentUser = false; - - targetNote.award_emoji.forEach((a) => { - if (a.name === counterAward && a.user.id === window.gon.current_user_id) { - noteHasAwardByCurrentUser = true; - } - }); - - if (noteHasAwardByCurrentUser) { - Object.assign(data, { awardName: counterAward }); - Object.assign(data, { skipMutalityCheck: true }); - - dispatch(types.TOGGLE_AWARD, data); - } - } + dispatch('toggleAward', data); }); }; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 15179a22b83a..f77f19c9b3c7 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -102,16 +102,13 @@ export default { [types.TOGGLE_AWARD](state, data) { const { awardName, note } = data; const { id, name, username } = state.userData; - let index = -1; - note.award_emoji.forEach((a, i) => { - if (a.name === awardName && a.user.id === id) { - index = i; - } - }); + const hasEmojiAwardedByCurrentUser = note.award_emoji + .filter(emoji => emoji.name === data.awardName && emoji.user.id === id); - if (index > -1) { // If current user has awarded this emoji, remove it. - note.award_emoji.splice(index, 1); + if (hasEmojiAwardedByCurrentUser.length) { + // If current user has awarded this emoji, remove it. + note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1); } else { note.award_emoji.push({ name: awardName, -- GitLab From 923a0623022210a5ac003375c56696fb095f5b81 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 16:22:38 +0100 Subject: [PATCH 132/243] [ci skip] Disable slash commands in edit mode --- .../notes/components/issue_comment_form.vue | 11 +++---- .../notes/components/issue_discussion.vue | 7 +++-- .../notes/components/issue_note_actions.vue | 10 ++++-- .../notes/components/issue_note_body.vue | 1 + .../notes/components/issue_note_form.vue | 31 +++++++++++++------ .../notes/components/issue_notes_app.vue | 5 ++- .../javascripts/notes/stores/getters.js | 2 +- .../javascripts/notes/stores/mutations.js | 14 ++++----- 8 files changed, 50 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index bb7867a800cf..54c1eb012b70 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -30,7 +30,7 @@ issueNoteSignedOutWidget, }, computed: { - ...mapGetters([ + ...mapGetters([ 'getCurrentUserLastNote', ]), isLoggedIn() { @@ -62,7 +62,7 @@ }, methods: { ...mapActions([ - 'saveNote' + 'saveNote', ]), handleSave(withIssueAction) { if (this.note.length) { @@ -89,7 +89,7 @@ if (res.errors.commands_only) { this.discard(); } else { - return Flash('Something went wrong while adding your comment. Please try again.'); + Flash('Something went wrong while adding your comment. Please try again.'); } } else { this.discard(); @@ -104,7 +104,7 @@ if (this.isIssueOpen) { this.issueState = constants.CLOSED; } else { - this.issueState =constants.REOPENED; + this.issueState = constants.REOPENED; } gl.issueData.state = this.issueState; @@ -149,7 +149,7 @@ destroyed() { eventHub.$off('issueStateChanged'); - } + }, }; </script> @@ -182,7 +182,6 @@ id="note-body" name="note[note]" class="note-textarea js-gfm-input markdown-area" - data-supports-slash-commands="true" data-supports-quick-actions="true" aria-label="Description" v-model="note" diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 7a3b136f5ba0..204af4f0b092 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -50,7 +50,7 @@ methods: { ...mapActions([ 'saveNote', - 'toggleDiscussion' + 'toggleDiscussion', ]), componentName(note) { if (note.isPlaceholderNote) { @@ -83,7 +83,7 @@ this.isReplying = false; }, - saveReply(note) { + saveReply(noteText) { const replyData = { endpoint: this.newNotePath, flashContainer: this.$el, @@ -91,7 +91,7 @@ in_reply_to_discussion_id: this.note.reply_id, target_type: 'issue', target_id: this.discussion.noteable_id, - note: { note: note }, + note: { note: noteText }, full_data: true, }, }; @@ -162,6 +162,7 @@ v-if="isReplying" saveButtonTitle="Comment" :discussion="note" + :is-editing="false" @handleFormUpdate="saveReply" @cancelFormEdition="cancelReplyForm" ref="noteForm" diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 7ac193bf5476..25eec8e45cd5 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -1,4 +1,5 @@ <script> + import { mapGetters } from 'vuex'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; @@ -53,13 +54,15 @@ emojiSmiling, emojiSmile, emojiSmiley, - currentUserId: window.gon.current_user_id, }; }, components: { loadingIcon, }, computed: { + ...mapGetters([ + 'getUserDataByProp', + ]), shouldShowActionsDropdown() { return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, @@ -67,8 +70,11 @@ return this.currentUserId; }, isAuthoredByCurrentUser() { - return this.authorId === this.currentUserId + return this.authorId === this.currentUserId; }, + currentUserId() { + return this.getUserDataByProp('id'); + } }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index fb663b59a8d1..a44ad1d822dc 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -73,6 +73,7 @@ ref="noteForm" @handleFormUpdate="handleFormUpdate" @cancelFormEdition="formCancelHandler" + :is-editing="isEditing" :note-body="noteBody" :note-id="note.id" /> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 9f5c174daf67..6c39b7397e7b 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -22,17 +22,16 @@ discussion: { type: Object, required: false, - } + }, + isEditing: { + type: Boolean, + required: true, + }, }, data() { - const { getIssueData, getNotesData } = this.$store.getters; - return { initialNote: this.noteBody, note: this.noteBody, - markdownPreviewUrl: getIssueData.preview_note_path, - markdownDocsUrl: getNotesData.markdownDocs, - quickActionsDocsUrl: getNotesData.quickActionsDocs, conflictWhileEditing: false, }; }, @@ -42,10 +41,25 @@ computed: { ...mapGetters([ 'getDiscussionLastNote', + 'getIssueDataByProp', + 'getNotesDataByProp', + 'getUserDataByProp', ]), noteHash() { return `#note_${this.noteId}`; }, + markdownPreviewUrl() { + return this.getIssueDataByProp('preview_note_path'); + }, + markdownDocsUrl() { + return this.getNotesDataByProp('markdownDocs'); + }, + quickActionsDocsUrl() { + return this.getNotesDataByProp('quickActionsDocs'); + }, + currentUserId() { + return this.getUserDataByProp('id'); + } }, methods: { handleUpdate() { @@ -53,7 +67,7 @@ }, editMyLastNote() { if (this.note === '') { - const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion, window.gon.current_user_id); + const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion, this.currentUserId); if (lastNoteInDiscussion) { eventHub.$emit('enterEditMode', { @@ -105,8 +119,7 @@ id="note-body" name="note[note]" class="note-textarea js-gfm-input js-autosize markdown-area" - data-supports-slash-commands="true" - data-supports-quick-actions="true" + :data-supports-quick-actions="!isEditing" aria-label="Description" v-model="note" ref="textarea" diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 734970d3bfc8..f39ff4cc55d7 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -1,10 +1,9 @@ <script> /* global Flash */ - import Vue from 'vue'; - import { mapGetters, mapActions, mapMutations } from 'vuex'; + import { mapGetters, mapActions } from 'vuex'; import store from '../stores/'; - import * as constants from '../constants' + import * as constants from '../constants'; import eventHub from '../event_hub'; import issueNote from './issue_note.vue'; import issueDiscussion from './issue_discussion.vue'; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index fab2252eeb86..0627da2f3d81 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -8,7 +8,7 @@ export const getIssueData = state => state.issueData; export const getIssueDataByProp = state => prop => state.issueData[prop]; export const getUserData = state => state.userData; -export const getUserDataByProp = state => prop => state.notesData[prop]; +export const getUserDataByProp = state => prop => state.userData[prop]; export const notesById = state => state.notes.reduce((acc, note) => { note.notes.every(n => Object.assign(acc, { [n.id]: n })); diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index f77f19c9b3c7..f38a4ccfbb35 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -59,26 +59,26 @@ export default { }, [types.SET_NOTES_DATA](state, data) { - state.notesData = data; + Object.assign(state, { notesData: data }); }, [types.SET_ISSUE_DATA](state, data) { - state.issueData = data; + Object.assign(state, { issueData: data }); }, [types.SET_USER_DATA](state, data) { - state.userData = data; + Object.assign(state, { userData: data }); }, - [types.SET_INITAL_NOTES](state, notes) { - state.notes = notes; + [types.SET_INITAL_NOTES](state, notesData) { + Object.assign(state, { notes: notesData }); }, [types.SET_LAST_FETCHED_AT](state, fetchedAt) { - state.lastFetchedAt = fetchedAt; + Object.assign(state, { lastFetchedAt: fetchedAt }); }, [types.SET_TARGET_NOTE_HASH](state, hash) { - state.targetNoteHash = hash; + Object.assign(state, { targetNoteHash: hash }); }, [types.SHOW_PLACEHOLDER_NOTE](state, data) { -- GitLab From 23b83d5f5a01fcff9c298c7d920993dd63a98a97 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 16:44:01 +0100 Subject: [PATCH 133/243] [ci skip] Disable main submit button while submiting --- .../notes/components/issue_comment_form.vue | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 54c1eb012b70..49e65e6457ab 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -22,6 +22,7 @@ endpoint: getIssueData.create_note_path, author: getUserData, canUpdateIssue: getIssueData.current_user.can_update, + isSubmitting: false, }; }, components: { @@ -59,6 +60,9 @@ 'js-note-target-reopen': !this.isIssueOpen, }; }, + canSubmit() { + return !this.note.length || this.isSubmitting; + } }, methods: { ...mapActions([ @@ -83,8 +87,11 @@ noteData.data.note.type = constants.DISCUSSION_NOTE; } + this.isSubmitting = true; + this.saveNote(noteData) .then((res) => { + this.isSubmitting = false; if (res.errors) { if (res.errors.commands_only) { this.discard(); @@ -96,6 +103,7 @@ } }) .catch(() => { + this.isSubmitting = false; this.discard(false); }); } @@ -196,13 +204,13 @@ <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button @click="handleSave()" - :disabled="!note.length" + :disabled="canSubmit" class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" type="button"> {{commentButtonTitle}} </button> <button - :disabled="!note.length" + :disabled="canSubmit" name="button" type="button" class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" -- GitLab From 85f2d7d428d9c1e3ad929386a7a05f764589600b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 16:52:24 +0100 Subject: [PATCH 134/243] [ci skip] Disable submit button when submitting a note or if the note is empty while editing or adding a reply to a discussion --- .../javascripts/notes/components/issue_note_form.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 6c39b7397e7b..963d6ecbeb47 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -33,6 +33,7 @@ initialNote: this.noteBody, note: this.noteBody, conflictWhileEditing: false, + isSubmitting: false, }; }, components: { @@ -59,10 +60,14 @@ }, currentUserId() { return this.getUserDataByProp('id'); - } + }, + isDisabled() { + return !this.note.length || this.isSubmitting; + }, }, methods: { handleUpdate() { + this.isSubmitting = true; this.$emit('handleFormUpdate', this.note); }, editMyLastNote() { @@ -79,7 +84,7 @@ cancelHandler(shouldConfirm = false) { // Sends information about confirm message and if the textarea has changed this.$emit('cancelFormEdition', shouldConfirm, this.initialNote !== this.note); - } + }, }, mounted() { this.$refs.textarea.focus(); @@ -133,7 +138,8 @@ <div class="note-form-actions clearfix"> <button type="button" - @click="handleUpdate" + @click="handleUpdate" + :disabled="isDisabled" class="btn btn-nr btn-save"> {{saveButtonTitle}} </button> -- GitLab From 20f2987aa16a4cbc8cb1dd0a43d54f1e7a77aec4 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 17:19:06 +0100 Subject: [PATCH 135/243] [ci skip] Fix broken links for signin and register path --- .../notes/components/issue_comment_form.vue | 3 ++- .../notes/components/issue_discussion.vue | 11 +++++++--- .../issue_note_signed_out_widget.vue | 20 ++++++++++++------- .../notes/components/issue_notes_app.vue | 3 ++- .../javascripts/notes/stores/getters.js | 2 +- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 49e65e6457ab..daec33e72a47 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -33,9 +33,10 @@ computed: { ...mapGetters([ 'getCurrentUserLastNote', + 'getUserData', ]), isLoggedIn() { - return window.gon.current_user_id; + return this.getUserData === null ? false : true; }, commentButtonTitle() { return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 204af4f0b092..4de4ab714643 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,6 +1,6 @@ <script> /* global Flash */ - import { mapActions } from 'vuex'; + import { mapActions, mapGetters } from 'vuex'; import { SYSTEM_NOTE } from '../constants'; import issueNote from './issue_note.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -21,7 +21,6 @@ }, data() { return { - newNotePath: window.gl.issueData.create_note_path, isReplying: false, }; }, @@ -37,6 +36,9 @@ placeholderSystemNote, }, computed: { + ...mapGetters([ + 'getIssueData', + ]), discussion() { return this.note.notes[0]; }, @@ -44,8 +46,11 @@ return this.discussion.author; }, canReply() { - return window.gl.issueData.current_user.can_create_note; + return this.getIssueData.current_user.can_create_note; }, + newNotePath() { + return this.getIssueData.create_note_path; + } }, methods: { ...mapActions([ diff --git a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue index 6d551225b29b..3f1e23d6e361 100644 --- a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue @@ -1,14 +1,20 @@ <script> + import { mapGetters } from 'vuex'; + export default { name: 'singInLinksNotes', - data() { - const { newSessionPath, registerPath } = this.$store.getters.notesData; + computed: { + ...mapGetters([ + 'getNotesDataByProp' + ]), + registerLink() { + return this.getNotesDataByProp('registerPath') - return { - signInLink: newSessionPath, - registerLink: registerPath, - }; - }, + }, + signInLink(){ + return this.getNotesDataByProp('newSessionPath'); + } + } }; </script> diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index f39ff4cc55d7..d3df0ec8c7f3 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -26,7 +26,8 @@ }, userData: { type: Object, - required: true, + required: false, + default: {} }, }, store, diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 0627da2f3d81..386e5e2050d8 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -8,7 +8,7 @@ export const getIssueData = state => state.issueData; export const getIssueDataByProp = state => prop => state.issueData[prop]; export const getUserData = state => state.userData; -export const getUserDataByProp = state => prop => state.userData[prop]; +export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const notesById = state => state.notes.reduce((acc, note) => { note.notes.every(n => Object.assign(acc, { [n.id]: n })); -- GitLab From b55ad844e8d9b92961c712fa0e1d93d1eecabeae Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 2 Aug 2017 17:37:22 +0100 Subject: [PATCH 136/243] [ci skip] Remove all global data --- .../javascripts/notes/components/issue_comment_form.vue | 4 ++-- app/views/projects/issues/_discussion.html.haml | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index daec33e72a47..407954f3272e 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -34,6 +34,7 @@ ...mapGetters([ 'getCurrentUserLastNote', 'getUserData', + 'getIssueData', ]), isLoggedIn() { return this.getUserData === null ? false : true; @@ -78,7 +79,7 @@ full_data: true, note: { noteable_type: 'Issue', - noteable_id: window.gl.issueData.id, + noteable_id: this.getIssueData.id, note: this.note, }, }, @@ -116,7 +117,6 @@ this.issueState = constants.REOPENED; } - gl.issueData.state = this.issueState; this.isIssueOpen = !this.isIssueOpen; // This is out of scope for the Notes Vue component. diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 12ac93addd7e..8e17895c1c72 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -18,7 +18,3 @@ = webpack_bundle_tag 'notes' = render "layouts/init_auto_complete" - -:javascript - window.gl.issueData = #{serialize_issuable(@issue)}; - window.gl.currentUserData = #{UserSerializer.new.represent(current_user).to_json}; -- GitLab From 0d78eeb2a834d2f74efaaaebe4a80c3c38339e48 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 3 Aug 2017 12:15:54 +0100 Subject: [PATCH 137/243] [ci skip] Fix eslint errors --- .../notes/components/issue_comment_form.vue | 4 ++-- .../javascripts/notes/components/issue_discussion.vue | 2 +- .../javascripts/notes/components/issue_note.vue | 4 ++-- .../notes/components/issue_note_actions.vue | 2 +- .../notes/components/issue_note_awards_list.vue | 8 ++++---- .../javascripts/notes/components/issue_note_body.vue | 2 +- .../javascripts/notes/components/issue_note_form.vue | 5 ++++- .../notes/components/issue_note_header.vue | 3 +-- .../notes/components/issue_note_signed_out_widget.vue | 11 +++++------ .../javascripts/notes/components/issue_notes_app.vue | 9 ++++----- .../notes/components/issue_placeholder_note_spec.js | 11 ----------- 11 files changed, 25 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 407954f3272e..4bfea0990234 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -37,7 +37,7 @@ 'getIssueData', ]), isLoggedIn() { - return this.getUserData === null ? false : true; + return this.getUserData !== null; }, commentButtonTitle() { return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; @@ -64,7 +64,7 @@ }, canSubmit() { return !this.note.length || this.isSubmitting; - } + }, }, methods: { ...mapActions([ diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 4de4ab714643..d4754178933b 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -50,7 +50,7 @@ }, newNotePath() { return this.getIssueData.create_note_path; - } + }, }, methods: { ...mapActions([ diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 8b1f9dbca015..7a3d5c4e0c83 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -75,14 +75,14 @@ }); } }, - formUpdateHandler(note) { + formUpdateHandler(noteText) { const data = { endpoint: this.note.path, note: { full_data: true, target_type: 'issue', target_id: this.note.noteable_id, - note: { note: note }, + note: { note: noteText }, }, }; diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 25eec8e45cd5..f24abfb6a9d3 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -74,7 +74,7 @@ }, currentUserId() { return this.getUserDataByProp('id'); - } + }, }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 2dd27d650965..936be4523c86 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -52,10 +52,10 @@ // We need to do this otherwise we will render the same emoji over and over again. groupedAwards() { const awards = this.awards.reduce((acc, award) => { - if (acc.hasOwnProperty(award.name)) { + if (Object.prototype.hasOwnProperty.call(acc, award.name)) { acc[award.name].push(award); } else { - Object.assign(acc, {[award.name]: [award]}); + Object.assign(acc, { [award.name]: [award] }); } return acc; @@ -73,7 +73,7 @@ delete awards.thumbsdown; } - return Object.assign({}, orderedAwards, awards); + return Object.assign({}, orderedAwards, awards); }, isAuthoredByMe() { return this.noteAuthorId === window.gon.current_user_id; @@ -150,7 +150,7 @@ endpoint: this.toggleAwardPath, noteId: this.noteId, // 100 emoji is a number. Callback for v-for click sends it as a string - awardName: awardName === "100" ? 100: awardName, + awardName: awardName === '100' ? 100 : awardName, }; this.toggleAwardRequest(data) diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index a44ad1d822dc..087403e51a11 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -48,7 +48,7 @@ }, formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelFormEdition', shouldConfirm, isDirty); - } + }, }, mounted() { this.renderGFM(); diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 963d6ecbeb47..14d8742a4b91 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -72,7 +72,10 @@ }, editMyLastNote() { if (this.note === '') { - const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion, this.currentUserId); + const lastNoteInDiscussion = this.getDiscussionLastNote( + this.discussion, + this.currentUserId, + ); if (lastNoteInDiscussion) { eventHub.$emit('enterEditMode', { diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 17f3fe3b0003..a2a6b1400132 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -1,7 +1,6 @@ <script> import { mapActions } from 'vuex'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; - import * as types from '../stores/mutation_types'; export default { props: { @@ -55,7 +54,7 @@ }, methods: { ...mapActions([ - 'setTargetNoteHash' + 'setTargetNoteHash', ]), handleToggle() { this.isExpanded = !this.isExpanded; diff --git a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue index 3f1e23d6e361..77af3594c1c7 100644 --- a/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue @@ -5,16 +5,15 @@ name: 'singInLinksNotes', computed: { ...mapGetters([ - 'getNotesDataByProp' + 'getNotesDataByProp', ]), registerLink() { - return this.getNotesDataByProp('registerPath') - + return this.getNotesDataByProp('registerPath'); }, - signInLink(){ + signInLink() { return this.getNotesDataByProp('newSessionPath'); - } - } + }, + }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index d3df0ec8c7f3..14b7afe12870 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -27,7 +27,7 @@ userData: { type: Object, required: false, - default: {} + default: {}, }, }, store, @@ -89,7 +89,7 @@ this.checkLocationHash(); }); }) - .catch((error) => Flash('Something went wrong while fetching issue comments. Please try again.')); + .catch(() => Flash('Something went wrong while fetching issue comments. Please try again.')); }, initPolling() { this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); @@ -99,8 +99,7 @@ bindEventHubListeners() { this.$el.parentElement.addEventListener('toggleAward', (event) => { const { awardName, noteId } = event.detail; - this.actionToggleAward({ awardName, noteId }) - + this.actionToggleAward({ awardName, noteId }); }); // JQuery is needed here because it is a custom event being dispatched with jQuery. @@ -121,7 +120,7 @@ created() { this.setNotesData(this.notesData); this.setIssueData(this.issueData); - this.setUserData(this.userData) + this.setUserData(this.userData); }, mounted() { this.fetchNotes(); diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js index f4d8e01bfe60..64d4ed42dfa7 100644 --- a/spec/javascripts/notes/components/issue_placeholder_note_spec.js +++ b/spec/javascripts/notes/components/issue_placeholder_note_spec.js @@ -1,16 +1,5 @@ -import Vue from 'vue'; -import placeholderNote from '~/notes/components/issue_placeholder_note.vue'; - describe('issue placeholder system note component', () => { - let mountComponent; beforeEach(() => { - const PlaceholderNote = Vue.extend(placeholderNote); - - mountComponent = props => new PlaceholderNote({ - propsData: { - note: props, - }, - }).$mount(); }); describe('user information', () => { -- GitLab From fe81e7b7542bdbede80c8497d6fcaa7ac43ce21e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 3 Aug 2017 14:56:01 +0100 Subject: [PATCH 138/243] [ci skip] Show errors close to the textarea --- .../javascripts/notes/components/issue_comment_form.vue | 8 ++++++-- app/assets/javascripts/notes/components/issue_note.vue | 8 ++++++-- .../javascripts/notes/components/issue_note_body.vue | 4 ++-- .../javascripts/notes/components/issue_note_form.vue | 6 ++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 4bfea0990234..b7b7140d7727 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -98,7 +98,11 @@ if (res.errors.commands_only) { this.discard(); } else { - Flash('Something went wrong while adding your comment. Please try again.'); + Flash( + 'Something went wrong while adding your comment. Please try again.', + 'alert', + $(this.$refs.commentForm), + ); } } else { this.discard(); @@ -168,7 +172,7 @@ <ul v-if="isLoggedIn" class="notes notes-form timeline new-note"> - <li class="timeline-entry"> + <li class="timeline-entry" ref="commentForm"> <div class="timeline-entry-inner"> <div class="flash-container timeline-content"></div> <div class="timeline-icon hidden-xs hidden-sm"> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 7a3d5c4e0c83..e65757850351 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -75,7 +75,7 @@ }); } }, - formUpdateHandler(noteText) { + formUpdateHandler(noteText, parentElement) { const data = { endpoint: this.note.path, note: { @@ -92,7 +92,11 @@ // TODO: this could be moved down, by setting a prop $(this.$refs.noteBody.$el).renderGFM(); }) - .catch(() => Flash('Something went wrong while editing your comment. Please try again.')); + .catch(() => Flash( + 'Something went wrong while editing your comment. Please try again.', + 'alert', + $(parentElement), + )); }, formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 087403e51a11..29ac68598402 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -43,8 +43,8 @@ }); } }, - handleFormUpdate(note) { - this.$emit('handleFormUpdate', note); + handleFormUpdate(note, parentElement) { + this.$emit('handleFormUpdate', note, parentElement); }, formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelFormEdition', shouldConfirm, isDirty); diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 14d8742a4b91..4d68f52b5055 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -68,7 +68,7 @@ methods: { handleUpdate() { this.isSubmitting = true; - this.$emit('handleFormUpdate', this.note); + this.$emit('handleFormUpdate', this.note, this.$refs.editNoteForm); }, editMyLastNote() { if (this.note === '') { @@ -94,6 +94,7 @@ }, watch: { noteBody() { + debugger; if (this.note === this.initialNote) { this.note = this.noteBody; } else { @@ -105,7 +106,7 @@ </script> <template> - <div class="note-edit-form current-note-edit-form"> + <div ref="editNoteForm" class="note-edit-form current-note-edit-form"> <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> @@ -116,6 +117,7 @@ rel="noopener noreferrer">updated comment</a> to ensure information is not lost. </div> + <div class="flash-container timeline-content"></div> <form class="edit-note common-note-form"> <markdown-field -- GitLab From 7c130bf416e82d0dd5ea6917418725e7ac0d72f5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 3 Aug 2017 15:21:47 +0100 Subject: [PATCH 139/243] [ci skip] Make sure we show first class action errors properly. --- app/assets/javascripts/notes/components/issue_note_form.vue | 1 - app/assets/stylesheets/pages/notes.scss | 4 ---- 2 files changed, 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 4d68f52b5055..3b1ff05b4802 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -94,7 +94,6 @@ }, watch: { noteBody() { - debugger; if (this.note === this.initialNote) { this.note = this.noteBody; } else { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 2bb867052f68..e833d22d48ad 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -804,10 +804,6 @@ ul.notes { } } -.discussion-notes .flash-container { - margin-bottom: 0; -} - // Merge request notes in diffs .diff-file { // Diff is inline -- GitLab From b5b562a3bf22df0e40955e50ad9d42114624bda5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 3 Aug 2017 17:28:33 +0100 Subject: [PATCH 140/243] [ci skip] Fix alignment problems --- app/assets/javascripts/notes/components/issue_note.vue | 5 ++--- .../javascripts/notes/components/issue_note_actions.vue | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index e65757850351..05bf1d2bca09 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -39,7 +39,7 @@ return { 'is-editing': this.isEditing, 'disabled-content': this.isDeleting, - 'js-my-note': this.author.id === this.currentUserId, + //'js-my-note': this.author.id === this.currentUserId, target: this.targetNoteHash === this.noteAnchorId, }; }, @@ -123,8 +123,7 @@ class="note timeline-entry" :id="noteAnchorId" :class="classNameBindings" - :data-award-url="note.toggle_award_path" - :note-id="note.id"> + :data-award-url="note.toggle_award_path"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index f24abfb6a9d3..6982b201f41a 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -92,7 +92,7 @@ data-position="right" href="#" title="Add reaction"> - <loading-icon /> + <loading-icon :inline="true" /> <span v-html="emojiSmiling" class="link-highlight award-control-icon-neutral"> -- GitLab From 9e4164d417d1f145627a1eff9791f08dcbfc655b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 3 Aug 2017 19:27:33 +0100 Subject: [PATCH 141/243] Keep the replies when the user leaves the page --- app/assets/javascripts/autosave.js | 5 +++++ .../notes/components/issue_comment_form.vue | 13 ++++++++----- .../notes/components/issue_discussion.vue | 16 +++++++++++++++- .../notes/components/issue_note.vue | 1 - .../notes/components/issue_note_body.vue | 11 +++++++++++ .../notes/components/issue_notes_app.vue | 18 +++++------------- app/controllers/concerns/notes_actions.rb | 2 +- 7 files changed, 45 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index cfab6c40b34f..6000c56665bd 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -2,6 +2,11 @@ import AccessorUtilities from './lib/utils/accessor'; window.Autosave = (function() { + /** + * + * @param {*} field the textarea + * @param {Array} key Array with: ['Note', type, id, ] + */ function Autosave(field, key) { this.field = field; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index b7b7140d7727..2ef2af5bbac6 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,5 +1,5 @@ <script> - /* global Flash */ + /* global Flash, Autosave */ import { mapActions, mapGetters } from 'vuex'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -7,6 +7,7 @@ import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import eventHub from '../event_hub'; import * as constants from '../constants'; + import '../../autosave'; export default { data() { @@ -153,15 +154,17 @@ } } }, + initAutoSave() { + return new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id]); + }, }, mounted() { - eventHub.$on('issueStateChanged', (isClosed) => { + // jQuery is needed here because it is a custom event being dispatched with jQuery. + $(document).on('issuable:change', (e, isClosed) => { this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; }); - }, - destroyed() { - eventHub.$off('issueStateChanged'); + this.initAutoSave(); }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index d4754178933b..7952dfacc133 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,5 +1,5 @@ <script> - /* global Flash */ + /* global Flash, Autosave */ import { mapActions, mapGetters } from 'vuex'; import { SYSTEM_NOTE } from '../constants'; import issueNote from './issue_note.vue'; @@ -11,6 +11,7 @@ import issueNoteForm from './issue_note_form.vue'; import placeholderNote from './issue_placeholder_note.vue'; import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import '../../autosave'; export default { props: { @@ -107,6 +108,19 @@ }) .catch(() => Flash('Something went wrong while adding your reply. Please try again.')); }, + initAutoSave() { + return new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id]); + }, + }, + mounted() { + if (this.isReplying) { + this.initAutoSave(); + } + }, + updated() { + if (this.isReplying) { + this.initAutoSave(); + } }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 05bf1d2bca09..a41d646c0338 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -39,7 +39,6 @@ return { 'is-editing': this.isEditing, 'disabled-content': this.isDeleting, - //'js-my-note': this.author.id === this.currentUserId, target: this.targetNoteHash === this.noteAnchorId, }; }, diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 29ac68598402..61990340391c 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -1,8 +1,10 @@ <script> + /* global Autosave */ import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteAwardsList from './issue_note_awards_list.vue'; import issueNoteForm from './issue_note_form.vue'; import TaskList from '../../task_list'; + import '../../autosave'; export default { props: { @@ -49,13 +51,22 @@ formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelFormEdition', shouldConfirm, isDirty); }, + initAutoSave() { + return new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id]); + }, }, mounted() { this.renderGFM(); this.initTaskList(); + if (this.isEditing) { + this.initAutoSave(); + } }, updated() { this.initTaskList(); + if (this.isEditing) { + this.initAutoSave(); + } }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 14b7afe12870..4c0351f701bb 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -4,7 +4,6 @@ import { mapGetters, mapActions } from 'vuex'; import store from '../stores/'; import * as constants from '../constants'; - import eventHub from '../event_hub'; import issueNote from './issue_note.vue'; import issueDiscussion from './issue_discussion.vue'; import issueSystemNote from './issue_system_note.vue'; @@ -96,17 +95,6 @@ this.poll(); }, - bindEventHubListeners() { - this.$el.parentElement.addEventListener('toggleAward', (event) => { - const { awardName, noteId } = event.detail; - this.actionToggleAward({ awardName, noteId }); - }); - - // JQuery is needed here because it is a custom event being dispatched with jQuery. - $(document).on('issuable:change', (e, isClosed) => { - eventHub.$emit('issueStateChanged', isClosed); - }); - }, checkLocationHash() { const hash = gl.utils.getLocationHash(); const $el = $(`#${hash}`); @@ -125,7 +113,11 @@ mounted() { this.fetchNotes(); this.initPolling(); - this.bindEventHubListeners(); + + this.$el.parentElement.addEventListener('toggleAward', (event) => { + const { awardName, noteId } = event.detail; + this.actionToggleAward({ awardName, noteId }); + }); }, }; </script> diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 0f90137ad3d1..1d0587aad3ad 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -179,7 +179,7 @@ def note_params def set_polling_interval_header return unless noteable.is_a?(Issue) - Gitlab::PollingInterval.set_header(response, interval: 3_000) + Gitlab::PollingInterval.set_header(response, interval: 6_000) end def noteable -- GitLab From 507b15e7055f8ca8a237c0e6a892a0667aea1c62 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 10:29:01 +0100 Subject: [PATCH 142/243] [ci skip] Enable submit button on paste Reset form after response is submitted --- .../notes/components/issue_comment_form.vue | 59 ++++++++++++++----- .../notes/components/issue_discussion.vue | 4 +- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 2ef2af5bbac6..15b9aacc0282 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -2,6 +2,7 @@ /* global Flash, Autosave */ import { mapActions, mapGetters } from 'vuex'; + import _ from 'underscore'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; @@ -11,19 +12,14 @@ export default { data() { - const { getUserData, getIssueData, getNotesData } = this.$store.getters; - return { note: '', - markdownDocsUrl: getNotesData.markdownDocs, - quickActionsDocsUrl: getNotesData.quickActionsDocs, - markdownPreviewUrl: getIssueData.preview_note_path, noteType: constants.COMMENT, - issueState: getIssueData.state, - endpoint: getIssueData.create_note_path, - author: getUserData, - canUpdateIssue: getIssueData.current_user.can_update, + // Can't use mapGetters, + // this needs to be in the data object because it belongs to the state + issueState: this.$store.getters.getIssueData.state, isSubmitting: false, + isSubmitButtonDisabled: true, }; }, components: { @@ -31,11 +27,28 @@ markdownField, issueNoteSignedOutWidget, }, + watch: { + note(newNote) { + if (!_.isEmpty(newNote) && !this.isSubmitting) { + this.isSubmitButtonDisabled = false; + } else { + this.isSubmitButtonDisabled = true; + } + }, + isSubmitting(newValue) { + if (!_.isEmpty(this.note) && !newValue) { + this.isSubmitButtonDisabled = false; + } else { + this.isSubmitButtonDisabled = true; + } + }, + }, computed: { ...mapGetters([ 'getCurrentUserLastNote', 'getUserData', 'getIssueData', + 'getNotesData', ]), isLoggedIn() { return this.getUserData !== null; @@ -63,8 +76,23 @@ 'js-note-target-reopen': !this.isIssueOpen, }; }, - canSubmit() { - return !this.note.length || this.isSubmitting; + markdownDocsUrl() { + return this.getNotesData.markdownDocs; + }, + quickActionsDocsUrl() { + return this.getNotesData.quickActionsDocs; + }, + markdownPreviewUrl() { + return this.getIssueData.preview_note_path; + }, + author() { + return this.getUserData; + }, + canUpdateIssue() { + return this.getIssueData.current_user.can_update; + }, + endpoint() { + return this.getIssueData.create_note_path; }, }, methods: { @@ -139,6 +167,9 @@ if (shouldClear) { this.note = ''; } + + // reset autostave + this.autosave.reset(); }, setNoteType(type) { this.noteType = type; @@ -155,7 +186,7 @@ } }, initAutoSave() { - return new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id]); + this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id]); }, }, mounted() { @@ -212,13 +243,13 @@ <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button @click="handleSave()" - :disabled="canSubmit" + :disabled="isSubmitButtonDisabled" class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" type="button"> {{commentButtonTitle}} </button> <button - :disabled="canSubmit" + :disabled="isSubmitButtonDisabled" name="button" type="button" class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 7952dfacc133..82c4340d914d 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -79,10 +79,8 @@ }, cancelReplyForm(shouldConfirm) { if (shouldConfirm && this.$refs.noteForm.isDirty) { - const msg = 'Are you sure you want to cancel creating this comment?'; // eslint-disable-next-line no-alert - const isConfirmed = confirm(msg); - if (!isConfirmed) { + if (!confirm('Are you sure you want to cancel creating this comment?')) { return; } } -- GitLab From 03436e61e2bd82aca502bfb21d3337a08de8bb2f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 10:53:58 +0100 Subject: [PATCH 143/243] Handle autosave reset when form is submitted --- .../notes/components/issue_discussion.vue | 16 +++++++++++----- .../javascripts/notes/components/issue_note.vue | 3 ++- .../notes/components/issue_note_body.vue | 14 +++++++++----- app/assets/javascripts/notes/mixins/autosave.js | 16 ++++++++++++++++ 4 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/notes/mixins/autosave.js diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 82c4340d914d..f8ccaa826ed8 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -11,7 +11,7 @@ import issueNoteForm from './issue_note_form.vue'; import placeholderNote from './issue_placeholder_note.vue'; import placeholderSystemNote from './issue_placeholder_system_note.vue'; - import '../../autosave'; + import autosave from '../mixins/autosave';; export default { props: { @@ -36,6 +36,9 @@ placeholderNote, placeholderSystemNote, }, + mixins: [ + autosave, + ], computed: { ...mapGetters([ 'getIssueData', @@ -85,6 +88,7 @@ } } + this.resetAutoSave(); this.isReplying = false; }, saveReply(noteText) { @@ -103,12 +107,10 @@ this.saveNote(replyData) .then(() => { this.isReplying = false; + this.resetAutoSave(); }) .catch(() => Flash('Something went wrong while adding your reply. Please try again.')); }, - initAutoSave() { - return new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id]); - }, }, mounted() { if (this.isReplying) { @@ -117,7 +119,11 @@ }, updated() { if (this.isReplying) { - this.initAutoSave(); + if (!this.autosave) { + this.initAutoSave(); + } else { + this.setAutoSave(); + } } }, }; diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index a41d646c0338..d93b415654de 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -90,6 +90,7 @@ this.isEditing = false; // TODO: this could be moved down, by setting a prop $(this.$refs.noteBody.$el).renderGFM(); + this.$refs.noteBody.resetAutoSave(); }) .catch(() => Flash( 'Something went wrong while editing your comment. Please try again.', @@ -102,7 +103,7 @@ // eslint-disable-next-line no-alert if (!confirm('Are you sure you want to cancel editing this comment?')) return; } - + this.$refs.noteBody.resetAutoSave(); this.isEditing = false; }, }, diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 61990340391c..eb31c0305dcf 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -4,7 +4,7 @@ import issueNoteAwardsList from './issue_note_awards_list.vue'; import issueNoteForm from './issue_note_form.vue'; import TaskList from '../../task_list'; - import '../../autosave'; + import autosave from '../mixins/autosave'; export default { props: { @@ -22,6 +22,9 @@ default: false, }, }, + mixins: [ + autosave, + ], components: { issueNoteEditedText, issueNoteAwardsList, @@ -51,9 +54,6 @@ formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelFormEdition', shouldConfirm, isDirty); }, - initAutoSave() { - return new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id]); - }, }, mounted() { this.renderGFM(); @@ -65,7 +65,11 @@ updated() { this.initTaskList(); if (this.isEditing) { - this.initAutoSave(); + if (!this.autosave) { + this.initAutoSave(); + } else { + this.setAutoSave(); + } } }, }; diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js new file mode 100644 index 000000000000..8723395cf56b --- /dev/null +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -0,0 +1,16 @@ +/* globals Autosave */ +import '../../autosave'; + +export default { + methods: { + initAutoSave() { + this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id]); + }, + resetAutoSave() { + this.autosave.reset(); + }, + setAutoSave() { + this.autosave.save(); + }, + }, +}; -- GitLab From 08b76721f3efb38b9e379eabce3a8c8a09de4f87 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 11:46:55 +0100 Subject: [PATCH 144/243] Fix eslint --- app/assets/javascripts/notes/components/issue_discussion.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index f8ccaa826ed8..91f3e5391dd5 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -11,7 +11,7 @@ import issueNoteForm from './issue_note_form.vue'; import placeholderNote from './issue_placeholder_note.vue'; import placeholderSystemNote from './issue_placeholder_system_note.vue'; - import autosave from '../mixins/autosave';; + import autosave from '../mixins/autosave'; export default { props: { -- GitLab From 1c1fef7af3772c2e8baaf83516a43caa7ac8e6fc Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 16:51:35 +0100 Subject: [PATCH 145/243] [ci skip] Adds tests cases --- .../notes/components/issue_notes_app.vue | 2 + .../components/issue_comment_form_spec.js | 86 ++++++++++++++++ .../notes/components/issue_discussion_spec.js | 44 +++++++++ .../components/issue_note_actions_spec.js | 35 +++++++ .../notes/components/issue_note_app_spec.js | 36 +++++++ .../components/issue_note_awards_list_spec.js | 13 +++ .../notes/components/issue_note_body_spec.js | 17 ++++ .../components/issue_note_edited_text_spec.js | 17 ++++ .../notes/components/issue_note_form_spec.js | 65 ++++++++++++ .../components/issue_note_header_spec.js | 15 +++ .../issue_note_signed_out_widget_spec.js | 2 +- .../notes/components/issue_note_spec.js | 17 ++++ spec/javascripts/notes/stores/actions_spec.js | 96 ++++++++++++++++++ spec/javascripts/notes/stores/getters_spec.js | 70 +++++++++++++ .../javascripts/notes/stores/mutation_spec.js | 99 +++++++++++++++++++ 15 files changed, 613 insertions(+), 1 deletion(-) create mode 100644 spec/javascripts/notes/components/issue_comment_form_spec.js create mode 100644 spec/javascripts/notes/components/issue_discussion_spec.js create mode 100644 spec/javascripts/notes/components/issue_note_actions_spec.js create mode 100644 spec/javascripts/notes/components/issue_note_app_spec.js create mode 100644 spec/javascripts/notes/components/issue_note_awards_list_spec.js create mode 100644 spec/javascripts/notes/components/issue_note_body_spec.js create mode 100644 spec/javascripts/notes/components/issue_note_edited_text_spec.js create mode 100644 spec/javascripts/notes/components/issue_note_form_spec.js create mode 100644 spec/javascripts/notes/components/issue_note_header_spec.js create mode 100644 spec/javascripts/notes/components/issue_note_spec.js create mode 100644 spec/javascripts/notes/stores/actions_spec.js create mode 100644 spec/javascripts/notes/stores/getters_spec.js create mode 100644 spec/javascripts/notes/stores/mutation_spec.js diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 4c0351f701bb..2d26d77d5193 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -94,6 +94,8 @@ this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); this.poll(); + + debugger; }, checkLocationHash() { const hash = gl.utils.getLocationHash(); diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js new file mode 100644 index 000000000000..389ccd6e8863 --- /dev/null +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -0,0 +1,86 @@ +describe('issue_comment_form component', () => { + + describe('user is logged in', () => { + it('should render user avatar with link', () => { + + }); + + describe('textarea', () => { + it('should render textarea with placeholder', () => { + + }); + + it('should support quick actions', () => { + + }); + + it('should link to markdown docs', () => { + + }); + + it('should link to quick actions docs', () => { + + }); + + describe('edit mode', () => { + it('should enter edit mode when arrow up is pressed', () => { + + }); + }); + + describe('preview mode', () => { + it('should be possible to preview the note', () => { + + }); + }); + + describe('event enter', () => { + it('should save note when cmd/ctrl+enter is pressed', () => { + + }); + }); + }); + + describe('actions', () => { + describe('with empty note', () => { + it('should render dropdown as disabled', () => { + + }); + }); + + describe('with note', () => { + it('should render enabled dropdown with 2 actions', () => { + + }); + + it('should render be possible to discard draft', () => { + + }); + }); + + describe('with open issue', () => { + it('should be possible to close the issue', () => { + + }); + }); + + describe('with closed issue', () => { + it('should be possible to reopen the issue', () => { + + }); + }); + }); + + + }); + + describe('user is not logged in', () => { + it('should render signed out widget', () => { + + }); + + it('should not render submission form', () => { + + }); + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js new file mode 100644 index 000000000000..3d6b45d2dade --- /dev/null +++ b/spec/javascripts/notes/components/issue_discussion_spec.js @@ -0,0 +1,44 @@ +describe('issue_discussion component', () => { + + it('should render user avatar', () => { + + }); + + it('should render discussion header', () => { + + }); + + describe('updated note', () => { + it('should show information about update', () => { + + }); + }); + + describe('with open discussion', () => { + it('should render system note', () => { + + }); + + it('should render placeholder note', () => { + + }); + + it('should render regular note', () => { + + }); + + describe('actions', () => { + it('should render reply button', () => { + + }); + + it('should toggle reply form', () => { + + }); + + it('should render signout widget when user is logged out', () => { + + }); + }); + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_note_actions_spec.js b/spec/javascripts/notes/components/issue_note_actions_spec.js new file mode 100644 index 000000000000..f23c73dd73fc --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_actions_spec.js @@ -0,0 +1,35 @@ +describe('issse_note_actions component', () => { + it('should render access level badge', () => { + + }); + + describe('user is logged in', () => { + it('should render emoji link', () => { + + }); + + describe('actions dropdown', () => { + it('should be possible to edit the comment', () => { + + }); + + it('should be possible to report as abuse', () => { + + }); + + it('should be possible to delete comment', () => { + + }); + }); + }); + + describe('user is not logged in', () => { + it('should not render emoji link', () => { + + }); + + it('should not render actions dropdown', () => { + + }); + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js new file mode 100644 index 000000000000..df8d250ed5e3 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -0,0 +1,36 @@ +describe('issue_note_app', () => { + + it('should set notes data', () => { + + }); + + it('should set issue data', () => { + + }); + + it('should set user data', () => { + + }); + + it('should fetch notes', () => { + + }); + + it('should render list of notes', () => { + + }); + + it('should render form', () => { + + }); + + describe('while fetching', () => { + it('should render loading icon', () => { + + }); + + it('should render form', () => { + + }); + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/issue_note_awards_list_spec.js new file mode 100644 index 000000000000..8620932004de --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_awards_list_spec.js @@ -0,0 +1,13 @@ +describe('issue_note_awards_list component', () => { + it('should render awarded emojis', () => { + + }); + + it('should be possible to remove awareded emoji', () => { + + }); + + it('should be possible to add new emoji', () => { + + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/issue_note_body_spec.js new file mode 100644 index 000000000000..1c0d07af644f --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_body_spec.js @@ -0,0 +1,17 @@ +describe('issue_note_body component', () => { + it('should render the note', () => { + + }); + + it('should be render form if user is editing', () => { + + }); + + it('should render information if note was edited', () => { + + }); + + it('should render awards list', () => { + + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_note_edited_text_spec.js b/spec/javascripts/notes/components/issue_note_edited_text_spec.js new file mode 100644 index 000000000000..f7b4c49dfbe9 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_edited_text_spec.js @@ -0,0 +1,17 @@ +describe('issue_note_edited_text', () => { + it('should render block with provided className', () => { + + }); + + it('should render provided actionText', () => { + + }); + + it('should render time ago with provided timestamp', () => { + + }); + + it('should render provided user information', () => { + + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js new file mode 100644 index 000000000000..e431598e8852 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_form_spec.js @@ -0,0 +1,65 @@ +describe('issue_note_form component', () => { + + describe('conflicts editing', () => { + it('should show conflict message if note changes outside the component', () => { + + }); + }); + + describe('form', () => { + it('should render text area with placeholder', () => { + + }); + + it('should link to markdown docs', () => { + + }); + + it('should link to quick actions docs', () => { + + }); + + it('should preview the content', () => { + + }); + + it('should allow quick actions', () => { + + }); + + + describe('keyboard events', () => { + describe('up', () => { + it('should ender edit mode', () => { + + }); + }); + + describe('enter', () => { + it('should submit note', () => { + + }); + }); + + describe('esc', () => { + it('should show exit', () => { + + }); + }); + }); + + describe('actions', () => { + it('should be possible to cancel', () => { + + }); + + it('should be possible to update the note', () => { + + }); + + it('should not be possible to save an empty note', () => { + + }); + }); + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_note_header_spec.js b/spec/javascripts/notes/components/issue_note_header_spec.js new file mode 100644 index 000000000000..31c65ed7f439 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_header_spec.js @@ -0,0 +1,15 @@ +describe('issue_note_header component', () => { + it('should render user information', () => { + + }); + + it('should render timestamp link', () => { + + }); + + describe('discussion', () => { + it('should render toggle button', () => { + + }); + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js index 1ba202090d99..f7fe500c8535 100644 --- a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js +++ b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js @@ -1,4 +1,4 @@ -describe('issue note signed out widget component', () => { +describe('issue_note_signed_out_widget component', () => { it('should render sign in link provided in the store', () => { }); diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js new file mode 100644 index 000000000000..6ec81a5f5922 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_spec.js @@ -0,0 +1,17 @@ +describe('issue_note', () => { + it('should render user information', () => { + + }); + + it('should render note header content', () => { + + }); + + it('should render note actions', () => { + + }); + + it('should render issue body', () => { + + }); +}); \ No newline at end of file diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js new file mode 100644 index 000000000000..18b1c2c8f6ff --- /dev/null +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -0,0 +1,96 @@ + +import * as actions from '~/notes/stores/actions'; + +describe('Actions Notes Store', () => { + describe('setNotesData', () => { + it('should set received notes data', () => { + + }); + }); + + describe('setIssueData', () => { + it('should set received issue data', () => {}); + }); + + describe('setUserData', () => { + it('should set received user data', () => {}); + }); + + describe('setLastFetchedAt', () => { + it('should set received timestamp', () => {}); + }); + + describe('setInitialNotes', () => { + it('should set initial notes', () => { + + }); + }); + + describe('setTargetNoteHash', () => { + it('should set target note hash', () => {}); + }); + + describe('toggleDiscussion', () => { + it('should toggle discussion', () => { + + }); + }); + + describe('fetchNotes', () => { + it('should request notes', () => { + + }); + }); + + describe('deleteNote', () => { + it('should delete note', () => {}); + }); + + describe('updateNote', () => { + it('should update note', () => { + + }); + }); + + describe('replyToDiscussion', () => { + it('should add a reply to a discussion', () => { + + }); + }); + + describe('createNewNote', () => { + it('should create a new note', () => {}); + }); + + describe('saveNote', () => { + it('should save the received note', () => { + + }); + }); + + describe('poll', () => { + it('should start polling the received endoint', () => { + + }); + }); + + describe('toggleAward', () => { + it('should toggle received award', () => { + + }); + }); + + describe('toggleAwardRequest', () => { + it('should make a request to toggle the award', () => { + + }); + }); + + describe('scrollToNoteIfNeeded', () => { + it('should call `scrollToElement` if note is not in viewport', () => { + }); + + it('should note call `scrollToElement` if note is in viewport', () => { + }); + }); +}); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js new file mode 100644 index 000000000000..ad8fc97362a8 --- /dev/null +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -0,0 +1,70 @@ +import { getters } from '~/notes/stores/getters'; + +describe('Getters Notes Store', () => { + + describe('notes', () => { + it('should return all notes in the store', () => { + + }); + }); + + describe('targetNoteHash', () => { + it('should return `targetNoteHash`', () => { + + }); + }); + + describe('getNotesData', () => { + it('should return all data in `notesData`', () => { + + }); + }); + + describe('getNotesDataByProp', () => { + it('should return the given prop', () => { + + }); + }); + + describe('getIssueData', () => { + it('should return all data in `issueData`', () => { + + }); + }); + + describe('getIssueDataByProp', () => { + it('should return the given prop', () => { + + }); + }); + + describe('getUserData', () => { + it('should return all data in `userData`', () => { + + }); + }); + + describe('getUserDataByProp', () => { + it('should return the given prop', () => { + + }); + }); + + describe('notesById', () => { + it('should return the note for the given id', () => { + + }); + }); + + describe('getCurrentUserLastNote', () => { + it('should return the last note of the current user', () => { + + }); + }); + + describe('getDiscussionLastNote', () => { + it('should return the last discussion note of the current user', () => { + + }); + }); +}); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js new file mode 100644 index 000000000000..0fdba840f2e5 --- /dev/null +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -0,0 +1,99 @@ +import { mutations } from '~/notes/stores/mutations'; + +describe('Mutation Notes Store', () => { + describe('ADD_NEW_NOTE', () => { + it('should add a new note to an array of notes', () => { + + }); + }); + + describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { + it('should add a reply to a specific discussion', () => { + + }); + }); + + describe('DELETE_NOTE', () => { + it('should delete an indivudal note', () => { + + }); + + it('should delete a note from a discussion', () => { + + }); + }); + + describe('REMOVE_PLACEHOLDER_NOTES', () => { + it('should remove all placeholder notes in indivudal notes and discussion', () => { + + }); + }); + + describe('SET_NOTES_DATA', () => { + it('should set an object with notesData', () => { + + }); + }); + + describe('SET_ISSUE_DATA', () => { + it('should set the issue data', () => { + + }); + }); + + describe('SET_USER_DATA', () => { + it('should set the user data', () => { + + }); + }); + + describe('SET_INITAL_NOTES', () => { + it('should set the initial notes received', () => { + + }); + }); + + describe('SET_LAST_FETCHED_AT', () => { + it('should set timestamp', () => {}); + }); + + describe('SET_TARGET_NOTE_HASH', () => { + it('should set the note hash', () => {}); + }); + + describe('SHOW_PLACEHOLDER_NOTE', () => { + it('should set a placeholder note', () => { + + }); + }); + + describe('TOGGLE_AWARD', () => { + it('should add award if user has not reacted yet', () => { + + }); + + it('should remove award if user already reacted', () => { + + }); + }); + + describe('TOGGLE_DISCUSSION', () => { + it('should open a closed discussion', () => { + + }); + + it('should close a opened discussion', () => { + + }); + }); + + describe('UPDATE_NOTE', () => { + it('should update an individual note', () => { + + }); + + it('should update a note in a discussion', () => { + + }); + }); +}); -- GitLab From 667f8b53ee63e4e1e14ae90d39cc4ec717cf872b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 18:51:57 +0100 Subject: [PATCH 146/243] Adds mock data --- spec/javascripts/notes/mock_data.js | 221 ++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 spec/javascripts/notes/mock_data.js diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js new file mode 100644 index 000000000000..42d1bf9adc00 --- /dev/null +++ b/spec/javascripts/notes/mock_data.js @@ -0,0 +1,221 @@ +export const notesDataMock = { + discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', + lastFetchedAt: '1501862675', + markdownDocs: '/help/user/markdown', + newSessionPath: '/users/sign_in?redirect_to_referer=yes', + notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes?full_data=1', + quickActionsDocs: '/help/user/project/quick_actions', + registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane', +}; + +export const userDataMock = { + avatar_url: 'mock_path', + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', +}; + +export const issueData = { + assignees: [], + author_id: 1, + branch_name: null, + confidential: false, + create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue', + created_at: '2017-02-07T10:11:18.395Z', + current_user: { + can_create_note: true, + can_update: true, + }, + deleted_at: null, + description: '', + due_date: null, + human_time_estimate: null, + human_total_time_spent: null, + id: 98, + iid: 26, + labels: [], + lock_version: null, + milestone: null, + milestone_id: null, + moved_to_id: null, + preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', + project_id: 2, + state: 'opened', + time_estimate: 0, + title: '14', + total_time_spent: 0, + updated_at: '2017-08-04T09:53:01.226Z', + updated_by_id: 1, + web_url: '/gitlab-org/gitlab-ce/issues/26', +}; + +export const lastFetchedAt = '1501862675'; + +export const individualNote = { + expanded: true, + id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + individual_note: true, + notes: [{ + id: 1390, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-01T17: 09: 33.762Z', + updated_at: '2017-08-01T17: 09: 33.762Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: null, + human_access: 'Owner', + note: 'sdfdsaf', + note_html: '<p dir=\'auto\'>sdfdsaf</p>', + current_user: { can_edit: true }, + discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + emoji_awardable: true, + award_emoji: [ + { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } }, + { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, + ], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', + report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1390', + }], + reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', +}; + +export const discussionMock = { + id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + expanded: true, + notes: [{ + id: 1395, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:51:58.559Z', + updated_at: '2017-08-02T10:51:58.559Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'THIS IS A DICUSSSION!', + note_html: '<p dir=\'auto\'>THIS IS A DICUSSSION!</p>', + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji', + report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1395', + }, { + id: 1396, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:56:50.980Z', + updated_at: '2017-08-03T14:19:35.691Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'sadfasdsdgdsf', + note_html: '<p dir=\'auto\'>sadfasdsdgdsf</p>', + last_edited_at: '2017-08-03T14:19:35.691Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji', + report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1396', + }, { + id: 1437, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-03T18:11:18.780Z', + updated_at: '2017-08-04T09:52:31.062Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'adsfasf Should disappear', + note_html: '<p dir=\'auto\'>adsfasf Should disappear</p>', + last_edited_at: '2017-08-04T09:52:31.062Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji', + report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1437', + }], + individual_note: false, +}; \ No newline at end of file -- GitLab From a432ae9d06f7dc28d0825e87bafb33a04ae3cf20 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 19:37:02 +0100 Subject: [PATCH 147/243] Tests --- app/assets/javascripts/notes/components/issue_note_form.vue | 2 +- spec/javascripts/awards_handler_spec.js | 1 + spec/javascripts/behaviors/quick_submit_spec.js | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 3b1ff05b4802..731080fd3963 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -118,7 +118,7 @@ </div> <div class="flash-container timeline-content"></div> <form - class="edit-note common-note-form"> + class="edit-note common-note-form js-vue-quick-submit"> <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 8e0568821086..c90970b7ba1c 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -28,6 +28,7 @@ import '~/lib/utils/common_utils'; preloadFixtures('issues/issue_with_comment.html.raw'); beforeEach(function(done) { loadFixtures('issues/issue_with_comment.html.raw'); + $('body').data('page', 'projects:issues:show'); loadAwardsHandler(true).then((obj) => { awardsHandler = obj; spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 6dc48f9a2935..0d3f3e9673ac 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -38,19 +38,19 @@ import '~/behaviors/quick_submit'; return expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('disables input of type submit', function() { - const submitButton = $('.js-quick-submit input[type=submit]'); + const submitButton = $('.js-vue-quick-submit input[type=submit]'); this.textarea.trigger(keydownEvent()); expect(submitButton).toBeDisabled(); }); it('disables button of type submit', function() { - const submitButton = $('.js-quick-submit input[type=submit]'); + const submitButton = $('.js-vue-quick-submit input[type=submit]'); this.textarea.trigger(keydownEvent()); expect(submitButton).toBeDisabled(); }); it('only clicks one submit', function() { - const existingSubmit = $('.js-quick-submit input[type=submit]'); + const existingSubmit = $('.js-vue-quick-submit input[type=submit]'); // Add an extra submit button const newSubmit = $('<button type="submit">Submit it</button>'); newSubmit.insertAfter(this.textarea); -- GitLab From 781ef6dbc21987c44c5535630a494190c98918f6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 20:28:04 +0100 Subject: [PATCH 148/243] [ci skip] Remove lost debugger statement --- app/assets/javascripts/notes/components/issue_notes_app.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 2d26d77d5193..4c0351f701bb 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -94,8 +94,6 @@ this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); this.poll(); - - debugger; }, checkLocationHash() { const hash = gl.utils.getLocationHash(); -- GitLab From c28df4bce9bb1ca6f6cfb5861ab28fc2b08689e3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 20:50:52 +0100 Subject: [PATCH 149/243] Fix some broken tests --- spec/javascripts/shortcuts_issuable_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 3515dfbc60b8..26866b03d017 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -24,7 +24,7 @@ import '~/shortcuts_issuable'; }; }; beforeEach(function() { - this.selector = 'form.js-main-target-form textarea#note_note'; + this.selector = 'form.js-main-target-form textarea#note-body'; }); describe('with empty selection', function() { it('does not return an error', function() { -- GitLab From 7065bb3ef31fe04bea39aa07d620552eaaa26aba Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 4 Aug 2017 21:57:03 +0100 Subject: [PATCH 150/243] Fix vue broken test --- .../notes/components/issue_comment_form.vue | 11 ++++++----- .../javascripts/notes/components/issue_note_form.vue | 6 +++--- spec/javascripts/behaviors/quick_submit_spec.js | 6 +++--- .../components/issue_placeholder_system_note_spec.js | 5 ++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 15b9aacc0282..450438180559 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -218,8 +218,10 @@ :img-size="40" /> </div> - <div class="js-main-target-form timeline-content timeline-content-form common-note-form"> - <form> + <div > + <form + class="js-main-target-form timeline-content timeline-content-form common-note-form" + @submit="handleSave(true)"> <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" @@ -228,7 +230,7 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-gfm-input markdown-area" + class="note-textarea js-gfm-input js-autosize markdown-area" data-supports-quick-actions="true" aria-label="Description" v-model="note" @@ -300,8 +302,7 @@ </ul> </div> <button - type="button" - @click="handleSave(true)" + type="submit" v-if="canUpdateIssue" :class="actionButtonClassNames" class="btn btn-nr btn-comment btn-comment-and-close"> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 731080fd3963..6bd5bd2d8a10 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -118,7 +118,8 @@ </div> <div class="flash-container timeline-content"></div> <form - class="edit-note common-note-form js-vue-quick-submit"> + class="edit-note common-note-form" + @submit="handleUpdate"> <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" @@ -141,8 +142,7 @@ </markdown-field> <div class="note-form-actions clearfix"> <button - type="button" - @click="handleUpdate" + type="submit" :disabled="isDisabled" class="btn btn-nr btn-save"> {{saveButtonTitle}} diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 0d3f3e9673ac..6dc48f9a2935 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -38,19 +38,19 @@ import '~/behaviors/quick_submit'; return expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('disables input of type submit', function() { - const submitButton = $('.js-vue-quick-submit input[type=submit]'); + const submitButton = $('.js-quick-submit input[type=submit]'); this.textarea.trigger(keydownEvent()); expect(submitButton).toBeDisabled(); }); it('disables button of type submit', function() { - const submitButton = $('.js-vue-quick-submit input[type=submit]'); + const submitButton = $('.js-quick-submit input[type=submit]'); this.textarea.trigger(keydownEvent()); expect(submitButton).toBeDisabled(); }); it('only clicks one submit', function() { - const existingSubmit = $('.js-vue-quick-submit input[type=submit]'); + const existingSubmit = $('.js-quick-submit input[type=submit]'); // Add an extra submit button const newSubmit = $('<button type="submit">Submit it</button>'); newSubmit.insertAfter(this.textarea); diff --git a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js index fd28b33d60b3..d508a49f710f 100644 --- a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js +++ b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js @@ -18,8 +18,7 @@ describe('issue placeholder system note component', () => { it('should render system note placeholder with plain text', () => { const vm = mountComponent('This is a placeholder'); - expect(vm.$el.tagName).toEqua('LI'); - - expect(vm.$el.querySelector('.timeline-content i').textContent.trim()).toEqua('This is a placeholder'); + expect(vm.$el.tagName).toEqual('LI'); + expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); }); }); -- GitLab From 6f5f7e404997c1b82d30cd3e551245756c21a73f Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 7 Aug 2017 12:59:34 +0100 Subject: [PATCH 151/243] Convert ?full_data=1 -> ?view=full_data --- .../javascripts/notes/components/issue_comment_form.vue | 2 +- .../javascripts/notes/components/issue_discussion.vue | 2 +- app/assets/javascripts/notes/components/issue_note.vue | 2 +- app/controllers/concerns/notes_actions.rb | 6 ++++-- app/helpers/notes_helper.rb | 8 +++++--- app/views/projects/issues/_discussion.html.haml | 2 +- spec/helpers/notes_helper_spec.rb | 8 ++++++++ spec/javascripts/notes/mock_data.js | 4 ++-- 8 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 450438180559..390be5c26afd 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -105,7 +105,7 @@ endpoint: this.endpoint, flashContainer: this.$el, data: { - full_data: true, + view: 'full_data', note: { noteable_type: 'Issue', noteable_id: this.getIssueData.id, diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 91f3e5391dd5..d6dd0be55ace 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -100,7 +100,7 @@ target_type: 'issue', target_id: this.discussion.noteable_id, note: { note: noteText }, - full_data: true, + view: 'full_data', }, }; diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index d93b415654de..63df996a0e00 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -78,7 +78,7 @@ const data = { endpoint: this.note.path, note: { - full_data: true, + view: 'full_data', target_type: 'issue', target_id: this.note.noteable_id, note: { note: noteText }, diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 1d0587aad3ad..1809464f4f4d 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -18,7 +18,8 @@ def index @notes = prepare_notes_for_rendering(@notes) notes_json[:notes] = - if params[:full_data] + case params[:view] + when 'full_data' note_serializer.represent(@notes) else @notes.map { |note| note_json(note) } @@ -87,7 +88,8 @@ def note_json(note) if note.persisted? attrs[:valid] = true - if params[:full_data] + case params[:view] + when 'full_data' attrs.merge!(note_serializer.represent(note)) else attrs.merge!( diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index e857e837c161..ea20f4b8d870 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -93,11 +93,13 @@ def discussion_path(discussion) end end - def notes_url + def notes_url(extra_params = {}) if @snippet.is_a?(PersonalSnippet) - snippet_notes_path(@snippet) + snippet_notes_path(@snippet, extra_params) else - project_noteable_notes_path(@project, target_id: @noteable.id, target_type: @noteable.class.name.underscore) + params = { target_id: @noteable.id, target_type: @noteable.class.name.underscore } + + project_noteable_notes_path(@project, params.merge(extra_params)) end end diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 8e17895c1c72..538bb50d3c91 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -9,7 +9,7 @@ new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), markdown_docs: help_page_path('user/markdown'), quick_actions_docs: help_page_path('user/project/quick_actions'), - notes_path: "#{notes_url}?full_data=1", + notes_path: notes_url(view: 'full_data'), last_fetched_at: Time.now.to_i, issue_data: serialize_issuable(@issue), current_user_data: UserSerializer.new.represent(current_user).to_json }} diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 9921ca1af336..41474c327556 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -205,6 +205,14 @@ expect(helper.notes_url).to eq("/nm/test/noteable/issue/#{@noteable.id}/notes") end + + it 'adds extra params' do + namespace = create(:namespace, path: 'nm') + @project = create(:project, path: 'test', namespace: namespace) + @noteable = create(:issue, project: @project) + + expect(helper.notes_url(view: 'full_data')).to eq("/nm/test/noteable/issue/#{@noteable.id}/notes?view=full_data") + end end describe '#note_url' do diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 42d1bf9adc00..bade5b9925a9 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -3,7 +3,7 @@ export const notesDataMock = { lastFetchedAt: '1501862675', markdownDocs: '/help/user/markdown', newSessionPath: '/users/sign_in?redirect_to_referer=yes', - notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes?full_data=1', + notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes?view=full_data', quickActionsDocs: '/help/user/project/quick_actions', registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane', }; @@ -218,4 +218,4 @@ export const discussionMock = { path: '/gitlab-org/gitlab-ce/notes/1437', }], individual_note: false, -}; \ No newline at end of file +}; -- GitLab From 25fcee16f5e1640b88bfc893cabdddbed5bc2551 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Mon, 7 Aug 2017 18:23:56 +0100 Subject: [PATCH 152/243] Init adding tests --- app/assets/javascripts/autosave.js | 5 - .../notes/components/issue_notes_app.vue | 12 +- .../javascripts/notes/stores/actions.js | 2 +- .../notes/components/issue_note_app_spec.js | 183 ++++++++++++++++-- spec/javascripts/notes/mock_data.js | 100 +++++++++- 5 files changed, 278 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 6000c56665bd..cfab6c40b34f 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -2,11 +2,6 @@ import AccessorUtilities from './lib/utils/accessor'; window.Autosave = (function() { - /** - * - * @param {*} field the textarea - * @param {Array} key Array with: ['Note', type, id, ] - */ function Autosave(field, key) { this.field = field; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 4c0351f701bb..371addd937e2 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -114,10 +114,12 @@ this.fetchNotes(); this.initPolling(); - this.$el.parentElement.addEventListener('toggleAward', (event) => { - const { awardName, noteId } = event.detail; - this.actionToggleAward({ awardName, noteId }); - }); + if (this.$el.parentElement) { + this.$el.parentElement.addEventListener('toggleAward', (event) => { + const { awardName, noteId } = event.detail; + this.actionToggleAward({ awardName, noteId }); + }); + } }, }; </script> @@ -126,7 +128,7 @@ <div id="notes"> <div v-if="isLoading" - class="loading"> + class="js-loading loading"> <loading-icon /> </div> diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 0c9eb0462986..1ad7648e533b 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -134,7 +134,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }; const pollSuccessCallBack = (resp, commit, state, getters) => { - if (resp.notes.length) { + if (resp.notes && resp.notes.length) { const { notesById } = getters; resp.notes.forEach((note) => { diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js index df8d250ed5e3..847cb2a23ff7 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -1,36 +1,197 @@ -describe('issue_note_app', () => { +import Vue from 'vue'; +import issueNotesApp from '~/notes/components/issue_notes_app.vue'; +import * as mockData from '../mock_data'; - it('should set notes data', () => { +fdescribe('issue_note_app', () => { + let mountComponent; + beforeEach(() => { + const IssueNotesApp = Vue.extend(issueNotesApp); + + mountComponent = props => new IssueNotesApp({ + propsData: props, + }).$mount(); }); - it('should set issue data', () => { + describe('set data', () => { + let vm; + + const responseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(responseInterceptor); + vm = mountComponent({ + issueData: mockData.issueDataMock, + notesData: mockData.notesDataMock, + userData: mockData.userDataMock, + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); + }); + + it('should set notes data', () => { + expect(vm.$store.state.notesData).toEqual(mockData.notesDataMock); + }); + + it('should set issue data', () => { + expect(vm.$store.state.issueData).toEqual(mockData.issueDataMock); + }); + + it('should set user data', () => { + expect(vm.$store.state.userData).toEqual(mockData.userDataMock); + }); + it('should fetch notes', () => { + expect(vm.$store.state.notes).toEqual([]); + }); }); - it('should set user data', () => { + fdescribe('render', () => { + let vm; + + const responseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(mockData.discussionResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(responseInterceptor); + vm = mountComponent({ + issueData: mockData.issueDataMock, + notesData: mockData.notesDataMock, + userData: mockData.userDataMock, + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); + }); + it('should render list of notes', () => { + console.log(vm); + }); + it('should render form', () => { + expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); }); - it('should fetch notes', () => { + describe('while fetching data', () => { + let vm; + beforeEach(() => { + vm = mountComponent({ + issueData: mockData.issueDataMock, + notesData: mockData.notesDataMock, + userData: mockData.userDataMock, + }); + }); + + it('should render loading icon', () => { + expect(vm.$el.querySelector('.js-loading')).toBeDefined(); + }); + it('should render form', () => { + expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); }); - it('should render list of notes', () => { + describe('update note', () => { + describe('individual note', () => { + describe('shortup up key', () => { + it('shows correct editing form when user clicks up', () => { + }); + }); + + describe('dropdown', () => { + it('renders edit form', () => { + }); + }); + + it('updates the note and resets the edit form', () => {}); + }); + + describe('dicussion note note', () => { + describe('shortup up key', () => { + it('shows correct editing form when user clicks up', () => { + }); + }); + + describe('dropdown', () => { + it('renders edit form', () => { + }); + }); + it('updates the note and resets the edit form', () => {}); + }); }); - it('should render form', () => { + describe('set target hash', () => { + it('updates the URL when the note date is clicked', () => { + + }); + + it('stores the correct hash', () => { + + }); + + it('updates visually the target note', () => { + }); }); - describe('while fetching', () => { - it('should render loading icon', () => { + describe('create new note', () => { + it('should show placeholder note while new comment is being posted', () => {}); + it('should remove placeholder note when new comment is done posting', () => {}); + it('should show actual note element when new comment is done posting', () => {}); + it('should show flash error message when new comment failed to be posted', () => {}); + it('should show flash error message when comment failed to be updated', () => {}); + }); + describe('quick actions', () => { + it('should return executing quick action description when note has single quick action', () => { }); - it('should render form', () => { + it('should return generic multiple quick action description when note has multiple quick actions', () => { + }); + it('should return generic quick action description when available quick actions list is not populated', () => { }); }); -}); \ No newline at end of file + + describe('new note form', () => { + it('should render markdown docs url', () => { + + }); + + it('should render quick action docs url', () => { + + }); + + it('should preview markdown', () => { + + }); + + describe('discard draft', () => { + it('should reset form when reset button is clicked', () => { + + }); + }); + }); + + describe('edit form', () => { + it('should render markdown docs url', () => {}); + it('should not render quick actions docs url', () => {}); + }); +}); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 42d1bf9adc00..ce969f0f460c 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -1,3 +1,5 @@ +/* eslint disable */ + export const notesDataMock = { discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', lastFetchedAt: '1501862675', @@ -17,7 +19,7 @@ export const userDataMock = { username: 'root', }; -export const issueData = { +export const issueDataMock = { assignees: [], author_id: 1, branch_name: null, @@ -218,4 +220,98 @@ export const discussionMock = { path: '/gitlab-org/gitlab-ce/notes/1437', }], individual_note: false, -}; \ No newline at end of file +}; + +export const discussionResponse = [{ + "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "expanded": true, + "notes": [{ + "id": 1390, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-01T17:09:33.762Z", + "updated_at": "2017-08-01T17:09:33.762Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "sdfdsaf", + "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }, { + "name": "art", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1390" + }], + "individual_note": true +}, { + "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "expanded": true, + "notes": [{ + "id": 1391, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-02T10:51:38.685Z", + "updated_at": "2017-08-02T10:51:38.685Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "New note!", + "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1391" + }], + "individual_note": true +}]; \ No newline at end of file -- GitLab From 15441f0ef564e87f2ffb672452b0fb9d7bd1d44e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 8 Aug 2017 14:54:43 +0100 Subject: [PATCH 153/243] [ci skip] Init testing vue app for issue notes --- .../notes/components/issue_comment_form.vue | 4 +- .../notes/components/issue_note.vue | 9 ++- .../notes/components/issue_note_actions.vue | 20 +++--- .../notes/components/issue_note_form.vue | 8 +-- .../notes/components/issue_notes_app.vue | 6 +- .../javascripts/notes/stores/actions.js | 11 +++- .../notes/components/issue_note_app_spec.js | 61 +++++++++++++------ 7 files changed, 78 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 390be5c26afd..751d7578087a 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -175,6 +175,7 @@ this.noteType = type; }, editCurrentUserLastNote() { + console.log('editCurrentUserLastNote') if (this.note === '') { const lastNote = this.getCurrentUserLastNote(window.gon.current_user_id); @@ -228,9 +229,8 @@ :quick-actions-docs="quickActionsDocsUrl" :add-spacing-classes="false"> <textarea - id="note-body" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area" + class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area" data-supports-quick-actions="true" aria-label="Description" v-model="note" diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 63df996a0e00..3b0c684f2a15 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -60,10 +60,9 @@ }, deleteHandler() { // eslint-disable-next-line no-alert - const isConfirmed = confirm('Are you sure you want to delete this list?'); - - if (isConfirmed) { + if (confirm('Are you sure you want to delete this list?')) { this.isDeleting = true; + this.deleteNote(this.note) .then(() => { this.isDeleting = false; @@ -149,8 +148,8 @@ :can-delete="note.current_user.can_edit" :can-report-as-abuse="canReportAsAbuse" :report-abuse-path="note.report_abuse_path" - :edit-handler="editHandler" - :delete-handler="deleteHandler" + @editHandler="editHandler" + @deleteHandler="deleteHandler" /> </div> <issue-note-body diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 6982b201f41a..638362f4588d 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -37,14 +37,6 @@ type: Boolean, required: true, }, - editHandler: { - type: Function, - required: true, - }, - deleteHandler: { - type: Function, - required: true, - }, }, directives: { tooltip, @@ -76,6 +68,14 @@ return this.getUserDataByProp('id'); }, }, + methods: { + onEdit() { + this.$emit('editHandler'); + }, + onDelete() { + this.$emit('deleteHandler'); + } + } }; </script> @@ -125,7 +125,7 @@ <template v-if="canEdit"> <li> <button - @click="editHandler" + @click="onEdit" type="button" class="btn btn-transparent js-note-edit"> Edit comment @@ -140,7 +140,7 @@ </li> <li v-if="canEdit"> <button - @click.prevent="deleteHandler" + @click.prevent="onDelete" class="btn btn-transparent js-note-delete js-note-delete" type="button"> <span class="text-danger"> diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 6bd5bd2d8a10..a4d677a780fb 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -118,8 +118,7 @@ </div> <div class="flash-container timeline-content"></div> <form - class="edit-note common-note-form" - @submit="handleUpdate"> + class="edit-note common-note-form"> <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" @@ -128,7 +127,7 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area" + class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form" :data-supports-quick-actions="!isEditing" aria-label="Description" v-model="note" @@ -143,8 +142,9 @@ <div class="note-form-actions clearfix"> <button type="submit" + @click="handleUpdate" :disabled="isDisabled" - class="btn btn-nr btn-save"> + class="js-vue-issue-save btn btn-save"> {{saveButtonTitle}} </button> <button diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 371addd937e2..adb08f97b22f 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -113,9 +113,11 @@ mounted() { this.fetchNotes(); this.initPolling(); + const parentElement = this.$el.parentElement; - if (this.$el.parentElement) { - this.$el.parentElement.addEventListener('toggleAward', (event) => { + if (parentElement && + parentElement.classList.contains('js-vue-notes-event')) { + parentElement.addEventListener('toggleAward', (event) => { const { awardName, noteId } = event.detail; this.actionToggleAward({ awardName, noteId }); }); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 1ad7648e533b..bb9c2c53b799 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -87,7 +87,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const commandsChanges = res.commands_changes; if (hasQuickActions && Object.keys(errors).length) { - dispatch('poll'); + dispatch('fetchData'); $('.js-gfm-input').trigger('clear-commands-cache.atwho'); Flash('Commands applied', 'notice', $(noteData.flashContainer)); @@ -186,6 +186,15 @@ export const poll = ({ commit, state, getters }) => { }); }; +export const fetchData = ({ commit, state, getters }) => { + const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; + + service.poll(requestData) + .then(resp => resp.json) + .then(data => pollSuccessCallBack(data, commit, state, getters)) + .catch(() => Flash('Something went wrong while fetching latest comments.')); +}; + export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => { commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); }; diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js index 847cb2a23ff7..9b48d3817a6e 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -1,8 +1,10 @@ import Vue from 'vue'; import issueNotesApp from '~/notes/components/issue_notes_app.vue'; +import service from '~/notes/services/issue_notes_service'; +import { keyboardDownEvent } from '../../issue_show/helpers'; import * as mockData from '../mock_data'; -fdescribe('issue_note_app', () => { +describe('issue_note_app', () => { let mountComponent; beforeEach(() => { @@ -52,7 +54,7 @@ fdescribe('issue_note_app', () => { }); }); - fdescribe('render', () => { + describe('render', () => { let vm; const responseInterceptor = (request, next) => { @@ -73,8 +75,18 @@ fdescribe('issue_note_app', () => { afterEach(() => { Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); }); - it('should render list of notes', () => { - console.log(vm); + + it('should render list of notes', (done) => { + const note = mockData.discussionResponse[0].notes[0]; + + setTimeout(() => { + expect( + vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(), + ).toEqual(note.author.name); + + expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(note.note_html); + done(); + }, 0); }); it('should render form', () => { @@ -109,28 +121,43 @@ fdescribe('issue_note_app', () => { describe('update note', () => { describe('individual note', () => { - describe('shortup up key', () => { - it('shows correct editing form when user clicks up', () => { + let vm; + + const responseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(mockData.discussionResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(responseInterceptor); + + vm = mountComponent({ + issueData: mockData.issueDataMock, + notesData: mockData.notesDataMock, + userData: mockData.userDataMock, }); + }); - describe('dropdown', () => { - it('renders edit form', () => { - }); + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); + }); + + it('renders edit form', () => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); + }); + }, 0); }); it('updates the note and resets the edit form', () => {}); }); describe('dicussion note note', () => { - describe('shortup up key', () => { - it('shows correct editing form when user clicks up', () => { - }); - }); - - describe('dropdown', () => { - it('renders edit form', () => { - }); + it('renders edit form', () => { }); it('updates the note and resets the edit form', () => {}); -- GitLab From 0c1cf67975f7d4c51da8c1aed59019e026d2e3be Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 9 Aug 2017 00:24:49 +0100 Subject: [PATCH 154/243] [ci skip] Adds tests for issue app --- .../notes/components/issue_comment_form.vue | 5 +- .../notes/components/issue_note_form.vue | 8 +- .../notes/components/issue_notes_app.vue | 6 +- .../notes/components/issue_note_app_spec.js | 222 ++++++++++------- spec/javascripts/notes/mock_data.js | 229 +++++++++++------- 5 files changed, 275 insertions(+), 195 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 751d7578087a..961b3f3c890e 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -175,7 +175,6 @@ this.noteType = type; }, editCurrentUserLastNote() { - console.log('editCurrentUserLastNote') if (this.note === '') { const lastNote = this.getCurrentUserLastNote(window.gon.current_user_id); @@ -254,7 +253,7 @@ :disabled="isSubmitButtonDisabled" name="button" type="button" - class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" + class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" data-toggle="dropdown" aria-label="Open comment type dropdown"> <i @@ -305,7 +304,7 @@ type="submit" v-if="canUpdateIssue" :class="actionButtonClassNames" - class="btn btn-nr btn-comment btn-comment-and-close"> + class="btn btn-comment btn-comment-and-close"> {{issueActionButtonTitle}} </button> <button diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index a4d677a780fb..74fbdb1fd871 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -56,7 +56,7 @@ return this.getNotesDataByProp('markdownDocs'); }, quickActionsDocsUrl() { - return this.getNotesDataByProp('quickActionsDocs'); + return !this.isEditing ? this.getNotesDataByProp('quickActionsDocs') : undefined; }, currentUserId() { return this.getUserDataByProp('id'); @@ -134,15 +134,15 @@ ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." - @keydown.meta.enter="handleUpdate" - @keydown.up="editMyLastNote" + @keydown.meta.enter="handleUpdate()" + @keydown.up="editMyLastNote()" @keydown.esc="cancelHandler(true)"> </textarea> </markdown-field> <div class="note-form-actions clearfix"> <button type="submit" - @click="handleUpdate" + @click="handleUpdate()" :disabled="isDisabled" class="js-vue-issue-save btn btn-save"> {{saveButtonTitle}} diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index adb08f97b22f..8ea4c33c4e78 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -97,11 +97,11 @@ }, checkLocationHash() { const hash = gl.utils.getLocationHash(); - const $el = $(`#${hash}`); + const element = document.getElementById(hash); - if (hash && $el) { + if (hash && element) { this.setTargetNoteHash(hash); - this.scrollToNoteIfNeeded($el); + this.scrollToNoteIfNeeded($(element)); } }, }, diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js index 9b48d3817a6e..084dde29ca84 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -1,23 +1,45 @@ import Vue from 'vue'; import issueNotesApp from '~/notes/components/issue_notes_app.vue'; import service from '~/notes/services/issue_notes_service'; -import { keyboardDownEvent } from '../../issue_show/helpers'; import * as mockData from '../mock_data'; describe('issue_note_app', () => { let mountComponent; + let vm; + + const individualNoteInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), { + status: 200, + })); + }; + + const discussionNoteInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), { + status: 200, + })); + }; beforeEach(() => { const IssueNotesApp = Vue.extend(issueNotesApp); - mountComponent = props => new IssueNotesApp({ - propsData: props, - }).$mount(); + mountComponent = (data) => { + const props = data || { + issueData: mockData.issueDataMock, + notesData: mockData.notesDataMock, + userData: mockData.userDataMock, + }; + + return new IssueNotesApp({ + propsData: props, + }).$mount(); + }; }); - describe('set data', () => { - let vm; + afterEach(() => { + vm.$destroy(); + }); + describe('set data', () => { const responseInterceptor = (request, next) => { next(request.respondWith(JSON.stringify([]), { status: 200, @@ -26,11 +48,7 @@ describe('issue_note_app', () => { beforeEach(() => { Vue.http.interceptors.push(responseInterceptor); - vm = mountComponent({ - issueData: mockData.issueDataMock, - notesData: mockData.notesDataMock, - userData: mockData.userDataMock, - }); + vm = mountComponent(); }); afterEach(() => { @@ -55,29 +73,17 @@ describe('issue_note_app', () => { }); describe('render', () => { - let vm; - - const responseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(mockData.discussionResponse), { - status: 200, - })); - }; - beforeEach(() => { - Vue.http.interceptors.push(responseInterceptor); - vm = mountComponent({ - issueData: mockData.issueDataMock, - notesData: mockData.notesDataMock, - userData: mockData.userDataMock, - }); + Vue.http.interceptors.push(individualNoteInterceptor); + vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); }); it('should render list of notes', (done) => { - const note = mockData.discussionResponse[0].notes[0]; + const note = mockData.individualNoteServerResponse[0].notes[0]; setTimeout(() => { expect( @@ -95,16 +101,17 @@ describe('issue_note_app', () => { vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), ).toEqual('Write a comment or drag your files here...'); }); + + it('should render form comment button as disabled', () => { + expect( + vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'), + ).toEqual('disabled'); + }); }); describe('while fetching data', () => { - let vm; beforeEach(() => { - vm = mountComponent({ - issueData: mockData.issueDataMock, - notesData: mockData.notesDataMock, - userData: mockData.userDataMock, - }); + vm = mountComponent(); }); it('should render loading icon', () => { @@ -121,104 +128,137 @@ describe('issue_note_app', () => { describe('update note', () => { describe('individual note', () => { - let vm; - - const responseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(mockData.discussionResponse), { - status: 200, - })); - }; - beforeEach(() => { - Vue.http.interceptors.push(responseInterceptor); - - vm = mountComponent({ - issueData: mockData.issueDataMock, - notesData: mockData.notesDataMock, - userData: mockData.userDataMock, - }); - + Vue.http.interceptors.push(individualNoteInterceptor); + spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); }); - it('renders edit form', () => { + it('renders edit form', (done) => { setTimeout(() => { vm.$el.querySelector('.js-note-edit').click(); Vue.nextTick(() => { expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); + done(); }); }, 0); }); - it('updates the note and resets the edit form', () => {}); - }); + it('calls the service to update the note', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); - describe('dicussion note note', () => { - it('renders edit form', () => { + expect(service.updateNote).toHaveBeenCalled(); + done(); + }); + }, 0); }); - - it('updates the note and resets the edit form', () => {}); }); - }); - - describe('set target hash', () => { - it('updates the URL when the note date is clicked', () => { - }); + describe('dicussion note', () => { + beforeEach(() => { + Vue.http.interceptors.push(discussionNoteInterceptor); + spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + vm = mountComponent(); + }); - it('stores the correct hash', () => { + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor); + }); - }); + it('renders edit form', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); + done(); + }); + }, 0); + }); - it('updates visually the target note', () => { + it('updates the note and resets the edit form', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + expect(service.updateNote).toHaveBeenCalled(); + done(); + }); + }, 0); + }); }); }); - describe('create new note', () => { - it('should show placeholder note while new comment is being posted', () => {}); - it('should remove placeholder note when new comment is done posting', () => {}); - it('should show actual note element when new comment is done posting', () => {}); - it('should show flash error message when new comment failed to be posted', () => {}); - it('should show flash error message when comment failed to be updated', () => {}); - }); - - describe('quick actions', () => { - it('should return executing quick action description when note has single quick action', () => { + describe('new note form', () => { + beforeEach(() => { + vm = mountComponent(); }); - it('should return generic multiple quick action description when note has multiple quick actions', () => { + it('should render markdown docs url', () => { + const { markdownDocs } = mockData.notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocs}"]`).textContent.trim()).toEqual('Markdown'); }); - it('should return generic quick action description when available quick actions list is not populated', () => { + it('should render quick action docs url', () => { + const { quickActionsDocs } = mockData.notesDataMock; + expect(vm.$el.querySelector(`a[href="${quickActionsDocs}"]`).textContent.trim()).toEqual('quick actions'); }); }); - describe('new note form', () => { - it('should render markdown docs url', () => { - + describe('edit form', () => { + beforeEach(() => { + Vue.http.interceptors.push(individualNoteInterceptor); + vm = mountComponent(); }); - it('should render quick action docs url', () => { - + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); }); - it('should preview markdown', () => { - + it('should render markdown docs url', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + const { markdownDocs } = mockData.notesDataMock; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector(`.edit-note a[href="${markdownDocs}"]`).textContent.trim(), + ).toEqual('Markdown is supported'); + done(); + }); + }, 0); }); - describe('discard draft', () => { - it('should reset form when reset button is clicked', () => { - - }); + it('should not render quick actions docs url', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + const { quickActionsDocs } = mockData.notesDataMock; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector(`.edit-note a[href="${quickActionsDocs}"]`), + ).toEqual(null); + done(); + }); + }, 0); }); }); - describe('edit form', () => { - it('should render markdown docs url', () => {}); - it('should not render quick actions docs url', () => {}); + // TODO: FILIPA + describe('create new note', () => { + it('should show placeholder note while new comment is being posted', () => {}); + it('should remove placeholder note when new comment is done posting', () => {}); + it('should show actual note element when new comment is done posting', () => {}); + it('should show flash error message when new comment failed to be posted', () => {}); + it('should show flash error message when comment failed to be updated', () => {}); }); }); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 000ea84cb9dd..0777d377ac93 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -1,5 +1,4 @@ -/* eslint disable */ - +/* eslint-disable */ export const notesDataMock = { discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', lastFetchedAt: '1501862675', @@ -222,96 +221,138 @@ export const discussionMock = { individual_note: false, }; -export const discussionResponse = [{ - "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "expanded": true, - "notes": [{ - "id": 1390, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-01T17:09:33.762Z", - "updated_at": "2017-08-01T17:09:33.762Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "sdfdsaf", - "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "emoji_awardable": true, - "award_emoji": [{ - "name": "baseball", - "user": { - "id": 1, - "name": "Root", - "username": "root" - } - }, { - "name": "art", - "user": { - "id": 1, - "name": "Root", - "username": "root" - } - }], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1390" - }], - "individual_note": true -}, { - "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "expanded": true, - "notes": [{ - "id": 1391, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-02T10:51:38.685Z", - "updated_at": "2017-08-02T10:51:38.685Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "New note!", - "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1391" - }], - "individual_note": true +export const individualNoteServerResponse = [{ + "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "expanded": true, + "notes": [{ + "id": 1390, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-01T17:09:33.762Z", + "updated_at": "2017-08-01T17:09:33.762Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "sdfdsaf", + "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }, { + "name": "art", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1390" + }], + "individual_note": true + }, { + "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "expanded": true, + "notes": [{ + "id": 1391, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-02T10:51:38.685Z", + "updated_at": "2017-08-02T10:51:38.685Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "New note!", + "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1391" + }], + "individual_note": true }]; + +export const discussionNoteServerResponse = [{ + "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "expanded": true, + "notes": [{ + "id": 1471, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-08T16:53:00.666Z", + "updated_at": "2017-08-08T16:53:00.666Z", + "system": false, + "noteable_id": 124, + "noteable_type": "Issue", + "noteable_iid": 29, + "type": "DiscussionNote", + "human_access": "Owner", + "note": "Adding a comment", + "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1471" + }], + "individual_note": false +}]; \ No newline at end of file -- GitLab From c15174c08692a13fe281e2914490b6b751574e9b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 9 Aug 2017 10:16:29 +0100 Subject: [PATCH 155/243] [ci skip] Adds tests for placeholder note component --- .../notes/components/issue_note_form_spec.js | 1 - .../components/issue_placeholder_note_spec.js | 33 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js index e431598e8852..f41146463f53 100644 --- a/spec/javascripts/notes/components/issue_note_form_spec.js +++ b/spec/javascripts/notes/components/issue_note_form_spec.js @@ -27,7 +27,6 @@ describe('issue_note_form component', () => { }); - describe('keyboard events', () => { describe('up', () => { it('should ender edit mode', () => { diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js index 64d4ed42dfa7..06be26a1a626 100644 --- a/spec/javascripts/notes/components/issue_placeholder_note_spec.js +++ b/spec/javascripts/notes/components/issue_placeholder_note_spec.js @@ -1,32 +1,35 @@ +import Vue from 'vue'; +import issuePlaceholderNote from '~/notes/components/issue_placeholder_note.vue'; +import store from '~/notes/stores'; +import { userDataMock } from '../mock_data'; + describe('issue placeholder system note component', () => { + let vm; + beforeEach(() => { + const Component = Vue.extend(issuePlaceholderNote); + store.dispatch('setUserData', userDataMock); + vm = new Component({ + store, + propsData: { note: { body: 'Foo' } }, + }).$mount(); }); describe('user information', () => { it('should render user avatar with link', () => { - + expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); }); }); describe('note content', () => { it('should render note header information', () => { - + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); }); it('should render note body', () => { - - }); - - it('should render system note placeholder with markdown', () => { - - }); - - it('should render emojis', () => { - - }); - - it('should render slash commands', () => { - + expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo'); }); }); }); -- GitLab From d37881b75f15f407c4078ff98322df97f73199d0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 9 Aug 2017 14:18:43 +0100 Subject: [PATCH 156/243] [ci skip] Adds unit tests for issue_note_header component --- .../notes/components/issue_note_header.vue | 3 +- .../components/issue_note_header_spec.js | 85 ++++++++++++++++++- .../components/issue_placeholder_note_spec.js | 4 + 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index a2a6b1400132..812f0891ad13 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -34,6 +34,7 @@ toggleHandler: { type: Function, required: false, + default: () => {}, }, }, data() { @@ -102,7 +103,7 @@ class="discussion-actions"> <button @click="handleToggle" - class="note-action-button discussion-toggle-button js-toggle-button" + class="note-action-button discussion-toggle-button js-vue-toggle-button" type="button"> <i :class="toggleChevronClass" diff --git a/spec/javascripts/notes/components/issue_note_header_spec.js b/spec/javascripts/notes/components/issue_note_header_spec.js index 31c65ed7f439..83ea18508aea 100644 --- a/spec/javascripts/notes/components/issue_note_header_spec.js +++ b/spec/javascripts/notes/components/issue_note_header_spec.js @@ -1,15 +1,94 @@ +import Vue from 'vue'; +import issueNoteHeader from '~/notes/components/issue_note_header.vue'; +import store from '~/notes/stores'; + describe('issue_note_header component', () => { - it('should render user information', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(issueNoteHeader); + }); + afterEach(() => { + vm.$destroy(); }); - it('should render timestamp link', () => { + describe('individual note', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'commented', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: false, + noteId: 1394, + }, + }).$mount(); + }); + + it('should render user information', () => { + expect( + vm.$el.querySelector('.note-header-author-name').textContent.trim(), + ).toEqual('Root'); + expect( + vm.$el.querySelector('.note-header-info a').getAttribute('href'), + ).toEqual('/root'); + }); + it('should render timestamp link', () => { + expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); + }); }); describe('discussion', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'started a discussion', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: true, + noteId: 1395, + }, + }).$mount(); + }); + it('should render toggle button', () => { + expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); + }); + + it('should toggle the disucssion icon', (done) => { + expect( + vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-up'), + ).toEqual(true); + + vm.$el.querySelector('.js-vue-toggle-button').click(); + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-down'), + ).toEqual(true); + done(); + }); }); }); -}); \ No newline at end of file +}); diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js index 06be26a1a626..6e5275087f3e 100644 --- a/spec/javascripts/notes/components/issue_placeholder_note_spec.js +++ b/spec/javascripts/notes/components/issue_placeholder_note_spec.js @@ -15,6 +15,10 @@ describe('issue placeholder system note component', () => { }).$mount(); }); + afterEach(() => { + vm.$destroy(); + }); + describe('user information', () => { it('should render user avatar with link', () => { expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); -- GitLab From 18091353f4055fb8e7e9fb0839bcb941e7ba79fa Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 9 Aug 2017 14:30:23 +0100 Subject: [PATCH 157/243] [ci skip] Adds tests for issue_note_edited_text component --- .../components/issue_note_edited_text.vue | 3 +- .../components/issue_note_edited_text_spec.js | 40 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue index bea013419eca..315fa61cfcc9 100644 --- a/app/assets/javascripts/notes/components/issue_note_edited_text.vue +++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue @@ -2,6 +2,7 @@ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { + name: 'editedNoteText', props: { actionText: { type: String, @@ -37,7 +38,7 @@ by <a :href="editedBy.path" - class="author_link"> + class="js-vue-author author_link"> {{editedBy.name}} </a> </div> diff --git a/spec/javascripts/notes/components/issue_note_edited_text_spec.js b/spec/javascripts/notes/components/issue_note_edited_text_spec.js index f7b4c49dfbe9..6603241eb64f 100644 --- a/spec/javascripts/notes/components/issue_note_edited_text_spec.js +++ b/spec/javascripts/notes/components/issue_note_edited_text_spec.js @@ -1,17 +1,47 @@ +import Vue from 'vue'; +import issueNoteEditedText from '~/notes/components/issue_note_edited_text.vue'; + describe('issue_note_edited_text', () => { - it('should render block with provided className', () => { + let vm; + let props; - }); + beforeEach(() => { + const Component = Vue.extend(issueNoteEditedText); + props = { + actionText: 'Edited', + className: 'foo-bar', + editedAt: '2017-08-04T09:52:31.062Z', + editedBy: { + avatar_url: 'path', + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + }; - it('should render provided actionText', () => { + vm = new Component({ + propsData: props, + }).$mount(); + }); + afterEach(() => { + vm.$destroy(); }); - it('should render time ago with provided timestamp', () => { + it('should render block with provided className', () => { + expect(vm.$el.className).toEqual(props.className); + }); + it('should render provided actionText', () => { + expect(vm.$el.textContent).toContain(props.actionText); }); it('should render provided user information', () => { + const authorLink = vm.$el.querySelector('.js-vue-author'); + expect(authorLink.getAttribute('href')).toEqual(props.editedBy.path); + expect(authorLink.textContent.trim()).toEqual(props.editedBy.name); }); -}); \ No newline at end of file +}); -- GitLab From 02c66dadaec447d9e031d4370f95a14b51186c42 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 9 Aug 2017 14:38:21 +0100 Subject: [PATCH 158/243] [ci skip] Adds unit tests for issue_note_signed_out_widget component --- .../issue_note_signed_out_widget_spec.js | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js index f7fe500c8535..f20d9ce92683 100644 --- a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js +++ b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js @@ -1,9 +1,37 @@ +import Vue from 'vue'; +import issueNoteSignedOut from '~/notes/components/issue_note_signed_out_widget.vue'; +import store from '~/notes/stores'; +import { notesDataMock } from '../mock_data'; + describe('issue_note_signed_out_widget component', () => { - it('should render sign in link provided in the store', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issueNoteSignedOut); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + }).$mount(); + }); + afterEach(() => { + vm.$destroy(); + }); + + it('should render sign in link provided in the store', () => { + expect( + vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent, + ).toEqual('sign in'); }); it('should render register link provided in the store', () => { + expect( + vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent, + ).toEqual('register'); + }); + it('should render information text', () => { + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); }); }); -- GitLab From 3372ae8b51a15db59bb12a3653215d200aea10d1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 9 Aug 2017 15:08:37 +0100 Subject: [PATCH 159/243] [ci skip] Adds unit tests for issue_system_note component --- .../components/issue_system_note_spec.js | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/spec/javascripts/notes/components/issue_system_note_spec.js b/spec/javascripts/notes/components/issue_system_note_spec.js index 779de4ab6575..c317ce327167 100644 --- a/spec/javascripts/notes/components/issue_system_note_spec.js +++ b/spec/javascripts/notes/components/issue_system_note_spec.js @@ -1,17 +1,53 @@ +import Vue from 'vue'; +import issueSystemNote from '~/notes/components/issue_system_note.vue'; +import store from '~/notes/stores'; + describe('issue system note', () => { - it('should render a list item with correct id', () => { + let vm; + let props; + + beforeEach(() => { + props = { + note: { + id: 1424, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'path', + path: '/root', + }, + note_html: '<p dir="auto">closed</p>', + system_note_icon_name: 'icon_status_closed', + created_at: '2017-08-02T10:51:58.559Z', + }, + }; + store.dispatch('setTargetNoteHash', `note_${props.note.id}`); + + const Component = Vue.extend(issueSystemNote); + vm = new Component({ + store, + propsData: props, + }).$mount(); }); - it('should render target class is note is target note', () => { + it('should render a list item with correct id', () => { + expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); + }); + it('should render target class is note is target note', () => { + expect(vm.$el.classList).toContain('target'); }); it('should render svg icon', () => { - + expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); }); it('should render note header component', () => { - + expect( + vm.$el.querySelector('.system-note-message').innerHTML, + ).toEqual(props.note.note_html); }); }); -- GitLab From 16b15a11761ca93377f536a744f21fcd19fc78bf Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 9 Aug 2017 20:13:00 +0100 Subject: [PATCH 160/243] [ci skip] Adds unit tests for issue comment form spec --- .../notes/components/issue_comment_form.vue | 2 +- .../notes/components/issue_note_actions.vue | 4 +- .../components/issue_comment_form_spec.js | 106 ++++++++++++------ .../notes/components/issue_discussion_spec.js | 3 +- .../components/issue_note_actions_spec.js | 2 +- .../components/issue_note_awards_list_spec.js | 2 +- .../notes/components/issue_note_body_spec.js | 2 +- .../notes/components/issue_note_form_spec.js | 3 +- .../notes/components/issue_note_spec.js | 2 +- spec/javascripts/notes/mock_data.js | 41 +++++++ 10 files changed, 122 insertions(+), 45 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 961b3f3c890e..acb15a7c601d 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -204,7 +204,7 @@ <div> <issue-note-signed-out-widget v-if="!isLoggedIn" /> <ul - v-if="isLoggedIn" + v-else class="notes notes-form timeline new-note"> <li class="timeline-entry" ref="commentForm"> <div class="timeline-entry-inner"> diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 638362f4588d..892787df45a0 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -74,8 +74,8 @@ }, onDelete() { this.$emit('deleteHandler'); - } - } + }, + }, }; </script> diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index 389ccd6e8863..deaafb5e8a9e 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -1,86 +1,124 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueCommentForm from '~/notes/components/issue_comment_form.vue'; +import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data'; +import { keyboardDownEvent } from '../../issue_show/helpers'; + describe('issue_comment_form component', () => { + let vm; + const Component = Vue.extend(issueCommentForm); + let mountComponent; + + beforeEach(() => { + mountComponent = () => new Component({ + store, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); describe('user is logged in', () => { - it('should render user avatar with link', () => { + beforeEach(() => { + store.dispatch('setUserData', userDataMock); + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = mountComponent(); + }); + it('should render user avatar with link', () => { + expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); }); describe('textarea', () => { it('should render textarea with placeholder', () => { - + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); }); it('should support quick actions', () => { - + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), + ).toEqual('true'); }); it('should link to markdown docs', () => { - + const { markdownDocs } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocs}"]`).textContent.trim()).toEqual('Markdown'); }); it('should link to quick actions docs', () => { - + const { quickActionsDocs } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${quickActionsDocs}"]`).textContent.trim()).toEqual('quick actions'); }); describe('edit mode', () => { it('should enter edit mode when arrow up is pressed', () => { + spyOn(vm, 'editCurrentUserLastNote').and.callThrough(); + vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; + vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(38, true)); - }); - }); - - describe('preview mode', () => { - it('should be possible to preview the note', () => { - + expect(vm.editCurrentUserLastNote).toHaveBeenCalled(); }); }); describe('event enter', () => { it('should save note when cmd/ctrl+enter is pressed', () => { + spyOn(vm, 'handleSave').and.callThrough(); + vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; + vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); + expect(vm.handleSave).toHaveBeenCalled(); }); }); }); describe('actions', () => { - describe('with empty note', () => { - it('should render dropdown as disabled', () => { - - }); + it('should be possible to close the issue', () => { + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close issue'); }); - describe('with note', () => { - it('should render enabled dropdown with 2 actions', () => { - - }); - - it('should render be possible to discard draft', () => { - - }); + it('should render comment button as disabled', () => { + expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual('disabled'); }); - describe('with open issue', () => { - it('should be possible to close the issue', () => { - + it('should enable comment button if it has note', (done) => { + vm.note = 'Foo'; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(null); + done(); }); }); - describe('with closed issue', () => { - it('should be possible to reopen the issue', () => { - + it('should update buttons texts when it has note', (done) => { + vm.note = 'Foo'; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Comment & close issue'); + expect(vm.$el.querySelector('.js-note-discard')).toBeDefined(); + done(); }); }); }); - - }); describe('user is not logged in', () => { - it('should render signed out widget', () => { + beforeEach(() => { + store.dispatch('setUserData', null); + store.dispatch('setIssueData', loggedOutIssueData); + store.dispatch('setNotesData', notesDataMock); + vm = mountComponent(); }); - it('should not render submission form', () => { + it('should render signed out widget', () => { + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + }); + it('should not render submission form', () => { + expect(vm.$el.querySelector('textarea')).toEqual(null); }); }); -}); \ No newline at end of file +}); diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js index 3d6b45d2dade..9bc1ec372506 100644 --- a/spec/javascripts/notes/components/issue_discussion_spec.js +++ b/spec/javascripts/notes/components/issue_discussion_spec.js @@ -1,5 +1,4 @@ describe('issue_discussion component', () => { - it('should render user avatar', () => { }); @@ -41,4 +40,4 @@ describe('issue_discussion component', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/spec/javascripts/notes/components/issue_note_actions_spec.js b/spec/javascripts/notes/components/issue_note_actions_spec.js index f23c73dd73fc..eb3dc691f18e 100644 --- a/spec/javascripts/notes/components/issue_note_actions_spec.js +++ b/spec/javascripts/notes/components/issue_note_actions_spec.js @@ -32,4 +32,4 @@ describe('issse_note_actions component', () => { }); }); -}); \ No newline at end of file +}); diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/issue_note_awards_list_spec.js index 8620932004de..82d6f677feaa 100644 --- a/spec/javascripts/notes/components/issue_note_awards_list_spec.js +++ b/spec/javascripts/notes/components/issue_note_awards_list_spec.js @@ -10,4 +10,4 @@ describe('issue_note_awards_list component', () => { it('should be possible to add new emoji', () => { }); -}); \ No newline at end of file +}); diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/issue_note_body_spec.js index 1c0d07af644f..5061b97e9a6a 100644 --- a/spec/javascripts/notes/components/issue_note_body_spec.js +++ b/spec/javascripts/notes/components/issue_note_body_spec.js @@ -14,4 +14,4 @@ describe('issue_note_body component', () => { it('should render awards list', () => { }); -}); \ No newline at end of file +}); diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js index f41146463f53..678374de60ad 100644 --- a/spec/javascripts/notes/components/issue_note_form_spec.js +++ b/spec/javascripts/notes/components/issue_note_form_spec.js @@ -1,5 +1,4 @@ describe('issue_note_form component', () => { - describe('conflicts editing', () => { it('should show conflict message if note changes outside the component', () => { @@ -61,4 +60,4 @@ describe('issue_note_form component', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js index 6ec81a5f5922..69846a8038bd 100644 --- a/spec/javascripts/notes/components/issue_note_spec.js +++ b/spec/javascripts/notes/components/issue_note_spec.js @@ -14,4 +14,4 @@ describe('issue_note', () => { it('should render issue body', () => { }); -}); \ No newline at end of file +}); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 0777d377ac93..795d67a24a04 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -221,6 +221,47 @@ export const discussionMock = { individual_note: false, }; +export const loggedOutIssueData = { + "id": 98, + "iid": 26, + "author_id": 1, + "description": "", + "lock_version": 1, + "milestone_id": null, + "state": "opened", + "title": "asdsa", + "updated_by_id": 1, + "created_at": "2017-02-07T10:11:18.395Z", + "updated_at": "2017-08-08T10:22:51.564Z", + "deleted_at": null, + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null, + "milestone": null, + "labels": [], + "branch_name": null, + "confidential": false, + "assignees": [{ + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "web_url": "http://localhost:3000/root" + }], + "due_date": null, + "moved_to_id": null, + "project_id": 2, + "web_url": "/gitlab-org/gitlab-ce/issues/26", + "current_user": { + "can_create_note": false, + "can_update": false + }, + "create_note_path": "/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue", + "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue" +} + export const individualNoteServerResponse = [{ "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", -- GitLab From 3e0a76dae605a5f83b0bec3e3531b78b0f7cdfd0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 12:25:08 +0100 Subject: [PATCH 161/243] [ci skip] Fix async bug of note being updated at the same time --- .../javascripts/notes/components/issue_note_form.vue | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 74fbdb1fd871..0c16911978b1 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -22,6 +22,7 @@ discussion: { type: Object, required: false, + default: () => ({}), }, isEditing: { type: Boolean, @@ -30,7 +31,6 @@ }, data() { return { - initialNote: this.noteBody, note: this.noteBody, conflictWhileEditing: false, isSubmitting: false, @@ -72,10 +72,7 @@ }, editMyLastNote() { if (this.note === '') { - const lastNoteInDiscussion = this.getDiscussionLastNote( - this.discussion, - this.currentUserId, - ); + const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion); if (lastNoteInDiscussion) { eventHub.$emit('enterEditMode', { @@ -86,7 +83,7 @@ }, cancelHandler(shouldConfirm = false) { // Sends information about confirm message and if the textarea has changed - this.$emit('cancelFormEdition', shouldConfirm, this.initialNote !== this.note); + this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); }, }, mounted() { @@ -94,7 +91,7 @@ }, watch: { noteBody() { - if (this.note === this.initialNote) { + if (this.note === this.noteBody) { this.note = this.noteBody; } else { this.conflictWhileEditing = true; @@ -157,3 +154,4 @@ </form> </div> </template> + -- GitLab From c438106732539d37fcb524abfe4b78e177f81691 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 12:25:59 +0100 Subject: [PATCH 162/243] [ci skip] Improve performance of getting last note --- app/assets/javascripts/notes/stores/getters.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 386e5e2050d8..18308d338c02 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export const notes = state => state.notes; export const targetNoteHash = state => state.targetNoteHash; @@ -16,15 +18,11 @@ export const notesById = state => state.notes.reduce((acc, note) => { }, {}); const reverseNotes = array => array.slice(0).reverse(); -const isLastNote = (note, userId) => !note.system && note.author.id === userId; - -export const getCurrentUserLastNote = state => userId => reverseNotes(state.notes) - .reduce((acc, note) => { - acc.push(reverseNotes(note.notes).find(el => isLastNote(el, userId))); - return acc; - }, []).filter(el => el !== undefined)[0]; +const isLastNote = (note, state) => !note.system && note.author.id === state.userData.id; -// eslint-disable-next-line no-unused-vars -export const getDiscussionLastNote = state => (discussion, userId) => reverseNotes(discussion.notes) - .find(el => isLastNote(el, userId)); +export const getCurrentUserLastNote = state => _.flatten(reverseNotes(state.notes) + .map(note => note.notes)) + .find(el => isLastNote(el, state)); +export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) + .find(el => isLastNote(el, state)); -- GitLab From 3158fa7e89107b2fdbd5c81892a9ec17918464a7 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 12:26:29 +0100 Subject: [PATCH 163/243] [ci skip] Improve code as per code reviews --- .../notes/components/issue_comment_form.vue | 21 +++++++++---------- .../notes/components/issue_discussion.vue | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index acb15a7c601d..5b2bfeb0043c 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -29,18 +29,10 @@ }, watch: { note(newNote) { - if (!_.isEmpty(newNote) && !this.isSubmitting) { - this.isSubmitButtonDisabled = false; - } else { - this.isSubmitButtonDisabled = true; - } + this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); }, isSubmitting(newValue) { - if (!_.isEmpty(this.note) && !newValue) { - this.isSubmitButtonDisabled = false; - } else { - this.isSubmitButtonDisabled = true; - } + this.setIsSubmitButtonDisabled(this.note, newValue); }, }, computed: { @@ -99,6 +91,13 @@ ...mapActions([ 'saveNote', ]), + setIsSubmitButtonDisabled(note, isSubmitting) { + if (!_.isEmpty(note) && !isSubmitting) { + this.isSubmitButtonDisabled = false; + } else { + this.isSubmitButtonDisabled = true; + } + }, handleSave(withIssueAction) { if (this.note.length) { const noteData = { @@ -176,7 +175,7 @@ }, editCurrentUserLastNote() { if (this.note === '') { - const lastNote = this.getCurrentUserLastNote(window.gon.current_user_id); + const lastNote = this.getCurrentUserLastNote; if (lastNote) { eventHub.$emit('enterEditMode', { diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index d6dd0be55ace..b806f1bfc01f 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -183,7 +183,7 @@ title="Add a reply">Reply...</button> <issue-note-form v-if="isReplying" - saveButtonTitle="Comment" + save-button-title="Comment" :discussion="note" :is-editing="false" @handleFormUpdate="saveReply" -- GitLab From f62939e7e0e8252b84069772e6592dd07e468af3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 12:37:21 +0100 Subject: [PATCH 164/243] [ci skip] Fallback to empty object if no user is signed in --- app/assets/javascripts/notes/components/issue_note_form.vue | 1 - app/assets/javascripts/notes/stores/getters.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 0c16911978b1..ab8582bb9264 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -154,4 +154,3 @@ </form> </div> </template> - diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 18308d338c02..21f8cd548228 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -9,7 +9,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getIssueData = state => state.issueData; export const getIssueDataByProp = state => prop => state.issueData[prop]; -export const getUserData = state => state.userData; +export const getUserData = state => state.userData || {}; export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const notesById = state => state.notes.reduce((acc, note) => { -- GitLab From f67ff8e23eed62689ed5e6195b01e5810149742f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 12:49:08 +0100 Subject: [PATCH 165/243] [ci skip] Removes use of window object in all components Improves performance by removing emojis from data object --- .../notes/components/issue_note.vue | 4 +- .../components/issue_note_awards_list.vue | 37 +++++---- app/assets/javascripts/notes/index.js | 78 ++++++++----------- 3 files changed, 54 insertions(+), 65 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 3b0c684f2a15..c44f735d5e7e 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -19,7 +19,6 @@ return { isEditing: false, isDeleting: false, - currentUserId: window.gon.current_user_id, }; }, components: { @@ -31,6 +30,7 @@ computed: { ...mapGetters([ 'targetNoteHash', + 'getUserData', ]), author() { return this.note.author; @@ -43,7 +43,7 @@ }; }, canReportAsAbuse() { - return this.note.report_abuse_path && this.author.id !== this.currentUserId; + return this.note.report_abuse_path && this.author.id !== this.getUserData.id; }, noteAnchorId() { return `note_${this.note.id}`; diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 936be4523c86..e5ef071fdb87 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -1,7 +1,7 @@ <script> /* global Flash */ - import { mapActions } from 'vuex'; + import { mapActions, mapGetters } from 'vuex'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; @@ -30,18 +30,10 @@ directives: { tooltip, }, - data() { - const userId = window.gon.current_user_id; - - return { - emojiSmiling, - emojiSmile, - emojiSmiley, - canAward: !!userId, - myUserId: userId, - }; - }, computed: { + ...mapGetters([ + 'getUserData', + ]), // `this.awards` is an array with emojis but they are not grouped by emoji name. See below. // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ] // This method will group emojis by their name as an Object. See below. @@ -76,7 +68,10 @@ return Object.assign({}, orderedAwards, awards); }, isAuthoredByMe() { - return this.noteAuthorId === window.gon.current_user_id; + return this.noteAuthorId === this.getUserData.id; + }, + isLoggedIn() { + return this.getUserData.id; }, }, methods: { @@ -95,17 +90,16 @@ canInteractWithEmoji(awardList, awardName) { let isAllowed = true; const restrictedEmojis = ['thumbsup', 'thumbsdown']; - const { myUserId, noteAuthorId } = this; // Users can not add :+1: and :-1: to their own notes - if (myUserId === noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) { + if (this.getUserData.id === this.noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) { isAllowed = false; } - return this.canAward && isAllowed; + return this.getUserData.id && isAllowed; }, hasReactionByCurrentUser(awardList) { - return awardList.filter(award => award.user.id === this.myUserId).length; + return awardList.filter(award => award.user.id === this.getUserData.id).length; }, awardTitle(awardsList) { const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); @@ -114,7 +108,7 @@ // Filter myself from list if I am awarded. if (hasReactionByCurrentUser) { - awardList = awardList.filter(award => award.user.id !== this.myUserId); + awardList = awardList.filter(award => award.user.id !== this.getUserData.id); } // Get only 9-10 usernames to show in tooltip text. @@ -157,6 +151,11 @@ .catch(() => Flash('Something went wrong on our end.')); }, }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = this.emojiSmile; + this.emojiSmiley = this.emojiSmiley; + }, }; </script> @@ -179,7 +178,7 @@ </span> </button> <div - v-if="canAward" + v-if="isLoggedIn" class="award-menu-holder"> <button v-tooltip diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 6ae4a7cb45eb..af2e1ef0731f 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,48 +1,38 @@ import Vue from 'vue'; import issueNotesApp from './components/issue_notes_app.vue'; -document.addEventListener('DOMContentLoaded', () => { - const vm = new Vue({ - el: '#js-vue-notes', - components: { - issueNotesApp, - }, - data() { - const notesDataset = document.getElementById('js-vue-notes').dataset; - - return { - issueData: JSON.parse(notesDataset.issueData), - currentUserData: JSON.parse(notesDataset.currentUserData), - notesData: { - lastFetchedAt: notesDataset.lastFetchedAt, - discussionsPath: notesDataset.discussionsPath, - newSessionPath: notesDataset.newSessionPath, - registerPath: notesDataset.registerPath, - notesPath: notesDataset.notesPath, - markdownDocs: notesDataset.markdownDocs, - quickActionsDocs: notesDataset.quickActionsDocs, - }, - }; - }, - render(createElement) { - return createElement('issue-notes-app', { - attrs: { - ref: 'notes', - }, - props: { - issueData: this.issueData, - notesData: this.notesData, - userData: this.currentUserData, - }, - }); - }, - }); - - // This is used in note_polling_spec - window.issueNotes = { - refresh() { - vm.$refs.notes.$store.dispatch('poll'); - }, - }; -}); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#js-vue-notes', + components: { + issueNotesApp, + }, + data() { + const notesDataset = document.getElementById('js-vue-notes').dataset; + return { + issueData: JSON.parse(notesDataset.issueData), + currentUserData: JSON.parse(notesDataset.currentUserData), + notesData: { + lastFetchedAt: notesDataset.lastFetchedAt, + discussionsPath: notesDataset.discussionsPath, + newSessionPath: notesDataset.newSessionPath, + registerPath: notesDataset.registerPath, + notesPath: notesDataset.notesPath, + markdownDocs: notesDataset.markdownDocs, + quickActionsDocs: notesDataset.quickActionsDocs, + }, + }; + }, + render(createElement) { + return createElement('issue-notes-app', { + attrs: { + ref: 'notes', + }, + props: { + issueData: this.issueData, + notesData: this.notesData, + userData: this.currentUserData, + }, + }); + }, +})); -- GitLab From 3008e9956222b8765ec2d7852c61da19a3933c8f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 14:08:57 +0100 Subject: [PATCH 166/243] Fixes broken tests of quick_submit_spec and reduces tech debt --- .../behaviors/quick_submit_spec.js | 189 +++++++++--------- 1 file changed, 90 insertions(+), 99 deletions(-) diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 6dc48f9a2935..2f8d4ea92e43 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,119 +1,110 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */ - import '~/behaviors/quick_submit'; -(function() { - describe('Quick Submit behavior', function() { - var keydownEvent; - preloadFixtures('issues/open-issue.html.raw'); - beforeEach(function() { - loadFixtures('issues/open-issue.html.raw'); - $('form').submit(function(e) { - // Prevent a form submit from moving us off the testing page - return e.preventDefault(); - }); - this.spies = { - submit: spyOnEvent('form', 'submit') - }; +describe('Quick Submit behavior', () => { + const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options); - this.textarea = $('.js-quick-submit textarea').first(); - }); - it('does not respond to other keyCodes', function() { - this.textarea.trigger(keydownEvent({ - keyCode: 32 - })); - return expect(this.spies.submit).not.toHaveBeenTriggered(); - }); - it('does not respond to Enter alone', function() { - this.textarea.trigger(keydownEvent({ - ctrlKey: false, - metaKey: false - })); - return expect(this.spies.submit).not.toHaveBeenTriggered(); - }); - it('does not respond to repeated events', function() { - this.textarea.trigger(keydownEvent({ - repeat: true - })); - return expect(this.spies.submit).not.toHaveBeenTriggered(); - }); - it('disables input of type submit', function() { - const submitButton = $('.js-quick-submit input[type=submit]'); - this.textarea.trigger(keydownEvent()); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - expect(submitButton).toBeDisabled(); + beforeEach(() => { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + $('form').submit((e) => { + // Prevent a form submit from moving us off the testing page + e.preventDefault(); }); - it('disables button of type submit', function() { - const submitButton = $('.js-quick-submit input[type=submit]'); - this.textarea.trigger(keydownEvent()); + this.spies = { + submit: spyOnEvent('form', 'submit'), + }; - expect(submitButton).toBeDisabled(); - }); - it('only clicks one submit', function() { - const existingSubmit = $('.js-quick-submit input[type=submit]'); - // Add an extra submit button - const newSubmit = $('<button type="submit">Submit it</button>'); - newSubmit.insertAfter(this.textarea); + this.textarea = $('.js-quick-submit textarea').first(); + }); - const oldClick = spyOnEvent(existingSubmit, 'click'); - const newClick = spyOnEvent(newSubmit, 'click'); + it('does not respond to other keyCodes', () => { + this.textarea.trigger(keydownEvent({ + keyCode: 32, + })); + expect(this.spies.submit).not.toHaveBeenTriggered(); + }); - this.textarea.trigger(keydownEvent()); + it('does not respond to Enter alone', () => { + this.textarea.trigger(keydownEvent({ + ctrlKey: false, + metaKey: false, + })); + expect(this.spies.submit).not.toHaveBeenTriggered(); + }); - expect(oldClick).not.toHaveBeenTriggered(); - expect(newClick).toHaveBeenTriggered(); - }); - // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll - // only run the tests that apply to the current platform - if (navigator.userAgent.match(/Macintosh/)) { - it('responds to Meta+Enter', function() { - this.textarea.trigger(keydownEvent()); - return expect(this.spies.submit).toHaveBeenTriggered(); - }); - it('excludes other modifier keys', function() { - this.textarea.trigger(keydownEvent({ - altKey: true - })); - this.textarea.trigger(keydownEvent({ - ctrlKey: true - })); - this.textarea.trigger(keydownEvent({ - shiftKey: true - })); - return expect(this.spies.submit).not.toHaveBeenTriggered(); - }); - } else { - it('responds to Ctrl+Enter', function() { + it('does not respond to repeated events', () => { + this.textarea.trigger(keydownEvent({ + repeat: true, + })); + expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + + it('disables input of type submit', () => { + const submitButton = $('.js-quick-submit input[type=submit]'); + this.textarea.trigger(keydownEvent()); + + expect(submitButton).toBeDisabled(); + }); + it('disables button of type submit', () => { + const submitButton = $('.js-quick-submit input[type=submit]'); + this.textarea.trigger(keydownEvent()); + + expect(submitButton).toBeDisabled(); + }); + it('only clicks one submit', () => { + const existingSubmit = $('.js-quick-submit input[type=submit]'); + // Add an extra submit button + const newSubmit = $('<button type="submit">Submit it</button>'); + newSubmit.insertAfter(this.textarea); + + const oldClick = spyOnEvent(existingSubmit, 'click'); + const newClick = spyOnEvent(newSubmit, 'click'); + + this.textarea.trigger(keydownEvent()); + + expect(oldClick).not.toHaveBeenTriggered(); + expect(newClick).toHaveBeenTriggered(); + }); + // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll + // only run the tests that apply to the current platform + if (navigator.userAgent.match(/Macintosh/)) { + describe('In Macintosh', () => { + it('responds to Meta+Enter', () => { this.textarea.trigger(keydownEvent()); return expect(this.spies.submit).toHaveBeenTriggered(); }); - it('excludes other modifier keys', function() { + + it('excludes other modifier keys', () => { this.textarea.trigger(keydownEvent({ - altKey: true + altKey: true, })); this.textarea.trigger(keydownEvent({ - metaKey: true + ctrlKey: true, })); this.textarea.trigger(keydownEvent({ - shiftKey: true + shiftKey: true, })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); - } - return keydownEvent = function(options) { - var defaults; - if (navigator.userAgent.match(/Macintosh/)) { - defaults = { - keyCode: 13, - metaKey: true - }; - } else { - defaults = { - keyCode: 13, - ctrlKey: true - }; - } - return $.Event('keydown', $.extend({}, defaults, options)); - }; - }); -}).call(window); + }); + } else { + it('responds to Ctrl+Enter', () => { + this.textarea.trigger(keydownEvent()); + return expect(this.spies.submit).toHaveBeenTriggered(); + }); + + it('excludes other modifier keys', () => { + this.textarea.trigger(keydownEvent({ + altKey: true, + })); + this.textarea.trigger(keydownEvent({ + metaKey: true, + })); + this.textarea.trigger(keydownEvent({ + shiftKey: true, + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + } +}); -- GitLab From d323bb79ebc84751cc0a27e99db7ae1c2ec64fd8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 16:48:22 +0100 Subject: [PATCH 167/243] Change fixtures of old tests --- .../notes/components/issue_notes_app.vue | 11 +- spec/features/issues/note_polling_spec.rb | 2 - spec/javascripts/awards_handler_spec.js | 8 +- spec/javascripts/fixtures/merge_requests.rb | 5 + spec/javascripts/notes_spec.js | 8 +- spec/javascripts/shortcuts_issuable_spec.js | 124 +++++++++--------- spec/javascripts/shortcuts_spec.js | 2 +- 7 files changed, 80 insertions(+), 80 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 8ea4c33c4e78..77e2ae4dc1f3 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -79,16 +79,18 @@ return note.individual_note ? note.notes[0] : note; }, fetchNotes() { - this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) + return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) .then(() => { - this.isLoading = false; - // Scroll to note if we have hash fragment in the page URL this.$nextTick(() => { this.checkLocationHash(); }); }) - .catch(() => Flash('Something went wrong while fetching issue comments. Please try again.')); + .catch(() => Flash('Something went wrong while fetching issue comments. Please try again.')) + .then(() => { + this.isLoading = false; + this.initPolling(); + }); }, initPolling() { this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); @@ -112,7 +114,6 @@ }, mounted() { this.fetchNotes(); - this.initPolling(); const parentElement = this.$el.parentElement; if (parentElement && diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 3b738b856a72..b2f5c7e62a60 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -13,7 +13,6 @@ it 'displays the new comment' do note = create(:note, noteable: issue, project: project, note: 'Looks good!') - page.execute_script('issueNotes.refresh();') wait_for_requests expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') @@ -115,7 +114,6 @@ def update_note(note, new_text) note.update(note: new_text) - page.execute_script('issueNotes.refresh();') wait_for_requests end diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index c90970b7ba1c..a22b71fd1dcf 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -25,10 +25,10 @@ import '~/lib/utils/common_utils'; }; describe('AwardsHandler', function() { - preloadFixtures('issues/issue_with_comment.html.raw'); + preloadFixtures('merge_requests/diff_comment.html.raw'); beforeEach(function(done) { - loadFixtures('issues/issue_with_comment.html.raw'); - $('body').data('page', 'projects:issues:show'); + loadFixtures('merge_requests/diff_comment.html.raw'); + $('body').data('page', 'projects:merge_requests:show'); loadAwardsHandler(true).then((obj) => { awardsHandler = obj; spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); @@ -140,7 +140,7 @@ import '~/lib/utils/common_utils'; }); describe('::getAwardUrl', function() { return it('returns the url for request', function() { - return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); + return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1/toggle_award_emoji'); }); }); describe('::addAward and ::checkMutuality', function() { diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index f9d8b5c569c9..bb70603b4696 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -55,6 +55,11 @@ render_merge_request(example.description, merge_request) end + it 'merge_requests/merge_request_with_comment.html.raw' do |example| + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request, note: '- [ ] Task List Item') + render_merge_request(example.description, merge_request) + end + private def render_merge_request(fixture_file_name, merge_request) diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2c096ed08a8b..bd01d823e68e 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -30,16 +30,16 @@ import '~/notes'; return escapedString; }; - describe('Notes', function() { + fdescribe('Notes', function() { const FLASH_TYPE_ALERT = 'alert'; - var commentsTemplate = 'issues/issue_with_comment.html.raw'; + var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw'; preloadFixtures(commentsTemplate); beforeEach(function () { loadFixtures(commentsTemplate); gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; - $('body').data('page', 'projects:issues:show'); + $('body').data('page', 'projects:merge_requets:show'); }); describe('task lists', function() { @@ -60,7 +60,7 @@ import '~/notes'; it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('http://test.host/frontend-fixtures/issues-project/notes/1'); + expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/notes/3'); return expect(req.data.note).not.toBe(null); }); $('.js-task-list-field').trigger('tasklist:changed'); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 26866b03d017..65da2e8b4068 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,78 +1,74 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */ /* global ShortcutsIssuable */ import '~/copy_as_gfm'; import '~/shortcuts_issuable'; -(function() { - describe('ShortcutsIssuable', function() { - var fixtureName = 'issues/open-issue.html.raw'; - preloadFixtures(fixtureName); - beforeEach(function() { - loadFixtures(fixtureName); - document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); - this.shortcut = new ShortcutsIssuable(); - }); - describe('replyWithSelectedText', function() { - var stubSelection; - // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. - stubSelection = function(html) { - window.gl.utils.getSelectedFragment = function() { - var node = document.createElement('div'); - node.innerHTML = html; - return node; - }; +describe('ShortcutsIssuable', () => { + const fixtureName = 'merge_requests/diff_comment.html.raw'; + preloadFixtures(fixtureName); + beforeEach(() => { + loadFixtures(fixtureName); + document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); + this.shortcut = new ShortcutsIssuable(); + }); + describe('replyWithSelectedText', () => { + // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. + const stubSelection = (html) => { + window.gl.utils.getSelectedFragment = () => { + const node = document.createElement('div'); + node.innerHTML = html; + return node; }; - beforeEach(function() { - this.selector = 'form.js-main-target-form textarea#note-body'; + }; + beforeEach(() => { + this.selector = 'form.js-main-target-form textarea#note-body'; + }); + describe('with empty selection', () => { + it('does not return an error', () => { + this.shortcut.replyWithSelectedText(); + expect($(this.selector).val()).toBe(''); }); - describe('with empty selection', function() { - it('does not return an error', function() { - this.shortcut.replyWithSelectedText(); - expect($(this.selector).val()).toBe(''); - }); - it('triggers `focus`', function() { - this.shortcut.replyWithSelectedText(); - expect(document.activeElement).toBe(document.querySelector(this.selector)); - }); + it('triggers `focus`', () => { + this.shortcut.replyWithSelectedText(); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); - describe('with any selection', function() { - beforeEach(function() { - stubSelection('<p>Selected text.</p>'); - }); - it('leaves existing input intact', function() { - $(this.selector).val('This text was already here.'); - expect($(this.selector).val()).toBe('This text was already here.'); - this.shortcut.replyWithSelectedText(); - expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); - }); - it('triggers `input`', function() { - var triggered = false; - $(this.selector).on('input', function() { - triggered = true; - }); - this.shortcut.replyWithSelectedText(); - expect(triggered).toBe(true); - }); - it('triggers `focus`', function() { - this.shortcut.replyWithSelectedText(); - expect(document.activeElement).toBe(document.querySelector(this.selector)); - }); + }); + describe('with any selection', () => { + beforeEach(() => { + stubSelection('<p>Selected text.</p>'); }); - describe('with a one-line selection', function() { - it('quotes the selection', function() { - stubSelection('<p>This text has been selected.</p>'); - this.shortcut.replyWithSelectedText(); - expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); - }); + it('leaves existing input intact', () => { + $(this.selector).val('This text was already here.'); + expect($(this.selector).val()).toBe('This text was already here.'); + this.shortcut.replyWithSelectedText(); + expect($(this.selector).val()).toBe('This text was already here.\n\n> Selected text.\n\n'); }); - describe('with a multi-line selection', function() { - it('quotes the selected lines as a group', function() { - stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>"); - this.shortcut.replyWithSelectedText(); - expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); + it('triggers `input`', () => { + let triggered = false; + $(this.selector).on('input', () => { + triggered = true; }); + this.shortcut.replyWithSelectedText(); + expect(triggered).toBe(true); + }); + it('triggers `focus`', () => { + this.shortcut.replyWithSelectedText(); + expect(document.activeElement).toBe(document.querySelector(this.selector)); + }); + }); + describe('with a one-line selection', () => { + it('quotes the selection', () => { + stubSelection('<p>This text has been selected.</p>'); + this.shortcut.replyWithSelectedText(); + expect($(this.selector).val()).toBe('> This text has been selected.\n\n'); + }); + }); + describe('with a multi-line selection', () => { + it('quotes the selected lines as a group', () => { + stubSelection('<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>'); + this.shortcut.replyWithSelectedText(); + expect($(this.selector).val()).toBe('> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n'); }); }); }); -}).call(window); +}); diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js index 9b8373df29e7..53e4c68beb31 100644 --- a/spec/javascripts/shortcuts_spec.js +++ b/spec/javascripts/shortcuts_spec.js @@ -1,6 +1,6 @@ /* global Shortcuts */ describe('Shortcuts', () => { - const fixtureName = 'issues/issue_with_comment.html.raw'; + const fixtureName = 'merge_requests/diff_comment.html.raw'; const createEvent = (type, target) => $.Event(type, { target, }); -- GitLab From 2faf28fe1c03802be69651e98d0c57803629126a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 17:27:17 +0100 Subject: [PATCH 168/243] Remove forgotten fdescribe --- .../javascripts/notes/components/issue_comment_form.vue | 2 +- .../javascripts/notes/components/issue_note_awards_list.vue | 4 ++-- features/steps/project/issues/issues.rb | 1 + features/steps/shared/note.rb | 2 ++ spec/javascripts/notes_spec.js | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 5b2bfeb0043c..65183143e5d1 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -92,7 +92,7 @@ 'saveNote', ]), setIsSubmitButtonDisabled(note, isSubmitting) { - if (!_.isEmpty(note) && !isSubmitting) { + if (!_.isEmpty(note) && !isSubmitting) { this.isSubmitButtonDisabled = false; } else { this.isSubmitButtonDisabled = true; diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index e5ef071fdb87..229127db68c9 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -153,8 +153,8 @@ }, created() { this.emojiSmiling = emojiSmiling; - this.emojiSmile = this.emojiSmile; - this.emojiSmiley = this.emojiSmiley; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; }, }; </script> diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 2deef9036d36..b9460f5b5345 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -168,6 +168,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps author: project.users.first, description: "# Description header" ) + wait_for_requests end step 'project "Shop" have "Tweet control" open issue' do diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 80187b83feeb..f53919f41357 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -163,5 +163,7 @@ module SharedNote page.within(".note") do expect(page).to have_content("+1 Awesome!") end + + wait_for_requests end end diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index bd01d823e68e..2b9baa84c0ab 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -30,7 +30,7 @@ import '~/notes'; return escapedString; }; - fdescribe('Notes', function() { + describe('Notes', function() { const FLASH_TYPE_ALERT = 'alert'; var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw'; preloadFixtures(commentsTemplate); -- GitLab From 57f52b9e53a5ddf56d2331dd740870d9bbdc00eb Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 18:25:32 +0100 Subject: [PATCH 169/243] Restore r shortcut --- app/assets/javascripts/shortcuts_issuable.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 0be141eb5f9a..ec33c4738643 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -20,7 +20,7 @@ import './shortcuts_navigation'; Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone')); Mousetrap.bind('r', (function(_this) { return function() { - _this.replyWithSelectedText(); + _this.replyWithSelectedText(isMergeRequest); return false; }; })(this)); @@ -38,9 +38,15 @@ import './shortcuts_navigation'; } } - ShortcutsIssuable.prototype.replyWithSelectedText = function() { + ShortcutsIssuable.prototype.replyWithSelectedText = function(isMergeRequest) { var quote, documentFragment, el, selected, separator; - var replyField = $('.js-main-target-form #note_note'); + let replyField; + + if (isMergeRequest) { + replyField = $('.js-main-target-form #note_note'); + } else { + replyField = $('.js-main-target-form .js-vue-comment-form'); + } documentFragment = window.gl.utils.getSelectedFragment(); if (!documentFragment) { -- GitLab From ea448039c8954e5a1d175f827b0205754dfb6dd2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 18:36:39 +0100 Subject: [PATCH 170/243] Fix broken tests --- .../notes/components/issue_comment_form.vue | 4 ++-- features/steps/shared/note.rb | 7 +++++-- .../notes/components/issue_discussion_spec.js | 21 +++++++++++++++++++ spec/javascripts/zen_mode_spec.js | 2 +- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 65183143e5d1..d4344a0ec744 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -43,7 +43,7 @@ 'getNotesData', ]), isLoggedIn() { - return this.getUserData !== null; + return this.getUserData.id; }, commentButtonTitle() { return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; @@ -92,7 +92,7 @@ 'saveNote', ]), setIsSubmitButtonDisabled(note, isSubmitting) { - if (!_.isEmpty(note) && !isSubmitting) { + if (!_.isEmpty(note) && !isSubmitting) { this.isSubmitButtonDisabled = false; } else { this.isSubmitButtonDisabled = true; diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index f53919f41357..1c3432c3ca17 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -137,7 +137,7 @@ module SharedNote step 'The comment with the header should not have an ID' do page.within(".note-body > .note-text") do - expect(page).to have_content("Comment with a header") + expect(page).to have_content("Comment with a header") expect(page).not_to have_css("#comment-with-a-header") end end @@ -153,10 +153,13 @@ module SharedNote note.find('.js-note-edit').click end + page.find('.current-note-edit-form textarea') + page.within(".current-note-edit-form") do - fill_in 'note[note]', with: '+1 Awesome!' + fill_in '#note-body', with: '+1 Awesome!' click_button 'Save comment' end + wait_for_requests end step 'I should see +1 in the description' do diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js index 9bc1ec372506..85fea7a0c370 100644 --- a/spec/javascripts/notes/components/issue_discussion_spec.js +++ b/spec/javascripts/notes/components/issue_discussion_spec.js @@ -1,5 +1,26 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueDiscussion from '~/notes/components/issue_discussion.vue'; +import { issueDataMock, discussionMock } from '../mock_data'; + describe('issue_discussion component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issueDiscussion); + + store.dispatch('setIssueData', issueDataMock); + + vm = new Component({ + store, + propsData: { + note: discussionMock, + }, + }).$mount(); + }); + it('should render user avatar', () => { + console.log('vm', vm.$el); }); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index a225b04c47e1..bd18f79cea7d 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -8,7 +8,7 @@ import ZenMode from '~/zen_mode'; var enterZen, escapeKeydown, exitZen; describe('ZenMode', function() { - var fixtureName = 'issues/open-issue.html.raw'; + var fixtureName = 'merge_requests/merge_request_with_comment.html.raw'; preloadFixtures(fixtureName); beforeEach(function() { loadFixtures(fixtureName); -- GitLab From e7d8744641f228306bf6c1c992212c877d1ab536 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 10 Aug 2017 19:05:28 +0100 Subject: [PATCH 171/243] Remove issue discussion started spec --- .../notes/components/issue_discussion_spec.js | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js index 85fea7a0c370..cb29b4176ad0 100644 --- a/spec/javascripts/notes/components/issue_discussion_spec.js +++ b/spec/javascripts/notes/components/issue_discussion_spec.js @@ -1,27 +1,6 @@ -import Vue from 'vue'; -import store from '~/notes/stores'; -import issueDiscussion from '~/notes/components/issue_discussion.vue'; -import { issueDataMock, discussionMock } from '../mock_data'; describe('issue_discussion component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(issueDiscussion); - - store.dispatch('setIssueData', issueDataMock); - - vm = new Component({ - store, - propsData: { - note: discussionMock, - }, - }).$mount(); - }); - it('should render user avatar', () => { - console.log('vm', vm.$el); - }); it('should render discussion header', () => { -- GitLab From 106f0df394c375673ab963c953c8263d254855a3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 00:13:12 +0100 Subject: [PATCH 172/243] Fix broken spec for issuable shortcuts and notes --- .../notes/components/issue_comment_form.vue | 12 +++++++++++- .../notes/components/issue_notes_app.vue | 4 ++-- app/assets/javascripts/shortcuts_issuable.js | 1 + .../notes/components/issue_note_app_spec.js | 4 ++++ spec/javascripts/notes_spec.js | 9 +++++---- spec/javascripts/shortcuts_issuable_spec.js | 18 +++++++++--------- 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index d4344a0ec744..e326c849a21b 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -11,6 +11,7 @@ import '../../autosave'; export default { + name: 'issueCommentForm', data() { return { note: '', @@ -92,7 +93,7 @@ 'saveNote', ]), setIsSubmitButtonDisabled(note, isSubmitting) { - if (!_.isEmpty(note) && !isSubmitting) { + if (!_.isEmpty(note) && !isSubmitting) { this.isSubmitButtonDisabled = false; } else { this.isSubmitButtonDisabled = true; @@ -187,6 +188,13 @@ initAutoSave() { this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id]); }, + initTaskList() { + return new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes', + }); + } }, mounted() { // jQuery is needed here because it is a custom event being dispatched with jQuery. @@ -195,6 +203,7 @@ }); this.initAutoSave(); + this.initTaskList(); }, }; </script> @@ -227,6 +236,7 @@ :quick-actions-docs="quickActionsDocsUrl" :add-spacing-classes="false"> <textarea + id="note-body" name="note[note]" class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area" data-supports-quick-actions="true" diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 77e2ae4dc1f3..76c5e19cd280 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -1,6 +1,5 @@ <script> /* global Flash */ - import { mapGetters, mapActions } from 'vuex'; import store from '../stores/'; import * as constants from '../constants'; @@ -13,7 +12,7 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - name: 'IssueNotes', + name: 'issueNotesApp', props: { issueData: { type: Object, @@ -114,6 +113,7 @@ }, mounted() { this.fetchNotes(); + const parentElement = this.$el.parentElement; if (parentElement && diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index ec33c4738643..14997fe30e9c 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -63,6 +63,7 @@ import './shortcuts_navigation'; quote = _.map(selected.split("\n"), function(val) { return ("> " + val).trim() + "\n"; }); + // If replyField already has some content, add a newline before our quote separator = replyField.val().trim() !== "" && "\n\n" || ''; replyField.val(function(a, current) { diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js index 084dde29ca84..bff0ae96cebc 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -261,4 +261,8 @@ describe('issue_note_app', () => { it('should show flash error message when new comment failed to be posted', () => {}); it('should show flash error message when comment failed to be updated', () => {}); }); + + describe('shortcuts issuable spec', () => { + + }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2b9baa84c0ab..d9748369d1dc 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -53,17 +53,18 @@ import '~/notes'; it('modifies the Markdown field', function() { const changeEvent = document.createEvent('HTMLEvents'); changeEvent.initEvent('change', true, true); - $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + $('input[type=checkbox]').attr('checked', true)[1].dispatchEvent(changeEvent); + + expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item'); }); it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/notes/3'); + expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1.json'); return expect(req.data.note).not.toBe(null); }); - $('.js-task-list-field').trigger('tasklist:changed'); + $('.js-task-list-field.original-task-list').trigger('tasklist:changed'); }); }); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 65da2e8b4068..a912e150e9be 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -9,7 +9,7 @@ describe('ShortcutsIssuable', () => { beforeEach(() => { loadFixtures(fixtureName); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); - this.shortcut = new ShortcutsIssuable(); + this.shortcut = new ShortcutsIssuable(true); }); describe('replyWithSelectedText', () => { // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. @@ -21,15 +21,15 @@ describe('ShortcutsIssuable', () => { }; }; beforeEach(() => { - this.selector = 'form.js-main-target-form textarea#note-body'; + this.selector = '.js-main-target-form #note_note'; }); describe('with empty selection', () => { it('does not return an error', () => { - this.shortcut.replyWithSelectedText(); + this.shortcut.replyWithSelectedText(true); expect($(this.selector).val()).toBe(''); }); it('triggers `focus`', () => { - this.shortcut.replyWithSelectedText(); + this.shortcut.replyWithSelectedText(true); expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); @@ -40,7 +40,7 @@ describe('ShortcutsIssuable', () => { it('leaves existing input intact', () => { $(this.selector).val('This text was already here.'); expect($(this.selector).val()).toBe('This text was already here.'); - this.shortcut.replyWithSelectedText(); + this.shortcut.replyWithSelectedText(true); expect($(this.selector).val()).toBe('This text was already here.\n\n> Selected text.\n\n'); }); it('triggers `input`', () => { @@ -48,25 +48,25 @@ describe('ShortcutsIssuable', () => { $(this.selector).on('input', () => { triggered = true; }); - this.shortcut.replyWithSelectedText(); + this.shortcut.replyWithSelectedText(true); expect(triggered).toBe(true); }); it('triggers `focus`', () => { - this.shortcut.replyWithSelectedText(); + this.shortcut.replyWithSelectedText(true); expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); describe('with a one-line selection', () => { it('quotes the selection', () => { stubSelection('<p>This text has been selected.</p>'); - this.shortcut.replyWithSelectedText(); + this.shortcut.replyWithSelectedText(true); expect($(this.selector).val()).toBe('> This text has been selected.\n\n'); }); }); describe('with a multi-line selection', () => { it('quotes the selected lines as a group', () => { stubSelection('<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>'); - this.shortcut.replyWithSelectedText(); + this.shortcut.replyWithSelectedText(true); expect($(this.selector).val()).toBe('> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n'); }); }); -- GitLab From a64452990ed91e37ebd57758729220bbc05d4844 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 00:46:18 +0100 Subject: [PATCH 173/243] Add missing dependency --- app/assets/javascripts/notes/components/issue_comment_form.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index e326c849a21b..e9b6e89202a4 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -9,6 +9,7 @@ import eventHub from '../event_hub'; import * as constants from '../constants'; import '../../autosave'; + import TaskList from '../../task_list'; export default { name: 'issueCommentForm', -- GitLab From 4e6138bd3196e2fd00596524f4ec49a6be83879e Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova <jarka@gitlab.com> Date: Fri, 11 Aug 2017 07:26:54 +0200 Subject: [PATCH 174/243] Fix merge request json schema (add labels, milestone) --- spec/fixtures/api/schemas/entities/merge_request.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json index 2f12b671dec0..1030f323a1f5 100644 --- a/spec/fixtures/api/schemas/entities/merge_request.json +++ b/spec/fixtures/api/schemas/entities/merge_request.json @@ -18,6 +18,8 @@ "total_time_spent": { "type": "integer" }, "human_time_estimate": { "type": ["integer", "null"] }, "human_total_time_spent": { "type": ["integer", "null"] }, + "milestone": { "type": ["object", "null"] }, + "labels": { "type": ["array", "null"] }, "in_progress_merge_commit_sha": { "type": ["string", "null"] }, "merge_error": { "type": ["string", "null"] }, "merge_commit_sha": { "type": ["string", "null"] }, -- GitLab From b219dcd028ceea5345c17160b6849d540fd77924 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 11:45:05 +0100 Subject: [PATCH 175/243] Adds confidential issue information --- .../notes/components/issue_comment_form.vue | 26 ++++++++++++------- .../notes/components/issue_note_form.vue | 9 ++++++- .../issue/confidential_issue_warning.vue | 16 ++++++++++++ app/views/projects/_md_preview.html.haml | 7 ----- .../projects/issues/_discussion.html.haml | 2 -- .../components/issue_comment_form_spec.js | 10 +++++++ .../issue/confidential_issue_warning_spec.js | 20 ++++++++++++++ 7 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue create mode 100644 spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index e9b6e89202a4..06e69fdc653d 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -3,13 +3,14 @@ import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import markdownField from '../../vue_shared/components/markdown/field.vue'; - import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; - import eventHub from '../event_hub'; - import * as constants from '../constants'; import '../../autosave'; import TaskList from '../../task_list'; + import * as constants from '../constants'; + import eventHub from '../event_hub'; + import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue'; + import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; + import markdownField from '../../vue_shared/components/markdown/field.vue'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; export default { name: 'issueCommentForm', @@ -25,9 +26,10 @@ }; }, components: { - userAvatarLink, - markdownField, + confidentialIssue, issueNoteSignedOutWidget, + markdownField, + userAvatarLink, }, watch: { note(newNote) { @@ -88,6 +90,9 @@ endpoint() { return this.getIssueData.create_note_path; }, + isConfidentialIssue() { + return this.getIssueData.confidential; + }, }, methods: { ...mapActions([ @@ -195,7 +200,7 @@ fieldName: 'note', selector: '.notes', }); - } + }, }, mounted() { // jQuery is needed here because it is a custom event being dispatched with jQuery. @@ -231,11 +236,14 @@ <form class="js-main-target-form timeline-content timeline-content-form common-note-form" @submit="handleSave(true)"> + <confidentialIssue v-if="isConfidentialIssue" /> + <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" :quick-actions-docs="quickActionsDocsUrl" - :add-spacing-classes="false"> + :add-spacing-classes="false" + :is-confidential-issue="isConfidentialIssue"> <textarea id="note-body" name="note[note]" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index ab8582bb9264..fcfeb17edb5b 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -1,9 +1,11 @@ <script> import { mapGetters } from 'vuex'; - import markdownField from '../../vue_shared/components/markdown/field.vue'; import eventHub from '../event_hub'; + import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue'; + import markdownField from '../../vue_shared/components/markdown/field.vue'; export default { + name: 'issueNoteForm', props: { noteBody: { type: String, @@ -37,6 +39,7 @@ }; }, components: { + confidentialIssue, markdownField, }, computed: { @@ -64,6 +67,9 @@ isDisabled() { return !this.note.length || this.isSubmitting; }, + isConfidentialIssue() { + return this.getIssueDataByProp('confidential'); + }, }, methods: { handleUpdate() { @@ -116,6 +122,7 @@ <div class="flash-container timeline-content"></div> <form class="edit-note common-note-form"> + <confidentialIssue v-if="isConfidentialIssue" /> <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" diff --git a/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue new file mode 100644 index 000000000000..397d16331d58 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue @@ -0,0 +1,16 @@ +<script> + export default { + name: 'confidentialIssueWarning', + }; +</script> +<template> + <div class="confidential-issue-warning"> + <i + aria-hidden="true" + class="fa fa-eye-slash"> + </i> + <span> + This is a confidential issue. Your comment will not be visible to the public. + </span> + </div> +</template> diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 6e13bf47ff6b..71424593f2e6 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,12 +1,5 @@ - referenced_users = local_assigns.fetch(:referenced_users, nil) -- if defined?(@issue) && @issue.confidential? - %li.confidential-issue-warning - = confidential_icon(@issue) - %span This is a confidential issue. Your comment will not be visible to the public. -- else - %li.confidential-issue-warning.not-confidential - .md-area .md-header %ul.nav-links.clearfix diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 538bb50d3c91..683efef22ea1 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -16,5 +16,3 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' - -= render "layouts/init_auto_complete" diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index deaafb5e8a9e..5ec4c7ebd0a0 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -102,6 +102,16 @@ describe('issue_comment_form component', () => { }); }); }); + + describe('issue is confidential', () => { + it('shows information warning', (done) => { + store.dispatch('setIssueData', Object.assign(issueDataMock, { confidential: true })); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined(); + done(); + }); + }); + }); }); describe('user is not logged in', () => { diff --git a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js new file mode 100644 index 000000000000..6df08f3ebe76 --- /dev/null +++ b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import confidentialIssue from '~/vue_shared/components/issue/confidential_issue_warning.vue'; + +describe('Confidential Issue Warning Component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(confidentialIssue); + vm = new Component().$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render confidential issue warning information', () => { + expect(vm.$el.querySelector('i').className).toEqual('fa fa-eye-slash'); + expect(vm.$el.querySelector('span').textContent.trim()).toEqual('This is a confidential issue. Your comment will not be visible to the public.'); + }); +}); -- GitLab From fc9e57434c57c8ebd36e7f038cf7535422e893e9 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova <jarka@gitlab.com> Date: Fri, 11 Aug 2017 13:53:51 +0200 Subject: [PATCH 176/243] Fix failing static-analysis (rubocop) --- app/helpers/issuables_helper.rb | 2 +- app/serializers/issue_entity.rb | 2 +- app/serializers/note_entity.rb | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 4a3a03d0bfb4..afcf1a467c57 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -213,7 +213,7 @@ def issuable_initial_data(issuable) initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionText: issuable.description, - initialTaskStatus: issuable.task_status, + initialTaskStatus: issuable.task_status } data.merge!(updated_at_by(issuable)) diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index fbdd9f947634..0d6feb781733 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -23,7 +23,7 @@ class IssueEntity < IssuableEntity end expose :create_note_path do |issue| - namespace_project_notes_path(issue.project.namespace, issue.project, target_type: 'issue', target_id: issue.id) + project_notes_path(issue.project, target_type: 'issue', target_id: issue.id) end expose :preview_note_path do |issue| diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index 53b3ed419401..f84343aa71a3 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -37,7 +37,7 @@ class NoteEntity < API::Entities::Note if note.for_personal_snippet? toggle_award_emoji_snippet_note_path(note.noteable, note) else - toggle_award_emoji_namespace_project_note_path(note.project.namespace, note.project, note.id) + toggle_award_emoji_project_note_path(note.project, note.id) end end @@ -49,12 +49,12 @@ class NoteEntity < API::Entities::Note if note.for_personal_snippet? snippet_note_path(note.noteable, note) else - namespace_project_note_path(note.project.namespace, note.project, note) + project_note_path(note.project, note) end end expose :attachment, using: NoteAttachmentEntity expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note| - delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note) + delete_attachment_project_note_path(note.project, note) end end -- GitLab From b38e690344a3d37e3be0305c88ea96adbc82aca6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 12:51:41 +0100 Subject: [PATCH 177/243] Fix open discussions --- .../notes/components/issue_comment_form.vue | 26 +++++++------------ .../notes/components/issue_discussion.vue | 2 +- .../notes/components/issue_note_actions.vue | 13 +++++----- .../components/issue_note_awards_list.vue | 4 +-- .../notes/components/issue_note_body.vue | 1 - .../notes/components/issue_note_header.vue | 7 +---- .../notes/components/issue_notes_app.vue | 13 +++++----- .../components/issue_placeholder_note.vue | 21 ++++++++------- .../notes/components/issue_system_note.vue | 8 +++--- app/assets/javascripts/notes/constants.js | 1 + app/assets/javascripts/notes/index.js | 3 --- .../javascripts/notes/stores/getters.js | 13 ++++++---- .../vue_shared/components/markdown/field.vue | 5 ++-- .../projects/issues/_discussion.html.haml | 2 ++ .../shared/notes/_notes_with_form.html.haml | 2 +- features/project/issues/issues.feature | 1 + 16 files changed, 54 insertions(+), 68 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 06e69fdc653d..d3fe70a2bac4 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,6 +1,5 @@ <script> /* global Flash, Autosave */ - import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; import '../../autosave'; @@ -113,7 +112,7 @@ data: { view: 'full_data', note: { - noteable_type: 'Issue', + noteable_type: constants.NOTEABLE_TYPE, noteable_id: this.getIssueData.id, note: this.note, }, @@ -123,7 +122,6 @@ if (this.noteType === constants.DISCUSSION) { noteData.data.note.type = constants.DISCUSSION_NOTE; } - this.isSubmitting = true; this.saveNote(noteData) @@ -150,13 +148,7 @@ } if (withIssueAction) { - if (this.isIssueOpen) { - this.issueState = constants.CLOSED; - } else { - this.issueState = constants.REOPENED; - } - - this.isIssueOpen = !this.isIssueOpen; + this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED; // This is out of scope for the Notes Vue component. // It was the shortest path to update the issue state and relevant places. @@ -220,9 +212,8 @@ <ul v-else class="notes notes-form timeline new-note"> - <li class="timeline-entry" ref="commentForm"> + <li class="timeline-entry"> <div class="timeline-entry-inner"> - <div class="flash-container timeline-content"></div> <div class="timeline-icon hidden-xs hidden-sm"> <user-avatar-link v-if="author" @@ -234,10 +225,10 @@ </div> <div > <form - class="js-main-target-form timeline-content timeline-content-form common-note-form" - @submit="handleSave(true)"> + ref="commentForm" + class="js-main-target-form timeline-content timeline-content-form common-note-form"> + <div class="flash-container timeline-content"></div> <confidentialIssue v-if="isConfidentialIssue" /> - <markdown-field :markdown-preview-url="markdownPreviewUrl" :markdown-docs="markdownDocsUrl" @@ -263,7 +254,7 @@ <button @click="handleSave()" :disabled="isSubmitButtonDisabled" - class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button" + class="btn btn-create comment-btn js-comment-button js-comment-submit-button" type="button"> {{commentButtonTitle}} </button> @@ -319,7 +310,8 @@ </ul> </div> <button - type="submit" + type="button" + @click="handleSave(true)" v-if="canUpdateIssue" :class="actionButtonClassNames" class="btn btn-comment btn-comment-and-close"> diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index b806f1bfc01f..54bc607c4319 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -148,7 +148,7 @@ :created-at="discussion.created_at" :note-id="discussion.id" :include-toggle="true" - :toggle-handler="toggleDiscussionHandler" + @toggleHandler="toggleDiscussionHandler" action-text="started a discussion" /> <issue-note-edited-text diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 892787df45a0..23b511778cb1 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -7,6 +7,7 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { + name: 'issueNoteActions', props: { authorId: { type: Number, @@ -41,13 +42,6 @@ directives: { tooltip, }, - data() { - return { - emojiSmiling, - emojiSmile, - emojiSmiley, - }; - }, components: { loadingIcon, }, @@ -76,6 +70,11 @@ this.$emit('deleteHandler'); }, }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + } }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 229127db68c9..b44be05c8ebc 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -5,7 +5,7 @@ import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; - import * as Emoji from '../../emoji'; + import { glEmojiTag } from '../../emoji'; import tooltip from '../../vue_shared/directives/tooltip'; export default { @@ -79,7 +79,7 @@ 'toggleAwardRequest', ]), getAwardHTML(name) { - return Emoji.glEmojiTag(name); + return glEmojiTag(name); }, getAwardClassBindings(awardList, awardName) { return { diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index eb31c0305dcf..462ecf065159 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -1,5 +1,4 @@ <script> - /* global Autosave */ import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteAwardsList from './issue_note_awards_list.vue'; import issueNoteForm from './issue_note_form.vue'; diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 812f0891ad13..3b658f00f1fa 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -31,11 +31,6 @@ required: false, default: false, }, - toggleHandler: { - type: Function, - required: false, - default: () => {}, - }, }, data() { return { @@ -59,7 +54,7 @@ ]), handleToggle() { this.isExpanded = !this.isExpanded; - this.toggleHandler(); + this.$emit('toggleHandler'); }, updateTargetNoteHash() { this.setTargetNoteHash(this.noteTimestampLink); diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 76c5e19cd280..0e5ae78e45c2 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -79,16 +79,15 @@ }, fetchNotes() { return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) + .then(() => this.initPolling()) .then(() => { - // Scroll to note if we have hash fragment in the page URL - this.$nextTick(() => { - this.checkLocationHash(); - }); + this.isLoading = false; }) - .catch(() => Flash('Something went wrong while fetching issue comments. Please try again.')) - .then(() => { + .then(() => this.$nextTick()) + .then(() => this.checkLocationHash()) + .catch(() => { this.isLoading = false; - this.initPolling(); + Flash('Something went wrong while fetching issue comments. Please try again.'); }); }, initPolling() { diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_note.vue index 4c089d755c85..6921d91372f8 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue +++ b/app/assets/javascripts/notes/components/issue_placeholder_note.vue @@ -1,4 +1,5 @@ <script> + import { mapGetters } from 'vuex'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; export default { @@ -12,10 +13,10 @@ components: { userAvatarLink, }, - data() { - return { - currentUser: this.$store.getters.getUserData, - }; + computed: { + ...mapGetters([ + 'getUserData', + ]), }, }; </script> @@ -25,9 +26,9 @@ <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link - :link-href="currentUser.path" - :img-src="currentUser.avatar_url" - :size="40" + :link-href="getUserData.path" + :img-src="getUserData.avatar_url" + :img-size="40" /> </div> <div @@ -35,9 +36,9 @@ class="timeline-content"> <div class="note-header"> <div class="note-header-info"> - <a :href="currentUser.path"> - <span class="hidden-xs">{{currentUser.name}}</span> - <span class="note-headline-light">@{{currentUser.username}}</span> + <a :href="getUserData.path"> + <span class="hidden-xs">{{getUserData.name}}</span> + <span class="note-headline-light">@{{getUserData.username}}</span> </a> </div> </div> diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index d7af04581980..5bb8f871b9d9 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -11,11 +11,6 @@ required: true, }, }, - data() { - return { - svg: iconsMap[this.note.system_note_icon_name], - }; - }, components: { issueNoteHeader, }, @@ -30,6 +25,9 @@ return this.targetNoteHash === this.noteAnchorId; }, }, + created() { + this.svg = iconsMap[this.note.system_note_icon_name]; + }, }; </script> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 0ebde2dccb82..a6961063c014 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -8,3 +8,4 @@ export const REOPENED = 'reopened'; export const CLOSED = 'closed'; export const EMOJI_THUMBSUP = 'thumbsup'; export const EMOJI_THUMBSDOWN = 'thumbsdown'; +export const NOTEABLE_TYPE = 'Issue'; diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index af2e1ef0731f..7c90cf20019b 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -25,9 +25,6 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, render(createElement) { return createElement('issue-notes-app', { - attrs: { - ref: 'notes', - }, props: { issueData: this.issueData, notesData: this.notesData, diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 21f8cd548228..ec3f9e5b7a01 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -18,11 +18,14 @@ export const notesById = state => state.notes.reduce((acc, note) => { }, {}); const reverseNotes = array => array.slice(0).reverse(); -const isLastNote = (note, state) => !note.system && note.author.id === state.userData.id; - -export const getCurrentUserLastNote = state => _.flatten(reverseNotes(state.notes) - .map(note => note.notes)) - .find(el => isLastNote(el, state)); +const isLastNote = (note, state) => !note.system && + state.userData && + note.author.id === state.userData.id; + +export const getCurrentUserLastNote = state => _.flatten( + reverseNotes(state.notes) + .map(note => reverseNotes(note.notes)), + ).find(el => isLastNote(el, state)); export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) .find(el => isLastNote(el, state)); diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index af7ea7480771..f84652d397c8 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -3,8 +3,6 @@ import markdownHeader from './header.vue'; import markdownToolbar from './toolbar.vue'; - const REFERENCED_USERS_THRESHOLD = 10; - export default { props: { markdownPreviewUrl: { @@ -41,7 +39,8 @@ }, computed: { shouldShowReferencedUsers() { - return this.referencedUsers.length >= REFERENCED_USERS_THRESHOLD; + const referencedUsersThreshold = 10; + return this.referencedUsers.length >= referencedUsersThreshold; }, }, methods: { diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 683efef22ea1..8f4da97850f2 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -16,3 +16,5 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' + += render "layouts/init_auto_complete" \ No newline at end of file diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index 04866064905c..1264f07eac9d 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -15,7 +15,7 @@ .timeline-content.timeline-content-form = render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete - elsif !current_user - .disabled-comment.text-center.prepend-top-default.js-disabled-comment + .disabled-comment.text-center.prepend-top-default Please = link_to "register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-register-link' or diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index abc23257de56..4f905674d8cc 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -46,6 +46,7 @@ Feature: Project Issues Given I visit issue page "Release 0.4" And I leave a comment like "XML attached" Then I should see comment "XML attached" + And I should see an error alert section within the comment form @javascript Scenario: Visiting Issues after being sorted the list -- GitLab From b96a3d4d60747102bc437c99b4d040c5c0329689 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 15:28:46 +0100 Subject: [PATCH 178/243] Fix open discussions Put back deleted tests --- .../notes/components/issue_discussion.vue | 2 +- .../components/issue_note_awards_list.vue | 18 ++++++++++++++++-- .../notes/components/issue_note_form.vue | 1 - app/assets/javascripts/notes/stores/actions.js | 2 +- .../projects/issues/_discussion.html.haml | 2 +- features/steps/shared/note.rb | 2 +- .../user_uses_slash_commands_spec.rb | 6 +++--- .../features/participants_autocomplete_spec.rb | 2 +- 8 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 54bc607c4319..58770111378e 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,5 +1,5 @@ <script> - /* global Flash, Autosave */ + /* global Flash */ import { mapActions, mapGetters } from 'vuex'; import { SYSTEM_NOTE } from '../constants'; import issueNote from './issue_note.vue'; diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index b44be05c8ebc..f6d27e0a0aed 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -140,11 +140,25 @@ return title; }, handleAward(awardName) { + let parsedName; + + // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string + switch(awardName) { + case '100': + parsedName = 100; + break; + case '1234': + parsedName = 1234; + break; + default: + parsedName = awardName; + break; + } + const data = { endpoint: this.toggleAwardPath, noteId: this.noteId, - // 100 emoji is a number. Callback for v-for click sends it as a string - awardName: awardName === '100' ? 100 : awardName, + awardName: parsedName, }; this.toggleAwardRequest(data) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index fcfeb17edb5b..fb6e16c55909 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -129,7 +129,6 @@ :quick-actions-docs="quickActionsDocsUrl" :add-spacing-classes="false"> <textarea - id="note-body" name="note[note]" class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form" :data-supports-quick-actions="!isEditing" diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index bb9c2c53b799..b52ec9700d8b 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -86,7 +86,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const { errors } = res; const commandsChanges = res.commands_changes; - if (hasQuickActions && Object.keys(errors).length) { + if (hasQuickActions && errors && Object.keys(errors).length) { dispatch('fetchData'); $('.js-gfm-input').trigger('clear-commands-cache.atwho'); diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 8f4da97850f2..538bb50d3c91 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -17,4 +17,4 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' -= render "layouts/init_auto_complete" \ No newline at end of file += render "layouts/init_auto_complete" diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 1c3432c3ca17..86c2600da6ce 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -156,7 +156,7 @@ module SharedNote page.find('.current-note-edit-form textarea') page.within(".current-note-edit-form") do - fill_in '#note-body', with: '+1 Awesome!' + fill_in 'note[note]', with: '+1 Awesome!' click_button 'Save comment' end wait_for_requests diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 9b5c21d752cf..95c50df18966 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -67,7 +67,7 @@ it 'does not change the WIP prefix' do write_note("/wip") - expect(page).to have_content '/wip' + expect(page).not_to have_content '/wip' expect(page).not_to have_content 'Commands applied' expect(merge_request.reload.work_in_progress?).to eq false @@ -78,7 +78,7 @@ describe 'merging the MR from the note' do context 'when the current user can merge the MR' do it 'merges the MR' do - write_note("/merge", false) + write_note("/merge") expect(page).to have_content 'Commands applied' @@ -197,7 +197,7 @@ it 'does not change target branch' do write_note('/target_branch merge-test') - expect(page).to have_content '/target_branch merge-test' + expect(page).not_to have_content '/target_branch merge-test' expect(merge_request.target_branch).to eq 'feature' end diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index e71b7861efa2..5ba2b6273be3 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -15,7 +15,7 @@ before do page.within('.new-note') do if note.noteable_type === 'Issue' - find('#note-body').send_keys('@') + find('.js-vue-comment-form').send_keys('@') else find('#note_note').send_keys('@') end -- GitLab From 345df20870b718daa74c0177221c00cdec6083f3 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova <jarka@gitlab.com> Date: Fri, 11 Aug 2017 17:26:43 +0200 Subject: [PATCH 179/243] Fix specs - delete comment is now a button instead of simple link --- spec/support/features/reportable_note_shared_examples.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index dbe1c9b8aec2..c3a0623409b9 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -16,7 +16,7 @@ open_dropdown(dropdown) expect(dropdown).to have_button('Edit comment') - expect(dropdown).to have_link('Delete comment') + expect(dropdown).to have_button('Delete comment') expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) end -- GitLab From d78037c246d14817937abbc4369e30ae12b022bc Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova <jarka@gitlab.com> Date: Fri, 11 Aug 2017 18:48:06 +0200 Subject: [PATCH 180/243] Use UserNoteEntity instead of UserEntity for notes --- app/serializers/note_entity.rb | 4 +- app/serializers/user_entity.rb | 2 - app/serializers/user_note_entity.rb | 9 ++++ .../projects/issues_controller_spec.rb | 16 ++++++ spec/serializers/note_entity_spec.rb | 51 +++++++++++++++++++ 5 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 app/serializers/user_note_entity.rb create mode 100644 spec/serializers/note_entity_spec.rb diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index f84343aa71a3..e5295f5f34d0 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -3,7 +3,7 @@ class NoteEntity < API::Entities::Note expose :type - expose :author, using: UserEntity + expose :author, using: UserNoteEntity expose :human_access do |note| note.project.team.human_max_access(note.author_id) @@ -15,7 +15,7 @@ class NoteEntity < API::Entities::Note expose :redacted_note_html, as: :note_html expose :last_edited_at, if: -> (note, _) { note.is_edited? } - expose :last_edited_by, using: UserEntity, if: -> (note, _) { note.is_edited? } + expose :last_edited_by, using: UserNoteEntity, if: -> (note, _) { note.is_edited? } expose :current_user do expose :can_edit do |note| diff --git a/app/serializers/user_entity.rb b/app/serializers/user_entity.rb index 3bb340065c44..876512b12dc6 100644 --- a/app/serializers/user_entity.rb +++ b/app/serializers/user_entity.rb @@ -1,8 +1,6 @@ class UserEntity < API::Entities::UserBasic include RequestAwareEntity - unexpose :web_url - expose :path do |user| user_path(user) end diff --git a/app/serializers/user_note_entity.rb b/app/serializers/user_note_entity.rb new file mode 100644 index 000000000000..fbd87470380b --- /dev/null +++ b/app/serializers/user_note_entity.rb @@ -0,0 +1,9 @@ +class UserNoteEntity < API::Entities::UserBasic + include RequestAwareEntity + + unexpose :web_url + + expose :path do |user| + user_path(user) + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 23601c457b08..c07c035fcda2 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -877,4 +877,20 @@ def create_merge_request format: :json end end + + describe 'GET #discussions' do + let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } + + before do + project.add_developer(user) + sign_in(user) + end + + it 'returns discussion json' do + get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid + + expect(JSON.parse(response.body).first.keys).to match_array( + ['id', 'reply_id', 'expanded', 'notes', 'individual_note']) + end + end end diff --git a/spec/serializers/note_entity_spec.rb b/spec/serializers/note_entity_spec.rb new file mode 100644 index 000000000000..3459cc72063a --- /dev/null +++ b/spec/serializers/note_entity_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe NoteEntity do + include Gitlab::Routing + + let(:request) { double('request', current_user: user, noteable: note.noteable) } + + let(:entity) { described_class.new(note, request: request) } + let(:note) { create(:note) } + let(:user) { create(:user) } + subject { entity.as_json } + + context 'basic note' do + it 'exposes correct elements' do + expect(subject).to include(:type, :author, :human_access, :note, :note_html, :current_user, + :discussion_id, :emoji_awardable, :award_emoji, :toggle_award_path, :report_abuse_path, :path, :attachment) + end + + it 'does not expose elements for specific notes cases' do + expect(subject).not_to include(:last_edited_by, :last_edited_at, :system_note_icon_name) + end + + it 'exposes author correctly' do + expect(subject[:author]).to include(:id, :name, :username, :state, :avatar_url, :path) + end + + it 'does not expose web_url for author' do + expect(subject[:author]).not_to include(:web_url) + end + end + + context 'when note was edited' do + before do + note.update(updated_at: 1.minute.from_now, updated_by: user) + end + + it 'exposes last_edited_at and last_edited_by elements' do + expect(subject).to include(:last_edited_at, :last_edited_by) + end + end + + context 'when note is a system note' do + before do + note.update(system: true) + end + + it 'exposes system_note_icon_name element' do + expect(subject).to include(:system_note_icon_name) + end + end +end -- GitLab From e81dd24251c20a89231637b05f04abf9d2e039ea Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 16:38:57 +0100 Subject: [PATCH 181/243] Fix broken notes spec --- spec/javascripts/notes_spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index d9748369d1dc..8c5ad8914b00 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -64,7 +64,8 @@ import '~/notes'; expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1.json'); return expect(req.data.note).not.toBe(null); }); - $('.js-task-list-field.original-task-list').trigger('tasklist:changed'); + + $('.js-task-list-field.js-note-text').trigger('tasklist:changed'); }); }); -- GitLab From 3e92f44ff223c9252f7726dcad46a3cb46298001 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 16:39:16 +0100 Subject: [PATCH 182/243] Adds unit tests to issue_note_actions component --- .../components/issue_note_actions_spec.js | 70 +++++++++++++++++-- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/spec/javascripts/notes/components/issue_note_actions_spec.js b/spec/javascripts/notes/components/issue_note_actions_spec.js index eb3dc691f18e..7bcc061f1670 100644 --- a/spec/javascripts/notes/components/issue_note_actions_spec.js +++ b/spec/javascripts/notes/components/issue_note_actions_spec.js @@ -1,35 +1,91 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueActions from '~/notes/components/issue_note_actions.vue'; +import { userDataMock } from '../mock_data'; + describe('issse_note_actions component', () => { - it('should render access level badge', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(issueActions); + }); + afterEach(() => { + vm.$destroy(); }); describe('user is logged in', () => { - it('should render emoji link', () => { + let props; + + beforeEach(() => { + props = { + accessLevel: 'Master', + authorId: 26, + canDelete: true, + canEdit: true, + canReportAsAbuse: true, + noteId: 539, + reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', + }; + store.dispatch('setUserData', userDataMock); + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + it('should render access level badge', () => { + expect(vm.$el.querySelector('.note-role').textContent.trim()).toEqual(props.accessLevel); + }); + + it('should render emoji link', () => { + expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); }); describe('actions dropdown', () => { it('should be possible to edit the comment', () => { - + expect(vm.$el.querySelector('.js-note-edit')).toBeDefined(); }); it('should be possible to report as abuse', () => { - + expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined(); }); it('should be possible to delete comment', () => { - + expect(vm.$el.querySelector('.js-note-delete')).toBeDefined(); }); }); }); describe('user is not logged in', () => { - it('should not render emoji link', () => { + let props; + beforeEach(() => { + store.dispatch('setUserData', {}); + props = { + accessLevel: 'Master', + authorId: 26, + canDelete: false, + canEdit: false, + canReportAsAbuse: false, + noteId: 539, + reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', + }; + vm = new Component({ + store, + propsData: props, + }).$mount(); }); - it('should not render actions dropdown', () => { + it('should not render emoji link', () => { + expect(vm.$el.querySelector('.js-add-award')).toEqual(null); + }); + it('should not render actions dropdown', () => { + expect(vm.$el.querySelector('.more-actions')).toEqual(null); }); }); }); -- GitLab From cf77cdba51059cd3ae99a66b7bf103cd0bde1171 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 16:52:29 +0100 Subject: [PATCH 183/243] Adds unit tests for discussion component --- .../notes/components/issue_discussion.vue | 2 +- .../notes/components/issue_discussion_spec.js | 61 +++++++++++-------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 58770111378e..7fd6be12261c 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -179,7 +179,7 @@ v-if="canReply && !isReplying" @click="showReplyForm" type="button" - class="btn btn-text-field" + class="js-vue-discussion-reply btn btn-text-field" title="Add a reply">Reply...</button> <issue-note-form v-if="isReplying" diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js index cb29b4176ad0..05c6b57f93eb 100644 --- a/spec/javascripts/notes/components/issue_discussion_spec.js +++ b/spec/javascripts/notes/components/issue_discussion_spec.js @@ -1,42 +1,49 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueDiscussion from '~/notes/components/issue_discussion.vue'; +import { issueDataMock, discussionMock, notesDataMock } from '../mock_data'; describe('issue_discussion component', () => { - it('should render user avatar', () => { - }); - - it('should render discussion header', () => { + let vm; - }); + beforeEach(() => { + const Component = Vue.extend(issueDiscussion); - describe('updated note', () => { - it('should show information about update', () => { + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); - }); + vm = new Component({ + store, + propsData: { + note: discussionMock, + }, + }).$mount(); }); - describe('with open discussion', () => { - it('should render system note', () => { - - }); - - it('should render placeholder note', () => { + afterEach(() => { + vm.$destroy(); + }); - }); + it('should render user avatar', () => { + expect(vm.$el.querySelector('.user-avatar-link')).toBeDefined(); + }); - it('should render regular note', () => { + it('should render discussion header', () => { + expect(vm.$el.querySelector('.discussion-header')).toBeDefined(); + expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length); + }); + describe('actions', () => { + it('should render reply button', () => { + expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual('Reply...'); }); - describe('actions', () => { - it('should render reply button', () => { - - }); - - it('should toggle reply form', () => { - - }); - - it('should render signout widget when user is logged out', () => { - + it('should toggle reply form', (done) => { + vm.$el.querySelector('.js-vue-discussion-reply').click(); + Vue.nextTick(() => { + expect(vm.$refs.noteForm).toBeDefined(); + expect(vm.isReplying).toEqual(true); + done(); }); }); }); -- GitLab From cedee0124020120f4dc74a5548038df715fb1213 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 17:36:59 +0100 Subject: [PATCH 184/243] Adds missing unit tests for vue components: issue_note_awards_list; issue_note_body; issue_note_form;issue_note_spec --- .../components/issue_note_awards_list_spec.js | 46 +++++++++++++++- .../notes/components/issue_note_body_spec.js | 36 +++++++++++-- .../notes/components/issue_note_form_spec.js | 41 ++++++++++++++- .../notes/components/issue_note_spec.js | 35 +++++++++++-- spec/javascripts/notes/mock_data.js | 52 ++++++++++++++++++- 5 files changed, 197 insertions(+), 13 deletions(-) diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/issue_note_awards_list_spec.js index 82d6f677feaa..d4156e29ba09 100644 --- a/spec/javascripts/notes/components/issue_note_awards_list_spec.js +++ b/spec/javascripts/notes/components/issue_note_awards_list_spec.js @@ -1,13 +1,55 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import awardsNote from '~/notes/components/issue_note_awards_list.vue'; +import { issueDataMock, notesDataMock } from '../mock_data'; + describe('issue_note_awards_list component', () => { - it('should render awarded emojis', () => { + let vm; + let awardsMock; + + beforeEach(() => { + const Component = Vue.extend(awardsNote); + + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + awardsMock = [ + { + name: 'flag_tz', + user: { id: 1, name: 'Administrator', username: 'root' }, + }, + { + name: 'cartwheel_tone3', + user: { id: 12, name: 'Bobbie Stehr', username: 'erin' }, + }, + ]; + + vm = new Component({ + store, + propsData: { + awards: awardsMock, + noteAuthorId: 2, + noteId: 545, + toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', + }, + }).$mount(); + }); + afterEach(() => { + vm.$destroy(); + }); + + it('should render awarded emojis', () => { + expect(vm.$el.querySelectorAll('.js-awards-block button').length).toEqual(awardsMock.length); }); it('should be possible to remove awareded emoji', () => { + spyOn(vm, 'handleAward').and.callThrough(); + vm.$el.querySelector('.js-awards-block button').click(); + expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); }); it('should be possible to add new emoji', () => { - + expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); }); }); diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/issue_note_body_spec.js index 5061b97e9a6a..c0e06b0985b6 100644 --- a/spec/javascripts/notes/components/issue_note_body_spec.js +++ b/spec/javascripts/notes/components/issue_note_body_spec.js @@ -1,17 +1,45 @@ + +import Vue from 'vue'; +import store from '~/notes/stores'; +import noteBody from '~/notes/components/issue_note_body.vue'; +import { issueDataMock, notesDataMock, note } from '../mock_data'; + describe('issue_note_body component', () => { - it('should render the note', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(noteBody); + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + propsData: { + note, + canEdit: true, + }, + }).$mount(); }); - it('should be render form if user is editing', () => { + afterEach(() => { + vm.$destroy(); + }); + it('should render the note', () => { + expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); }); - it('should render information if note was edited', () => { + it('should be render form if user is editing', (done) => { + vm.isEditing = true; + Vue.nextTick(() => { + expect(vm.$el.querySelector('textarea.js-task-list-field')).toBeDefined(); + done(); + }); }); it('should render awards list', () => { - + expect(vm.$el.querySelectorAll('.js-awards-block button').length).toEqual(note.award_emoji.length); }); }); diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js index 678374de60ad..78f8ed87d50d 100644 --- a/spec/javascripts/notes/components/issue_note_form_spec.js +++ b/spec/javascripts/notes/components/issue_note_form_spec.js @@ -1,7 +1,44 @@ -describe('issue_note_form component', () => { - describe('conflicts editing', () => { +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueNote from '~/notes/components/issue_note.vue'; +import { issueDataMock, notesDataMock } from '../mock_data'; + +fdescribe('issue_note_form component', () => { + let vm; + let props; + + beforeEach(() => { + const Component = Vue.extend(issueNote); + + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + props = { + isEditing: true, + noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', + noteId: 545, + saveButtonTitle: 'Save comment', + }; + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('conflicts editing', (done) => { it('should show conflict message if note changes outside the component', () => { + vm.noteBody = 'Foo'; + + Vue.nextTick(() => { + console.log('', vm.$el); + done(); + }); }); }); diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js index 69846a8038bd..7ef85d5b4f0d 100644 --- a/spec/javascripts/notes/components/issue_note_spec.js +++ b/spec/javascripts/notes/components/issue_note_spec.js @@ -1,17 +1,44 @@ + +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueNote from '~/notes/components/issue_note.vue'; +import { issueDataMock, notesDataMock, note } from '../mock_data'; + describe('issue_note', () => { - it('should render user information', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issueNote); + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + propsData: { + note, + }, + }).$mount(); }); - it('should render note header content', () => { + afterEach(() => { + vm.$destroy(); + }); + it('should render user information', () => { + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(note.author.avatar_url); }); - it('should render note actions', () => { + it('should render note header content', () => { + expect(vm.$el.querySelector('.note-header .note-header-author-name').textContent.trim()).toEqual(note.author.name); + expect(vm.$el.querySelector('.note-header .note-headline-meta').textContent.trim()).toContain('commented'); + }); + it('should render note actions', () => { + expect(vm.$el.querySelector('.note-actions')).toBeDefined(); }); it('should render issue body', () => { - + expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); }); }); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 795d67a24a04..968188c1cf5d 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -70,7 +70,7 @@ export const individualNote = { name: 'Root', username: 'root', state: 'active', - avatar_url: null, + avatar_url: 'test', path: '/root', }, created_at: '2017-08-01T17: 09: 33.762Z', @@ -96,6 +96,56 @@ export const individualNote = { reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', }; +export const note = { + "id": 546, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "path": "/root" + }, + "created_at": "2017-08-10T15:24:03.087Z", + "updated_at": "2017-08-10T15:24:03.087Z", + "system": false, + "noteable_id": 67, + "noteable_type": "Issue", + "noteable_iid": 7, + "type": null, + "human_access": "Owner", + "note": "Vel id placeat reprehenderit sit numquam.", + "note_html": "<p dir=\"auto\">Vel id placeat reprehenderit sit numquam.</p>", + "current_user": { + "can_edit": true + }, + "discussion_id": "d3842a451b7f3d9a5dfce329515127b2d29a4cd0", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Administrator", + "username": "root" + } + }, { + "name": "bath_tone3", + "user": { + "id": 1, + "name": "Administrator", + "username": "root" + } + }], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/546" + } + export const discussionMock = { id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', -- GitLab From cbddad5a2d22767d2dc7d7214f09e7b3ab8bf322 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 19:54:26 +0100 Subject: [PATCH 185/243] Adds tests for mutations and getters --- .../notes/components/issue_note_actions.vue | 2 +- .../components/issue_note_awards_list.vue | 2 +- .../notes/components/issue_note_form.vue | 2 +- .../notes/components/issue_notes_app.vue | 1 - .../notes/components/issue_note_app_spec.js | 13 -- .../notes/components/issue_note_form_spec.js | 80 ++++++----- spec/javascripts/notes/stores/getters_spec.js | 54 +++---- .../javascripts/notes/stores/mutation_spec.js | 136 ++++++++++++++++-- 8 files changed, 192 insertions(+), 98 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 23b511778cb1..c7b1106ee9dc 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -74,7 +74,7 @@ this.emojiSmiling = emojiSmiling; this.emojiSmile = emojiSmile; this.emojiSmiley = emojiSmiley; - } + }, }; </script> diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index f6d27e0a0aed..518042e39af1 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -143,7 +143,7 @@ let parsedName; // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string - switch(awardName) { + switch (awardName) { case '100': parsedName = 100; break; diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index fb6e16c55909..61a9d0e391c0 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -152,7 +152,7 @@ </button> <button @click="cancelHandler()" - class="btn btn-nr btn-cancel note-edit-cancel" + class="btn btn-cancel note-edit-cancel" type="button"> Cancel </button> diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 0e5ae78e45c2..b6fc5e5036f8 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -46,7 +46,6 @@ computed: { ...mapGetters([ 'notes', - 'notesById', 'getNotesDataByProp', ]), }, diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js index bff0ae96cebc..1a782a32c430 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -252,17 +252,4 @@ describe('issue_note_app', () => { }, 0); }); }); - - // TODO: FILIPA - describe('create new note', () => { - it('should show placeholder note while new comment is being posted', () => {}); - it('should remove placeholder note when new comment is done posting', () => {}); - it('should show actual note element when new comment is done posting', () => {}); - it('should show flash error message when new comment failed to be posted', () => {}); - it('should show flash error message when comment failed to be updated', () => {}); - }); - - describe('shortcuts issuable spec', () => { - - }); }); diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js index 78f8ed87d50d..702e22bb6dcc 100644 --- a/spec/javascripts/notes/components/issue_note_form_spec.js +++ b/spec/javascripts/notes/components/issue_note_form_spec.js @@ -1,23 +1,23 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueNote from '~/notes/components/issue_note.vue'; +import issueNoteForm from '~/notes/components/issue_note_form.vue'; import { issueDataMock, notesDataMock } from '../mock_data'; +import { keyboardDownEvent } from '../../issue_show/helpers'; -fdescribe('issue_note_form component', () => { +describe('issue_note_form component', () => { let vm; let props; beforeEach(() => { - const Component = Vue.extend(issueNote); + const Component = Vue.extend(issueNoteForm); store.dispatch('setIssueData', issueDataMock); store.dispatch('setNotesData', notesDataMock); props = { - isEditing: true, + isEditing: false, noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', noteId: 545, - saveButtonTitle: 'Save comment', }; vm = new Component({ @@ -30,13 +30,16 @@ fdescribe('issue_note_form component', () => { vm.$destroy(); }); - describe('conflicts editing', (done) => { - it('should show conflict message if note changes outside the component', () => { + describe('conflicts editing', () => { + it('should show conflict message if note changes outside the component', (done) => { + vm.isEditing = true; vm.noteBody = 'Foo'; + const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; Vue.nextTick(() => { - console.log('', vm.$el); - + expect( + vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual(message); done(); }); }); @@ -44,56 +47,65 @@ fdescribe('issue_note_form component', () => { describe('form', () => { it('should render text area with placeholder', () => { - + expect( + vm.$el.querySelector('textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); }); it('should link to markdown docs', () => { - - }); - - it('should link to quick actions docs', () => { - - }); - - it('should preview the content', () => { - - }); - - it('should allow quick actions', () => { - + const { markdownDocs } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocs}"]`).textContent.trim()).toEqual('Markdown'); }); describe('keyboard events', () => { describe('up', () => { it('should ender edit mode', () => { + spyOn(vm, 'editMyLastNote').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(38, true)); + expect(vm.editMyLastNote).toHaveBeenCalled(); }); }); describe('enter', () => { it('should submit note', () => { + spyOn(vm, 'handleUpdate').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true)); - }); - }); - - describe('esc', () => { - it('should show exit', () => { - + expect(vm.handleUpdate).toHaveBeenCalled(); }); }); }); describe('actions', () => { - it('should be possible to cancel', () => { + it('should be possible to cancel', (done) => { + spyOn(vm, 'cancelHandler').and.callThrough(); + vm.isEditing = true; - }); - - it('should be possible to update the note', () => { + Vue.nextTick(() => { + vm.$el.querySelector('.note-edit-cancel').click(); + Vue.nextTick(() => { + expect(vm.cancelHandler).toHaveBeenCalled(); + done(); + }); + }); }); - it('should not be possible to save an empty note', () => { + it('should be possible to update the note', (done) => { + vm.isEditing = true; + Vue.nextTick(() => { + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + Vue.nextTick(() => { + expect(vm.isSubmitting).toEqual(true); + done(); + }); + }); }); }); }); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index ad8fc97362a8..48ee1bf9a521 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -1,70 +1,58 @@ -import { getters } from '~/notes/stores/getters'; +import * as getters from '~/notes/stores/getters'; +import { notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; describe('Getters Notes Store', () => { - + let state; + beforeEach(() => { + state = { + notes: [individualNote], + targetNoteHash: 'hash', + lastFetchedAt: 'timestamp', + + notesData: notesDataMock, + userData: userDataMock, + issueData: issueDataMock, + }; + }); describe('notes', () => { it('should return all notes in the store', () => { - + expect(getters.notes(state)).toEqual([individualNote]); }); }); describe('targetNoteHash', () => { it('should return `targetNoteHash`', () => { - + expect(getters.targetNoteHash(state)).toEqual('hash'); }); }); describe('getNotesData', () => { it('should return all data in `notesData`', () => { - - }); - }); - - describe('getNotesDataByProp', () => { - it('should return the given prop', () => { - + expect(getters.getNotesData(state)).toEqual(notesDataMock); }); }); describe('getIssueData', () => { it('should return all data in `issueData`', () => { - - }); - }); - - describe('getIssueDataByProp', () => { - it('should return the given prop', () => { - + expect(getters.getIssueData(state)).toEqual(issueDataMock); }); }); describe('getUserData', () => { it('should return all data in `userData`', () => { - - }); - }); - - describe('getUserDataByProp', () => { - it('should return the given prop', () => { - + expect(getters.getUserData(state)).toEqual(userDataMock); }); }); describe('notesById', () => { it('should return the note for the given id', () => { - + expect(getters.notesById(state)).toEqual({ 1390: individualNote.notes[0] }); }); }); describe('getCurrentUserLastNote', () => { it('should return the last note of the current user', () => { - - }); - }); - - describe('getDiscussionLastNote', () => { - it('should return the last discussion note of the current user', () => { - + expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]); }); }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 0fdba840f2e5..a38f29c1e39c 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -1,99 +1,207 @@ -import { mutations } from '~/notes/stores/mutations'; +import mutations from '~/notes/stores/mutations'; +import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; describe('Mutation Notes Store', () => { describe('ADD_NEW_NOTE', () => { it('should add a new note to an array of notes', () => { - + const state = { notes: [] }; + mutations.ADD_NEW_NOTE(state, note); + + expect(state).toEqual({ + notes: [{ + expanded: true, + id: note.discussion_id, + individual_note: true, + notes: [note], + reply_id: note.discussion_id, + }], + }); }); }); describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { it('should add a reply to a specific discussion', () => { + const state = { notes: [discussionMock] }; + const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); + mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); + expect(state.notes[0].notes.length).toEqual(4); }); }); describe('DELETE_NOTE', () => { - it('should delete an indivudal note', () => { - - }); + it('should delete a note ', () => { + const state = { notes: [discussionMock] }; + const toDelete = discussionMock.notes[0]; + const lengthBefore = discussionMock.notes.length; - it('should delete a note from a discussion', () => { + mutations.DELETE_NOTE(state, toDelete); + expect(state.notes[0].notes.length).toEqual(lengthBefore - 1); }); }); describe('REMOVE_PLACEHOLDER_NOTES', () => { it('should remove all placeholder notes in indivudal notes and discussion', () => { + const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); + const state = { notes: [placeholderNote] }; + mutations.REMOVE_PLACEHOLDER_NOTES(state); + expect(state.notes).toEqual([]); }); }); describe('SET_NOTES_DATA', () => { it('should set an object with notesData', () => { + const state = { + notesData: {}, + }; + mutations.SET_NOTES_DATA(state, notesDataMock); + expect(state.notesData).toEqual(notesDataMock); }); }); describe('SET_ISSUE_DATA', () => { it('should set the issue data', () => { + const state = { + issueData: {}, + }; + mutations.SET_ISSUE_DATA(state, issueDataMock); + expect(state.issueData).toEqual(issueDataMock); }); }); describe('SET_USER_DATA', () => { it('should set the user data', () => { + const state = { + userData: {}, + }; + mutations.SET_USER_DATA(state, userDataMock); + expect(state.userData).toEqual(userDataMock); }); }); - describe('SET_INITAL_NOTES', () => { + describe('SET_INITIAL_NOTES', () => { it('should set the initial notes received', () => { + const state = { + notes: [], + }; + mutations.SET_INITIAL_NOTES(state, [note]); + expect(state.notes).toEqual([note]); }); }); describe('SET_LAST_FETCHED_AT', () => { - it('should set timestamp', () => {}); + it('should set timestamp', () => { + const state = { + lastFetchedAt: [], + }; + + mutations.SET_LAST_FETCHED_AT(state, 'timestamp'); + expect(state.lastFetchedAt).toEqual('timestamp'); + }); }); describe('SET_TARGET_NOTE_HASH', () => { - it('should set the note hash', () => {}); + it('should set the note hash', () => { + const state = { + targetNoteHash: [], + }; + + mutations.SET_TARGET_NOTE_HASH(state, 'hash'); + expect(state.targetNoteHash).toEqual('hash'); + }); }); describe('SHOW_PLACEHOLDER_NOTE', () => { it('should set a placeholder note', () => { - + const state = { + notes: [], + }; + mutations.SHOW_PLACEHOLDER_NOTE(state, note); + expect(state.notes[0].isPlaceholderNote).toEqual(true); }); }); describe('TOGGLE_AWARD', () => { it('should add award if user has not reacted yet', () => { + const state = { + notes: [note], + userData: userDataMock, + }; + const data = { + note, + awardName: 'cartwheel', + }; + + mutations.TOGGLE_AWARD(state, data); + const lastIndex = state.notes[0].award_emoji.length - 1; + + expect(state.notes[0].award_emoji[lastIndex]).toEqual({ + name: 'cartwheel', + user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, + }); }); it('should remove award if user already reacted', () => { - + const state = { + notes: [note], + userData: { + id: 1, + name: 'Administrator', + username: 'root', + }, + }; + + const data = { + note, + awardName: 'bath_tone3', + }; + mutations.TOGGLE_AWARD(state, data); + expect(state.notes[0].award_emoji.length).toEqual(2); }); }); describe('TOGGLE_DISCUSSION', () => { it('should open a closed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + notes: [discussion], + }; + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.notes[0].expanded).toEqual(true); }); it('should close a opened discussion', () => { + const state = { + notes: [discussionMock], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); + expect(state.notes[0].expanded).toEqual(false); }); }); describe('UPDATE_NOTE', () => { - it('should update an individual note', () => { + it('should update a note', () => { + const state = { + notes: [individualNote], + }; - }); + const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); - it('should update a note in a discussion', () => { + mutations.UPDATE_NOTE(state, updated); + expect(state.notes[0].notes[0].note).toEqual('Foo'); }); }); }); -- GitLab From 8b01ef826df97fa23db657ba5826cb0dd0d0b38f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 20:58:20 +0100 Subject: [PATCH 186/243] Adds helper to test Vuex actions --- spec/javascripts/notes/stores/actions_spec.js | 75 +++++++++++++++---- spec/javascripts/notes/stores/helpers.js | 37 +++++++++ 2 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 spec/javascripts/notes/stores/helpers.js diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 18b1c2c8f6ff..be304893a74e 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,44 +1,91 @@ import * as actions from '~/notes/stores/actions'; - -describe('Actions Notes Store', () => { +import testAction from './helpers'; +import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; +import service from '~/notes/services/issue_notes_service'; + +// use require syntax for inline loaders. +// with inject-loader, this returns a module factory +// that allows us to inject mocked dependencies. +// const actionsInjector = require('inject-loader!./actions'); + +// const actions = actionsInjector({ +// '../api/shop': { +// getProducts (cb) { +// setTimeout(() => { +// cb([ /* mocked response */ ]) +// }, 100) +// } +// } +// }); + +fdescribe('Actions Notes Store', () => { describe('setNotesData', () => { - it('should set received notes data', () => { - + it('should set received notes data', (done) => { + testAction(actions.setNotesData, null, { notesData: {} }, [ + { type: 'SET_NOTES_DATA', payload: notesDataMock }, + ], done); }); }); describe('setIssueData', () => { - it('should set received issue data', () => {}); + it('should set received issue data', (done) => { + testAction(actions.setIssueData, null, { issueData: {} }, [ + { type: 'SET_ISSUE_DATA', payload: issueDataMock }, + ], done); + }); }); describe('setUserData', () => { - it('should set received user data', () => {}); + it('should set received user data', (done) => { + testAction(actions.setUserData, null, { userData: {} }, [ + { type: 'SET_USER_DATA', payload: userDataMock }, + ], done); + }); }); describe('setLastFetchedAt', () => { - it('should set received timestamp', () => {}); + it('should set received timestamp', (done) => { + testAction(actions.setLastFetchedAt, null, { lastFetchedAt: {} }, [ + { type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }, + ], done); + }); }); describe('setInitialNotes', () => { - it('should set initial notes', () => { - + it('should set initial notes', (done) => { + testAction(actions.setInitialNotes, null, { notes: [] }, [ + { type: 'SET_INITAL_NOTES', payload: [individualNote] }, + ], done); }); }); describe('setTargetNoteHash', () => { - it('should set target note hash', () => {}); + it('should set target note hash', (done) => { + testAction(actions.setTargetNoteHash, null, { notes: [] }, [ + { type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }, + ], done); + }); }); describe('toggleDiscussion', () => { - it('should toggle discussion', () => { - + it('should toggle discussion', (done) => { + testAction(actions.toggleDiscussion, null, { notes: [discussionMock] }, [ + { type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }, + ], done); }); }); describe('fetchNotes', () => { - it('should request notes', () => { - + it('should request notes', (done) => { + spyOn(service, 'fetchNotes').and.returnValue(Promise.resolve({ + json() { + return [individualNote]; + }, + })); + testAction(actions.fetchNotes, null, { notes: [] }, [ + { type: 'TOGGLE_DISCUSSION', payload: [individualNote] }, + ], done); }); }); diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/notes/stores/helpers.js new file mode 100644 index 000000000000..2d386fe1da56 --- /dev/null +++ b/spec/javascripts/notes/stores/helpers.js @@ -0,0 +1,37 @@ +/* eslint-disable */ + +/** + * helper for testing action with expected mutations + * https://vuex.vuejs.org/en/testing.html + */ +export default (action, payload, state, expectedMutations, done) => { + let count = 0; + + // mock commit + const commit = (type, payload) => { + const mutation = expectedMutations[count]; + + try { + expect(mutation.type).to.equal(type); + if (payload) { + expect(mutation.payload).to.deep.equal(payload); + } + } catch (error) { + done(error); + } + + count++; + if (count >= expectedMutations.length) { + done(); + } + }; + + // call the action with mocked store and arguments + action({ commit, state }, payload); + + // check if no mutations should have been dispatched + if (expectedMutations.length === 0) { + expect(count).to.equal(0); + done(); + } +}; -- GitLab From c7dbba8bf1e87bb69656ce7a99fb10633d6c78da Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 21:15:33 +0100 Subject: [PATCH 187/243] Adds tests for sync actions --- spec/javascripts/notes/stores/actions_spec.js | 85 +------------------ 1 file changed, 2 insertions(+), 83 deletions(-) diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index be304893a74e..68b71f14bd66 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,25 +1,9 @@ import * as actions from '~/notes/stores/actions'; import testAction from './helpers'; -import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; -import service from '~/notes/services/issue_notes_service'; +import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; -// use require syntax for inline loaders. -// with inject-loader, this returns a module factory -// that allows us to inject mocked dependencies. -// const actionsInjector = require('inject-loader!./actions'); - -// const actions = actionsInjector({ -// '../api/shop': { -// getProducts (cb) { -// setTimeout(() => { -// cb([ /* mocked response */ ]) -// }, 100) -// } -// } -// }); - -fdescribe('Actions Notes Store', () => { +describe('Actions Notes Store', () => { describe('setNotesData', () => { it('should set received notes data', (done) => { testAction(actions.setNotesData, null, { notesData: {} }, [ @@ -75,69 +59,4 @@ fdescribe('Actions Notes Store', () => { ], done); }); }); - - describe('fetchNotes', () => { - it('should request notes', (done) => { - spyOn(service, 'fetchNotes').and.returnValue(Promise.resolve({ - json() { - return [individualNote]; - }, - })); - testAction(actions.fetchNotes, null, { notes: [] }, [ - { type: 'TOGGLE_DISCUSSION', payload: [individualNote] }, - ], done); - }); - }); - - describe('deleteNote', () => { - it('should delete note', () => {}); - }); - - describe('updateNote', () => { - it('should update note', () => { - - }); - }); - - describe('replyToDiscussion', () => { - it('should add a reply to a discussion', () => { - - }); - }); - - describe('createNewNote', () => { - it('should create a new note', () => {}); - }); - - describe('saveNote', () => { - it('should save the received note', () => { - - }); - }); - - describe('poll', () => { - it('should start polling the received endoint', () => { - - }); - }); - - describe('toggleAward', () => { - it('should toggle received award', () => { - - }); - }); - - describe('toggleAwardRequest', () => { - it('should make a request to toggle the award', () => { - - }); - }); - - describe('scrollToNoteIfNeeded', () => { - it('should call `scrollToElement` if note is not in viewport', () => { - }); - - it('should note call `scrollToElement` if note is in viewport', () => { - }); - }); }); -- GitLab From a3cf3e5074fdad83120d1b2815e422f342c6de42 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 11 Aug 2017 21:43:33 +0100 Subject: [PATCH 188/243] Fix broken markdown field test --- .../javascripts/vue_shared/components/markdown/field.vue | 9 ++++----- .../vue_shared/components/markdown/field_spec.js | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index f84652d397c8..4e1d623647e2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -64,12 +64,11 @@ .then(resp => resp.json()) .then((data) => { this.markdownPreviewLoading = false; - this.markdownPreview = data.body; - this.referencedCommands = data.references.commands; - this.referencedUsers = data.references.users; + this.markdownPreview = data.body || 'Nothing to preview.'; - if (!this.markdownPreview) { - this.markdownPreview = 'Nothing to preview.'; + if (data.references) { + this.referencedCommands = data.references.commands; + this.referencedUsers = data.references.users; } this.$nextTick(() => { diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 291e19c9f3c6..84d9a3782a6b 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -92,6 +92,7 @@ describe('Markdown field component', () => { it('renders GFM with jQuery', (done) => { spyOn($.fn, 'renderGFM'); + previewLink.click(); setTimeout(() => { @@ -100,7 +101,7 @@ describe('Markdown field component', () => { ).toHaveBeenCalled(); done(); - }); + }, 0); }); }); -- GitLab From ab88fcf117c4f55071ffaa476b7e72e0c11a967f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 12 Aug 2017 00:14:45 +0100 Subject: [PATCH 189/243] Uncomments tests --- .../discussion_comments_shared_example.rb | 17 +++++++++-------- spec/support/quick_actions_helpers.rb | 6 +----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 1508783a1660..831d0d5880cc 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -11,6 +11,7 @@ expect(page).to have_selector toggle_selector find("#{form_selector} .note-textarea").send_keys('a') + find(submit_selector).click wait_for_requests @@ -75,17 +76,17 @@ expect(page).not_to have_selector menu_selector end - # it 'clicking the ul padding or divider should not change the text' do - # find(menu_selector).trigger 'click' + it 'clicking the ul padding or divider should not change the text' do + find(menu_selector).trigger 'click' - # expect(page).to have_selector menu_selector - # expect(find(dropdown_selector)).to have_content 'Comment' + expect(page).to have_selector menu_selector + expect(find(dropdown_selector)).to have_content 'Comment' - # find("#{menu_selector} .divider").trigger 'click' + find("#{menu_selector} .divider").trigger 'click' - # expect(page).to have_selector menu_selector - # expect(find(dropdown_selector)).to have_content 'Comment' - # end + expect(page).to have_selector menu_selector + expect(find(dropdown_selector)).to have_content 'Comment' + end describe 'when selecting "Start discussion"' do before do diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb index e1f4a8f0a573..d2aaae7518f8 100644 --- a/spec/support/quick_actions_helpers.rb +++ b/spec/support/quick_actions_helpers.rb @@ -1,13 +1,9 @@ module QuickActionsHelpers - def write_note(text, wait = true) + def write_note(text) Sidekiq::Testing.fake! do page.within('.js-main-target-form') do fill_in 'note[note]', with: text find('.js-comment-submit-button').trigger('click') - - if wait - wait_for_requests - end end end end -- GitLab From 1e41440b0beb5803dd0e2bc901922ea362092119 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 12 Aug 2017 00:35:44 +0100 Subject: [PATCH 190/243] Fix broken tests --- app/views/projects/issues/_discussion.html.haml | 2 +- .../notes/components/issue_note_awards_list_spec.js | 3 ++- spec/javascripts/notes/components/issue_note_body_spec.js | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 538bb50d3c91..57391c3f1481 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -12,7 +12,7 @@ notes_path: notes_url(view: 'full_data'), last_fetched_at: Time.now.to_i, issue_data: serialize_issuable(@issue), - current_user_data: UserSerializer.new.represent(current_user).to_json }} + current_user_data: UserSerializer.new.represent(current_user).to_json } } - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/issue_note_awards_list_spec.js index d4156e29ba09..3b6c34f1494f 100644 --- a/spec/javascripts/notes/components/issue_note_awards_list_spec.js +++ b/spec/javascripts/notes/components/issue_note_awards_list_spec.js @@ -39,7 +39,8 @@ describe('issue_note_awards_list component', () => { }); it('should render awarded emojis', () => { - expect(vm.$el.querySelectorAll('.js-awards-block button').length).toEqual(awardsMock.length); + expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined(); + expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined(); }); it('should be possible to remove awareded emoji', () => { diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/issue_note_body_spec.js index c0e06b0985b6..81f07ed47cc9 100644 --- a/spec/javascripts/notes/components/issue_note_body_spec.js +++ b/spec/javascripts/notes/components/issue_note_body_spec.js @@ -40,6 +40,7 @@ describe('issue_note_body component', () => { }); it('should render awards list', () => { - expect(vm.$el.querySelectorAll('.js-awards-block button').length).toEqual(note.award_emoji.length); + expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).toBeDefined(); + expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).toBeDefined(); }); }); -- GitLab From 1852dfa3ae21adfb63f5461844fe3b125999ab0b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 12 Aug 2017 21:02:45 +0100 Subject: [PATCH 191/243] Adds type for shared note specs --- spec/features/reportable_note/commit_spec.rb | 4 ++-- spec/features/reportable_note/issue_spec.rb | 2 +- spec/features/reportable_note/merge_request_spec.rb | 4 ++-- spec/features/reportable_note/snippets_spec.rb | 2 +- .../features/reportable_note_shared_examples.rb | 12 +++++++++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/spec/features/reportable_note/commit_spec.rb b/spec/features/reportable_note/commit_spec.rb index 3bf25221e36d..9b6864eb90f0 100644 --- a/spec/features/reportable_note/commit_spec.rb +++ b/spec/features/reportable_note/commit_spec.rb @@ -18,7 +18,7 @@ visit project_commit_path(project, sample_commit.id) end - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'commit' end context 'a diff note' do @@ -28,6 +28,6 @@ visit project_commit_path(project, sample_commit.id) end - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'commit' end end diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb index 21e96f6f1031..f5a1950e48e8 100644 --- a/spec/features/reportable_note/issue_spec.rb +++ b/spec/features/reportable_note/issue_spec.rb @@ -13,5 +13,5 @@ visit project_issue_path(project, issue) end - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'issue' end diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb index bb296546e068..1f69257f7ed7 100644 --- a/spec/features/reportable_note/merge_request_spec.rb +++ b/spec/features/reportable_note/merge_request_spec.rb @@ -15,12 +15,12 @@ context 'a normal note' do let!(:note) { create(:note_on_merge_request, noteable: merge_request, project: project) } - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'merge_request' end context 'a diff note' do let!(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'merge_request' end end diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb index f1e48ed46bec..a7771847941e 100644 --- a/spec/features/reportable_note/snippets_spec.rb +++ b/spec/features/reportable_note/snippets_spec.rb @@ -17,6 +17,6 @@ visit project_snippet_path(project, snippet) end - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'snippets' end end diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index c3a0623409b9..be7d092d28c2 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -1,6 +1,6 @@ require 'spec_helper' -shared_examples 'reportable note' do +shared_examples 'reportable note' do |type| include NotesHelper let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") } @@ -15,8 +15,14 @@ dropdown = comment.find(more_actions_selector) open_dropdown(dropdown) - expect(dropdown).to have_button('Edit comment') - expect(dropdown).to have_button('Delete comment') + if type == 'issue' + expect(dropdown).to have_button('Edit comment') + expect(dropdown).to have_button('Delete comment') + else + expect(dropdown).to have_link('Edit comment') + expect(dropdown).to have_link('Delete comment') + end + expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) end -- GitLab From dff60db28d6c8506f655225b9b3dab2b9b04592a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 12 Aug 2017 23:38:25 +0100 Subject: [PATCH 192/243] Look for a button on issues --- spec/support/features/reportable_note_shared_examples.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index be7d092d28c2..e2158b8ed344 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -16,13 +16,12 @@ open_dropdown(dropdown) if type == 'issue' - expect(dropdown).to have_button('Edit comment') expect(dropdown).to have_button('Delete comment') else - expect(dropdown).to have_link('Edit comment') expect(dropdown).to have_link('Delete comment') end + expect(dropdown).to have_button('Edit comment') expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) end -- GitLab From 60adf5a93a2c7232f514e42abbd15b324d948111 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 12 Aug 2017 23:38:45 +0100 Subject: [PATCH 193/243] Update dropdown tests to match bootstrap one on issue page and droplab on other pages --- .../discussion_comments_shared_example.rb | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 831d0d5880cc..b5004a5bebae 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -76,16 +76,26 @@ expect(page).not_to have_selector menu_selector end + it 'clicking the ul padding or divider should not change the text' do - find(menu_selector).trigger 'click' + if resource_name == 'issue' + find(menu_selector).trigger 'click' + expect(find(dropdown_selector)).to have_content 'Comment' - expect(page).to have_selector menu_selector - expect(find(dropdown_selector)).to have_content 'Comment' + find(toggle_selector).click + find("#{menu_selector} .divider").trigger 'click' + expect(find(dropdown_selector)).to have_content 'Comment' + else + find(menu_selector).trigger 'click' - find("#{menu_selector} .divider").trigger 'click' + expect(page).to have_selector menu_selector + expect(find(dropdown_selector)).to have_content 'Comment' - expect(page).to have_selector menu_selector - expect(find(dropdown_selector)).to have_content 'Comment' + find("#{menu_selector} .divider").trigger 'click' + + expect(page).to have_selector menu_selector + expect(find(dropdown_selector)).to have_content 'Comment' + end end describe 'when selecting "Start discussion"' do -- GitLab From 8345f2e67f5137e94ecf50f49a17d94102776537 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 12 Aug 2017 23:57:41 +0100 Subject: [PATCH 194/243] Fix note polling specs with the new behavior of not updating an external updated note being edited --- spec/features/issues/note_polling_spec.rb | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index b2f5c7e62a60..2503aecdc23f 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -39,33 +39,22 @@ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) end - it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do + it 'when editing but have not changed anything, and an update comes in, show warning and does not update the note' do click_edit_action(existing_note) - expect(page).to have_field("note-body", with: note_text) + expect(page).to have_field("note[note]", with: note_text) update_note(existing_note, updated_text) - expect(page).to have_field("note-body", with: updated_text) - end - - it 'when editing but you changed some things, and an update comes in, show a warning' do - click_edit_action(existing_note) - - expect(page).to have_field("note-body", with: note_text) - - find("#note_#{existing_note.id} .js-note-text").set('something random') - update_note(existing_note, updated_text) - + expect(page).not_to have_field("note[note]", with: 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 click_edit_action(existing_note) - expect(page).to have_field("note-body", with: note_text) - - find("#note_#{existing_note.id} .js-note-text").set('something random') + expect(page).to have_field("note[note]", with: note_text) update_note(existing_note, updated_text) -- GitLab From c9ac28faaa52062055250fc09fed94a94ebd13c2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 13 Aug 2017 01:00:22 +0100 Subject: [PATCH 195/243] Fix broken task list spec --- .../notes/components/issue_note_form.vue | 1 + spec/features/participants_autocomplete_spec.rb | 6 +----- spec/features/task_lists_spec.rb | 13 +++++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 61a9d0e391c0..95ac4e9f385c 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -129,6 +129,7 @@ :quick-actions-docs="quickActionsDocsUrl" :add-spacing-classes="false"> <textarea + id="note_note" name="note[note]" class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form" :data-supports-quick-actions="!isEditing" diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index 5ba2b6273be3..a22d548eef31 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -14,11 +14,7 @@ shared_examples "open suggestions when typing @" do before do page.within('.new-note') do - if note.noteable_type === 'Issue' - find('.js-vue-comment-form').send_keys('@') - else - find('#note_note').send_keys('@') - end + find('#note_note').send_keys('@') end end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 23e24527d4ee..3d0eac8c4f2e 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Task Lists', js: true do +feature 'Task Lists' do include Warden::Test::Helpers let(:project) { create(:project) } @@ -181,7 +181,7 @@ def visit_issue(project, issue) project: project, author: user) end - it 'renders for note body' do + it 'renders for note body', :js do visit_issue(project, issue) expect(page).to have_selector('.note ul.task-list', count: 1) @@ -189,14 +189,14 @@ def visit_issue(project, issue) expect(page).to have_selector('.note ul input[checked]', count: 2) end - it 'contains the required selectors' do + it 'contains the required selectors', :js 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') end - it 'is only editable by author' do + it 'is only editable by author', :js do visit_issue(project, issue) expect(page).to have_selector('.js-task-list-container') @@ -214,7 +214,7 @@ def visit_issue(project, issue) project: project, author: user) end - it 'renders for note body' do + it 'renders for note body', :js do visit_issue(project, issue) expect(page).to have_selector('.note ul.task-list', count: 1) @@ -229,7 +229,7 @@ def visit_issue(project, issue) project: project, author: user) end - it 'renders for note body' do + it 'renders for note body', :js do visit_issue(project, issue) expect(page).to have_selector('.note ul.task-list', count: 1) @@ -263,6 +263,7 @@ def visit_merge_request(project, merge) 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-issuable-update') expect(page).to have_selector('a.btn-close') end -- GitLab From fdfab012f0b87e3e8c3689a1f7a4fa731f330a67 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 13 Aug 2017 12:02:31 +0100 Subject: [PATCH 196/243] Fixes autosave not enabling comment button when it is restored. Adds back missing CSS class that was breaking tests --- app/assets/javascripts/autosave.js | 21 ++++++++++++------- .../notes/components/issue_comment_form.vue | 8 +++---- .../javascripts/notes/mixins/autosave.js | 2 +- .../javascripts/notes/stores/getters.js | 2 +- app/assets/stylesheets/pages/note_form.scss | 4 ---- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index cfab6c40b34f..7db696159bce 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -2,17 +2,17 @@ import AccessorUtilities from './lib/utils/accessor'; window.Autosave = (function() { - function Autosave(field, key) { + function Autosave(field, key, resource) { this.field = field; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); - + this.resource = resource; if (key.join != null) { - key = key.join("/"); + key = key.join('/'); } - this.key = "autosave/" + key; - this.field.data("autosave", this); + this.key = 'autosave/' + key; + this.field.data('autosave', this); this.restore(); - this.field.on("input", (function(_this) { + this.field.on('input', (function(_this) { return function() { return _this.save(); }; @@ -29,7 +29,14 @@ window.Autosave = (function() { if ((text != null ? text.length : void 0) > 0) { this.field.val(text); } - return this.field.trigger("input"); + if (!this.resource && this.resource !== 'issue') { + this.field.trigger('input'); + } else { + // v-model does not update with jQuery trigger + // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137 + const event = new Event('change', { bubbles: true, cancelable: false }); + this.field.get(0).dispatchEvent(event); + } }; Autosave.prototype.save = function() { diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index d3fe70a2bac4..259644b13a43 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -184,7 +184,7 @@ } }, initAutoSave() { - this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id]); + this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue'); }, initTaskList() { return new TaskList({ @@ -211,7 +211,7 @@ <issue-note-signed-out-widget v-if="!isLoggedIn" /> <ul v-else - class="notes notes-form timeline new-note"> + class="notes notes-form timeline"> <li class="timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon hidden-xs hidden-sm"> @@ -223,10 +223,10 @@ :img-size="40" /> </div> - <div > + <div class="timeline-content timeline-content-form"> <form ref="commentForm" - class="js-main-target-form timeline-content timeline-content-form common-note-form"> + class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"> <div class="flash-container timeline-content"></div> <confidentialIssue v-if="isConfidentialIssue" /> <markdown-field diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index 8723395cf56b..5843b97f2255 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -4,7 +4,7 @@ import '../../autosave'; export default { methods: { initAutoSave() { - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id]); + this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id], 'issue'); }, resetAutoSave() { this.autosave.reset(); diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index ec3f9e5b7a01..669728f0c209 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -19,7 +19,7 @@ export const notesById = state => state.notes.reduce((acc, note) => { const reverseNotes = array => array.slice(0).reverse(); const isLastNote = (note, state) => !note.system && - state.userData && + state.userData !== undefined && note.author.id === state.userData.id; export const getCurrentUserLastNote = state => _.flatten( diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index b4468d6d0a2c..3123e367b2cf 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -20,10 +20,6 @@ } } -.new-note { - display: none; -} - .new-note, .note-edit-form { .note-form-actions { -- GitLab From 240d262db3ab073a6ce75acc4f365e48530146a8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 13 Aug 2017 17:18:45 +0100 Subject: [PATCH 197/243] Only starts autosave if user is logged in --- app/assets/javascripts/autosave.js | 5 ++++- .../javascripts/notes/components/issue_comment_form.vue | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 7db696159bce..4d2d4db7c0e3 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -35,7 +35,10 @@ window.Autosave = (function() { // v-model does not update with jQuery trigger // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137 const event = new Event('change', { bubbles: true, cancelable: false }); - this.field.get(0).dispatchEvent(event); + const field = this.field.get(0); + if (field) { + field.dispatchEvent(event); + } } }; diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 259644b13a43..816db03230cb 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -184,7 +184,9 @@ } }, initAutoSave() { - this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue'); + if (this.isLoggedIn) { + this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue'); + } }, initTaskList() { return new TaskList({ -- GitLab From b67e333ce8dbb7bb81959c293e3e9c4aef5e9702 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova <jarka@gitlab.com> Date: Mon, 14 Aug 2017 07:15:52 +0200 Subject: [PATCH 198/243] fix static_analysis (rubocop) --- spec/controllers/projects/issues_controller_spec.rb | 3 +-- spec/features/issues/note_polling_spec.rb | 1 - .../features/discussion_comments_shared_example.rb | 8 ++++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index c07c035fcda2..66d6a51578e6 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -889,8 +889,7 @@ def create_merge_request it 'returns discussion json' do get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid - expect(JSON.parse(response.body).first.keys).to match_array( - ['id', 'reply_id', 'expanded', 'notes', 'individual_note']) + expect(JSON.parse(response.body).first.keys).to match_array(%w[id reply_id expanded notes individual_note]) end end end diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 2503aecdc23f..9145ebb385ac 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -50,7 +50,6 @@ 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 click_edit_action(existing_note) diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index b5004a5bebae..81cb94ab8c44 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -76,15 +76,14 @@ expect(page).not_to have_selector menu_selector end - it 'clicking the ul padding or divider should not change the text' do + find(menu_selector).trigger 'click' + if resource_name == 'issue' - find(menu_selector).trigger 'click' expect(find(dropdown_selector)).to have_content 'Comment' find(toggle_selector).click find("#{menu_selector} .divider").trigger 'click' - expect(find(dropdown_selector)).to have_content 'Comment' else find(menu_selector).trigger 'click' @@ -94,8 +93,9 @@ find("#{menu_selector} .divider").trigger 'click' expect(page).to have_selector menu_selector - expect(find(dropdown_selector)).to have_content 'Comment' end + + expect(find(dropdown_selector)).to have_content 'Comment' end describe 'when selecting "Start discussion"' do -- GitLab From 6bf781e486fd9e86c37b365b0ea210a32b26ed4a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Mon, 14 Aug 2017 11:35:58 +0100 Subject: [PATCH 199/243] Fix autocomplete broken tests --- app/assets/javascripts/dispatcher.js | 2 +- .../notes/components/issue_comment_form.vue | 2 +- .../notes/components/issue_note_form.vue | 2 +- app/views/projects/issues/_discussion.html.haml | 3 +-- spec/features/participants_autocomplete_spec.rb | 14 +++++++++----- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 07f14b3ddb78..54135f69b05b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -98,7 +98,7 @@ import initChangesDropdown from './init_changes_dropdown'; path = page.split(':'); shortcut_handler = null; - $('.js-gfm-input').each((i, el) => { + $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete); gfm.setup($(el), { diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 816db03230cb..9befba4e5e2d 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -240,7 +240,7 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area" + class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea" data-supports-quick-actions="true" aria-label="Description" v-model="note" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 95ac4e9f385c..1e5c14201013 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -131,7 +131,7 @@ <textarea id="note_note" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form" + class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" :data-supports-quick-actions="!isEditing" aria-label="Description" v-model="note" diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 57391c3f1481..f2992a7c6871 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,3 +1,4 @@ +- @gfm_form = true - content_for :note_actions do - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' @@ -16,5 +17,3 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'notes' - -= render "layouts/init_auto_complete" diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index a22d548eef31..96f6df587e17 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -11,10 +11,14 @@ sign_in(user) end - shared_examples "open suggestions when typing @" do + shared_examples "open suggestions when typing @" do |resource_name| before do page.within('.new-note') do - find('#note_note').send_keys('@') + if resource_name == 'issue' + find('#note-body').send_keys('@') + else + find('#note_note').send_keys('@') + end end end @@ -32,7 +36,7 @@ visit project_issue_path(project, noteable) end - include_examples "open suggestions when typing @" + include_examples "open suggestions when typing @", 'issue' end context 'adding a new note on a Merge Request' do @@ -45,7 +49,7 @@ visit project_merge_request_path(project, noteable) end - include_examples "open suggestions when typing @" + include_examples "open suggestions when typing @", 'merge_request' end context 'adding a new note on a Commit' do @@ -60,6 +64,6 @@ visit project_commit_path(project, noteable) end - include_examples "open suggestions when typing @" + include_examples "open suggestions when typing @", 'commit' end end -- GitLab From 772e560337b3c11c703512b27568ad0fdc3a7bb5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Mon, 14 Aug 2017 12:52:57 +0100 Subject: [PATCH 200/243] Fix broken spinach test --- .../javascripts/notes/components/issue_comment_form.vue | 2 +- app/assets/javascripts/notes/components/issue_note_form.vue | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 9befba4e5e2d..4415c05a1546 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -229,7 +229,7 @@ <form ref="commentForm" class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"> - <div class="flash-container timeline-content"></div> + <div class="flash-container error-alert timeline-content"></div> <confidentialIssue v-if="isConfidentialIssue" /> <markdown-field :markdown-preview-url="markdownPreviewUrl" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 1e5c14201013..e0c40f77b004 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -121,7 +121,7 @@ </div> <div class="flash-container timeline-content"></div> <form - class="edit-note common-note-form"> + class="edit-note common-note-form js-quick-submit gfm-form"> <confidentialIssue v-if="isConfidentialIssue" /> <markdown-field :markdown-preview-url="markdownPreviewUrl" @@ -145,7 +145,7 @@ </markdown-field> <div class="note-form-actions clearfix"> <button - type="submit" + type="button" @click="handleUpdate()" :disabled="isDisabled" class="js-vue-issue-save btn btn-save"> -- GitLab From 5604348fccd6d584f5d890e42766dc911ae2b86b Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 16 Aug 2017 21:23:59 +0300 Subject: [PATCH 201/243] IssueNotesRefactor: Implement missing attachment image. --- .../components/issue_note_attachment.vue | 37 +++++++++++++++++++ .../notes/components/issue_note_body.vue | 6 +++ .../components/issue_note_attachment_spec.js | 23 ++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 app/assets/javascripts/notes/components/issue_note_attachment.vue create mode 100644 spec/javascripts/notes/components/issue_note_attachment_spec.js diff --git a/app/assets/javascripts/notes/components/issue_note_attachment.vue b/app/assets/javascripts/notes/components/issue_note_attachment.vue new file mode 100644 index 000000000000..7134a3eb47ec --- /dev/null +++ b/app/assets/javascripts/notes/components/issue_note_attachment.vue @@ -0,0 +1,37 @@ +<script> + export default { + name: 'issueNoteAttachment', + props: { + attachment: { + type: Object, + required: true, + }, + }, + }; +</script> + +<template> + <div class="note-attachment"> + <a + v-if="attachment.image" + :href="attachment.url" + target="_blank" + rel="noopener noreferrer"> + <img + :src="attachment.url" + class="note-image-attach" /> + </a> + <div class="attachment"> + <a + v-if="attachment.url" + :href="attachment.url" + target="_blank" + rel="noopener noreferrer"> + <i + class="fa fa-paperclip" + aria-hidden="true"></i> + {{attachment.filename}} + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 462ecf065159..d73717c6767d 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -1,6 +1,7 @@ <script> import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteAwardsList from './issue_note_awards_list.vue'; + import issueNoteAttachment from './issue_note_attachment.vue'; import issueNoteForm from './issue_note_form.vue'; import TaskList from '../../task_list'; import autosave from '../mixins/autosave'; @@ -27,6 +28,7 @@ components: { issueNoteEditedText, issueNoteAwardsList, + issueNoteAttachment, issueNoteForm, }, computed: { @@ -109,5 +111,9 @@ :awards="note.award_emoji" :toggle-award-path="note.toggle_award_path" /> + <issue-note-attachment + v-if="note.attachment.url" + :attachment="note.attachment" + /> </div> </template> diff --git a/spec/javascripts/notes/components/issue_note_attachment_spec.js b/spec/javascripts/notes/components/issue_note_attachment_spec.js new file mode 100644 index 000000000000..8f33b874ad6b --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_attachment_spec.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import issueNoteAttachment from '~/notes/components/issue_note_attachment.vue'; + +describe('issue note attachment', () => { + it('should render properly', () => { + const props = { + attachment: { + filename: 'dk.png', + image: true, + url: '/dk.png', + }, + }; + + const Component = Vue.extend(issueNoteAttachment); + const vm = new Component({ + propsData: props, + }).$mount(); + + expect(vm.$el.classList.contains('note-attachment')).toBeTruthy(); + expect(vm.$el.querySelector('img').src).toContain(props.attachment.url); + expect(vm.$el.querySelector('a').href).toContain(props.attachment.url); + }); +}); -- GitLab From 6aeb99c98bee304c5010a1173c47777eff1e04a5 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 16 Aug 2017 22:26:30 +0300 Subject: [PATCH 202/243] IssueNotesRefactor: Add existence check to find the note. --- app/assets/javascripts/notes/stores/getters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 669728f0c209..2649debebc5b 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -19,7 +19,7 @@ export const notesById = state => state.notes.reduce((acc, note) => { const reverseNotes = array => array.slice(0).reverse(); const isLastNote = (note, state) => !note.system && - state.userData !== undefined && + state.userData !== undefined && note.author && note.author.id === state.userData.id; export const getCurrentUserLastNote = state => _.flatten( -- GitLab From 3ef93db5a8dfb62f877731237a53fbdb8bc6e5f8 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Thu, 17 Aug 2017 15:00:10 +0200 Subject: [PATCH 203/243] Return null attachment when there is none --- app/assets/javascripts/notes/components/issue_note_body.vue | 2 +- app/serializers/note_entity.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index d73717c6767d..7d4333e4d270 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -112,7 +112,7 @@ :toggle-award-path="note.toggle_award_path" /> <issue-note-attachment - v-if="note.attachment.url" + v-if="note.attachment" :attachment="note.attachment" /> </div> diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index e5295f5f34d0..416730470dc7 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -53,7 +53,7 @@ class NoteEntity < API::Entities::Note end end - expose :attachment, using: NoteAttachmentEntity + expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? } expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note| delete_attachment_project_note_path(note.project, note) end -- GitLab From 99ba5d0d5097e3170747d17261bb3b9f60aa49fb Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Thu, 17 Aug 2017 18:55:04 +0200 Subject: [PATCH 204/243] Remove view=full_data from NotesController --- .../javascripts/notes/components/issue_comment_form.vue | 1 - .../javascripts/notes/components/issue_discussion.vue | 1 - app/assets/javascripts/notes/components/issue_note.vue | 1 - app/controllers/concerns/notes_actions.rb | 8 ++------ app/views/projects/issues/_discussion.html.haml | 2 +- spec/helpers/notes_helper_spec.rb | 8 -------- spec/javascripts/notes/mock_data.js | 4 ++-- 7 files changed, 5 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 4415c05a1546..d80fc4bd3739 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -110,7 +110,6 @@ endpoint: this.endpoint, flashContainer: this.$el, data: { - view: 'full_data', note: { noteable_type: constants.NOTEABLE_TYPE, noteable_id: this.getIssueData.id, diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 7fd6be12261c..1bec06ea969a 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -100,7 +100,6 @@ target_type: 'issue', target_id: this.discussion.noteable_id, note: { note: noteText }, - view: 'full_data', }, }; diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index c44f735d5e7e..600142a6866f 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -77,7 +77,6 @@ const data = { endpoint: this.note.path, note: { - view: 'full_data', target_type: 'issue', target_id: this.note.noteable_id, note: { note: noteText }, diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 1809464f4f4d..29142e70ed8b 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -18,8 +18,7 @@ def index @notes = prepare_notes_for_rendering(@notes) notes_json[:notes] = - case params[:view] - when 'full_data' + if noteable.is_a?(Issue) note_serializer.represent(@notes) else @notes.map { |note| note_json(note) } @@ -88,8 +87,7 @@ def note_json(note) if note.persisted? attrs[:valid] = true - case params[:view] - when 'full_data' + if noteable.is_a?(Issue) attrs.merge!(note_serializer.represent(note)) else attrs.merge!( @@ -179,8 +177,6 @@ def note_params end def set_polling_interval_header - return unless noteable.is_a?(Issue) - Gitlab::PollingInterval.set_header(response, interval: 6_000) end diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index f2992a7c6871..50aae5388e95 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -10,7 +10,7 @@ new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), markdown_docs: help_page_path('user/markdown'), quick_actions_docs: help_page_path('user/project/quick_actions'), - notes_path: notes_url(view: 'full_data'), + notes_path: notes_url, last_fetched_at: Time.now.to_i, issue_data: serialize_issuable(@issue), current_user_data: UserSerializer.new.represent(current_user).to_json } } diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 41474c327556..9921ca1af336 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -205,14 +205,6 @@ expect(helper.notes_url).to eq("/nm/test/noteable/issue/#{@noteable.id}/notes") end - - it 'adds extra params' do - namespace = create(:namespace, path: 'nm') - @project = create(:project, path: 'test', namespace: namespace) - @noteable = create(:issue, project: @project) - - expect(helper.notes_url(view: 'full_data')).to eq("/nm/test/noteable/issue/#{@noteable.id}/notes?view=full_data") - end end describe '#note_url' do diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 968188c1cf5d..54048cc68f2c 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -4,7 +4,7 @@ export const notesDataMock = { lastFetchedAt: '1501862675', markdownDocs: '/help/user/markdown', newSessionPath: '/users/sign_in?redirect_to_referer=yes', - notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes?view=full_data', + notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes', quickActionsDocs: '/help/user/project/quick_actions', registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane', }; @@ -446,4 +446,4 @@ export const discussionNoteServerResponse = [{ "path": "/gitlab-org/gitlab-ce/notes/1471" }], "individual_note": false -}]; \ No newline at end of file +}]; -- GitLab From 09338e397c95b51a7c515524d7f3eb5f4eed2fb4 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Thu, 17 Aug 2017 18:58:28 +0200 Subject: [PATCH 205/243] Remove cross-references user cannot see from issue discussions JSON --- app/controllers/projects/issues_controller.rb | 8 +++++--- app/models/concerns/noteable.rb | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index dfde6a23c079..e0fb070a8418 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -97,9 +97,11 @@ def show end def discussions - @discussions = @issue.discussions - @discussions.reject! { |d| d.individual_note? && d.first_note.cross_reference_not_visible_for?(current_user) } - prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + notes = @issue.notes.inc_relations_for_view.includes(:noteable).fresh.to_a + notes.reject! { |n| n.cross_reference_not_visible_for?(current_user) } + prepare_notes_for_rendering(notes) + + @discussions = Discussion.build_collection(notes, @issue) render json: DiscussionSerializer.new(project: @project, noteable: @issue, current_user: current_user).represent(@discussions) end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index c7bdc997ecac..a30deffec7bb 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -38,7 +38,7 @@ def discussions def grouped_diff_discussions(*args) # Doesn't use `discussion_notes`, because this may include commit diff notes - # besides MR diff notes, that we do no want to display on the MR Changes tab. + # besides MR diff notes, that we do not want to display on the MR Changes tab. notes.inc_relations_for_view.grouped_diff_discussions(*args) end -- GitLab From 9c22974cd14f41ed156ced0a6742dbae7412b0db Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Thu, 17 Aug 2017 19:25:56 +0200 Subject: [PATCH 206/243] Rename a few attribute suffixes from `_url` to `_path` --- .../javascripts/issue_show/components/app.vue | 12 ++++++------ .../issue_show/components/fields/description.vue | 8 ++++---- .../components/fields/project_move.vue | 4 ++-- .../javascripts/issue_show/components/form.vue | 12 ++++++------ app/assets/javascripts/issue_show/index.js | 6 +++--- .../notes/components/issue_comment_form.vue | 16 ++++++++-------- .../notes/components/issue_note_form.vue | 16 ++++++++-------- app/assets/javascripts/notes/index.js | 4 ++-- .../vue_shared/components/markdown/field.vue | 12 ++++++------ .../vue_shared/components/markdown/toolbar.vue | 14 +++++++------- app/helpers/issuables_helper.rb | 6 +++--- app/views/projects/issues/_discussion.html.haml | 4 ++-- .../issue_show/components/app_spec.js | 6 +++--- .../components/fields/description_spec.js | 4 ++-- .../components/fields/project_move_spec.js | 2 +- .../issue_show/components/form_spec.js | 6 +++--- .../notes/components/issue_comment_form_spec.js | 8 ++++---- .../notes/components/issue_note_app_spec.js | 16 ++++++++-------- .../notes/components/issue_note_form_spec.js | 4 ++-- spec/javascripts/notes/mock_data.js | 4 ++-- .../vue_shared/components/markdown/field_spec.js | 4 ++-- 21 files changed, 84 insertions(+), 84 deletions(-) diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index efae112923da..eaaafd4c1493 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -80,11 +80,11 @@ export default { type: Boolean, required: true, }, - markdownPreviewUrl: { + markdownPreviewPath: { type: String, required: true, }, - markdownDocs: { + markdownDocsPath: { type: String, required: true, }, @@ -96,7 +96,7 @@ export default { type: String, required: true, }, - projectsAutocompleteUrl: { + projectsAutocompletePath: { type: String, required: true, }, @@ -242,11 +242,11 @@ export default { :can-move="canMove" :can-destroy="canDestroy" :issuable-templates="issuableTemplates" - :markdown-docs="markdownDocs" - :markdown-preview-url="markdownPreviewUrl" + :markdown-docs-path="markdownDocsPath" + :markdown-preview-path="markdownPreviewPath" :project-path="projectPath" :project-namespace="projectNamespace" - :projects-autocomplete-url="projectsAutocompleteUrl" + :projects-autocomplete-path="projectsAutocompletePath" /> <div v-else> <title-component diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 27b1b814f9a5..dc902eefc5fe 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -10,11 +10,11 @@ type: Object, required: true, }, - markdownPreviewUrl: { + markdownPreviewPath: { type: String, required: true, }, - markdownDocs: { + markdownDocsPath: { type: String, required: true, }, @@ -36,8 +36,8 @@ Description </label> <markdown-field - :markdown-preview-url="markdownPreviewUrl" - :markdown-docs="markdownDocs"> + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath"> <textarea id="issue-description" class="note-textarea js-gfm-input js-autosize markdown-area" diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue index 7bf2be8b28a6..e514bebc5f60 100644 --- a/app/assets/javascripts/issue_show/components/fields/project_move.vue +++ b/app/assets/javascripts/issue_show/components/fields/project_move.vue @@ -10,7 +10,7 @@ type: Object, required: true, }, - projectsAutocompleteUrl: { + projectsAutocompletePath: { type: String, required: true, }, @@ -20,7 +20,7 @@ $moveDropdown.select2({ ajax: { - url: this.projectsAutocompleteUrl, + url: this.projectsAutocompletePath, quietMillis: 125, data(term, page, context) { return { diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 76ec3dc9a5da..d9b53bc55cf0 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -26,11 +26,11 @@ required: false, default: () => [], }, - markdownPreviewUrl: { + markdownPreviewPath: { type: String, required: true, }, - markdownDocs: { + markdownDocsPath: { type: String, required: true, }, @@ -42,7 +42,7 @@ type: String, required: true, }, - projectsAutocompleteUrl: { + projectsAutocompletePath: { type: String, required: true, }, @@ -89,14 +89,14 @@ </div> <description-field :form-state="formState" - :markdown-preview-url="markdownPreviewUrl" - :markdown-docs="markdownDocs" /> + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" /> <confidential-checkbox :form-state="formState" /> <project-move v-if="canMove" :form-state="formState" - :projects-autocomplete-url="projectsAutocompleteUrl" /> + :projects-autocomplete-path="projectsAutocompletePath" /> <edit-actions :form-state="formState" :can-destroy="canDestroy" /> diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index ad8cb6465e28..60b69b300fd6 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -37,11 +37,11 @@ document.addEventListener('DOMContentLoaded', () => { initialDescriptionText: this.initialDescriptionText, issuableTemplates: this.issuableTemplates, isConfidential: this.isConfidential, - markdownPreviewUrl: this.markdownPreviewUrl, - markdownDocs: this.markdownDocs, + markdownPreviewPath: this.markdownPreviewPath, + markdownDocsPath: this.markdownDocsPath, projectPath: this.projectPath, projectNamespace: this.projectNamespace, - projectsAutocompleteUrl: this.projectsAutocompleteUrl, + projectsAutocompletePath: this.projectsAutocompletePath, updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index d80fc4bd3739..7d69dbc807fc 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -71,13 +71,13 @@ 'js-note-target-reopen': !this.isIssueOpen, }; }, - markdownDocsUrl() { - return this.getNotesData.markdownDocs; + markdownDocsPath() { + return this.getNotesData.markdownDocsPath; }, - quickActionsDocsUrl() { - return this.getNotesData.quickActionsDocs; + quickActionsDocsPath() { + return this.getNotesData.quickActionsDocsPath; }, - markdownPreviewUrl() { + markdownPreviewPath() { return this.getIssueData.preview_note_path; }, author() { @@ -231,9 +231,9 @@ <div class="flash-container error-alert timeline-content"></div> <confidentialIssue v-if="isConfidentialIssue" /> <markdown-field - :markdown-preview-url="markdownPreviewUrl" - :markdown-docs="markdownDocsUrl" - :quick-actions-docs="quickActionsDocsUrl" + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" :add-spacing-classes="false" :is-confidential-issue="isConfidentialIssue"> <textarea diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index e0c40f77b004..591008a02853 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -52,14 +52,14 @@ noteHash() { return `#note_${this.noteId}`; }, - markdownPreviewUrl() { + markdownPreviewPath() { return this.getIssueDataByProp('preview_note_path'); }, - markdownDocsUrl() { - return this.getNotesDataByProp('markdownDocs'); + markdownDocsPath() { + return this.getNotesDataByProp('markdownDocsPath'); }, - quickActionsDocsUrl() { - return !this.isEditing ? this.getNotesDataByProp('quickActionsDocs') : undefined; + quickActionsDocsPath() { + return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined; }, currentUserId() { return this.getUserDataByProp('id'); @@ -124,9 +124,9 @@ class="edit-note common-note-form js-quick-submit gfm-form"> <confidentialIssue v-if="isConfidentialIssue" /> <markdown-field - :markdown-preview-url="markdownPreviewUrl" - :markdown-docs="markdownDocsUrl" - :quick-actions-docs="quickActionsDocsUrl" + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" :add-spacing-classes="false"> <textarea id="note_note" diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 7c90cf20019b..e2ea37408cf3 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -18,8 +18,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ newSessionPath: notesDataset.newSessionPath, registerPath: notesDataset.registerPath, notesPath: notesDataset.notesPath, - markdownDocs: notesDataset.markdownDocs, - quickActionsDocs: notesDataset.quickActionsDocs, + markdownDocsPath: notesDataset.markdownDocsPath, + quickActionsDocsPath: notesDataset.quickActionsDocsPath, }, }; }, diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 4e1d623647e2..11eaeb89c4ca 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -5,12 +5,12 @@ export default { props: { - markdownPreviewUrl: { + markdownPreviewPath: { type: String, required: false, default: '', }, - markdownDocs: { + markdownDocsPath: { type: String, required: true, }, @@ -19,7 +19,7 @@ required: false, default: true, }, - quickActionsDocs: { + quickActionsDocsPath: { type: String, required: false, }, @@ -52,7 +52,7 @@ } else { this.markdownPreviewLoading = true; this.$http.post( - this.markdownPreviewUrl, + this.markdownPreviewPath, { /* Can't use `$refs` as the component is technically in the parent component @@ -117,8 +117,8 @@ </i> </a> <markdown-toolbar - :markdown-docs="markdownDocs" - :quick-actions-docs="quickActionsDocs" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" /> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 13402f34c5b6..65fe7bbd94e9 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,11 +1,11 @@ <script> export default { props: { - markdownDocs: { + markdownDocsPath: { type: String, required: true, }, - quickActionsDocs: { + quickActionsDocsPath: { type: String, required: false, }, @@ -16,24 +16,24 @@ <template> <div class="comment-toolbar clearfix"> <div class="toolbar-text"> - <template v-if="!quickActionsDocs && markdownDocs"> + <template v-if="!quickActionsDocsPath && markdownDocsPath"> <a - :href="markdownDocs" + :href="markdownDocsPath" target="_blank" tabindex="-1"> Markdown is supported </a> </template> - <template v-if="quickActionsDocs && markdownDocs"> + <template v-if="quickActionsDocsPath && markdownDocsPath"> <a - :href="markdownDocs" + :href="markdownDocsPath" target="_blank" tabindex="-1"> Markdown </a> and <a - :href="quickActionsDocs" + :href="quickActionsDocsPath" target="_blank" tabindex="-1"> quick actions diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8c1a4767643b..d3b6a9ff5485 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -210,9 +210,9 @@ def issuable_initial_data(issuable) canMove: current_user ? issuable.can_move?(current_user) : false, issuableRef: issuable.to_reference, isConfidential: issuable.confidential, - markdownPreviewUrl: preview_markdown_path(@project), - markdownDocs: help_page_path('user/markdown'), - projectsAutocompleteUrl: autocomplete_projects_path(project_id: @project.id), + markdownPreviewPath: preview_markdown_path(@project), + markdownDocsPath: help_page_path('user/markdown'), + projectsAutocompletePath: autocomplete_projects_path(project_id: @project.id), issuableTemplates: issuable_templates(issuable), projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path, diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 50aae5388e95..3e63758a5e5e 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -8,8 +8,8 @@ #js-vue-notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), register_path: "#{new_session_path(:user, redirect_to_referer: 'yes')}#register-pane", new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), - markdown_docs: help_page_path('user/markdown'), - quick_actions_docs: help_page_path('user/project/quick_actions'), + markdown_docs_path: help_page_path('user/markdown'), + quick_actions_docs_path: help_page_path('user/project/quick_actions'), notes_path: notes_url, last_fetched_at: Time.now.to_i, issue_data: serialize_issuable(@issue), diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 81ce18bf2fb7..3af26e2f28f1 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -41,9 +41,9 @@ describe('Issuable output', () => { initialTitleText: '', initialDescriptionHtml: '', initialDescriptionText: '', - markdownPreviewUrl: '/', - markdownDocs: '/', - projectsAutocompleteUrl: '/', + markdownPreviewPath: '/', + markdownDocsPath: '/', + projectsAutocompletePath: '/', isConfidential: false, projectNamespace: '/', projectPath: '/', diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js index df8189d9290b..299f88e7778a 100644 --- a/spec/javascripts/issue_show/components/fields/description_spec.js +++ b/spec/javascripts/issue_show/components/fields/description_spec.js @@ -25,8 +25,8 @@ describe('Description field component', () => { vm = new Component({ el, propsData: { - markdownPreviewUrl: '/', - markdownDocs: '/', + markdownPreviewPath: '/', + markdownDocsPath: '/', formState: store.formState, }, }).$mount(); diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js index 86d35c33ff47..8b6ed6a03a99 100644 --- a/spec/javascripts/issue_show/components/fields/project_move_spec.js +++ b/spec/javascripts/issue_show/components/fields/project_move_spec.js @@ -15,7 +15,7 @@ describe('Project move field component', () => { vm = new Component({ propsData: { formState, - projectsAutocompleteUrl: '/autocomplete', + projectsAutocompletePath: '/autocomplete', }, }).$mount(); diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index 9a85223208c7..d8af52874311 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -18,9 +18,9 @@ describe('Inline edit form component', () => { description: 'a', lockedWarningVisible: false, }, - markdownPreviewUrl: '/', - markdownDocs: '/', - projectsAutocompleteUrl: '/', + markdownPreviewPath: '/', + markdownDocsPath: '/', + projectsAutocompletePath: '/', projectPath: '/', projectNamespace: '/', }, diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index 5ec4c7ebd0a0..cca5ec887a36 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -46,13 +46,13 @@ describe('issue_comment_form component', () => { }); it('should link to markdown docs', () => { - const { markdownDocs } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocs}"]`).textContent.trim()).toEqual('Markdown'); + const { markdownDocsPath } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); }); it('should link to quick actions docs', () => { - const { quickActionsDocs } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocs}"]`).textContent.trim()).toEqual('quick actions'); + const { quickActionsDocsPath } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); }); describe('edit mode', () => { diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js index 1a782a32c430..22e91c4c40f2 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -204,13 +204,13 @@ describe('issue_note_app', () => { }); it('should render markdown docs url', () => { - const { markdownDocs } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocs}"]`).textContent.trim()).toEqual('Markdown'); + const { markdownDocsPath } = mockData.notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); }); it('should render quick action docs url', () => { - const { quickActionsDocs } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocs}"]`).textContent.trim()).toEqual('quick actions'); + const { quickActionsDocsPath } = mockData.notesDataMock; + expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); }); }); @@ -227,11 +227,11 @@ describe('issue_note_app', () => { it('should render markdown docs url', (done) => { setTimeout(() => { vm.$el.querySelector('.js-note-edit').click(); - const { markdownDocs } = mockData.notesDataMock; + const { markdownDocsPath } = mockData.notesDataMock; Vue.nextTick(() => { expect( - vm.$el.querySelector(`.edit-note a[href="${markdownDocs}"]`).textContent.trim(), + vm.$el.querySelector(`.edit-note a[href="${markdownDocsPath}"]`).textContent.trim(), ).toEqual('Markdown is supported'); done(); }); @@ -241,11 +241,11 @@ describe('issue_note_app', () => { it('should not render quick actions docs url', (done) => { setTimeout(() => { vm.$el.querySelector('.js-note-edit').click(); - const { quickActionsDocs } = mockData.notesDataMock; + const { quickActionsDocsPath } = mockData.notesDataMock; Vue.nextTick(() => { expect( - vm.$el.querySelector(`.edit-note a[href="${quickActionsDocs}"]`), + vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`), ).toEqual(null); done(); }); diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js index 702e22bb6dcc..a90dbcb72b55 100644 --- a/spec/javascripts/notes/components/issue_note_form_spec.js +++ b/spec/javascripts/notes/components/issue_note_form_spec.js @@ -53,8 +53,8 @@ describe('issue_note_form component', () => { }); it('should link to markdown docs', () => { - const { markdownDocs } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocs}"]`).textContent.trim()).toEqual('Markdown'); + const { markdownDocsPath } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); }); describe('keyboard events', () => { diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 54048cc68f2c..89ba3a002b7b 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -2,10 +2,10 @@ export const notesDataMock = { discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', lastFetchedAt: '1501862675', - markdownDocs: '/help/user/markdown', + markdownDocsPath: '/help/user/markdown', newSessionPath: '/users/sign_in?redirect_to_referer=yes', notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes', - quickActionsDocs: '/help/user/project/quick_actions', + quickActionsDocsPath: '/help/user/project/quick_actions', registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane', }; diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 84d9a3782a6b..60a5c2ae74e8 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -16,8 +16,8 @@ describe('Markdown field component', () => { }, template: ` <field-component - marodown-preview-url="/preview" - markdown-docs="/docs" + markdown-preview-path="/preview" + markdown-docs-path="/docs" > <textarea slot="textarea" -- GitLab From 5f758aff57dc54df7d92d0fb63e706d58cf1093d Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Thu, 17 Aug 2017 19:26:45 +0200 Subject: [PATCH 207/243] Prefer polymorphism over `is_a?` --- app/controllers/concerns/notes_actions.rb | 4 ++-- app/models/award_emoji.rb | 2 +- app/models/concerns/noteable.rb | 4 ++++ app/models/issue.rb | 4 ++++ app/models/note.rb | 6 +++--- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 29142e70ed8b..c568b401a2f9 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -18,7 +18,7 @@ def index @notes = prepare_notes_for_rendering(@notes) notes_json[:notes] = - if noteable.is_a?(Issue) + if noteable.discussions_rendered_on_frontend? note_serializer.represent(@notes) else @notes.map { |note| note_json(note) } @@ -87,7 +87,7 @@ def note_json(note) if note.persisted? attrs[:valid] = true - if noteable.is_a?(Issue) + if noteable.discussions_rendered_on_frontend? attrs.merge!(note_serializer.represent(note)) else attrs.merge!( diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 1f07caf33662..b4e8bf836c2f 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -37,7 +37,7 @@ def upvote? end def expire_etag_cache - return unless awardable.is_a?(Note) + return unless awardable.respond_to?(:expire_etag_cache) awardable.expire_etag_cache end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index a30deffec7bb..1c4ddabcad5b 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -24,6 +24,10 @@ def supports_discussions? DiscussionNote::NOTEABLE_TYPES.include?(base_class_name) end + def discussions_rendered_on_frontend? + false + end + def discussion_notes notes end diff --git a/app/models/issue.rb b/app/models/issue.rb index 1c948c8957ef..2f7624ae2bb7 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -269,6 +269,10 @@ def as_json(options = {}) end end + def discussions_rendered_on_frontend? + true + end + private # Returns `true` if the given User can read the current Issue. diff --git a/app/models/note.rb b/app/models/note.rb index aa8e03ce3022..d807d5ad6188 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -300,12 +300,12 @@ def in_reply_to?(other) end def expire_etag_cache - return unless for_issue? + return unless noteable.discussions_rendered_on_frontend? key = Gitlab::Routing.url_helpers.project_noteable_notes_path( - noteable.project, + project, target_type: noteable_type.underscore, - target_id: noteable.id + target_id: noteable_id ) Gitlab::EtagCaching::Store.new.touch(key) end -- GitLab From 7c491d4fefbd0a079a497e5cae07056bfe467622 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Thu, 17 Aug 2017 19:27:11 +0200 Subject: [PATCH 208/243] Misc tweaks --- app/helpers/notes_helper.rb | 8 ++++---- app/serializers/note_entity.rb | 4 ++-- app/serializers/note_user_entity.rb | 3 +++ app/serializers/user_note_entity.rb | 9 --------- app/views/projects/issues/_discussion.html.haml | 8 +++----- app/views/projects/issues/show.html.haml | 5 +++++ app/views/shared/notes/_notes_with_form.html.haml | 2 +- 7 files changed, 18 insertions(+), 21 deletions(-) create mode 100644 app/serializers/note_user_entity.rb delete mode 100644 app/serializers/user_note_entity.rb diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index ea20f4b8d870..8c5e258f519f 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -93,13 +93,13 @@ def discussion_path(discussion) end end - def notes_url(extra_params = {}) + def notes_url(params = {}) if @snippet.is_a?(PersonalSnippet) - snippet_notes_path(@snippet, extra_params) + snippet_notes_path(@snippet, params) else - params = { target_id: @noteable.id, target_type: @noteable.class.name.underscore } + params.merge!(target_id: @noteable.id, target_type: @noteable.class.name.underscore) - project_noteable_notes_path(@project, params.merge(extra_params)) + project_noteable_notes_path(@project, params) end end diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index 416730470dc7..663a9c06c400 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -3,7 +3,7 @@ class NoteEntity < API::Entities::Note expose :type - expose :author, using: UserNoteEntity + expose :author, using: NoteUserEntity expose :human_access do |note| note.project.team.human_max_access(note.author_id) @@ -15,7 +15,7 @@ class NoteEntity < API::Entities::Note expose :redacted_note_html, as: :note_html expose :last_edited_at, if: -> (note, _) { note.is_edited? } - expose :last_edited_by, using: UserNoteEntity, if: -> (note, _) { note.is_edited? } + expose :last_edited_by, using: NoteUserEntity, if: -> (note, _) { note.is_edited? } expose :current_user do expose :can_edit do |note| diff --git a/app/serializers/note_user_entity.rb b/app/serializers/note_user_entity.rb new file mode 100644 index 000000000000..7289f3a02225 --- /dev/null +++ b/app/serializers/note_user_entity.rb @@ -0,0 +1,3 @@ +class NoteUserEntity < UserEntity + unexpose :web_url +end diff --git a/app/serializers/user_note_entity.rb b/app/serializers/user_note_entity.rb deleted file mode 100644 index fbd87470380b..000000000000 --- a/app/serializers/user_note_entity.rb +++ /dev/null @@ -1,9 +0,0 @@ -class UserNoteEntity < API::Entities::UserBasic - include RequestAwareEntity - - unexpose :web_url - - expose :path do |user| - user_path(user) - end -end diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 3e63758a5e5e..483f28c74f2c 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,12 +1,13 @@ - @gfm_form = true + - content_for :note_actions do - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %section.js-vue-notes-event - #js-vue-notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), - register_path: "#{new_session_path(:user, redirect_to_referer: 'yes')}#register-pane", + #js-vue-notes{ data: { discussions_path: discussions_project_issue_path(@project, @issue, format: :json), + register_path: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), markdown_docs_path: help_page_path('user/markdown'), quick_actions_docs_path: help_page_path('user/project/quick_actions'), @@ -14,6 +15,3 @@ last_fetched_at: Time.now.to_i, issue_data: serialize_issuable(@issue), current_user_data: UserSerializer.new.represent(current_user).to_json } } - - content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'notes' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f2141b84e6d1..04b4ed95a2df 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -2,6 +2,11 @@ - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes + +- content_for :page_specific_javascripts do + = webpack_bundle_tag 'common_vue' + = webpack_bundle_tag 'notes' + - can_update_issue = can?(current_user, :update_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index 1264f07eac9d..e3e86709b8fa 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -17,7 +17,7 @@ - elsif !current_user .disabled-comment.text-center.prepend-top-default Please - = link_to "register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-register-link' + = link_to "register", new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), class: 'js-register-link' or = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link' to comment -- GitLab From 9a8654455f47623d64b9056445338d7a5781d686 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 02:07:29 +0300 Subject: [PATCH 209/243] IssueNotesRefactor: Move edit button out the dropdown. Fix note action buttons spacing. --- .../notes/components/issue_note_actions.vue | 89 +++++++++++-------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index c7b1106ee9dc..4ae3465b9643 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -5,6 +5,8 @@ import emojiSmiley from 'icons/_emoji_smiley.svg'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; + import editSvg from 'icons/_icon_pencil.svg'; + import ellipsisSvg from 'icons/_ellipsis_v.svg'; export default { name: 'issueNoteActions', @@ -74,6 +76,8 @@ this.emojiSmiling = emojiSmiling; this.emojiSmile = emojiSmile; this.emojiSmiley = emojiSmiley; + this.editSvg = editSvg; + this.ellipsisSvg = ellipsisSvg; }, }; </script> @@ -83,55 +87,62 @@ <span v-if="accessLevel" class="note-role">{{accessLevel}}</span> - <a - v-tooltip - v-if="canAddAwardEmoji" - :class="{ 'js-user-authored': isAuthoredByCurrentUser }" - class="note-action-button note-emoji-button js-add-award js-note-emoji" - data-position="right" - href="#" - title="Add reaction"> - <loading-icon :inline="true" /> - <span - v-html="emojiSmiling" - class="link-highlight award-control-icon-neutral"> - </span> - <span - v-html="emojiSmiley" - class="link-highlight award-control-icon-positive"> - </span> - <span - v-html="emojiSmile" - class="link-highlight award-control-icon-super-positive"> - </span> - </a> + <div class="note-actions-item"> + <a + v-tooltip + v-if="canAddAwardEmoji" + :class="{ 'js-user-authored': isAuthoredByCurrentUser }" + class="note-action-button note-emoji-button js-add-award js-note-emoji" + data-position="right" + data-placement="bottom" + data-container="body" + href="#" + title="Add reaction"> + <loading-icon :inline="true" /> + <span + v-html="emojiSmiling" + class="link-highlight award-control-icon-neutral"> + </span> + <span + v-html="emojiSmiley" + class="link-highlight award-control-icon-positive"> + </span> + <span + v-html="emojiSmile" + class="link-highlight award-control-icon-super-positive"> + </span> + </a> + </div> + <div class="note-actions-item"> + <button + @click="onEdit" + v-tooltip + type="button" + title="Edit comment" + class="note-action-button js-note-edit btn btn-transparent" + data-container="body" + data-placement="bottom"> + <span + v-html="editSvg" + class="link-highlight"></span> + </button> + </div> <div v-if="shouldShowActionsDropdown" - class="dropdown more-actions"> + class="dropdown more-actions note-actions-item"> <button v-tooltip type="button" title="More actions" class="note-action-button more-actions-toggle btn btn-transparent" data-toggle="dropdown" - data-container="body"> - <i - aria-hidden="true" - class="fa fa-ellipsis-v icon"> - </i> + data-container="body" + data-placement="bottom"> + <span + class="icon" + v-html="ellipsisSvg"></span> </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> - <template v-if="canEdit"> - <li> - <button - @click="onEdit" - type="button" - class="btn btn-transparent js-note-edit"> - Edit comment - </button> - </li> - <li class="divider"></li> - </template> <li v-if="canReportAsAbuse"> <a :href="reportAbusePath"> Report as abuse -- GitLab From 200cc4f1679fc09684009113aa58b71f8b74544d Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 03:18:10 +0300 Subject: [PATCH 210/243] IssueNotesRefactor: Stop making request for disabled emojis. --- app/assets/javascripts/awards_handler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 023f35273098..4e00b02a17ba 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -109,6 +109,7 @@ class AwardsHandler { } $thumbsBtn.toggleClass('disabled', $userAuthored); + $thumbsBtn.prop('disabled', $userAuthored); } // Create the emoji menu with the first category of emojis. @@ -248,7 +249,7 @@ class AwardsHandler { }, }); - document.querySelector('.js-vue-notes-event').dispatchEvent(toggleAwardEvent); + return document.querySelector('.js-vue-notes-event').dispatchEvent(toggleAwardEvent); } const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); -- GitLab From 6c0add67e2ad289627a04849b73b8ecb26871863 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 03:23:05 +0300 Subject: [PATCH 211/243] IssueNotesRefactor: Fix bottom padding inconsistency of reply form. --- app/assets/javascripts/notes/components/issue_discussion.vue | 4 +++- app/assets/stylesheets/pages/note_form.scss | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 1bec06ea969a..b56bf72a5f79 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -173,7 +173,9 @@ /> </ul> <div class="flash-container"></div> - <div class="discussion-reply-holder"> + <div + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder"> <button v-if="canReply && !isReplying" @click="showReplyForm" diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 3123e367b2cf..84466f36d9a5 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -198,6 +198,10 @@ .discussion-reply-holder { background-color: $white-light; padding: 10px 16px; + + &.is-replying { + padding-bottom: $gl-padding; + } } } -- GitLab From 91be957cc984ee9c7eb7e8853c963881276e263e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 03:27:50 +0300 Subject: [PATCH 212/243] IssueNotesRefactor: Fix padding bottom of the discussion note header. --- app/assets/javascripts/notes/components/issue_discussion.vue | 1 + app/assets/stylesheets/pages/notes.scss | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index b56bf72a5f79..2a10fd45ac2b 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -149,6 +149,7 @@ :include-toggle="true" @toggleHandler="toggleDiscussionHandler" action-text="started a discussion" + class="discussion" /> <issue-note-edited-text v-if="note.last_updated_by" diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 48048e64d3e6..90cfa409139d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -402,6 +402,10 @@ ul.notes { .note-header-info { min-width: 0; padding-bottom: 8px; + + &.discussion { + padding-bottom: 0; + } } .system-note .note-header-info { -- GitLab From b7c1b05f30fc45484510f65a5889a89869a7937c Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Fri, 18 Aug 2017 12:47:50 +0200 Subject: [PATCH 213/243] No explicit `to_a` or instance variables needed. --- app/controllers/concerns/notes_actions.rb | 12 +++++++----- app/controllers/projects/issues_controller.rb | 12 ++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index c568b401a2f9..726838bb2844 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -13,15 +13,17 @@ def index notes_json = { notes: [], last_fetched_at: current_fetched_at } - @notes = notes_finder.execute.inc_relations_for_view.to_a - @notes.reject! { |n| n.cross_reference_not_visible_for?(current_user) } - @notes = prepare_notes_for_rendering(@notes) + notes = notes_finder.execute + .inc_relations_for_view + .reject { |n| n.cross_reference_not_visible_for?(current_user) } + + notes = prepare_notes_for_rendering(notes) notes_json[:notes] = if noteable.discussions_rendered_on_frontend? - note_serializer.represent(@notes) + note_serializer.represent(notes) else - @notes.map { |note| note_json(note) } + notes.map { |note| note_json(note) } end render json: notes_json diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e0fb070a8418..76bb2b7f8110 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -97,13 +97,17 @@ def show end def discussions - notes = @issue.notes.inc_relations_for_view.includes(:noteable).fresh.to_a - notes.reject! { |n| n.cross_reference_not_visible_for?(current_user) } + notes = @issue.notes + .inc_relations_for_view + .includes(:noteable) + .fresh + .reject { |n| n.cross_reference_not_visible_for?(current_user) } + prepare_notes_for_rendering(notes) - @discussions = Discussion.build_collection(notes, @issue) + discussions = Discussion.build_collection(notes, @issue) - render json: DiscussionSerializer.new(project: @project, noteable: @issue, current_user: current_user).represent(@discussions) + render json: DiscussionSerializer.new(project: @project, noteable: @issue, current_user: current_user).represent(discussions) end def create -- GitLab From 39ae8398049b717fb366e7fb77ab87197f8c70cc Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Fri, 18 Aug 2017 12:48:04 +0200 Subject: [PATCH 214/243] Use `try` instead of `repond_to?` and a method call --- app/models/award_emoji.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index b4e8bf836c2f..4d1a15c53aad 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -37,8 +37,6 @@ def upvote? end def expire_etag_cache - return unless awardable.respond_to?(:expire_etag_cache) - - awardable.expire_etag_cache + awardable.try(:expire_etag_cache) end end -- GitLab From fc0035f66b8e5c0ccb6c31af8176bdd4101c2d99 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Fri, 18 Aug 2017 12:48:11 +0200 Subject: [PATCH 215/243] Singular 'snippet' for consistency --- spec/features/reportable_note/snippets_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb index a7771847941e..98ef50b78de1 100644 --- a/spec/features/reportable_note/snippets_spec.rb +++ b/spec/features/reportable_note/snippets_spec.rb @@ -17,6 +17,6 @@ visit project_snippet_path(project, snippet) end - it_behaves_like 'reportable note', 'snippets' + it_behaves_like 'reportable note', 'snippet' end end -- GitLab From 4e77db396220ffea554ade7ea79f4f4120d1589b Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 18:40:07 +0300 Subject: [PATCH 216/243] IssueNotesRefactor: Fix discussion last updated text. --- .../notes/components/issue_discussion.vue | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 2a10fd45ac2b..96c63e587af4 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -55,6 +55,24 @@ newNotePath() { return this.getIssueData.create_note_path; }, + lastUpdatedBy() { + const { notes } = this.note; + + if (notes.length > 1) { + return notes[notes.length - 1].author; + } + + return null; + }, + lastUpdatedAt() { + const { notes } = this.note; + + if (notes.length > 1) { + return notes[notes.length - 1].created_at; + } + + return null; + } }, methods: { ...mapActions([ @@ -152,9 +170,9 @@ class="discussion" /> <issue-note-edited-text - v-if="note.last_updated_by" - :edited-at="note.last_updated_at" - :edited-by="note.last_updated_by" + v-if="lastUpdatedBy" + :edited-at="lastUpdatedAt" + :edited-by="lastUpdatedBy" action-text="Last updated" class-name="discussion-headline-light js-discussion-headline" /> -- GitLab From 70a322c5dad6859919483086d3c065ac0f9b1e23 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 18:51:43 +0300 Subject: [PATCH 217/243] IssueNotesRefactor: Support legacy last edited by case. --- .../notes/components/issue_discussion.vue | 2 +- .../notes/components/issue_note_body.vue | 2 +- .../notes/components/issue_note_edited_text.vue | 16 +++++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 96c63e587af4..1a8d3b983096 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -170,7 +170,7 @@ class="discussion" /> <issue-note-edited-text - v-if="lastUpdatedBy" + v-if="lastUpdatedAt" :edited-at="lastUpdatedAt" :edited-by="lastUpdatedBy" action-text="Last updated" diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 7d4333e4d270..8e35920e659d 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -99,7 +99,7 @@ :data-update-url="note.path" class="hidden js-task-list-field"></textarea> <issue-note-edited-text - v-if="note.last_edited_by" + v-if="note.last_edited_at" :edited-at="note.last_edited_at" :edited-by="note.last_edited_by" action-text="Edited" diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue index 315fa61cfcc9..49e09f0ecc5b 100644 --- a/app/assets/javascripts/notes/components/issue_note_edited_text.vue +++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue @@ -14,7 +14,7 @@ }, editedBy: { type: Object, - required: true, + required: false, }, className: { type: String, @@ -35,11 +35,13 @@ :time="editedAt" tooltip-placement="bottom" /> - by - <a - :href="editedBy.path" - class="js-vue-author author_link"> - {{editedBy.name}} - </a> + <template v-if="editedBy"> + by + <a + :href="editedBy.path" + class="js-vue-author author_link"> + {{editedBy.name}} + </a> + </template> </div> </template> -- GitLab From 9d18bfa3654867aba40cb3ac59e24fb8137d83a1 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 18:55:51 +0300 Subject: [PATCH 218/243] IssueNotesRefactor: Fix edit button and emoji dropdown for anon user. --- .../javascripts/notes/components/issue_note_actions.vue | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 4ae3465b9643..5476ce19836b 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -87,10 +87,11 @@ <span v-if="accessLevel" class="note-role">{{accessLevel}}</span> - <div class="note-actions-item"> + <div + v-if="canAddAwardEmoji" + class="note-actions-item"> <a v-tooltip - v-if="canAddAwardEmoji" :class="{ 'js-user-authored': isAuthoredByCurrentUser }" class="note-action-button note-emoji-button js-add-award js-note-emoji" data-position="right" @@ -113,7 +114,9 @@ </span> </a> </div> - <div class="note-actions-item"> + <div + v-if="canEdit" + class="note-actions-item"> <button @click="onEdit" v-tooltip -- GitLab From a75de77e10906781f2490bbbb00d1aec80f53efa Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 19:02:00 +0300 Subject: [PATCH 219/243] IssueNotesRefactor: Fix award handler. --- app/assets/javascripts/awards_handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 4e00b02a17ba..cdd54ae47729 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -249,7 +249,7 @@ class AwardsHandler { }, }); - return document.querySelector('.js-vue-notes-event').dispatchEvent(toggleAwardEvent); + document.querySelector('.js-vue-notes-event').dispatchEvent(toggleAwardEvent); } const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); -- GitLab From 99be7d6e2512724be5bfd8756e148c571d4fff07 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 19:02:21 +0300 Subject: [PATCH 220/243] =?UTF-8?q?IssueNotesRefactor:=20Don=E2=80=99t=20e?= =?UTF-8?q?xplicitly=20check=20against=20undefined.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/javascripts/notes/stores/getters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 2649debebc5b..1f0c6af61560 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -19,7 +19,7 @@ export const notesById = state => state.notes.reduce((acc, note) => { const reverseNotes = array => array.slice(0).reverse(); const isLastNote = (note, state) => !note.system && - state.userData !== undefined && note.author && + state.userData && note.author && note.author.id === state.userData.id; export const getCurrentUserLastNote = state => _.flatten( -- GitLab From 7e82e45d874170fe894f4caf3fba83b75ca16986 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 19:32:39 +0300 Subject: [PATCH 221/243] IssueNotesRefactor: Prevent anon user to try to add award. --- app/assets/javascripts/notes/components/issue_discussion.vue | 2 +- .../javascripts/notes/components/issue_note_actions.vue | 4 ++-- .../javascripts/notes/components/issue_note_awards_list.vue | 4 ++++ app/assets/javascripts/notes/stores/actions.js | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 1a8d3b983096..bbf9e0db45c2 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -72,7 +72,7 @@ } return null; - } + }, }, methods: { ...mapActions([ diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 5476ce19836b..4b3dc1ad1241 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -3,10 +3,10 @@ import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; import editSvg from 'icons/_icon_pencil.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { name: 'issueNoteActions', diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index 518042e39af1..d42e61e38992 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -140,6 +140,10 @@ return title; }, handleAward(awardName) { + if (!this.isLoggedIn) { + return; + } + let parsedName; // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index b52ec9700d8b..d8119d8ac2ec 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -201,6 +201,7 @@ export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, n export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { const { endpoint, awardName } = data; + return service .toggleAward(endpoint, { name: awardName }) .then(res => res.json()) -- GitLab From fb6421550aa26fd92116e1d5d63969bdd58f67e4 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 18 Aug 2017 23:53:58 +0300 Subject: [PATCH 222/243] Fix empty markdown render request. --- .../vue_shared/components/markdown/field.vue | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 11eaeb89c4ca..9e93feeda3bd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -47,36 +47,40 @@ toggleMarkdownPreview() { this.previewMarkdown = !this.previewMarkdown; + /* + Can't use `$refs` as the component is technically in the parent component + so we access the VNode & then get the element + */ + const text = this.$slots.textarea[0].elm.value; + if (!this.previewMarkdown) { this.markdownPreview = ''; } else { - this.markdownPreviewLoading = true; - this.$http.post( - this.markdownPreviewPath, - { - /* - Can't use `$refs` as the component is technically in the parent component - so we access the VNode & then get the element - */ - text: this.$slots.textarea[0].elm.value, - }, - ) - .then(resp => resp.json()) - .then((data) => { - this.markdownPreviewLoading = false; - this.markdownPreview = data.body || 'Nothing to preview.'; - - if (data.references) { - this.referencedCommands = data.references.commands; - this.referencedUsers = data.references.users; - } + if (text) { + this.markdownPreviewLoading = true; + this.$http.post(this.markdownPreviewPath, { text }) + .then(resp => resp.json()) + .then((data) => { + this.renderMarkdown(data); + }) + .catch(() => new Flash('Error loading markdown preview')); + } else { + this.renderMarkdown(); + } + } + }, + renderMarkdown(data = {}) { + this.markdownPreviewLoading = false; + this.markdownPreview = data.body || 'Nothing to preview.'; - this.$nextTick(() => { - $(this.$refs['markdown-preview']).renderGFM(); - }); - }) - .catch(() => new Flash('Error loading markdown preview')); + if (data.references) { + this.referencedCommands = data.references.commands; + this.referencedUsers = data.references.users; } + + this.$nextTick(() => { + $(this.$refs['markdown-preview']).renderGFM(); + }); }, }, mounted() { -- GitLab From 19f77cab3e6c564dcd99668db882aa61b5ef8dde Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Sat, 19 Aug 2017 00:05:23 +0300 Subject: [PATCH 223/243] IssueNotesRefactor: Fix flash container positioning. --- app/assets/javascripts/notes/components/issue_comment_form.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 7d69dbc807fc..0a9c6c2916ae 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -215,6 +215,7 @@ class="notes notes-form timeline"> <li class="timeline-entry"> <div class="timeline-entry-inner"> + <div class="flash-container error-alert timeline-content"></div> <div class="timeline-icon hidden-xs hidden-sm"> <user-avatar-link v-if="author" @@ -228,7 +229,6 @@ <form ref="commentForm" class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"> - <div class="flash-container error-alert timeline-content"></div> <confidentialIssue v-if="isConfidentialIssue" /> <markdown-field :markdown-preview-path="markdownPreviewPath" -- GitLab From 0fc45b6c19a89b196acafebf5295990b9100878b Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 21 Aug 2017 19:05:40 +0300 Subject: [PATCH 224/243] IssueNotesRefactor: Reenable button after failed submit attempt. --- app/assets/javascripts/notes/components/issue_note.vue | 5 +++-- app/assets/javascripts/notes/components/issue_note_body.vue | 4 ++-- app/assets/javascripts/notes/components/issue_note_form.vue | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 600142a6866f..ff854664046c 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -73,7 +73,7 @@ }); } }, - formUpdateHandler(noteText, parentElement) { + formUpdateHandler(noteText, parentElement, callback) { const data = { endpoint: this.note.path, note: { @@ -94,7 +94,8 @@ 'Something went wrong while editing your comment. Please try again.', 'alert', $(parentElement), - )); + )) + .then(callback); }, formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index 8e35920e659d..ec7ed4690eeb 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -49,8 +49,8 @@ }); } }, - handleFormUpdate(note, parentElement) { - this.$emit('handleFormUpdate', note, parentElement); + handleFormUpdate(note, parentElement, callback) { + this.$emit('handleFormUpdate', note, parentElement, callback); }, formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelFormEdition', shouldConfirm, isDirty); diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue index 591008a02853..626c0f2ce183 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/issue_note_form.vue @@ -74,7 +74,10 @@ methods: { handleUpdate() { this.isSubmitting = true; - this.$emit('handleFormUpdate', this.note, this.$refs.editNoteForm); + + this.$emit('handleFormUpdate', this.note, this.$refs.editNoteForm, () => { + this.isSubmitting = false; + }); }, editMyLastNote() { if (this.note === '') { -- GitLab From e0d583cb7311361597b118a72639247bdb122141 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 21 Aug 2017 19:06:20 +0300 Subject: [PATCH 225/243] IssueNotesRefactor: Minor naming change to improve readability. --- app/assets/javascripts/notes/components/issue_note.vue | 4 ++-- .../javascripts/notes/components/issue_note_actions.vue | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index ff854664046c..2ed803fab963 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -148,8 +148,8 @@ :can-delete="note.current_user.can_edit" :can-report-as-abuse="canReportAsAbuse" :report-abuse-path="note.report_abuse_path" - @editHandler="editHandler" - @deleteHandler="deleteHandler" + @handleEdit="editHandler" + @handleDelete="deleteHandler" /> </div> <issue-note-body diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 4b3dc1ad1241..60c172321d13 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -66,10 +66,10 @@ }, methods: { onEdit() { - this.$emit('editHandler'); + this.$emit('handleEdit'); }, onDelete() { - this.$emit('deleteHandler'); + this.$emit('handleDelete'); }, }, created() { -- GitLab From adc73379f3bc41c2ffa50e0437a101458ff65f58 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 21 Aug 2017 21:20:44 +0300 Subject: [PATCH 226/243] IssueNotesRefactor: First comment and then close the issue. --- .../notes/components/issue_comment_form.vue | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 0a9c6c2916ae..421ed7ab6868 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -139,21 +139,26 @@ } else { this.discard(); } + + if (withIssueAction) { + this.toggleIssueState(); + } }) .catch(() => { this.isSubmitting = false; this.discard(false); }); + } else { + this.toggleIssueState(); } + }, + toggleIssueState() { + this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED; - if (withIssueAction) { - this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED; - - // This is out of scope for the Notes Vue component. - // It was the shortest path to update the issue state and relevant places. - const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close'; - $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); - } + // This is out of scope for the Notes Vue component. + // It was the shortest path to update the issue state and relevant places. + const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close'; + $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); }, discard(shouldClear = true) { // `blur` is needed to clear slash commands autocomplete cache if event fired. -- GitLab From 48acfb5c058df92e419a4012c4a2effff9f5ddfa Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 21 Aug 2017 21:48:09 +0300 Subject: [PATCH 227/243] IssueNotesRefactor: Show Cmd+Enter to comment tooltip. --- app/assets/javascripts/awards_handler.js | 11 ++--------- app/assets/javascripts/behaviors/quick_submit.js | 5 ++++- app/assets/javascripts/lib/utils/common_utils.js | 7 +++++++ .../notes/components/issue_comment_form.vue | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index cdd54ae47729..22fa1f2a6091 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -237,7 +237,7 @@ class AwardsHandler { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length; - if (this.isInIssuePage() && !isMainAwardsBlock) { + if (gl.utils.isInIssuePage() && !isMainAwardsBlock) { const id = votesBlock.attr('id').replace('note_', ''); $('.emoji-menu').removeClass('is-visible'); @@ -287,15 +287,8 @@ class AwardsHandler { } } - isInIssuePage() { - const page = gl.utils.getPagePath(1); - const action = gl.utils.getPagePath(2); - - return page === 'issues' && action === 'show'; - } - getVotesBlock() { - if (this.isInIssuePage()) { + if (gl.utils.isInIssuePage()) { const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); if ($el.length) { diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index bc693616460a..ca0eae323877 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -44,7 +44,10 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { if (!$submitButton.attr('disabled')) { $submitButton.trigger('click', [e]); - $submitButton.disable(); + + if (!gl.utils.isInIssuePage) { + $submitButton.disable(); + } } }); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 52465f182c82..c2b56ee5904c 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -27,6 +27,13 @@ } }; + w.gl.utils.isInIssuePage = () => { + const page = gl.utils.getPagePath(1); + const action = gl.utils.getPagePath(2); + + return page === 'issues' && action === 'show'; + } + w.gl.utils.ajaxGet = function(url) { return $.ajax({ type: "GET", diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 421ed7ab6868..cb19dc9eb4b5 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -258,10 +258,10 @@ <div class="note-form-actions"> <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button - @click="handleSave()" + @click.prevent="handleSave()" :disabled="isSubmitButtonDisabled" class="btn btn-create comment-btn js-comment-button js-comment-submit-button" - type="button"> + type="submit"> {{commentButtonTitle}} </button> <button -- GitLab From 806c3063672793365c337be2cf4f8c55a20165c4 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 21 Aug 2017 22:16:41 +0300 Subject: [PATCH 228/243] IssueNotesRefactor: Fix GFM rendering after edits come in. --- app/assets/javascripts/notes/components/issue_note_body.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue index ec7ed4690eeb..5f9003bfd870 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/issue_note_body.vue @@ -59,12 +59,15 @@ mounted() { this.renderGFM(); this.initTaskList(); + if (this.isEditing) { this.initAutoSave(); } }, updated() { this.initTaskList(); + this.renderGFM(); + if (this.isEditing) { if (!this.autosave) { this.initAutoSave(); -- GitLab From 4707c766df836b3d2ec07d77a2d2d857a6aa7a58 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 21 Aug 2017 22:54:05 +0300 Subject: [PATCH 229/243] IssueNotesRefactor: Poll again when commands applied. --- app/assets/javascripts/notes/stores/actions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index d8119d8ac2ec..b2e4e62ebc63 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -8,6 +8,8 @@ import service from '../services/issue_notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; +let eTagPoll; + export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data); export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); @@ -87,7 +89,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const commandsChanges = res.commands_changes; if (hasQuickActions && errors && Object.keys(errors).length) { - dispatch('fetchData'); + eTagPoll.makeRequest(); $('.js-gfm-input').trigger('clear-commands-cache.atwho'); Flash('Commands applied', 'notice', $(noteData.flashContainer)); @@ -162,7 +164,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { export const poll = ({ commit, state, getters }) => { const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; - const eTagPoll = new Poll({ + eTagPoll = new Poll({ resource: service, method: 'poll', data: requestData, -- GitLab From 4e9f0a091ba0b160b0a09e3bdff2cfd8a75d00ae Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Mon, 21 Aug 2017 23:13:43 +0300 Subject: [PATCH 230/243] IssueNotesRefactor: Trigger change event for Vue to catch programmatically value set. --- app/assets/javascripts/shortcuts_issuable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 14997fe30e9c..78b257bf192d 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -71,7 +71,7 @@ import './shortcuts_navigation'; }); // Trigger autosave - replyField.trigger('input'); + replyField.trigger('input').trigger('change'); // Trigger autosize var event = document.createEvent('Event'); -- GitLab From 479670df94a4dea82350222c1b09c05e1e2ac18d Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 22 Aug 2017 21:47:29 +0300 Subject: [PATCH 231/243] IssueNotesRefactor: Fix consistent typo. --- app/assets/javascripts/notes/stores/actions.js | 4 ++-- app/assets/javascripts/notes/stores/mutation_types.js | 2 +- app/assets/javascripts/notes/stores/mutations.js | 2 +- spec/javascripts/notes/stores/actions_spec.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index b2e4e62ebc63..5b3b7528bef9 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -14,7 +14,7 @@ export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, d export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data); export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); -export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITAL_NOTES, data); +export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); @@ -22,7 +22,7 @@ export const fetchNotes = ({ commit }, path) => service .fetchNotes(path) .then(res => res.json()) .then((res) => { - commit(types.SET_INITAL_NOTES, res); + commit(types.SET_INITIAL_NOTES, res); }); export const deleteNote = ({ commit }, note) => service diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 4eccc2af56eb..cd71533ba9de 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -5,7 +5,7 @@ export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; export const SET_NOTES_DATA = 'SET_NOTES_DATA'; export const SET_ISSUE_DATA = 'SET_ISSUE_DATA'; export const SET_USER_DATA = 'SET_USER_DATA'; -export const SET_INITAL_NOTES = 'SET_INITIAL_NOTES'; +export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES'; export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH'; export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index f38a4ccfbb35..ce56fe74b1ee 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -69,7 +69,7 @@ export default { [types.SET_USER_DATA](state, data) { Object.assign(state, { userData: data }); }, - [types.SET_INITAL_NOTES](state, notesData) { + [types.SET_INITIAL_NOTES](state, notesData) { Object.assign(state, { notes: notesData }); }, diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 68b71f14bd66..72d362acb2f3 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -39,7 +39,7 @@ describe('Actions Notes Store', () => { describe('setInitialNotes', () => { it('should set initial notes', (done) => { testAction(actions.setInitialNotes, null, { notes: [] }, [ - { type: 'SET_INITAL_NOTES', payload: [individualNote] }, + { type: 'SET_INITIAL_NOTES', payload: [individualNote] }, ], done); }); }); -- GitLab From d0516216049e3231ab23a0f610940af615f1d9a3 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 22 Aug 2017 22:10:35 +0300 Subject: [PATCH 232/243] IssueNotesRefactor: Support legacy multiple notes for individual_note: true case. --- .../javascripts/notes/stores/mutations.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index ce56fe74b1ee..3b2b2089d6e5 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -70,7 +70,22 @@ export default { Object.assign(state, { userData: data }); }, [types.SET_INITIAL_NOTES](state, notesData) { - Object.assign(state, { notes: notesData }); + const notes = []; + + notesData.forEach((note) => { + // To support legacy notes, should be very rare case. + if (note.individual_note && note.notes.length > 1) { + note.notes.forEach((n) => { + const nn = Object.assign({}, note); + nn.notes = [n]; // override notes array to only have one item to mimick individual_note + notes.push(nn); + }); + } else { + notes.push(note); + } + }); + + Object.assign(state, { notes }); }, [types.SET_LAST_FETCHED_AT](state, fetchedAt) { -- GitLab From 704ec994fadf9a10212f1aab2a0c21349baf753b Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 22 Aug 2017 23:45:01 +0300 Subject: [PATCH 233/243] IssueNotesRefactor: Hide placeholders and flash messages before submitting a new note. --- app/assets/javascripts/notes/stores/actions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 5b3b7528bef9..7ddaadd648ee 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -64,6 +64,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const replyId = noteData.data.in_reply_to_discussion_id; const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; + commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders + $('.notes-form .flash-container').hide(); // hide previous flash notification + if (hasQuickActions) { placeholderText = utils.stripQuickActions(placeholderText); } -- GitLab From 0d08ba3dce8513e46f5be41b369b6aa4a12df31e Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 23 Aug 2017 01:10:24 +0300 Subject: [PATCH 234/243] IssueNotesRefactor: Fix error messages of edit/reply failure for discussions. --- .../notes/components/issue_comment_form.vue | 7 +++++++ .../notes/components/issue_discussion.vue | 12 ++++++++++-- app/assets/javascripts/notes/stores/actions.js | 11 +++-------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index cb19dc9eb4b5..7784b0191a22 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -96,6 +96,7 @@ methods: { ...mapActions([ 'saveNote', + 'removePlaceholderNotes', ]), setIsSubmitButtonDisabled(note, isSubmitting) { if (!_.isEmpty(note) && !isSubmitting) { @@ -147,6 +148,12 @@ .catch(() => { this.isSubmitting = false; this.discard(false); + Flash( + 'Your comment could not be submitted! Please check your network connection and try again.', + 'alert', + $(this.$el), + ); + this.removePlaceholderNotes(); }); } else { this.toggleIssueState(); diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index bbf9e0db45c2..6fdc85e52e3f 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -78,6 +78,7 @@ ...mapActions([ 'saveNote', 'toggleDiscussion', + 'removePlaceholderNotes', ]), componentName(note) { if (note.isPlaceholderNote) { @@ -126,7 +127,15 @@ this.isReplying = false; this.resetAutoSave(); }) - .catch(() => Flash('Something went wrong while adding your reply. Please try again.')); + .catch(() => { + Flash( + 'Your comment could not be submitted! Please check your network connection and try again.', + 'alert', + $(this.$el), + ); + this.removePlaceholderNotes(); + this.$refs.noteForm.isSubmitting = false; + }); }, }, mounted() { @@ -191,7 +200,6 @@ :key="note.id" /> </ul> - <div class="flash-container"></div> <div :class="{ 'is-replying': isReplying }" class="discussion-reply-holder"> diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 7ddaadd648ee..13cd74bfa1cb 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -57,6 +57,9 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service return res; }); +export const removePlaceholderNotes = ({ commit }) => + commit(types.REMOVE_PLACEHOLDER_NOTES); + export const saveNote = ({ commit, dispatch }, noteData) => { const { note } = noteData.data.note; let placeholderText = note; @@ -127,14 +130,6 @@ export const saveNote = ({ commit, dispatch }, noteData) => { commit(types.REMOVE_PLACEHOLDER_NOTES); return res; - }) - .catch(() => { - Flash( - 'Your comment could not be submitted! Please check your network connection and try again.', - 'alert', - $(noteData.flashContainer), - ); - commit(types.REMOVE_PLACEHOLDER_NOTES); }); }; -- GitLab From 46ed6cd8ca1a7292b62510d0c4aa13a91b4d4b2c Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 23 Aug 2017 01:16:57 +0300 Subject: [PATCH 235/243] IssueNotesRefactor: Fix eslint errors. --- .../javascripts/lib/utils/common_utils.js | 2 +- .../notes/components/issue_note.vue | 11 +++++----- .../vue_shared/components/markdown/field.vue | 20 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index c2b56ee5904c..dce5e76ecee7 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -32,7 +32,7 @@ const action = gl.utils.getPagePath(2); return page === 'issues' && action === 'show'; - } + }; w.gl.utils.ajaxGet = function(url) { return $.ajax({ diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 2ed803fab963..659043d9fa1e 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -86,16 +86,17 @@ this.updateNote(data) .then(() => { this.isEditing = false; - // TODO: this could be moved down, by setting a prop $(this.$refs.noteBody.$el).renderGFM(); this.$refs.noteBody.resetAutoSave(); + callback(); }) - .catch(() => Flash( + .catch(() => { + Flash( 'Something went wrong while editing your comment. Please try again.', 'alert', - $(parentElement), - )) - .then(callback); + $(parentElement)); + callback(); + }); }, formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 9e93feeda3bd..759d30c9c7c5 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -55,18 +55,16 @@ if (!this.previewMarkdown) { this.markdownPreview = ''; + } else if (text) { + this.markdownPreviewLoading = true; + this.$http.post(this.markdownPreviewPath, { text }) + .then(resp => resp.json()) + .then((data) => { + this.renderMarkdown(data); + }) + .catch(() => new Flash('Error loading markdown preview')); } else { - if (text) { - this.markdownPreviewLoading = true; - this.$http.post(this.markdownPreviewPath, { text }) - .then(resp => resp.json()) - .then((data) => { - this.renderMarkdown(data); - }) - .catch(() => new Flash('Error loading markdown preview')); - } else { - this.renderMarkdown(); - } + this.renderMarkdown(); } }, renderMarkdown(data = {}) { -- GitLab From 64820f9a6c17a348dc771a87618140a5c3c8874d Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 23 Aug 2017 03:31:17 +0300 Subject: [PATCH 236/243] IssueNotesRefactor: Show placeholder note immediately when editing. Obviously not one of the best commits I always do. --- .../notes/components/issue_note.vue | 31 +++++++++++++++---- .../notes/components/issue_note_header.vue | 8 ++++- app/assets/stylesheets/pages/notes.scss | 14 +++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 659043d9fa1e..3483f6c75388 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -19,6 +19,7 @@ return { isEditing: false, isDeleting: false, + isRequesting: false, }; }, components: { @@ -37,7 +38,8 @@ }, classNameBindings() { return { - 'is-editing': this.isEditing, + 'is-editing': this.isEditing && !this.isRequesting, + 'is-requesting being-posted': this.isRequesting, 'disabled-content': this.isDeleting, target: this.targetNoteHash === this.noteAnchorId, }; @@ -82,20 +84,27 @@ note: { note: noteText }, }, }; + this.isRequesting = true; + this.oldContent = this.note.note_html; + this.note.note_html = noteText; this.updateNote(data) .then(() => { this.isEditing = false; + this.isRequesting = false; $(this.$refs.noteBody.$el).renderGFM(); this.$refs.noteBody.resetAutoSave(); callback(); }) .catch(() => { - Flash( - 'Something went wrong while editing your comment. Please try again.', - 'alert', - $(parentElement)); - callback(); + this.isRequesting = false; + this.isEditing = true; + this.$nextTick(() => { + const msg = 'Something went wrong while editing your comment. Please try again.'; + Flash(msg, 'alert', $(this.$el)); + this.recoverNoteContent(noteText); + callback(); + }); }); }, formCancelHandler(shouldConfirm, isDirty) { @@ -104,8 +113,18 @@ if (!confirm('Are you sure you want to cancel editing this comment?')) return; } this.$refs.noteBody.resetAutoSave(); + if (this.oldContent) { + this.note.note_html = this.oldContent; + this.oldContent = null; + } this.isEditing = false; }, + recoverNoteContent(noteText) { + // we need to do this to prevent noteForm inconsistent content warning + // this is something we intentionally do so we need to recover the content + this.note.note = noteText; + this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better + }, }, created() { eventHub.$on('enterEditMode', ({ noteId }) => { diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue index 3b658f00f1fa..63aa3d777d07 100644 --- a/app/assets/javascripts/notes/components/issue_note_header.vue +++ b/app/assets/javascripts/notes/components/issue_note_header.vue @@ -85,12 +85,18 @@ </span> <a :href="noteTimestampLink" - @click="updateTargetNoteHash"> + @click="updateTargetNoteHash" + class="note-timestamp"> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" /> </a> + <i + class="fa fa-spinner fa-spin editing-spinner" + aria-label="Comment is being updated" + aria-hidden="true"> + </i> </span> </span> <div diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 90cfa409139d..838ca92d9057 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -100,6 +100,20 @@ ul.notes { } } + .editing-spinner { + display: none; + } + + &.is-requesting { + .note-timestamp { + display: none; + } + + .editing-spinner { + display: inline-block; + } + } + &.is-editing { .note-header, .note-text, -- GitLab From fa482111a482abbe1e319df5c2b86250d6d8950c Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Wed, 23 Aug 2017 11:34:11 +0200 Subject: [PATCH 237/243] Update yarn.lock --- yarn.lock | 187 ++++++------------------------------------------------ 1 file changed, 19 insertions(+), 168 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8ec3cffe8312..c6d40ccb9845 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,20 +49,13 @@ ajv-keywords@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" -ajv@^4.7.0: +ajv@^4.7.0, ajv@^4.9.1: version "4.11.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6" dependencies: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - ajv@^5.1.5: version "5.2.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486" @@ -224,18 +217,12 @@ async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@2.4.1: +async@2.4.1, async@^2.1.2, async@^2.1.4: version "2.4.1" resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" dependencies: lodash "^4.14.0" -async@^2.1.2, async@^2.1.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" - dependencies: - lodash "^4.14.0" - async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -270,7 +257,7 @@ axios@^0.16.2: follow-redirects "^1.2.3" is-buffer "^1.1.5" -babel-code-frame@^6.11.0, babel-code-frame@^6.22.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" dependencies: @@ -278,14 +265,6 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" -babel-code-frame@^6.16.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - babel-core@^6.22.1, babel-core@^6.23.0: version "6.23.1" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df" @@ -881,10 +860,6 @@ balanced-match@^0.4.1, balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -935,11 +910,7 @@ bluebird@^2.10.2: version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" -bluebird@^3.0.5, bluebird@^3.1.1: - version "3.4.7" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" - -bluebird@^3.3.0: +bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" @@ -990,13 +961,6 @@ brace-expansion@^1.0.0: balanced-match "^0.4.1" concat-map "0.0.1" -brace-expansion@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - braces@^0.1.2: version "0.1.5" resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" @@ -1077,10 +1041,6 @@ buffer-indexof@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982" -buffer-shims@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" - buffer-xor@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1464,11 +1424,7 @@ copy-webpack-plugin@^4.0.1: minimatch "^3.0.0" node-dir "^0.1.10" -core-js@^2.2.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" - -core-js@^2.4.0, core-js@^2.4.1: +core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -2063,11 +2019,7 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" -es6-promise@^3.0.2: - version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" - -es6-promise@~3.0.2: +es6-promise@^3.0.2, es6-promise@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" @@ -2248,10 +2200,6 @@ esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" -esprima@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" @@ -2720,18 +2668,7 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.1.1: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.3, glob@^7.0.5: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -2742,11 +2679,7 @@ glob@^7.0.3, glob@^7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^9.0.0: - version "9.14.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" - -globals@^9.14.0: +globals@^9.0.0, globals@^9.14.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -2945,14 +2878,10 @@ html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" -html-entities@1.2.0: +html-entities@1.2.0, html-entities@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2" -html-entities@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" - htmlparser2@^3.8.2: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" @@ -3336,10 +3265,6 @@ isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -3483,18 +3408,7 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - -js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.7.0: - version "3.8.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628" - dependencies: - argparse "^1.0.7" - esprima "^3.1.1" - -js-yaml@^3.5.1: +js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0: version "3.9.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" dependencies: @@ -4035,7 +3949,7 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.29.0 < 2", mime-db@~1.29.0: +"mime-db@>= 1.29.0 < 2": version "1.29.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" @@ -4043,13 +3957,7 @@ mime-db@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" -mime-types@^2.1.12, mime-types@~2.1.7: - version "2.1.16" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" - dependencies: - mime-db "~1.29.0" - -mime-types@~2.1.11, mime-types@~2.1.15: +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: version "2.1.15" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" dependencies: @@ -4077,12 +3985,6 @@ minimalistic-assert@^1.0.0: dependencies: brace-expansion "^1.0.0" -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - dependencies: - brace-expansion "^1.1.7" - minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -4272,16 +4174,7 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.3.2: - version "2.3.5" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df" - dependencies: - hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-package-data@^2.3.4: +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" dependencies: @@ -4448,7 +4341,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5144,7 +5037,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9: +readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.0, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -5167,18 +5060,6 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~0.10.x" util-deprecate "~1.0.1" -readable-stream@^2.1.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" - dependencies: - buffer-shims "^1.0.0" - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - readable-stream@~1.0.2: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -5452,14 +5333,10 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" -semver@^5.0.3: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" - semver@~4.3.3: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" @@ -5951,10 +5828,6 @@ thunky@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" -time-stamp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" - timeago.js@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c" @@ -5983,18 +5856,12 @@ tiny-emitter@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb" -tmp@0.0.31: +tmp@0.0.31, tmp@0.0.x: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" dependencies: os-tmpdir "~1.0.1" -tmp@0.0.x: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - dependencies: - os-tmpdir "~1.0.2" - to-array@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" @@ -6341,7 +6208,7 @@ webpack-bundle-analyzer@^2.8.2: opener "^1.4.3" ws "^2.3.1" -webpack-dev-middleware@^1.0.11: +webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" dependencies: @@ -6350,16 +6217,6 @@ webpack-dev-middleware@^1.0.11: path-is-absolute "^1.0.0" range-parser "^1.0.3" -webpack-dev-middleware@^1.11.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709" - dependencies: - memory-fs "~0.4.1" - mime "^1.3.4" - path-is-absolute "^1.0.0" - range-parser "^1.0.3" - time-stamp "^2.0.0" - webpack-dev-server@^2.6.1: version "2.7.1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz#21580f5a08cd065c71144cf6f61c345bca59a8b8" @@ -6448,18 +6305,12 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@^1.1.1, which@^1.2.1: +which@^1.1.1, which@^1.2.1, which@^1.2.9: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: isexe "^1.1.1" -which@^1.2.9: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" -- GitLab From aa3ff56c6031dcfa71685491d6e0b6ee8398cbf4 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 23 Aug 2017 13:25:40 +0300 Subject: [PATCH 238/243] IssueNotesRefactor: Empty textarea while submitting comment and restore content if request fails. --- .../javascripts/notes/components/issue_comment_form.vue | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 7784b0191a22..6c3d974bc256 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -123,6 +123,7 @@ noteData.data.note.type = constants.DISCUSSION_NOTE; } this.isSubmitting = true; + this.note = ''; // Empty textarea while being requested. Repopulate in catch this.saveNote(noteData) .then((res) => { @@ -148,11 +149,9 @@ .catch(() => { this.isSubmitting = false; this.discard(false); - Flash( - 'Your comment could not be submitted! Please check your network connection and try again.', - 'alert', - $(this.$el), - ); + const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + Flash(msg, 'alert', $(this.$el)); + this.note = noteData.data.note.note; // Restore textarea content. this.removePlaceholderNotes(); }); } else { -- GitLab From ebf7d52d7c994aab7c9890587a23f0fe7a049611 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Wed, 23 Aug 2017 15:40:52 +0300 Subject: [PATCH 239/243] IssueNotesRefactor: Hide note reply form when submitted and show it again if there is an error. --- .../notes/components/issue_discussion.vue | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index 6fdc85e52e3f..b131ef4b1827 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -110,7 +110,7 @@ this.resetAutoSave(); this.isReplying = false; }, - saveReply(noteText) { + saveReply(noteText, form, callback) { const replyData = { endpoint: this.newNotePath, flashContainer: this.$el, @@ -121,20 +121,22 @@ note: { note: noteText }, }, }; + this.isReplying = false; this.saveNote(replyData) .then(() => { - this.isReplying = false; this.resetAutoSave(); + callback(); }) - .catch(() => { - Flash( - 'Your comment could not be submitted! Please check your network connection and try again.', - 'alert', - $(this.$el), - ); + .catch((err) => { this.removePlaceholderNotes(); - this.$refs.noteForm.isSubmitting = false; + this.isReplying = true; + this.$nextTick(() => { + const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + Flash(msg, 'alert', $(this.$el)); + this.$refs.noteForm.note = noteText; + callback(err); + }); }); }, }, -- GitLab From d6692113598e33ac65b0977077dda1d5b9b9eca1 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Wed, 23 Aug 2017 17:28:00 +0200 Subject: [PATCH 240/243] Revert "Update yarn.lock" This reverts commit fa482111a482abbe1e319df5c2b86250d6d8950c. --- yarn.lock | 187 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 168 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index c6d40ccb9845..8ec3cffe8312 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,13 +49,20 @@ ajv-keywords@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" -ajv@^4.7.0, ajv@^4.9.1: +ajv@^4.7.0: version "4.11.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6" dependencies: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + ajv@^5.1.5: version "5.2.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486" @@ -217,12 +224,18 @@ async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@2.4.1, async@^2.1.2, async@^2.1.4: +async@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" dependencies: lodash "^4.14.0" +async@^2.1.2, async@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -257,7 +270,7 @@ axios@^0.16.2: follow-redirects "^1.2.3" is-buffer "^1.1.5" -babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" dependencies: @@ -265,6 +278,14 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" +babel-code-frame@^6.16.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + babel-core@^6.22.1, babel-core@^6.23.0: version "6.23.1" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df" @@ -860,6 +881,10 @@ balanced-match@^0.4.1, balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -910,7 +935,11 @@ bluebird@^2.10.2: version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" -bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0: +bluebird@^3.0.5, bluebird@^3.1.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + +bluebird@^3.3.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" @@ -961,6 +990,13 @@ brace-expansion@^1.0.0: balanced-match "^0.4.1" concat-map "0.0.1" +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + braces@^0.1.2: version "0.1.5" resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" @@ -1041,6 +1077,10 @@ buffer-indexof@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982" +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + buffer-xor@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1424,7 +1464,11 @@ copy-webpack-plugin@^4.0.1: minimatch "^3.0.0" node-dir "^0.1.10" -core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: +core-js@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" + +core-js@^2.4.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -2019,7 +2063,11 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" -es6-promise@^3.0.2, es6-promise@~3.0.2: +es6-promise@^3.0.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + +es6-promise@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" @@ -2200,6 +2248,10 @@ esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" +esprima@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" @@ -2668,7 +2720,18 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: +glob@^7.0.0, glob@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.0.5: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -2679,7 +2742,11 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^9.0.0, globals@^9.14.0: +globals@^9.0.0: + version "9.14.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" + +globals@^9.14.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -2878,10 +2945,14 @@ html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" -html-entities@1.2.0, html-entities@^1.2.0: +html-entities@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2" +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + htmlparser2@^3.8.2: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" @@ -3265,6 +3336,10 @@ isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -3408,7 +3483,18 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0: +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628" + dependencies: + argparse "^1.0.7" + esprima "^3.1.1" + +js-yaml@^3.5.1: version "3.9.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" dependencies: @@ -3949,7 +4035,7 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.29.0 < 2": +"mime-db@>= 1.29.0 < 2", mime-db@~1.29.0: version "1.29.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" @@ -3957,7 +4043,13 @@ mime-db@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" -mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: +mime-types@^2.1.12, mime-types@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" + dependencies: + mime-db "~1.29.0" + +mime-types@~2.1.11, mime-types@~2.1.15: version "2.1.15" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" dependencies: @@ -3985,6 +4077,12 @@ minimalistic-assert@^1.0.0: dependencies: brace-expansion "^1.0.0" +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -4174,7 +4272,16 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: +normalize-package-data@^2.3.2: + version "2.3.5" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^2.3.4: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" dependencies: @@ -4341,7 +4448,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5037,7 +5144,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.0, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9: +readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -5060,6 +5167,18 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~0.10.x" util-deprecate "~1.0.1" +readable-stream@^2.1.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + readable-stream@~1.0.2: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -5333,10 +5452,14 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +semver@^5.0.3: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + semver@~4.3.3: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" @@ -5828,6 +5951,10 @@ thunky@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" +time-stamp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" + timeago.js@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c" @@ -5856,12 +5983,18 @@ tiny-emitter@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb" -tmp@0.0.31, tmp@0.0.x: +tmp@0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" dependencies: os-tmpdir "~1.0.1" +tmp@0.0.x: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + to-array@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" @@ -6208,7 +6341,7 @@ webpack-bundle-analyzer@^2.8.2: opener "^1.4.3" ws "^2.3.1" -webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0: +webpack-dev-middleware@^1.0.11: version "1.11.0" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" dependencies: @@ -6217,6 +6350,16 @@ webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0: path-is-absolute "^1.0.0" range-parser "^1.0.3" +webpack-dev-middleware@^1.11.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709" + dependencies: + memory-fs "~0.4.1" + mime "^1.3.4" + path-is-absolute "^1.0.0" + range-parser "^1.0.3" + time-stamp "^2.0.0" + webpack-dev-server@^2.6.1: version "2.7.1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz#21580f5a08cd065c71144cf6f61c345bca59a8b8" @@ -6305,12 +6448,18 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@^1.1.1, which@^1.2.1, which@^1.2.9: +which@^1.1.1, which@^1.2.1: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: isexe "^1.1.1" +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" -- GitLab From c4eac02df893683b2e22945b5a9fe53ac35b8011 Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Fri, 25 Aug 2017 03:41:35 +0300 Subject: [PATCH 241/243] IssueNotesRefactor: Fix quick submit spec. --- app/assets/javascripts/behaviors/quick_submit.js | 2 +- spec/javascripts/behaviors/quick_submit_spec.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index ca0eae323877..79702c548522 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -45,7 +45,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { if (!$submitButton.attr('disabled')) { $submitButton.trigger('click', [e]); - if (!gl.utils.isInIssuePage) { + if (!gl.utils.isInIssuePage()) { $submitButton.disable(); } } diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 2f8d4ea92e43..f62bf43adb9f 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -7,6 +7,7 @@ describe('Quick Submit behavior', () => { beforeEach(() => { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + $('body').attr('data-page', 'projects:merge_requests:show'); $('form').submit((e) => { // Prevent a form submit from moving us off the testing page e.preventDefault(); -- GitLab From e5c8d2ca51566f18ef9f203ef90d4fe1fb4fbbfa Mon Sep 17 00:00:00 2001 From: Fatih Acet <acetfatih@gmail.com> Date: Tue, 29 Aug 2017 02:39:07 +0300 Subject: [PATCH 242/243] IssueNotesRefactor: Fix broken specs. --- app/assets/javascripts/notes/components/issue_comment_form.vue | 1 + spec/support/features/reportable_note_shared_examples.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 6c3d974bc256..16f4e22aa9b7 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -241,6 +241,7 @@ ref="commentForm" class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"> <confidentialIssue v-if="isConfidentialIssue" /> + <div class="error-alert"></div> <markdown-field :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 4d7d23f03411..d10006edd854 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -20,7 +20,7 @@ open_dropdown(dropdown) expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) - + if type == 'issue' expect(dropdown).to have_button('Delete comment') else -- GitLab From a540f55c6e8ff64f7284ec4194f4c16ca711c685 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Wed, 30 Aug 2017 10:43:10 +0200 Subject: [PATCH 243/243] Fix specs --- app/controllers/concerns/notes_actions.rb | 2 +- app/models/note.rb | 2 +- app/serializers/note_entity.rb | 4 ++-- .../projects/notes_controller_spec.rb | 17 ++++++++++++----- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 726838bb2844..18fd8eb114de 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -89,7 +89,7 @@ def note_json(note) if note.persisted? attrs[:valid] = true - if noteable.discussions_rendered_on_frontend? + if noteable.nil? || noteable.discussions_rendered_on_frontend? attrs.merge!(note_serializer.represent(note)) else attrs.merge!( diff --git a/app/models/note.rb b/app/models/note.rb index d807d5ad6188..1073c1156306 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -300,7 +300,7 @@ def in_reply_to?(other) end def expire_etag_cache - return unless noteable.discussions_rendered_on_frontend? + return unless noteable&.discussions_rendered_on_frontend? key = Gitlab::Routing.url_helpers.project_noteable_notes_path( project, diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index 663a9c06c400..7d50e0ff10d2 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -14,8 +14,8 @@ class NoteEntity < API::Entities::Note expose :redacted_note_html, as: :note_html - expose :last_edited_at, if: -> (note, _) { note.is_edited? } - expose :last_edited_by, using: NoteUserEntity, if: -> (note, _) { note.is_edited? } + expose :last_edited_at, if: -> (note, _) { note.edited? } + expose :last_edited_by, using: NoteUserEntity, if: -> (note, _) { note.edited? } expose :current_user do expose :can_edit do |note| diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index f280c55059c5..6ffe41b8608e 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -46,10 +46,13 @@ end context 'for a discussion note' do - let!(:note) { create(:discussion_note_on_issue, noteable: issue, project: project) } + let(:project) { create(:project, :repository) } + let!(:note) { create(:discussion_note_on_merge_request, project: project) } + + let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) } it 'responds with the expected attributes' do - get :index, request_params + get :index, params expect(note_json[:id]).to eq(note.id) expect(note_json[:discussion_html]).not_to be_nil @@ -104,10 +107,12 @@ end context 'for a regular note' do - let!(:note) { create(:note, noteable: issue, project: project) } + let!(:note) { create(:note_on_merge_request, project: project) } + + let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) } it 'responds with the expected attributes' do - get :index, request_params + get :index, params expect(note_json[:id]).to eq(note.id) expect(note_json[:html]).not_to be_nil @@ -125,7 +130,9 @@ note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' }, namespace_id: project.namespace, project_id: project, - merge_request_diff_head_sha: 'sha' + merge_request_diff_head_sha: 'sha', + target_type: 'merge_request', + target_id: merge_request.id } end -- GitLab